Introduction to Aurelia – Part 2

Just recently, the Aurelia framework Beta-1 release was rolled out. While we’re excited to play with it, since we started this introductory series on the alpha release, we will continue

Introduction

In my last post, I created a super small master/detail application in Aurelia. In it, we demonstrated how Aurelia, with it’s viewmodel/view pairs, can achieve very powerful results quickly and easily. In today’s post, I want to expand on some other features of Aurelia that allow you to create even more powerful applications. To be fair, there are a lot more features in Aurelia than I can possibly give fair coverage to, so this series is set up to focus on those features that I think most developers would use most often. You can get the code that we’re going to be modifying by doing a git clone https://github.com/dmarini78/aurelia-intro and then doing git checkout part-1 to roll back to where we left off in the last post. Let’s get started!

Before We Begin

Just recently, the Aurelia framework Beta-1 release was rolled out. While we’re excited to play with it, since we started this introductory series on the alpha release, we will continue in that vein for this post as well. From my understanding, there aren’t many breaking changes unless you are using more low level components the framework exposes. Perhaps upgrading from alpha to beta-1 will be a future post.

Providing Reusability with CustomElement

Our application currently has two routes, one that shows a list of the deals we’ve entered into the system, and another to add a new deal to that list. What we don’t have is a way to edit an existing deal if we make a typo. This is a rather glaring omission from our site, so we’ll remedy that now. The edit screen really has no different fields than the add screen, although the labeling might be different and it may require a slight change to the submission logic. We’re going to turn our deal module into a CustomElement. Just like Angular and Ember, CustomElements allow you to encapsulate logic and markup into a custom html element that can be used in various other templates across the application. In our case, since we will want to show our “deal.html” template for both add and edit functionality, it will be nice to reference that markup as <deal> rather than having to duplicate the full markup in multiple places.  Converting our deal module to a Custom Element follows the same pattern as building any other module in Aurelia, making the process super easy. Let’s start with the deal.js file:

deal.js

import {inject, customElement, bindable} from 'aurelia-framework';
import {Router} from 'aurelia-router';
import {DealManager} from './deal-manager';
 
@inject(DealManager, Router)
@customElement('deal')
export class Deal {
    @bindable index;
     
    constructor(dealManager, router) {
        this.dealManager = dealManager;
        this.router = router;
    }
 
    clearInputs() {
        this.store = '';
        this.item = '';
        this.price = '';
    }
 
    bind() {
        if(this.index) {
            var dealToEdit = this.dealManager.deals[this.index];
            this.store = dealToEdit.store;
            this.item = dealToEdit.item;
            this.price = dealToEdit.price;
        }
    }
     
    saveDeal() {
        if(!this.index) { //no index, so we're adding
            this
              .dealManager
              .addDeal(this.store, this.item, this.price);
             
            this.clearInputs();
        }
        else { //edit the deal at index
            this
              .dealManager
              .editDeal(this.index, this.store, this.item, this.price);
        }
         
        //go back to the deals listing screen
        this.router.navigateToRoute('deals');
    }
}

You’ll notice we’re now importing a couple of new modules from the framework: customElement and bindable. Together, these decorators provide the building blocks to creating custom elements:

The customElement decorator is an explicit way to tell Aurelia that this module is going to be used as a custom element and that we will be naming our element <deal/>. Aurelia’s conventions make this decorator unnecessary, as if we export our class as DealCustomElement it would achieve the same effect, the choice is yours.

The bindable decorator tells Aurelia which properties of our component will be able to be specified as attributes when declaring our element in a view. Our component needs to know if we’re targeting an existing index of the deals collection, so we declare our @bindable index property which will let us pass a static value like <deal index="1" /> or by using the .bind, .two-way, .one-way binding syntax we talked about in our last post to tie into a property from the calling module, as we will be doing in just a bit, with <deal index.bind="index" />.

We added a bind method to the class. When a custom element is being bound to data from the view, this method will be called (if defined) on the component. We will use this method to default our input boxes to the values of an existing deal if an index has been provided by the calling component. The presence of an index will indicate that we are editing a deal rather than adding a new deal. Note that we cannot do this logic in the constructor because the index value will not be defined until the bind point in the component’s lifecycle.

We made a few small refactorings as well. We renamed the addDeal() function to saveDeal() since it could be an add or an edit (depending on whether an index is present). We’re also importing the Router object from aurelia-router. We want to inject the current router into our component so that we can navigate back to the deals listing page whenever we save when adding or editing a deal. You can see this happening in the saveDeal() method via the navigateToRoute() method call. All we need to pass it is the route name from the router configuration and we’re good to go there.

Now let’s look at the modifications we need to make to our view in deal.html:

deal.html

<template>
    <div class="panel panel-default" style="width:250px;">
        <div class="panel-heading">${index ? 'Edit Deal' : '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="saveDeal()">Save</button>
            </form>
        </div>
    </div>
</template>

The only changes we had to make here were changing the text of the button from “Add Deal” to “Save” and changing the target function called when the button is clicked to saveDeal() instead of the addDeal() function, which no longer exists. Also, we have the panel heading text changing depending on whether we’re in edit or add mode.

Using the Custom Element

Now that we have our custom element defined, we should make use of it. We want to allow the user to edit and add new deals, but right now we only have a route for adding a new deal. We also have our new deal route pointing to our deal.js viewmodel, which is now a custom element, so we’ll want to create a new module to serve as our target for that route. Let’s modify the router first:

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-editor', title: 'New Deal', nav: true },
      { route: 'deal/:id', name: 'edit', moduleId: 'deal-editor', title: 'Edit Deal' }
    ]);
 
    this.router = router;
  }
}

We’ve changed our “create” route to use a new module called deal-editor.js, which we’ll define in a minute. More importantly, we added a new route for editing deals named, unsurprisingly, “edit”. Notice that the route is similar to the route template for “create” but has an additional /:id segment. This tells Aurelia that when it sees a route like http://whatsthedeal.io/#/deal/1 that it should take the url segment after deal/ and pass it to the deal-editor.js module as a parameter called id. We are going to use the same moduleId for both the “create” and “edit” routes since currently we don’t have any unique behavior or markup to distinguish them. Let’s look at what that module looks like now, starting with the viewmodel:

deal-editor.js

export class DealEditor {
    activate(path) {
        if(path && path.id) {
            this.index= path.id;
        }
    }
}

This is a very simple class with an activate() function defined. Remember from the last post that every module has a lifecycle that we can hook into at various points. The activate() function is called when Aurelia is finished navigating to our route, just before it starts the binding/rendering process. If the route has parameters defined on it, they will be accessible from the path argument passed into activate(). There will only be an id parameter on the path in the case of the edit route, so we simply check the path for an id, and set a local index property to the path.id parameter if it exists so we can use it in the view:

deal-editor.html

<template>
    <require from="./deal"></require>
    <deal index.bind="index">
</template>

Only two lines, granted we don’t have a very complex markup. This gives you an idea of how encapsulating the deal.js module reduces the amount of markup that can clutter multiple calling views if we didn’t make it a reusable element. the <require> tag tells Aurelia that we will be using our <deal> custom element in this view. If we want to use our <deal> element on other views, each of them will need to have the <require> statement at the top, but we’ll show how to get around this very shortly. Next, we simply declare our <deal> custom element and we bind the index property of the custom element to the index property of our deal-editor viewmodel, which effectively passes the deal identifier from the url through to the custom element when we go to the “edit” route. The value will be undefined for the “create” route, allowing our custom element to dictate it’s behavior accordingly in both scenarios.

We have one more change to make, and that is adding an “edit” button to each row in the deals.js module so that we can get to the “edit” route. Because we didn’t specify a nav property when defining the “edit” route, it will not appear in our nav menu of our application:

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>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    <tr repeat.for="deal of dealManager.deals">
                        <td>${deal.store}</td>
                        <td>${deal.item}</td>
                        <td>${deal.price}</td>
                        <td><a route-href="route: edit; params.bind: { id: $index}">Edit</a></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</template>

The only change we’ve made here is to add an additional table cell to each row with a link to the edit route for the current deal index. To achieve this, we use the route-href attribute of Aurelia. This attribute takes in multiple parameters separated by semicolon. The first parameter is the route name, which in our case will always be “edit”. Next, we bind the “params” property to an object containing the values to pass to the route. In the case of the “edit” route, we need to specify an id parameter, and we can use the $index reference property that is given to us by Aurelia when we’re iterating an array with the repeat.for attribute. Neat!

Text Is Money?

In our application, we want the price of a deal to be treated by the viewmodel as a float value and not a currency string so that if we need to do any kind of arithmetic on the value we don’t have to strip away the currency specific characters first. That doesn’t mean, however, that when the value is displayed on the view that we want to see 1499.99 instead of $1499.99. Aurelia provides a way for us to transform data bidirectionally between the viewmodel and view using Value Converters. If you’ve ever used Microsoft’s WPF or Silverlight frameworks to develop web or windows applications in the past, the notion of a value converter is very familiar to you. Value converters provide two translations, one for transforming a value flowing from viewmodel to view, and one for transforming data flowing from the view back to the viewmodel.

In our case, we don’t have anywhere that expects that a user will input a currency string, so we will only concern ourselves with converting a value from the viewmodel to the view. Our converter looks like this:

currency-converter.js

export class CurrencyValueConverter {
   toView(value) {
       return `$${parseFloat(value).toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,')}`;
   }
}

There are 3 steps to creating a ValueConverter in Aurelia:

  • The name of the exported class must ends with ValueConverter. When Aurelia is looking at the exported class, it will and strip off the trailing ValueConverter portion and camel case the remaining string, so our CurrencyValueConverter will be registered in Aurelia as currency.
  • The exported class should define a toView() function if logic is needed to transform a value from the viewmodel to the view. The function takes in a value parameter that represents the value being converted.
  • The exported class should define a fromView() function if logic is needed to transform a value from the view back to the viewmodel. The function takes in a value parameter that represents the value being converted. In our case, we don’t define this function because we don’t expect to use it.

In our deals view, we use the value converter like this:

deals.html

<template>
    <require from="./currency-converter"></require>
     
    <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>
                        <th></th>
                    </tr>
                </thead>
                <tbody>
                    <tr repeat.for="deal of dealManager.deals">
                        <td>${deal.store}</td>
                        <td>${deal.item}</td>
                        <td>${deal.price | currency}</td>
                        <td><a route-href="route: edit; params.bind: { id: $index}">Edit</a></td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</template>

As with our custom element above, we need to tell Aurelia that we plan to use the converter, and we do that by using the <require> tag, specifying the relative path to the .js file that exports the value converter. To invoke the converter, we simply pipe our value through it using the pipe | syntax: ${deal.price | currency}. If we wanted to, we could pass parameters to the value converters as well. To do this, we add the additional parameters to our definition of toView: toView(value, storeName, dealIndex)and when calling the converter from the view, separate the parameters by a colon :, so in our case: ${deal.price | currency:deal.store:$index}

Removing Unnecessary Ceremony

In cases where we plan to use a custom element or value converter on many pages of our application, it can quickly become troublesome to have to make the same <require> declaration on every view, and forgetting to add it will cause errors. Luckily, there is a way to tell Aurelia about the components we plan to make use of so that the declaration is no longer required at all. Remember that in our index.html file we use the aurelia-app attribute to tell Aurelia where on the page our application is going to live. Without any value, aurelia-app will let Aurelia configure itself using a predefined standard configuration, but we can tailor the configuration by specifying a value that tells Aurelia the name of a file that defines a configuration. Let’s do this now by simply changing the body tag in our index.html file from:

<body aurelia-app>

to:

<body aurelia-app="startup">

This tells Aurelia to look for it’s configuration in a file named startup.js. Aurelia will be looking for a configure() function to be exported in this file that contains instructions for how it will configure itself. When Aurelia finds this function, it will call it during the application’s bootstrap process. The body of our startup.js file looks as follows:

startup.js

import {LogManager} from 'aurelia-framework';
import {ConsoleAppender} from 'aurelia-logging-console';
 
LogManager.addAppender(new ConsoleAppender());
LogManager.setLevel(LogManager.logLevel.debug);
 
export function configure(aurelia) {
  aurelia.use
    .standardConfiguration()
    .globalResources('deal', 'currency-converter');
  
  aurelia.start().then(a => a.setRoot());
}

The configure() function is passed the Aurelia application as a parameter and using this we can modify a host of configuration options for our application from logging preferences to adding your own custom plugins. In the code above, we are configuring the logger to log out any messages of level “debug” or higher. standardConfiguration() is an easy helper function that configures Aurelia just like if we had used the simple aurelia-app attribute. Since the configuration object’s functions follow a fluent pattern, additional configuration function calls can be chained together.

I want to focus on the globalResources() function as it is the main reason we’re tweaking the configuration to start with. The globalResources() function takes in an array of strings that indicate the relative paths to the .js files containing the custom components we have created that we want to be globally available to all views in the application. In our case, we will specify that the <deal> element and currency value converter will be globally available. This means we can remove the from our deals.html file and the from our deal-editor.html file. For more information on the various functions you can use to configure all of Aurelia’s bells and whistles, you can go here.

Summary

In this post we’ve covered how to create a custom component for reusability in our application. We also discussed how to use value converters to transform viewmodel data being displayed on the UI or updated to the viewmodel. Finally, we scratched the surface of how to customize the configuration of our application using the configure() function in conjunction with the aurelia-app attribute and learned how to eliminate the need to explicitly declare our intent to use our custom components on each page by specifying them in the globalResources() function of the configuration.

This series has focused on critical aspects of Aurelia in an attempt to show you how simple and quick it is to create an application using it. There is a wealth of other features that you can check out at the Aurelia website. You can download the final version of our tiny application by doing git clone https://github.com/dmarini78/aurelia-intro or if you already did that at the beginning of this article, just do a git checkout master to bring your local repo up to speed with all the changes we just made.

The JBS Quick Launch Lab

Free Qualified Assessment

Quantify what it will take to implement your next big idea!

Our assessment session will deliver tangible timelines, costs, high-level requirements, and recommend architectures that will work best. Let JBS prove to you and your team why over 24 years of experience matters.

Get Your Assessment