Routing and resources

Routing is one of the most important parts of a web application. We already have our application deployed in http://localhost:9000, but it's time to start defining names and addresses for our resources. First, we need to define what a resource is. Conceptually, a resource is every related data belonging to one single object or element. For example, a person resource can have fields such as name, address, birthday, and more. So, expanding this, a resource can be a list of persons too. We will talk deeply about how your resources should be named, organized, and called, but at this moment, you just need to know the basics. In a web application, every resource has its own address. Let's look at an example:

We have an address book with some contacts:

  • http://localhost:9000/ is the server URL, the base of all resources, the father path. Generally related to the home/welcome page, or first you define the user's view. The page has a button to see all our contacts.
  • http://locahost:9000/persons is a URL related to the person list resource. Here, we will show a list of persons of our address book. You are able to pick one to see its contact details.
  • http://locahost:9000/persons/p001 will refer to the person with the p001 ID. Here, we will be able to see its details. If you give this URL to another user, they should be able to see the same data as you, because this URL belongs to one single contact—the p001.
  • http://locahost:9000/persons?search=p001 is a little different. Imagine that our contact list is composed of more than 500 people. Don't you think it could be easier for the user search them by ID, name, or the most generic parameter? Here, we are using a query-param to express our search criteria; of course, we're still working with our friend p001.

Now, let's configure our application to be ready for routing.

At this point, we have already created some components in our application. If not, don't worry, we will have enough time to practice that in the last part of this chapter.

Now, let's add some code to our app.js file. Remember that this file should be located in the base src folder of our application, since now it will represent our base route for all the applications:

export class App {
configureRouter(config, router) {
this.router = router;
config.title = 'Aurelia';
config.map([
{ route: ['', 'home'], name: 'home', moduleId: 'home/index' },
{ route: 'users', name: 'users', moduleId: 'users/index', nav: true, title: 'Users' },
{ route: 'users/:id/detail', name: 'userDetail', moduleId: 'users/detail' },
{ route: 'files/*path', name: 'files', moduleId: 'files/index', nav: false, title: 'Files', href:'#files' }
]);
}
}

Let's analyze the properties and methods used to define our routes:

  • configureRouter(config, router) is a reserved method that the framework will evaluate in the base view-model when the application starts. The parameters are referencing to the Router, RouterConfiguration from aurelia-router package. If they are not provided, the framework will inject them automatically.
  • this.router = router is a reference to the router element, just to allow us to access this from the view layer (app.html), allowing us to build navigation menus dynamically.
  • config.title refers to our application title displayed in the browser window. Technically, it's applied to the <title> element in the <head> of the HTML document.
  • config.map() adds route(s) to the router. Although only route, name, moduleId, href, and nav were shown earlier, there are other properties that can be included in a route. The interface name for a route is RouteConfig. You can also use config.mapRoute() to add a single route.
  • route is the pattern to match against the incoming URL fragments. It can be a string or array of strings. The route can contain parameterized routes or wildcards as well.

Now, let's analyze the routes we've created:

  • In the first route element, the first flag, route, is making reference to the base path ('') and the home path. If we directly access http//:localhost:9000/ or http//:localhost:9000/home, the application will display the same page. The name flag is the URL identifier to call directly from one link or href element. Finally, we need to reference which file we are referencing with the route; in this case, the component is located in home/index and will be represented inside the moduleId flag.
  • The second URL is referencing to the users resource, but it has some variations. The nav flag can be a Boolean or number property. When set to true, the route will be included in the router's navigation model. When specified as a number, the value will be used in sorting the routes; this makes it easier to create a dynamic menu or similar elements. Finally, the title flag will show the page title appended to the page title in the browser window.
  • The third is a little different. We can see a weird param in the middle of the route, the :id. This means that this part of the URL signature will be dynamic, do you remember our person p001? The :id parameter will be used to represent the p001 code and make the URL unique for this resource. Also, in the view-model file, we will be able to consume that parameter and retrieve some data related to it.
  • Lastly, we are seeing 'files/*path'. Wildcard routes are used to match the rest of a path. The href flag is a conditionally optional property. If it is not defined, route is used. If route has segments, href is required as in the case of files, because the router does not know how to fill out the parameterized portions of the pattern.

There can be some situations where you will need some extra features to deal with them.

For example, case-sensitive routes; Aurelia has that problem solved too:

config.map([
          { route: ['', 'home'], name: 'home',  moduleId: 'home/index' },
          { route: 'users',      name: 'users', moduleId: 'users/index', nav: true, title: 'Users', caseSensitive: true }
]);

The caseSensitive flag will be used in these cases.

Another situation, very common, can be the unknown routes; Aurelia has a nice way to deal with it:

config.map([
          { route: ['', 'home'], name: 'home',  moduleId: 'home/index' },
          { route: 'users',      name: 'users', moduleId: 'users/index', nav: true, title: 'Users' }
        ]);

config.mapUnknownRoutes('not-found');

The config.mapUnknownRoutes() method will make a reference to the 'not-found' component module. Another way is representing it as a function:

const handleUnknownRoutes = (instruction) => {
      return { route: 'not-found', moduleId: 'not-found' };
}

config.mapUnknownRoutes(handleUnknownRoutes);

Other common scenario could be redirected routes. This is very simple—you just need to add the redirect flag and specify the reference to the module you want to show:

config.map([
      { route: '', redirect: 'home' },
      { route: 'home', name: 'home', moduleId: 'home/index' }
]);

At this point, we know how to configure the routing at the view-model level, but what about the view? Don't worry, this will be our next topic.

All this configuration was performed in the app.js file, so now we need to go to our app.html file. You must consider some things before adding the routing property to your template.

Commonly, most web applications use a base layout. This can be composed by the header, a lateral menu, and the view content. That being said, the only element that should be refreshed and reloaded with the router is the view content; the header and the menu will always be the same for the entire application, so we need to define our router element inside that container; let's look at the code:

<template>
<div class="header">
<header-component></header-component>
</div>
<div class="menu">
<menu-component></menu-component>
</div>
<div class="main-content">
<router-view></router-view>
</div>
</template>

The <router-view></router-view> is the HTML flag that Aurelia router will use to render the components we've configured as routes. Graphically, the representation is as follows:

At this point, everything is okay. However, this is a very basic approach; let's explore some advanced way to make our layout more flexible and configurable. We know that the router-view element defined in HTML is always associated with one or more views referenced in a router configuration method defined in its parent view's view model.

To specify a layout on the router-view HTML element, we use the following attributes:

  • layout-view: Specifies the layout view to use through the filename (with path)
  • layout-model: Specifies the model parameter to pass to the activate function of view model
  • layout-view-model: Specifies the moduleId to use with the layout view

To explain that, we will implement a custom layout page totally decoupled from our app.html file:

<template>
      <div>
        <router-view layout-view="layout.html"></router-view>
      </div>
</template>

We are referencing a file called layout.html. This file will contain our basic layout distribution:

<template>
      <div class="left-content">
        <slot name="left-content"></slot>
      </div>
      <div class="right-content">
        <slot name="right-content"></slot>
      </div>
</template>

Also, note the <slot> tag. This is a mechanism to associate parts of the layout to part of some view referencing its name; in this case, let's create a home component with custom layout:

<template>
      <div slot="left-content">
        <home-header></home-header> 
</div>
<div slot="right-content">
<home-menu></home-menu>
</div>
</template>

Any content outside of the slot declared won't be rendered. We just have one more task to do—configure the router:

config.map([
          { route: '', name: 'home', moduleId: 'home' }
]);

We just need to declare the route and reference it to the home module. The layout will read the slot tags defined inside and will render to the main template. In this way we can customize the layout according to the route we are accessing the application, one use case could have custom menu options while displaying some routes.

There is one more thing we need to cover to have our router ready for work—the fallback route. Imagine that your application is based on roles. If the user is not allowed to access some resource, he should be redirected to the previous location. What if there's no previous location? The fallback route comes to the rescue!

Let the code show the magic:

export class App {
      configureRouter(config, router) {
        this.router = router;
        config.title = 'Example';
        config.map([
          { route: ['', 'home'], name: 'home',  moduleId: 'home/index' },
          { route: 'users',      name: 'users', moduleId: 'users/index', nav: true, title: 'Users' }
        ]);

        config.fallbackRoute('users');
}

Now you know the most important characteristics about Aurelia router and how to configure it to improve your application. We are almost ready to start creating components to our FIFA WC App. In the last chapter, we learned about testing, TDD, and debugging. Now, it's time to apply the learned concepts and test our components. Let's code!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset