Introduction to Aurelia

What is Aurelia?

For a recent project, my JBS project team was tasked with updating a web front-end implementation. We looked into various front-end libraries in order to provide the best solution for the customer.  AngularJS, being the de facto front runner based on previous projects, was mid development on 2.0 which, to be frank, looked to be very complicated and verbose. Ember was also evolving in a positive direction so we looked at that too, but we shied away from it because while Controller objects are slowly going away and components are taking center stage, it still seems to be very hard to avoid using controllers completely even as late as the 2.0 release. Then we stumbled across Aurelia, this simple framework that captivated us quickly.

Aurelia is yet another Javascript MV* framework that is, as of this writing, on the cusp of a beta release. It touts many of the same features as Angular 2.0, and it well should, since its creator was involved for a little while in the development of Angular 2.0. What’s refreshing about Aurelia is that it’s incredibly simple, non intrusive and has intuitive syntax that is easy to read both to the development team and perhaps down the road some fresh faced developer looking at the code for the first time.

In the spirit of the season, we’re going to demonstrate some of the powerful features of Aurelia by building a small application to track upcoming Black Friday deals. To get started we’ll need an index.html page that will house our application.

index.html

<html>
  <head>
    <title>What's the Deal.io?</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
  </head>
  <body aurelia-app>
    <script src="jspm_packages/system.js"></script>
    <script src="config.js"></script>
    <script>
      System.import('aurelia-bootstrapper');
    </script>
  </body>
</html>

This is a very standard looking page. Other than the script imports for jspm, which we’re using as our module loader, the only real part of the page that is important is the aurelia-app attribute that we added to the body tag. This is how we tell Aurelia the element of the page that will host our application’s content. By default, Aurelia will look for a root component in a file named app.js. So the next step is to create that file so Aurelia can find it.

A Quick note about tooling

All of my examples you will see in this article are written in ES6 using Babel as my transpiler du jour. We’ll also be using JSPM as our module loader. None of these are required in order to use Aurelia and I’m simply using them here for demonstrative ease of use. Don’t like ES6? No worries, other than some changes in the syntax you can use ES5 without issues. Like Typescript? go for it! Coffeescript? you’re covered. You can also use any transpiler and/or module loader you desire.

Keeping it simple

The biggest draw to Aurelia for me is its simplicity. At the most basic level, every component you build has a pair of basic parts: A viewmodel, which is the behavior of the component and a template, or view, that contains the markup and databinding to the underlying properties and methods of the viewmodel. Aurelia follows an MVVM pattern and when you use it you can definitely sense some influences from WPF, the Microsoft framework for desktop development that was conducive to MVVM development. Let’s create our app viewmodel first:

app.js

export class App {
    constructor() {
        this.deals = [];
        this.clearInputs();
    }

    clearInputs() {
        this.store = '';
        this.item = '';
        this.price = '';
    }

    get currentDeals() {
        return `There are currently ${this.deals.length} deals!`;
    }

    addDeal() {
        this.deals.push({
            "store": this.store,
            "item": this.item,
            "price": this.price
        });

        this.clearInputs();
    }
}

The most obvious and glaring observation would be that this is simply a plain old javascript class with properties and methods. There is nothing here to indicate I’m even using a framework so far. We have an array of deals and a few properties that correspond to key elements of a deal (the store name, the name of the item on sale, and the deal price). addDeal() is responsible for adding a new deal object to our deals array using the properties that we are going to bind to inputs in our view, and clearInputs() simply clears out the deal properties. By default, when Aurelia resolves the template to render for an activate viewmodel, it looks for an html file matching the name of the active viewmodel file and in the same directory as well. In this case, since Aurelia is loading app.js, we need to make an app.html file in the same folder where app.js resides:

app.html

<template>
	<div style="width: 500px;">
		<h2>Deal Tracker</h2>
		<div>
			<div>${currentDeals}</div>
			<table class="table table-striped table-bordered" if.bind="deals.length > 0">
				<thead>
					<tr>
						<th>Store</th>
						<th>Item</th>
						<th>Price</th>
					</tr>
				</thead>
				<tbody>
					<tr repeat.for="deal of deals">
						<td>${deal.store}</td>
						<td>${deal.item}</td>
						<td>${deal.price}</td>
					</tr>
				</tbody>
			</table>
		</div>
		<div class="panel panel-default" style="width: 250px;">
			<div class="panel-heading">Add A Deal</div>
			<div class="panel-body">
				<form>
					<div class="form-group">
						<label>Store</label>
						<input class="form-control" value.bind="store">
					</div>
					<div class="form-group">
						<label>Item</label>
						<input class="form-control" value.bind="item">
					</div>
					<div class="form-group">
						<label>Price</label>
						<input class="form-control" value.bind="price">
					</div>
					<button class="btn btn-primary" click.trigger="addDeal()">Add Deal</button>
				</form>
			</div>
		</div>
	</div>
</template>

The first thing to point out here is that we wrap our content inside of a <template> tag. Aurelia is built on the concepts laid out in the Web Components specification, and templates are part of that. When the view is rendered, Aurelia will not render the <template> tag. Also, notice that we’re binding to properties in markup using ES6 string interpolation syntax (e.g. ${currentDeals}). This is a nice touch because the syntax is uniform from template to viewmodel.

Our table of existing deals illustrates a couple of concepts. First, we only want to show the table if there is at least one deal, so we use the if custom attribute and bind it to the condition deals.length > 0. In the body, we’re looping over the deals in the viewmodel using repeat.for. The syntax inside the repeat just scopes the current deal to a reference named deal, which is how we access the properties of the deal object in the cells of the table row.

Finally, in the form at the bottom we’re binding the value of our inputs to properties in the viewmodel by using .bind. In Aurelia, there are three types of binding that can take place: two-way, one-way to template and one-time (where the value is only bound to the view once during activation). The “bind” keyword will treat any binding as one-way unless the binding is used in a form element, in which case two-way binding is the default. Want to be explicit? you can always use .two-way, .one-way or .one-time instead

Dependency injection

Eventually we will want to split out the addition of new deals and the display of existing deals into separate screens. Before we can do that, we need to encapsulate the functionality around deal management to a central location so that we can interact with our deals from multiple places. Aurelia’s dependency injection functionality makes it very straightforward. We’ll start by moving all of our deal logic from app.js into a new file called deal-manager.js:

deal-manager.js

export class DealManager {
    constructor() {
        this.deals = [];
    }

    get currentDeals() {
        return `There are currently ${this.deals.length} deals!`;
    }

    addDeal(store, item, price) {
        this.deals.push({
            "store": store,
            "item": item,
            "price": price
        });
    }
}

No lengthly explanation here, we just took the deals array and the methods that reference it and moved them to a new class. Now let’s look at our refactored app.js file:

app.js

import {inject} from 'aurelia-framework';
import {DealManager} from './deal-manager';

@inject(DealManager)
export class App {
    constructor(dealManager) {
        this.dealManager = dealManager;
        this.clearInputs();
    }

    clearInputs() {
        this.store = '';
        this.item = '';
        this.price = '';
    }

    addDeal() {
        this.dealManager.addDeal(this.store, this.item, this.price);
        this.clearInputs();
    }
}

Aurelia has a collection of decorators, which are basically extensions that can be applied to our viewmodel classes and properties. the @inject decorator is how we tell Aurelia what components are going to be injected into our class and the order they will be injected. Since we’re going to want to inject our new DealManager class, we import that from the deal-manager file and then use @inject(DealManager) to tell Aurelia to resolve an instance of it. The final step is to make the resolved instance of DealManager a parameter to the constructor of our App class. This allows us to set the instance to a local class property or otherwise use the resolved instance.

You can specify any number of Dependency objects to the @inject decorator separated by a comma. By default, all injected components are treated as single instances, and for our case that’s exactly what we want, but if you needed to inject a new DealManager into the viewmodel every time it was activated, you can do so by using another decorator, @transient which would be applied to the deal-manager.js file just above the class declaration for DealManager:

deal-manager.js

import {transient} from 'aurelia-framework'

@transient()
export class DealManager {
...
}

Adding routing

Now that the management of deals has been centralized, we can separate the form for adding new deals to a separate page in our app. Aurelia supports a variety of routing scenarios and what I find nice about it is how simple it is to setup. We’re going to shoot for the following routes:

Since our app.js file will now be responsible for handling our main routing, we’ll start by renaming the current app.js file to deal.js and the existing app.html will be renamed to deal.html. Then we need to create deals.js and deals.html files to show the existing deals table. those files will end up looking as follows:

deals.js

import {inject} from 'aurelia-framework';
import {DealManager} from './deal-manager';


@inject(DealManager)
export class Deals {
    constructor(dealManager) {
        this.dealManager = dealManager;
    }
}

Basically we only need the dealManager reference here since we’re only going to keep the table, which references it’s data out of the dealsManager instance. Finally, we cut the table markup out of deal.html and paste it into deals.html so that we get:

deals.html

<template>
	<div style="width: 500px;">
		<div>
			<div>${dealManager.currentDeals}</div>
			<table class="table table-striped table-bordered" 
                               if.bind="dealManager.deals.length > 0">
				<thead>
					<tr>
						<th>Store</th>
						<th>Item</th>
						<th>Price</th>
					</tr>
				</thead>
				<tbody>
					<tr repeat.for="deal of dealManager.deals">
						<td>${deal.store}</td>
						<td>${deal.item}</td>
						<td>${deal.price}</td>
					</tr>
				</tbody>
			</table>
		</div>
	</div>
</template>

Now that we have the modules needed for our routes, we’ll create a new app.js that will define our route map for the application as a whole:

app.js

export class App {
  configureRouter(config, router){
    config.title = 'What\'s the Deal.io?';
    config.map([
      { route: ['','deals'], name: 'deals', moduleId: 'deals', title: 'Deals', nav: true },
      { route: 'deal', name: 'create', moduleId: 'deal', title: 'New Deal', nav: true }
    ]);

    this.router = router;
  }
}

The configureRouter is where we can set up our routes. We’re setting a local router property on our viewmodel that will expose the router to the view we’re going to define, but the critical part of the method is where we set the route map, via the map() function that takes in an array of route objects that have the following properties:

  • route: Can be one, or an array, of route patterns to match. In our example, we want users that would go to http://whatsthedeal.io and those going to http://whatsthedeal.io/#/deals to end up seeing the same view. The values here can be static routes (like we currently have), parameterized routes (e.g. ‘deal/:id’ if we want to add a route for editing a deal), or wildcard routes (e.g. ‘search*’). In parameterized and wildcard routes, the parameters and globbed portion of the path are passed into the activate() function of the viewmodel (if defined). More on activate() below when we talk about route activation lifecycles.
  • name: The name of the route, this is used by the router.navigateToRoute() function to indicate which route to go to if we’re transitioning using code. It’s also used to generate the url for the route in markup if we’re using the route-href custom attribute.
  • moduleId: The most important property to specify, this tells Aurelia which viewmodel is going to be loaded when the route template is matched by the pattern(s) specified in the route property. The value should be the relative path to the module, so for our first route, the ‘deals’ moduleId tells the system to look for deals.js in the same directory that app.js resides in. You can specify the same moduleId for multiple routes in the same route map if you want multiple routes to be handled by the same viewmodel.
  • title: This is an optional property that is used to set the document title when the route is active.
  • nav: This is an optional property and is used by the router to decide whether the route will appear in the router.navigation collection. This collection is a list of navigable routes and allows views to create navigation menus from the router itself. If the property is truthy, the route is considered navigable.

Now let’s take a look at what the view for our app will look like with the routing markup:

app.html

<template>
	<nav class="navbar navbar-default">
  		<div class="container-fluid">
			<div class="navbar-header">
      			<a class="navbar-brand" href="#">${router.title}</a>
    		</div>
    		
			<div class="collapse navbar-collapse">
      			<ul class="nav navbar-nav">
        			<li repeat.for="route of router.navigation" class="${route.isActive ? 'active' : ''}">
          				<a href.bind="route.href">${route.title}</a>
					</li>
				</ul>
			</div>
		</div>
	</nav>
	
	<!--route views are swapped in here-->
	<router-view/>
</template>

Since our app now has routing the app.html file becomes the application’s shell. We’ve removed any notion of the deal logic from earlier and replaced it with a navigation bar that will allow us to transition to the different routes in the app. The router object from the app.js file is self describing, so we can use the router.navigation collection to get a list of all the routes that have a nav property that’s truthy. The router also has a property called isNavigating that will be true if the route transition is taking place. This property makes it easy to add in a visual indicator to show users that the route is loading should there be service calls or other long running work taking place when loading the route being navigated to.

Finally, the <router-view> element tells Aurelia where to place the view content for the currently active route. So to recap, creating routes for a module requires defining the configureRouter method in the viewmodel, and then using a <router-view> element in the view to indicate where the route content will be placed. Since any module can use this pattern, it makes nested routes simple.

Activation lifecycle

You may want to add some rules around whether or not a particular route can be accessed based on some criteria like user permissions or the state of an object. You may also just want to call out to a service to fetch data when you navigate to a route. Aurelia looks for a set of functions to be defined on any viewmodel being activated by the router and calls those functions if they’re found:

  • canActivate(): define this function if you want to validate whether the router will allow navigation to this module. you can return a boolean, or a promise that resolves to a boolean if the decision requires waiting on the decision.
  • activate(): define this function if you want to do something just before the router renders the view. If you have to load data asynchronously and want the router to wait for the data to be loaded before it renders the view, return a promise.
  • canDeactivate(): define this function in your viewModel if you need to validate whether it’s OK for the user to navigate away from the page. return a boolean, or a promise that resolves to a boolean if you want the router to wait for the decision to be made. This is a perfect place to handle the common use case of notifying a user that they will lose changes to a form if they navigate away.
  • deactivate(): define this function in your viewmodel if you want to do something just before the router navigates away. you can return a promise if you want the router to wait before navigating away.

Conclusion

As you can see creating modules in Aurelia is a simple two step process that is built on using simple ES6 classes that are empowered using conventions like lifecycle hooks and decorators to tell the framework when and how to do the heavy lifting you need. In this post we’ve covered creating a very basic master/detail application and then adding navigation and dependency injection to the mix. In the next post we’ll talk about how to customize the initialization of your Aurelia application, including:

  • custom application bootstrapping
  • custom elements
  • value converters
  • global resources

You can git clone https://github.com/dmarini78/aurelia-intro and do a git checkout part-1 to peruse all the code related to this post locally, or you can see it all online at this location. To learn more about Aurelia on your own, visit aurelia.io

To get started on your custom software solution, call 1-888-421-1155.