The Eisenberg Effect

Aurelia Update with Decorators, IE9 and More

Introduction

Rob Eisenberg

Rob Eisenberg

Sr. Program Manager on docs.microsoft.com. Creator of Caliburn, Caliburn.Micro, Durandal and Aurelia. My opinions are my own.


Aurelia Update with Decorators, IE9 and More

Posted by Rob Eisenberg on .
Featured

Aurelia Update with Decorators, IE9 and More

Posted by Rob Eisenberg on .

Today's release finally brings full ES7 and TypeScript decorators, IE9 support and a new, simplified HTML Behavior programming model. We've also made a few performance enhancements along the way.

Decorators

With the release of Babel 5.0 and the TypeScript 1.5 Alpha, we now have support for ES 2016 Decorators in the major compilers. To accompany these releases, we have now enabled full support for decorators in Aurelia. We've also done some renaming in our fallback mechanisms to account for this terminology (see "Fallbacks" below).

As of this release, if you are using Babel 5.0 or TypeScript 1.5, you can now optionally use a decorator to control dependency injection. Here's what it looks like:

import {inject} from 'aurelia-framework';  
import {HttpClient} from 'aurelia-http-client';

@inject(HttpClient)
export class Flickr{  
  constructor(http){
    this.http = http;
  }
}

With decorators, there's no need for special static properties or callbacks on your class anymore. You can use this new language feature today. We've also updated the navigation skeleton to take advantage of it.

It's also worth noting that for dependency injection, you can still use the static inject property/method in place of the decorator as well. It's up to you. In fact, that's all the decorator does. Here's the inject decorator implementation, in case you are curious how that works:

export function inject(...rest){  
  return function(target){
    target.inject = rest;
  }
}

HTML Behaviors

While we were working on Decorators, we wanted to take the time to update the HTML Behaviors design. If you aren't familiar with this, HTML Behaviors are ways in which your JavaScript code can plug into the HTML compiler.

As of this release, there are two core types of HTML Behaviors: Custom Element and Custom Attribute. These ideas are based on core DOM primitives so hopefully they just "make sense".

Custom Elements

As always, you can use export conventions to identify these:

export class NavBarCustomElement {}  

This creates a custom element named <nav-bar></nav-bar>. If no naming pattern is matched, then it will default to a custom element. So, the export could also be called NavBar.

Don't want to use conventions? No problem. You can always use decorators:

import {customElement} from 'aurelia-framework';

@customElement('nav-bar')
export class NavBar {}  

Custom Elements also have a number of other options. Here's a fun example to show a few things off:

@useShadowDOM
export class Expander {  
  @bindable isExpanded = false;
  @bindable header;
  ...
}

This behavior shows how to indicate that the element's view is rendered in the Shadow DOM. It also shows how to create two properties which will be settable/bindable on the <expander> element in HTML. In this case we are using the bindable decorator with the new ES7 property initializer syntax. Property initializers are available in TypeScript and in Babel, with the "es7.classProperties" option. If you don't want to use initializers, you can also specify them on the class like this:

@useShadowDOM
@bindable('isExpanded')
@bindable('header')
export class Expander {  
  ...
}

There are other options as well. Here's a summary of other decorators you may want to use on your custom elements:

  • @syncChildren(property, changeHandler, selector) - Creates an array property on your class that has its items automatically synchronized based on a query selector against its view.
  • @skipContentProcessing - Tells the compiler not to process the content of your custom element. It is expected that you will do custom processing yourself.
  • @useView(path) - Specifies a different view to use.
  • @noView - Indicates that this custom element does not have a view and that the author intends to handle its own rendering internally.

Custom Attributes

What about custom attributes? You can follow the convention:

export class ShowCustomAttribute {  
  valueChanged(newValue, oldValue){
      ...
  }
}

Now you can just put a show attribute on any HTML element to use it. If you'd rather be explicit with decorators:

import {customAttribute} from 'aurelia-framework';

@customAttribute('show')
export class Show {  
  valueChanged(newValue, oldValue){
      ...
  }
}

Note that in both cases, your attribute maps to a value property on the class and you can be notified when it changes by implementing a valueChanged method. If you want to map a single attribute to more than one property, you can simply create bindable properties on the attribute itself and then use the options syntax in HTML. There are also some special options for custom attributes:

  • @templateController - Allows a custom attribute to turn the attributed HTML into an HTMLTemplate which it can then generate on the fly. This is how behaviors like if and repeat can be created.
  • @dynamicOptions - This allows a custom attribute to have a dynamic set of properties which are all mapped from the options attribute syntax into the class at runtime.

Note: With the move to decorators, you may have noticed that we renamed withProperty to bindable. We have also changed the signature. If all you need to do is provide the name, you can provide it like so @bindable('someProperty') but if you need to specify more details, you should pass an options object like this:

@bindable({
  name:'myProperty', //name of the property on the class
  attribute:'my-property', //name of the attribute in HTML
  changeHandler:'myPropertyChanged', //name of the method to invoke on property changes
  defaultBindingMode: ONE_WAY, //default binding mode used with the .bind command
  defaultValue: undefined //default value of the property, if not bound or set in HTML
})

The defaults and conventions are shown above. So, you would only need to specify these options if you need to deviate.

Behaviors Summary

HTML Behaviors are powerful. Most of the time you can create them by following a simple naming convention and then adding some @bindable decorators, but you can do much more. By combining ES7 Decorators and Property Initializers you can have a clean, standards-based way of defining advanced behaviors for any scenario.

IE9

We now have support for IE9! In order to get Aurelia to run in legacy browsers like IE9 and Safari 5.1 you need to polyfill MutationObservers and WeakMap. This can be achieved by a jspm install of github:webreflection/es6-collections and github:polymer/mutationobservers. Load these two scripts before system.js.

In Skeleton Navigation index.html will look like this:

<script src="jspm_packages/github/webreflection/es6-collections@master/es6-collections.js"></script>  
<script src="jspm_packages/github/polymer/mutationobservers@0.4.2/MutationObserver.js"></script>  
<script src="jspm_packages/system.js"></script>  
<script src="config.js"></script>  
<script>  
  System.import('aurelia-bootstrapper');
</script>  

It should be noted that WeakMap is not required by Aurelia itself but it is used by the MutationObserver polyfill. So, with this configuration, you should be able to get things working with IE9 today. We are looking into making this even smoother and there still may be a few random issues. If IE9 support is important to you, please try this out and provide us with some feedback.

Performance

This isn't primarily a performance release. However, I wanted to mention that Core Team Member Martin Gustafsson has done some optimization work on our repeater which gives it up to a 200x performance boost in certain scenarios. I've also done work to optimize our internal metadata read/write system and we are moving steadily torward getting our benchmarking infrastructure in place so we can make the big optimizations in the binding system. Stay tuned...

Other Goodies

We had tons of bug fixes naturally and we've continued to improve the binding engine as well, with support for more scenarios, but without adding any additional syntax or complexity. With the addition of decorators, we can now make it easy for computed properties to avoid dirty checking (only done for computeds anyways) by specifying their dependencies. Here's an example of how to do that:

import {computedFrom} from 'aurelia-framework';

export class Welcome{  
  firstName = 'John';
  lastName = 'Doe';

  @computedFrom('firstName', 'lastName')
  get fullName(){
    return `${this.firstName} ${this.lastName}`;
  }
}

Also, now that Babel and TypeScript have matured significantly, we see no more need to consider AtScript as a viable language for building apps. This release removes all support for AtScript.

Fallbacks

In this post we've been showing a lot of ways to leverage ES7 (ES 2016) features such as Decorators and Property Initializers with the new update. If you aren't using a compiler that supports these, don't worry. We still have a fallback mechanism that you can use. Take note though, we have removed the metadata static member and replaced it with a decorators static callback. We also have a Decorators helper to use in place of the Metadata helper. Here's what our first example from above would look like in ES5 with CommonJS using this fallback mechanism.

var Decorators = require('aurelia-framework').Decorators;  
var HttpClient = require('aurelia-http-client').HttpClient;

function Flickr(http){  
  this.http = http;
}

Flickr.decorators = Decorators.inject(HttpClient);  
exports.Flickr = Flickr;  

The Decorators helper has methods that match every decorator in Aurelia, except computedFor. The names and casing are identical. The helper methods are chain-able, so you can easily compose them like this:

Foo.decorators = Decorators.customElement('my-element')  
                           .bindable('someProperty')
                           .inject(Element);

In fact there's nothing stopping you from using this in your ES7 code if you prefer it (though we think that decorators are superior for almost all cases).

How to Update

Clearly the new Decorators and HTML Behaviors design are breaking changes. The information above should get you through making the updates in your code, but I want to share here how to update your tooling and Aurelia itself.

First, you need to update the Babel compiler to 5.x. You can use the following commands to update your gulp tasks as well as your testing infrastructure:

sudo npm install --save-dev gulp-babel  
sudo npm install --save-dev karma-babel-preprocessor  
sudo npm install --save-dev karma-jspm  

You will also need to update your compiler options. Here are the options we are now using in the Navigation Skeleton, located at build/babel-options.js:

module.exports = {  
  modules: 'system',
  moduleIds: false,
  comments: false,
  compact: false,
  stage:2,
  optional: [
    "es7.decorators",
    "es7.classProperties"
  ]
};

Make sure you also update the babel options in your karma.conf.js file. If you are using TypeScript instead of Babel, you will need to move to using version 1.5 if you want to take advantage of decorators.

Next, you want to update Aurelia. Because these are breaking changes, you cannot do a simple jspm update. JSPM is smart in the way that it follows semver, and it won't let you break your app on accident. I recommend using jspm install for each "top level" Aurelia library. Just look in your package.json file, in the jspm section and be sure to re-install all the Aurelia dependencies listed there.

Next Steps

This release also includes some work on the router, route recognition and route generation, thanks to the work of community member Bryan Smith. The next release will yield a couple of breaking changes related to fixing the final issues with the router and making it easier and more capable in all scenarios.

As I mentioned, we are gearing up for big performance work too and we've got a couple of demoes we're putting together to spotlight that which I know you will find interesting. Hang in there :) I think you're going to like it.

Note: There are lots of updates in this release. At the moment, our site's docs are out of sync with these changes. We wanted to get this release out to you as soon as we could, so please be patient while we update the docs over the next day or so.

Upcoming Presentations

Interested in getting a walkthrough of building your first Aurelia app? I've got good news for you. I'll be in Montreal on Monday April 13, 2015 presenting an introductory talk at the Radio Canada studios at 7PM EDT. Don't live in Montreal? No problem. Radio-Canada Lab has generously offered to not only produce the show but live stream it on YouTube. You will be able to watch it live from anywhere in the world.

Rob Eisenberg

Rob Eisenberg

http://www.bluespire.com

Sr. Program Manager on docs.microsoft.com. Creator of Caliburn, Caliburn.Micro, Durandal and Aurelia. My opinions are my own.

View Comments...