API · Router

The default riot.js bundle does not contain any router allowing you to pick any library that fits better to your needs.

However we have developed and maintain riot-route, a small router library that could be used also as standalone module and fits perfectly to the minimal riot philosophy.

If you want to use riot-route in your project you just need to include it either adding via <script> tag:

<script src="path/to/dist/route.js"></script>

Or if you are using es6 syntax you could do:

import route from 'riot-route' // var route = require('riot-route') is also ok

API

The Riot Router is the minimal router implementation with such technologies:

Setup routing

route(callback)

Execute the given callback when the URL changes. For example

route(function(collection, id, action) {

})

If for example the url changes to customers/987987/edit then in the above example the arguments would be:

collection = 'customers'
id = '987987'
action = 'edit'

The url can change in the following ways:

  1. A new hash is typed into the location bar
  2. When the back/forward buttons are pressed
  3. When route(to) is called
  4. Anchor tag is clicked

route(filter, callback)

>= v2.3

Execute the given callback when the URL changes and it match the filter. For example:

// matches to just `/fruit`
route('/fruit', function(name) {
  console.log('The list of fruits')
})

Wildcards(*) are allowed in filter and you can capture them as arguments:

// if the url change to `/fruit/apple`,
// this will match and catch 'apple' as `name`
route('/fruit/*', function(name) {
  console.log('The detail of ' + name)
})

// if the url change to `/blog/2015-09/01`,
// this will match and catch '2015', '09' and '01'
route('/blog/*-*/*', function(year, month, date) {
  console.log('The page of ' + year + '-' + month + '-' date)
})

If you want to match the url /old and /old/and/anything, it could be written with ..:

route('/old..', function() {
  console.log('The pages under /old was moved.')
})

It could be useful when the url includes search queries.

// if the url change to `/search?keyword=Apple` this will match
route('/search..', function() {
  var q = route.query()
  console.log('Search keyword: ' + q.keyword)
})

// it could be written like this,
// but be aware that `*` can match only alphanumerics and underscore
route('/search?keyword=*', function(keyword) {
  console.log('Search keyword: ' + keyword)
})

Note: Internally wildcards are converted to such regular expressions:

route.create()

>= v2.3

Returns a new routing context. For example:

var subRoute = route.create()
subRoute('/fruit/apple', function() { /* */ })

See also Routing group and Routing priority section, for detail.

Use router

route(to[, title, shouldReplace])

Changes the browser URL and notifies all the listeners assigned with route(callback). For example:

route('customers/267393/edit')

From v2.3, you can set the title, too:

route('customers/267393/edit', 'Editing customer page')

With the third argument, you can replace the current history. It’s useful when the app needs redirect to another page.

route('not-found', 'Not found', true)

Internally…

route.start()

Start listening the url changes.

route.start()

>= v2.3

Riot doesn’t start its router automatically. DON’T FORGET TO START IT BY YOURSELF. This also means that you can choose your favorite router. (Note: before v2.3 Riot started the router automatically. The behavior was changed)

route.start(autoExec)

Start listening the url changes and also exec routing on the current url.

route.start(true)

This is a shorthand for:

route.start()
route.exec()

>= v2.3

Riot doesn’t start its router automatically. DON’T FORGET TO START IT BY YOURSELF. This also means that you can choose your favorite router. (Note: before v2.3 Riot started the router automatically. The behavior was changed)

route.stop()

Stop all the routings. It’ll removes the listeners and clear also the callbacks.

route.stop()

You typically use this method together with route.start. Example:

route.stop() // clear all the old router callbacks
route.start() // start again

subRoute.stop()

>= v2.3

Stop only subRoute’s routings. It’ll removes the listeners and clear also the callbacks.

var subRoute = route.create()
subRoute('/fruit/apple', function() { /* */ })
subRoute.stop()

route.exec()

Study the current browser path “in place” and emit routing without waiting for it to change.

route(function() { /* define routing */ })
route.exec()

Warning: route.exec(callback) was deprecated from v2.3.

route.query()

>= v2.3

This is an utility function to extract the query from the url. For example:

// if the url change to `/search?keyword=Apple&limit=30` this will match
route('/search..', function() {
  var q = route.query()
  console.log('Search keyword: ' + q.keyword)
  console.log('Search limit: ' + q.limit)
})

Customize router

route.base(base)

Change the base path. If you have the url like this:

http://riotexample.com/app/fruit/apple

You could set the base to /app, then you will have to take care of only /fruit/apple.

route.base('/app')

The default base value is “#”. If you’d like to use hashbang, change it to #!.

route.base('#!')

Warning

If you remove the # from the base, your web server needs to deliver your app no matter what url comes in, because your app, in the browser, is manipulating the url. The web server doesn’t know how to handle the URL.

route.parser(parser[, secondParser])

Changes the default parser to a custom one. Here’s one that parses paths like this:

!/user/activation?token=xyz

route.parser(function(path) {
  var raw = path.slice(2).split('?'),
      uri = raw[0].split('/'),
      qs = raw[1],
      params = {}

  if (qs) {
    qs.split('&').forEach(function(v) {
      var c = v.split('=')
      params[c[0]] = c[1]
    })
  }

  uri.push(params)
  return uri
})

And here you’ll receive the params when the URL changes:

route(function(target, action, params) {

  /*
    target = 'user'
    action = 'activation'
    params = { token: 'xyz' }
  */

})

Second parser

>= v2.3

If you specify secondParser, you can change the second parser, too. The second parser is used with url filter:

// This is the default parser
function second(path, filter) {
  var re = new RegExp('^' + filter.replace(/\*/g, '([^/?#]+?)').replace(/\.\./, '.*') + '$')
  if (args = path.match(re)) return args.slice(1)
}

route.parser(first, second)

If the parser return nothing, the next route matching will be tried.

Routing groups

Traditional router on server-side is highly centralized, but recently we use routers everywhere on the page. Think about this case:

<first-tag>
  <p>First tag</p>
  <script>
    route('/fruit/*', function(name) {
      /* do something common */
    })
  </script>
</first-tag>

<second-tag>
  <p>Second tag</p>
  <script>
    route('/fruit/apple', function(name) {
      /* do something SPECIAL */
    })
  </script>
</second-tag>

Two tags have routings, and looks good? No, this won’t work. Because only one routing will emit and we can’t know which routing will, too. Then, we have to make separated routing groups for each tag definition. For example:

<first-tag>
  <p>First tag</p>
  <script>
    var subRoute = route.create() // create another routing context
    subRoute('/fruit/*', function(name) {
      /* do something common */
    })
  </script>
</first-tag>

<second-tag>
  <p>Second tag</p>
  <script>
    var subRoute = route.create() // create another routing context
    subRoute('/fruit/apple', function(name) {
      /* do something SPECIAL */
    })
  </script>
</second-tag>

Routing priority

The router will try to match routing from the first. So in the next case, routing-B and -C will never emit.

route('/fruit/*', function(name) { /* */ }) // routing-A (1)
route('/fruit/apple', function() { /* */ }) // routing-B (2)
route('/fruit/orange', function() { /* */ }) // routing-C (3)

This will work fine:

route('/fruit/apple', function() { /* */ }) // routing-B (1)
route('/fruit/orange', function() { /* */ }) // routing-C (2)
route('/fruit/*', function(name) { /* */ }) // routing-A (3)

And routings with filter has higher priority than routing without filter. It means that routing-X is defined first but execute at last in the next example:

route(function() { /* */ }) // routing-X (3)
route('/fruit/*', function() { /* */ }) // routing-Y (1)
route('/sweet/*', function() { /* */ }) // routing-Z (2)

Tag-based routing

>= v3.1

This feature allows you to write your routes as declarative tags:

<app>
  <router>
    <route path="apple"><p>Apple</p></route>
    <route path="banana"><p>Banana</p></route>
    <route path="coffee"><p>Coffee</p></route>
  </router>
</app>

To use this feature, you need to load route+tag.js instead of route.js

<script src="path/to/dist/route+tag.js"></script>

Or for ES6:

import route from 'riot-route/lib/tag' // note that the path is bit different to cjs one

Or for CommonJS:

const route = require('riot-route/tag')

Available tags

Capturing wildcard arguments

Remember that we could use wildcards * in routing. Of cause we can also do the same in tag-based routing:

<app>
  <router>
    <route path="fruit/apple"><p>Apple</p></route>
    <route path="fruit/*"><inner-tag /></route>
  </router>
</app>

<inner-tag>
  <p>{ name } is not found</p>
  <script>
    this.on('route', name => this.name = name)
  </script>
</inner-tag>

See the example above. If it gets fruit/pineapple, the route event will fire in <inner-tag> and pass one argument 'pineapple'.

Real world example

Usually we would call external API to get some data during routing process. It’s handy to hook route event for such a purpose. For example:

<app>
  <router>
    <route path="user/*"><app-user /></route>
  </router>
</app>

<app-user>
  <p>{ message }!</p>
  <script>
    this.on('route', id => {
      this.message = 'now loading...'
      getUserById(id).then(user => {
        this.update({
          message: `Hello ${ user.name }!`
        })
      })
    })
  </script>
</app-user>

Some notes