Router Tutorial

Introduction

Aurelia comes with a powerful fully-featured router without the need to install any additional dependencies. If you are new to Aurelia, we recommend visiting the Getting Started section first to familiarise you with the framework.

You are here because you want to familiarize yourself with the router, so we'll make this quick. At the end of this tutorial, you will be familiar with all the concepts and APIs you need to add routing into your Aurelia applications.

While building our recipe application, we will cover the following:

  • How to create and configure routes

  • Navigating with load in your views as well as view-models

  • Styling active links

  • Programmatically navigating using router APIs

  • Working with route parameters

A working demo and code for the following tutorial can also be found here.

Installation

To do this tutorial, you'll need a working Aurelia application. We highly recommend following the Quick Start guide to scaffold an application. However, for this tutorial, we have a starter Aurelia 2 application ready to go that we recommend. It will allow you to follow along and live code.

As you will see, it's a boring application with no routing or anything exciting. All it says is Recipes (hardly a recipe application).

Add routes and a viewport

The viewport is where our loaded routes are dynamically loaded into. When we click on a route, the <au-viewport> element will be populated with the requested component.

Open my-app.html and add in the <au-viewport> and some navigation links.

my-app.html
<div>
  <h1>Recipes</h1>

  <nav>
    <a load="/">Home</a>&nbsp;&nbsp;
    <a load="/recipes">Recipes</a>
  </nav>

  <au-viewport></au-viewport>
</div>

This won't do anything just yet because we haven't enabled routing or created any routes.

Enable routing

Let's go into main.ts and configure Aurelia to use routing. Import the RouterConfiguration object and pass it to Aurelia's register method.

We also configure the router to use push-state routing instead of the hash-based router. This gives us cleaner URLs.

main.ts
import Aurelia from 'aurelia';
import { RouterConfiguration } from '@aurelia/router';
import { MyApp } from './my-app';

Aurelia
  .register(RouterConfiguration.customize({ useUrlFragmentHash: false }))
  .app(MyApp)
  .start();

Create some routes

Now that we have a viewport and the router enabled let's create some routes. We will start by adding our routes to our root my-app.ts component.

export class MyApp {
  static routes = [
    {
      path: '',
      component: '',
      title: 'Home',
    },
    {
      path: 'recipes',
      component: '',
      title: 'Recipes',
    },
    {
      path: 'recipes/:recipeId',
      component: '',
      title: 'Recipe',
    },
  ];
}

The important thing to note here is that we are not specifying a component to load for these routes. We will do that shortly, but the routing structure is what we are creating here.

The first route has an empty path , which means it's a default route (the router will load this first if no other route is requested). The second route recipes tells the router when the user visits /recipes to load our recipes component. Lastly, the recipes/:recipeId route has a route parameter that allows us to load specific recipes.

Create some components

We now need to create three components for our routes: the homepage, recipes page, and recipe detail page.

Unlike other frameworks and libraries, Aurelia works on the premise of a view-and-view model. If you familiarize yourself with the Getting Started section, you would already be familiar with these concepts.

Let's create the homepage first:

home-page.ts
export class HomePage {
}
home-page.html
<p>Welcome to flavortown. Aurelia Recipes is the only recipe application you will need to manage your recipes.</p>

Let's create the recipes page:

recipes-page.ts
export class RecipesPage {
}
recipes-page.html
<p>Your recipes. In one place.</p>

Let's create the recipe detail page:

recipe-detail.ts
export class RecipeDetail {
}
recipe-detail.html
<p>This is a recipe.</p>

Lastly, let's import our components in my-app.ts for the routes to load them.

my-app.ts
import { HomePage } from './home-page';
import { RecipesPage } from './recipes-page';
import { RecipeDetail } from './recipe-detail';

export class MyApp {
  static routes = [
    {
      path: '',
      component: HomePage,
      title: 'Home',
    },
    {
      path: '/recipes',
      component: RecipesPage,
      title: 'Recipes',
    },
    {
      path: '/recipes/:recipeId',
      component: RecipeDetail,
      title: 'Recipe',
    },
  ];
}

Listing the recipes

In a non-tutorial application, you would have your API and server providing this data. But, for our tutorial will use a public recipe API instead. We could use mock data, but it would be a lot of data for a recipe application.

The MealDB is a fantastic free meal API that can give us recipes. We will use the Aurelia Fetch Client to get this data, which wraps up the native Fetch API.

In recipes-page.ts add the following to the component view model:

recipes-page.ts
import { HttpClient } from '@aurelia/fetch-client';

export class RecipesPage {
    private http: HttpClient = new HttpClient();
    private recipes = [];

    async bound() {
        const response = await this.http.fetch(`https://www.themealdb.com/api/json/v1/1/search.php?f=b`);

        const result = await response.json();

        this.recipes = result.meals;
    }
}

We've loaded the recipes. Now it's time to display them. Open recipes-page.html and add in the following:

<ul>
    <li repeat.for="recipe of recipes"><a load="/recipes/${recipe.idMeal}">${recipe.strMeal}</a></li>
</ul>

We use Aurelia's repeat.for functionality to loop over the recipes. But take notice of the link with load attribute. Aurelia Router sees this and knows this is a route. We are providing the recipe detail route with the ID of the recipe.

It's not pretty, but we now have a list of recipes.

Add in support for 404 fallback

Sometimes a user might attempt to visit a recipe or page that doesn't exist. You might want to redirect the user or display a 404 page in those situations.

Let's create another component and call it fourohfour-page

fourohfour-page.ts
export class 404Page {
}
fourohfour-page.html
<h1>Oops!</h1>
<p>Sorry, that link doesn't exist.</p>

We then specify on our <au-viewport> what our fallback is. We need to import this 404 component to use it.

my-app.html
<import from="./fourohfour-page"></import>

<div>
  <h1>Recipes</h1>

  <nav>
    <a load="/">Home</a>&nbsp;&nbsp;&nbsp;&nbsp;
    <a load="/recipes">Recipes</a>
  </nav>
  <au-viewport fallback="fourohfour-page"></au-viewport>
</div

Reading route parameters

When we created our routes in my-app.ts you might recall we created a recipe detail route which had a recipeId parameter. Now, we are going to modify our recipe-detail component to read this value from the URL and load the content.

recipe-detail.ts
import { resolve } from 'aurelia';
import { HttpClient } from '@aurelia/fetch-client';
import { IRouter } from '@aurelia/router';

export class RecipeDetail {
  private http: HttpClient = new HttpClient();
  private recipe;

  readonly router: IRouter = resolve(IRouter);

  async canLoad(parameters) {
    if (parameters?.recipeId) {
      const loadRecipe = await this.loadRecipe(parameters.recipeId);

      if (loadRecipe) {
        this.recipe = loadRecipe;

        return true;
      } else {
        this.router.load(`/recipes`);
      }
    }
  }

  async loadRecipe(recipeId) {
    const request = await this.http.fetch(
      `https://www.themealdb.com/api/json/v1/1/lookup.php?i=${recipeId}`
    );
    const response = await request.json();

    return response.meals ? response.meals[0] : false;
  }
}

There is a little more to unpack here. We inject the router because we will programmatically redirect away if the user attempts to view a non-existent recipe ID. We use the canLoad method because loading our recipe view relies on existing recipes. If the recipe can't be found using the API, we redirect to the recipes page programmatically using the router.load method.

Inside recipe-detail.html we'll render our recipe:

recipe-detail.html
<h1>${recipe.strMeal}</h1>
<img src.bind="recipe.strMealThumb" />
<p textcontent.bind="recipe.strInstructions"></p>

The API we use returns ingredients on a line-by-line basis, so we've omitted those from this example. Now, you should be able to run your application and click on recipes to view them. A headline, image and instructions should now be visible.

As an additional step, you could add those in yourself.

The router automatically adds a active class to all route links when they are active. Because our routes all live in my-app We will edit my-app.css We don't even have to import it (Aurelia does that automatically for us).

my-app.css
.active {
  font-weight: bold;
}

a {
  color: #000;
  text-decoration: none;
}

The active class is now bold when active. We also do some quick styling tweaks to the route links to remove the underline and make them black so we can see the bold stand out more.

That's it

You just built a recipe application. It doesn't allow you to create recipes, and it's not very pretty, but it works. To see the application in action, a working demo of the above code can be seen here (or below).

Last updated