Telerik blogs
kendo_router_header

Routers are one of the most critical parts of any single page application. By design we should focus on a user centric routing model. A user centric routing model involves designing the URL around making it easy for the user to see where they are in the app by the URL.

The concepts behind the Kendo UI Router mirror the standards that developers aim for when building dynamic client-side applications. This means that it's available as for anybody to use and follows commonly used patterns to manage views, routes and data like other widely-used routing options in the industry.

The best thing, it's a completely standalone module, which means if you need an easy to use, powerful and lightweight routing module, Kendo's Router is a fantastic choice. It can also be integrated as with other frameworks or within standalone JavaScript projects. Of course, you can get the router with Kendo UI Professional but it is also available as part of the open-source Kendo UI Core (or via GitHub). While you don't have to use it with the rest of Kendo UI Core, feel free to try out all the free widgets that it gives you.

Let's explore the power of Kendo UI's router and how to keep our create routes and use the powerful router.

For this article, we're going to be using the ES2015 (aka ES6) for writing our JavaScript.

Hello World

First up, let's create the typical "Hello World" example and bootstrap our Kendo router. I'll assume, at this point, that you've got the relevant Kendo Router JavaScript file included in the page so that it's available in the global scope.

So what now? Let's dig in.

First we'll create a variable called router and then create a new instance of the kendo.Router class:

let router = new kendo.Router();

With the first task out the way, our router is now almost ready. Next, we need to define a very simple route for our first application view:

let router = new kendo.Router();

router.route('/', () => {

});

At this point, we've instantiated a new instance of the router and declared a route we wish to use. We're almost there! The icing on the cake to kick this off is to start the router. We do this with the start method provided on the router's prototype chain:

let router = new kendo.Router();

router.route('/', () => {

});

router.start();

The router.start method is typically best placed at the end of all your routing definitions, and can be invoked inside a DOMContentLoaded event callback (or jQuery's equivalent):

// DOMContentLoaded event
document.addEventListener('DOMContentLoaded', function () {
  router.start();
}, false);

// jQuery ready event
$(function() {
  router.start();
});

If your scripts are placed at the bottom of the HTML document however, you don't need either event listener. Simply plce your script tags at the bottom of the page just before the closing </body> tag. This also has performance benefits!

Capturing Routing Events

Router load events

Once the router is instantiated, we can capture it through the init event, which may be used to render a View or template partial:

router.bind('init', () => {
  // init logic here
});

Here's a pseudo example of getting a layout to render using the init callback and a layout instance (from kendo.Layout) that binds to <div class="app"></div>:

router.bind('init', () => layout.render(document.querySelector('.app')));

The above example uses the native DOM API querySelector, you might be more familiar with it's jQuery equivalent:

router.bind('init', () => layout.render($('.app')));

Route success callbacks

You may have already noticed that the second argument of the router takes a function callback. We're given a callback on each route definition that runs when that particular route changes. Let's add a simple console.log to allow us to see what's happening when the route is instantiated:

let router = new kendo.Router();

router.route('/', () => {
  console.log('Hello world!');
});

router.start();

These callbacks allow us to run any route specific logic, such as rendering a new view, which is crucial in single page applications to get things running.

Creating Routes

Statically driven routes

Now that we've reached "Hello world" status, let's define some more routes - because all single page apps need routes!

So, how do we do that? Again, we hook into the router instance we already have, and simply bind another route to it:

let router = new kendo.Router();

router.route('/', () => {
  console.log('Index route!');
});
router.route('/about', () => {
  console.log('About route!');
});
router.route('/dashboard', () => {
  console.log('Dashboard route!');
});

router.start();

Dynamically driven routes

So far in this tutorial, we've learned how to bootstrap the router and add some routes, but what we've achieved are what we'll call static routes. We define an about route, and we are taken to mydomain.com/#/about.

Not all URLs can remain static, we need to make them dynamic. To do this we can use a specific syntax inside the string in which we declare new routes.

Let's assume our dashboard has a few "nested" routes, we might want to add mydomain.com/#/dashboard/33m92ks21, which would be an API-driven custom route. We might also have mydomain.com/#/dashboard/62r01ad31, which serves the same template but makes a different, dynamic API call.

So, let's do this! We'll need to use the special : syntax in our route definitions to allow for dynamic routes:

router.route('/dashboard/:id', (id) => {
  console.log('Dashboard route', id);
});

We are then passed the particular id property back to us in the callback, which we can use for API calls or other business logic purposes.

If we visited mydomain.com/#/dashboard/62r01ad31, our callback would give us this associated return value:

router.route('/dashboard/:id', (id) => {
  // RESULT: Dashboard route 62r01ad31
  console.log('Dashboard route', id);
});

We can also take dynamic views further by nesting another level. Let's assume that we want to visit mydomain.com/#/dashboard/62r01ad31/2 for some pseudo pagination. We might then define the following:

router.route('/dashboard/:id/:page', (id, page) => {
  console.log('Dashboard id:', id);
});

Just like before, we get the expected return values:

router.route('/dashboard/:id/:page', (id, page) => {
  // RESULT: Dashboard id: 62r01ad31
  console.log('Dashboard id:', id);

  // RESULT: Dashboard page: 2
  console.log('Dashboard page:', page);
});

Advanced Routing

Our application may need to provide specific functionality to a set of routes. We have multiple options for how we control our routes, such as making them entirely optional, or ensuring a callback is invoked when something changes in a child state of a route.

Optional routes

Routes can be totally optional, we use a special syntax () inside the route definition:

router.route('/dashboard(/:id)(/:page)', (id, page) => {

});

Globbed routes

There may be scenarios where you wish to run a callback for routes under a specific segment, for this we can use * notation in the route definition to define globbed routes.

An example of a globbed route:

router.route('/dashboard/*suffix', (suffix) => {

});

This callback would run, for example, in the following route:

mydomain.com/#/dashboard/62r01ad31

And would give the following output in the callback:

router.route('/dashboard/*suffix', (suffix) => {
  // RESULT: Suffix 62r01ad31
  console.log('Suffix', suffix);
});

Let's assume another nested route with the /2 suffix:

mydomain.com/#/dashboard/62r01ad31/2

Our callback would then give us the following result:

router.route('/dashboard/*suffix', (suffix) => {
  // RESULT: Suffix 62r01ad31/2
  console.log('Suffix', suffix);
});

Safety Netting Missing Routes

If a particular route that doesn't exist is hit by the browser, we'll need to add some kind of fallback mechanism that prevents the app to failing. We can do this by hooking into the routeMissing event and running a callback if it fires. This API is super simple and is passed into the kendo.Router instance when invoked:

let router = new kendo.Router({
  routeMissing(e) {
    console.log(e.url);
  }
});

At this point we could force users back to a route they're familiar with, such as the initial route:

let router = new kendo.Router({
  routeMissing() {
    // navigate back to safety
    this.navigate('/');
  }
});

If you're logging client-side interactions in JavaScript, you could pass the e argument into a particular module you're using to capture JavaScript errors to help debug your application further.

Changing Routes with JavaScript

Control your route changes

So far we've covered setting up routes, exploring the various syntaxes available for routing definitions and given a very small glimpse at using the JavaScript API to control routes. The JavaScript API is a fantastic way to drive state changes inside your application's logic. It's a tiny API that allows you to control what's happening in your app.

Let's tell the router we want to advance to the about route:

router.navigate('/about');

It's as simple as that! Let's try another:

router.navigate('/dashboard/62r01ad31');

Yes that's absolutely right, you can navigate to dynamic routes, which would be fantastic when hitting an API response and dynamically setting necessary "dynamic" parts of the URL:

let someCallback = (response) => {
  router.navigate('/dashboard/' + response.id);
};

Of course, these router.navigate calls will fire the change event that we previously discussed in this tutorial.

When routes change

When we invoke the router.navigate method, something else special happens elsewhere in the internal state of the router. An extremely handy change event.

We've already learned that we can glob URLs to allow for some form of global-child-state route based callbacks, but for an entirely global solution to hooking into route change events. Simply pass in the change property to our instance Object:

let router = new kendo.Router({
  change(e) {
    console.log(e);
  }
});

This API gives us a nice object back through the e argument. We'll mostly want to use the e.url and e.params properties to write business logic around route changes. This is also a great place to run any event tracking or fire events to other parts of your application.

If a specific change occurs that might not be valid, we can also call e.preventDefault() to change the route back to the previous one, however you may wish to use something more explicit with the router.navigate API.

Data Management with Routes

Managing data can be done in a vast amount of different ways on the front-end, which allows us to store different states in JavaScript objects. This can be done through a JSON response or even a query string. A query string is a key value pair that we append to the URL in able to store some data, for whatever purpose.

The Kendo router supports query string parameters straight out-of-the-box. We can start storing data that maps across to JavaScript objects automatically for us using key/value pairs.

Let's assume that we've navigated to mydomain.com/#/about?user=Todd. In our change callback on the router instance, we'll be handed a full object that includes things such as e.url and e.params properties. The e.params property is the one we're interested in, and it'll give us something like this:

{
  user: 'Todd'
}

In our route specific definition, however, we aren't given the entire object that e gives us in the change callback. We're just given the params as an object. Let's take a look what it'd give us:

router.route('/about', (params) => {
  // RESULT: { user: 'Todd' }
  console.log(params);
});

We aren't limited to just one query string parameter either. We can chain multiple params. For instance, the URL mydomain.com/about?user=Todd&age=25 would give us key/value pairs that look like this:

{
  user: 'Todd',
  age: '25'
}

The only thing to note here is that the value of age is '25' as a string primitive - it will not persist type coercion to keep a number as a Number.

URI Syntax Output

Like most SPA routers, the Kendo router will create the URL with a hash for us, which uses the window.location.hash property to emulate view changes without fully reloading the page, but still allowing for the URL to change. This is for legacy browsers that don't support the history.pushState API.

To use the pushState API, instruct the kendo.Router class to use it:

let router = new kendo.Router({
  pushState: true
});

When we visit the about route for example, the router will point the browser to mydomain.com/#/about, and similarly for a route called dashboard, we'll get mydomain.com/#/dashboard.

Route Management Abstraction

The router API is extremely powerful, though we can end up with unorganized routes dotted around our JavaScript file(s). I'd like to share a simple technique using JavaScript objects to help keep clean and maintain your route object definitions.

First, let's take some example routes:

let router = new kendo.Router();

router.route('/', () => {
  console.log('Index route!');
});
router.route('/about', () => {
  console.log('About route!');
});
router.route('/dashboard', () => {
  console.log('Dashboard route!');
});

router.start();

Here we instantiate the router, then continuously call router.route() and pass in individual string and function configurations. This approach is great, however to tighten the bolts in our routing logic, we can move these definitions away into a single object store for a single point of management.

Let's first create an object and move the routes to it:

let router = new kendo.Router();

let routes = {
  index() {

  },
  about() {

  },
  dashboard() {

  }
};

router.start();

This immediately becomes much clearer. We'll need to bind our routes Object to the actual router.route API call now, which we can do dynamically with a for loop:

let router = new kendo.Router();

let routes = {
  index() {

  },
  about() {

  },
  dashboard() {

  }
};

for (let route in routes) {
  router.route('/' + (route === 'index' ? '' : route), routes[route]);
}

router.start();

I've called the "index" route index in the routes Object, which gives it an identity unlike the anonymous '/' definition we would create for the "index" route.

The important piece to note here is that we don't actually assign the property name index across to the router.route call, there's a ternary operator that cleverly detects the index property name and simply binds the empty '/' we're used to. It's also a lot nicer in the sense that we no longer need to define forward slashes before each route!

Now you can manage your routes in a single object, which works fantastic with ES2015 imports and exports, allowing you to break your code down into multiple modules and files and exporting a single routing object if necessary.

We can now break out our router functionality like we're used to inside each routes property function:

let router = new kendo.Router();

let routes = {
  index() {
    console.log('Index route!');
  },
  about() {
    console.log('About route!');
  },
  dashboard() {
    console.log('Dashboard route!');
  }
};

for (let route in routes) {
  router.route('/' + (route === 'index' ? '' : route), routes[route]);
}

router.start();

Conclusion

Routing is a very powerful and important aspect of building single page applications, and the pain of route management and creating user centric routing can be mitigated using the Kendo UI router. The router is open source for you to try out, grab a copy here and start routing!

Header image courtesy of Benson Kua


todd-motto
About the Author

Todd Motto

Todd Motto (@toddmotto) is a Google Developer Expert from England, UK. He's taught millions of developers world-wide through his blogs, videos, conferences and workshops. He focuses on teaching Angular and JavaScript courses over on Ultimate Courses.

Comments

Comments are disabled in preview mode.