Routing

The routing system in the framework is presented separately from the components and is located in the Akili.services.router. The key part of the router is states, which are responsible for managing a specific URL in the browser.

import router from 'akili/src/services/router';
console.log(router === Akili.services.router); // true

router.add(state, pattern, options)

  • state string | Object - router state object or name
  • pattern string - url pattern to link with the state
  • options Object - other state options

Below we have created an abstract state. It is abstract because doesn't have template. On the handler you can get transition - object with full information about current routing stage. It is an instance of router.Transition class. The last transition is always in router.transition property too.

router.add('app', '/app', {
  handler: (transition) => {
    return 'App data';
  }
});

It is better to use ^ in the route pattern, if it must be at the beginning of url.

router.add('app', '^/app', {
  handler: (transition) => {
    return 'App data';
  }
});

Because, for example, /some/thing/app is also corresponds to the first pattern, but not for the second.

router.init(defaultUrl, hashHistoryMode)

  • [defaultUrl] string - default url
  • [hashHistoryMode=true] string - use hash history mode or window.history
document.addEventListener('DOMContentLoaded', () => {
  router.init('/app', false);
  Akili.init().catch((err) => console.error(err));
});

The router must be initialized before Akili.

Transition

It is an object with all information about the routing.

transition.path is the last path of all states found. If the parent path exists it will be in the transition.path.parent property and so on recursively.

router.add('app.home', '/home', {
  handler: (transition) => {
    return 'App home data';
  }
});
router.add('app.home.hello', '/hello', {
  handler: (transition) => {
    return 'App home hello data';
  }
});

So now we have three states. Dots in the name separate the parent states. Accordingly, the complete url pattern is the result of combining the pattern of some state and its parent.

  • app => /app
  • app.home => /app/home
  • app.home.hello => /app/home/hello

When we return something in the handler function, we add it to the corresponding path of transition to the data property. For example, if we move to app.home.hello state we will have:

  • transition.path.state.name === 'app.home.hello'
    transition.path.data === 'App home hello data'
  • transition.path.parent.state.name === 'app.home'
    transition.path.parent.data === 'App home data'
  • transition.path.parent.parent.state.name === 'app'
    transition.path.parent.parent.data === 'App'

In transition.routes you can find all of paths as array. In transition.states is information about each state as an object with keys as state names.

router.state(state, params, query, hash, options)

  • state string | Object - router state object or name
  • [params] Object - url params
  • [query] Object - url query
  • [hash] string - url hash
  • [options] Object - state options
router.state('app.home.hello');
State params

In order to send any dynamic parameters to the route, you can use params.

router.add('app.outside', '/outside/:place/:time', {
  handler: (transition) => {}
});

router.state('app.outside', { place: 'library', time: 'afternoon' });
// => /app/outside/library/afternoon
State query

In order to add query to URL use query argument.

router.state('app.outside', { place: 'bar' }, { gender: 'male', eyes: 'black' });
// => /app/outside/bar?gender=male&eyes=black
State hash

If you need to add hash to URL use hash argument.

router.state('app.outside', { place: 'station' }, {}, 'platform1');
// => /app/outside/station#platform1

router.location(url, options)

  • url string - URL
  • [options] Object - state options

When you change a route, but its parents coincide with the previous ones, they will not be updated again. Use the option { reload: true } for that.

router.state('app.outside', { place: 'station' }, {}, '', { reload: true });
router.location('/app/outside/station', { reload: true });

If you want to save the scroll position after the redirect, you need to use { saveScrollPosition: true } option.

State change handling

Every time you change the state an event occurs in window.

// before the change
window.addEventListener('state-change', (e) => {
  console.log(e.detail === router.transaction); // true
});

// after the change
window.addEventListener('state-changed', (e) => {
  console.log(e.detail === router.transaction); // true
});

Templates

In most cases when you work with routes you need to change not only the URL, but also the html content. To do this, you can use two options when you create a state.

router.add('app.car', '/car', { 
  title: 'My car', // document.title 
  template: '<my-car-component>${ this.__transition.data }</my-car-component>',
  handler: () => 'Tesla model S'
});

router.add('app.bus', '/bus', {
  title: (transition) => transition.data, // document.title 
  templateUrl: '/templates/bus.html'
  handler: transition => 'The bus title'
});

class MyCarComponent extends Component {
  compiled() {
    // simplified transition info about the current transitionas a plain object for the scope 
    console.log(this.scope.__transition); 

    // there is all information about the current route
    console.log(router.transition.path);
    console.log(router.transition.states['app.car']);
    router.transition.path === router.transition.states['app.car']; // true
  }
}
<div>
  <route></route>
</div>

So all the patterns of the current state will be embedded in components according to their level of nesting. Each route component scope.__transition contains all the necessary information about the corresponding transition. Therefore, in the state of the car inside the my-car-component component element will be Tesla model S.

To change routes in templates you can use the new attributes for a tag.

Component instead of the template

class BikeComponent extends Component {
  static template = '${ this.name }';

  static define() {
    Akili.component('bike', this);

    router.add('app.bike', '/bike', {
      component: this
    });
  }

  created() {
    this.scope.name = 'My bike';
  }
}

BikeComponent.define();

It is the same as:

router.add('app.bike', '/bike', {
  template: '<bike></bike>'
});

class BikeComponent extends Component {
  static template = '${ this.name }';

  static define() {
    Akili.component('bike', this);
  }

  created() {
    this.scope.name = 'My bike';
  }
}

BikeComponent.define();

Default state values

When you add the state you can set the default values for params, query and hash. The default values has higher priority than passed values. But you can forcibly remove the necessary argument passing null.

router.add('app', '^/:lang', {
  params: {
    lang: args => args.params.lang || 'en'
  },
  query: {
    v: 2
    useless: 'oops'
  },
  hash: 'header1'
});

router.state('add', { lang: es }, { v: 3, useless: null }, hash: null); // /es/?v=2

Redirecting

If you want to redirect to another state in some handler, you need to use transition.redirect.

router.add('app.flat', '/flat/:room', {
  handler: (transition) => {
    if(!transition.path.params.type) {
      return transition.redirect('app.flat', { room: 'kitchen' });
    }
  }
});

To cancel transition use transition.cancel.

router.add('app.shapes', '/shapes', {
  handler: (transition) => {
    if(transition.path.query.shoes !== undefined) {
      transition.cancel();
    }
  }
});

Isolation

If you need to change url bypassing the framework router, use isolate method.

router.isolate(() => {
  window.history.pushState(null, '', '/change/myself')
});

router.reload(params, query, hash, options) Promise

  • [params] Object - url params
  • [query] Object - url query
  • [hash] string - url hash
  • [options] Object - state options

router.back()

router.forward()

router.go(position)

  • position integer - history position (window.history.go)