Introduction
Aurelia's templating system is simple to learn, and yet powerful enough to build even the most complex applications. This article will walk you through the construction of a static template, importing that template into parent templates, binding and manipulating data through the view-model, and the use of conditionals, repeaters, and events.
A Simple Template
All Aurelia templates must be wrapped with a <template>
element. The most basic template is a component that prints
"Hello World":
<template>
<p>
Hello, World!
</p>
</template>
This template could be a page in an Aurelia application, or it could be the view for a custom element. A template containing only boilerplate could be useful as a footer containing copyright information or disclaimers, but where's the fun in a static template?
All Aurelia templates work with a view-model, so let's create one:
export class Hello {
constructor() {
this.name = 'John Doe';
}
}
Let's bind the name
property in our view-model into our template using Aurelia's string interpolation:
<template>
<p>
Hello, ${name}!
</p>
</template>
One of the key features of Aurelia's templating system is helping to reduce context switching between your javascript
code and your template markup. String interpolation using the \${}
operator is a new feature in ES2015 that makes it
simple to insert values into a string. Thus, Aurelia uses this standard syntax in your templates.
When this template is run, Aurelia will insert the value of the name
property into the template where \${name}
appears. Pretty simple, right? But what if we want logic in our string interpolation. Can we add our own expressions?
Absolutely!
<template>
<p>
${arriving ? 'Hello' : 'Goodbye'}, ${name}!
</p>
</template>
export class Greeter {
constructor() {
this.name = 'John Doe';
this.arriving = true;
setTimeout(
() => this.arriving = false,
5000);
}
}
In our template, when arriving
is true, the ternary operator makes our result 'Hello'
, but when it's false, it
makes our result 'Goodbye'
. Our view-model code initializes arriving
to true
and changes it to false
after 5
seconds (5000 milliseconds). So when we run the template, it will say "Hello, John Doe!" and after 5 seconds, it will
say "Goodbye, John Doe!". Aurelia re-evaluates the string interpolation when the value of arriving
changes!
But don't worry, there is no dirty-checking. Aurelia uses an observable-based binding system that reacts to changes as they happen without having to do dirty-checking. This means that Aurelia doesn't slow down as you add more complex functionality to your template and view-model.
Binding
Binding in Aurelia allows data from the view-model to drive template behavior. The most basic example of binding
is linking a text box to the view model using value.bind
. What if we let the user decide who they want to greet,
and whether to say Hello or Goodbye?
<template>
<label for="nameField">
Who to greet?
</label>
<input type="text" value.bind="name" id="nameField">
<label for="arrivingBox">
Arriving?
</label>
<input type="checkbox" checked.bind="arriving" id="arrivingBox">
<p>
${arriving ? 'Hello' : 'Goodbye'}, ${name}!
</p>
</template>
export class Greeter {
constructor() {
this.name = 'John Doe';
this.arriving = true;
}
}
Above, we have a text box that asks for the name of the person to greet, and a checkbox indicating whether they're
arriving. Because we defined name
as "John Doe" in our view-model, the initial value of the text-box will be "John
Doe", and with arriving
set to true
, our checkbox will start checked. When we change the name, the person we're
greeting will change with it: "Hello, Jane Doe!". If we uncheck the box, the greeting will change: "Goodbye, Jane Doe!"
Notice that the way we set up the binding was by using value.bind
and checked.bind
. That .
within the attribute is important: whenever you see the .
, Aurelia is going to do something with that attribute. The most important thing to take away from this section is understanding that Aurelia will link attributes to the left of the .
with actions to the right of the .
.
You can learn more about data-binding in the Binding section of our docs.
Binding Focus
We can also use two-way data binding to communicate whether or not an element has focus:
<template>
<input focus.bind="hasFocus">
${hasFocus}
</template>
When we click the input field, we see "true" printed. When we click elsewhere, it changes to "false".
Binding Scopes Using with
We can also declare that certain parts of our markup will be referencing properties of an object in the view model, as below:
<template>
<p with.bind="first">
<input type="text" value.bind="message">
</p>
<p with.bind="second">
<input type="text" value.bind="message">
</p>
</template>
export class Hello {
constructor() {
this.first = {
message : 'Hello'
};
this.second = {
message : 'Goodbye'
}
}
}
Using with
is basically shorthand for "I'm working on properties of this object", which lets you reuse code as necessary.
Composition
In order to live by the DRY (Don't Repeat Yourself) Principle, we don't necessarily want to rely on tight coupling between our view and view-model pairs. Wouldn't it be great if there was a custom element that would arbitrarily combine an HTML template, a view-model, and maybe even some initialization data for us? As it turns out, we're in luck:
<template>
<compose view-model="hello"
view="./hello.html"
model.bind="{ target : 'World' }" ></compose>
</template>
<template>
Hello, ${friend}!
</template>
export class Hello {
activate(model) {
this.friend = model.target;
}
}
Note that the view-model we're composing into has an activate
method. When we use model.bind
, the contents are passed to activate
. We then pull the exact value that we need out of the passed model and assign it.
The as-element Attribute
In some cases, especially when creating table rows out of Aurelia custom elements, you may need to have a custom element masquerade as a standard HTML element. For example, if you're trying to fill table rows with data, you may need your custom element to appear as a <tr>
row or <td>
cell. This is where the as-element
attribute comes in handy:
<template>
<require from="./hello-row.html"></require>
<table>
<tr as-element="hello-row">
</table>
</template>
<template>
<td>Hello</td>
<td>World</td>
</template>
The as-element
attribute tells Aurelia that we want the content of the table row to be exactly what our hello-row
template wraps. The way different browsers render tables means this may be necessary sometimes.
Warn Internet explorer does not allow
<template>
elements inside<table>
or<select>
The View Resource Pipeline
The basic idea behind the View Resource Pipeline is that we're not limited to HTML or JavaScript. A basic example would be pulling in Bootstrap:
<template>
<require from="bootstrap/css/bootstrap.min.css"></require>
<p class="lead">Hello, World!</p>
<p>Lorem Ipsum...</p>
</template>
Here, the <require>
tag is taking a CSS file, instead of html or a view model. The View Resource Pipeline is the part of Aurelia that's responsible for recognizing that it's CSS, and handling it appropriately. One of the most powerful features of Aurelia is that the View Resource Pipeline is completely extensible, allowing you to define your own handler for any type of view resource you might want to define!
View and Compilation Spies
If you've installed the aurelia-testing
plugin, you have access to two special templating behaviors:
<template>
<p view-spy compile-spy>Hello!</p>
</template>
view-spy
drops Aurelia's copy of the View object into the console, while compile-spy
emits the compiler's TargetInstruction. This is especially useful for debugging any new View Resources you've created using the View Resource Pipeline.
Conditionals
Aurelia has two major tools for conditional display: if
, and show
. The difference is that if
removes the element
entirely from the DOM, and show
toggles the aurelia-hide
CSS class which controls the element's visibility.
The difference is subtle but important in terms of speed and usability. When the state changes in if
, the template
and all of its children are deleted from the DOM, which is computationally expensive if it's being done over and over.
However, if show
is being used for a very large template, such as a dashboard containing thousands of elements with
their own bound data, then keeping those elements loaded-but-hidden may not end up being a useful approach.
Here's our basic "Hello World" that asks the user if they want to greet the world first:
<template>
<label for="greet">Would you like me to greet the world?</label>
<input type="checkbox" id="greet" checked.bind="greet">
<div if.bind="greet">
Hello, World!
</div>
</template>
However, if we just want to hide the element from view instead of removing it from the DOM completely, we should use
show
instead of if
.
<template>
<label for="greet">Would you like me to greet the world?</label>
<input type="checkbox" id="greet" checked.bind="greet">
<div show.bind="greet">
Hello, World!
</div>
</template>
When unchecked, our "Hello World" div will have the aurelia-hide
class, which sets display: none
if you're using
Aurelia's CSS libraries. However, if you don't want to do that, you can also define your own CSS rules that treat
aurelia-hide
differently, like setting visibility: none
or height: 0px
.
Conditionals can also be one-time bound, so that parts of the template are fixed when they're instantiated:
<template>
<div if.one-time="greet">
Hello, World!
</div>
<div if.one-time="!greet">
Some other time.
</div>
</template>
export class ConditionalOneTimeTemplate {
greet = (Math.random() > 0.5);
}
export class ConditionalOneTimeTemplate {
greet = (Math.random() > 0.5);
}
There's a 50-50 chance that we'll greet the world, or put it off until later. Once the page loads, this is fixed,
because the data is one-time bound. Why don't we use show.one-time
? If we think about what show
does, it doesn't
really make sense. We're saying we want a CSS class to be applied that will hide an element, and that it will never
change. In most cases, we want if
to refuse to create an element we'll never use.
Complementing if
, there is else
. Used in conjunction with if
, else
will render its content when if
does
not evaluate to true.
<template>
<div if.bind="showMessage">
<span>${message}</span>
</div>
<div else>
<span>Nothing to see here</span>
</div>
</template>
Elements using the else
template modifier must follow an if
bound element to make contextual sense and function properly.
Caching the view instance when condition changes
By default, if
caches the underlying view model instance. Although the element is being removed from the DOM and
the component goes through detached
and unbind
lifecyle events, its instance is kept in memory for further condition
changes. Therefore, when the element is hidden and then shown again, if
does not need to initialize the component
again.
You can opt-out this default behavior by setting the cache
binding of the if
directive explicitly. This is
especially useful when using if
on custom elements and initializing them on every appearance is
crucial.
<template>
<div if="condition.bind: showMessage; cache: false">
<span>${message}</span>
</div>
</template>
Repeaters
Repeaters can be used on any element, including custom elements, and template elements too! Here are a few different data types that can be iterated with a repeater.
Arrays
Aurelia is also able to repeat elements for each element in an array.
<template>
<p repeat.for="friend of friends">Hello, ${friend}!</p>
</template>
export class RepeaterTemplate {
constructor() {
this.friends = [
'Alice',
'Bob',
'Carol',
'Dana'
];
}
}
This allows me to list out my friends and greet them one by one, rather than attempting to greet all 7 billion inhabitants of the world at once.
As mentioned before, we can also use the template element as our repeater - but we have to wrap it in a surrogate <template>
element:
<template>
<template repeat.for="friend of friends">
<p>Hello, ${friend}!</p>
</template>
</template>
Aurelia will not be able to observe changes to arrays using the array[index] = value
syntax. To ensure that
Aurelia can observe the changes on your array, make use of the Array methods: Array.prototype.push
, Array.prototype.pop
and
Array.prototype.splice
.
Two-way binding with arrays requires a special syntax due to the nature of the for-of
loop in javascript. Do not use repeat.for="item of dataArray"
;doing so will result in one-way binding only - values typed into an input will not be bound back. Instead use the following syntax:
<template>
<div repeat.for="i of dataArray.length">
<input type="text" value.bind="$parent.dataArray[i]">
</div>
</template>
Range
We can also iterate over a numerical range:
<template>
<p repeat.for="i of 10">${10-i}</p>
<p>Blast off!</p>
</template>
Note that the range will start at 0 with a length of 10, so our countdown really does start at 10 and end at 1 before blast off.
Sets
I can also use an ES6 Set in the same way:
<template>
<p repeat.for="friend of friends">Hello, ${friend}!</p>
</template>
export class RepeaterTemplate {
constructor() {
this.friends = new Set();
this.friends.add('Alice');
this.friends.add('Bob');
this.friends.add('Carol');
this.friends.add('Dana');
}
}
We can use repeaters with arrays, which is useful - but we can also use repeaters with other iterable data types, including objects, plus new ES6 standards such as Map and Set.
Map
One of the more useful iterables is the Map, because you can decompose your key and value into two variables directly in the repeater. Although you can repeat over objects straightforwardly, Maps can be two-way bound much more straightforwardly than Objects, so you should try to use Maps where possible.
<template>
<p repeat.for="[greeting, friend] of friends">${greeting}, ${friend.name}!</p>
</template>
export class RepeaterTemplate {
constructor() {
this.friends = new Map();
this.friends.set('Hello',
{ name : 'Alice' });
this.friends.set('Hola',
{ name : 'Bob' });
this.friends.set('Ni Hao',
{ name : 'Carol' });
this.friends.set('Molo',
{ name : 'Dana' });
}
}
One thing to notice in the example above is the dereference operator in [greeting, friend]
- which breaks apart the map's key-value pair into greeting
, the key, and friend
, the value. Note that because all of our values are objects with the name
property set, we can get our friend's name with \${friend.name}
, just as if we were getting it from JavaScript!
Objects
Let's do the same thing, except with a traditional JavaScript object in our view-model:
<template>
<p repeat.for="greeting of friends | keys">${greeting}, ${friends[greeting].name}!</p>
</template>
export class RepeaterTemplate {
constructor() {
this.friends = {
'Hello':
{ name : 'Alice' },
'Hola':
{ name : 'Bob' },
'Ni Hao':
{ name : 'Carol' },
'Molo':
{ name : 'Dana' }
}
}
}
export class KeysValueConverter {
toView(obj) {
return Reflect.ownKeys(obj);
}
}
We just introduced something called a "value converter". Basically, we take the object in our view model, friends
, and run it through our keys
value converter. Aurelia looks for a class named KeysValueConverter
and tries to call its toView()
method with our friends
object. That method returns an array of keys- which we can iterate. In a pinch, we can use this to iterate over Objects.
A common question pops up here: Why can't we just dereference the object into [key, value]
like we did with Maps? The short answer is that JavaScript objects aren't iterable in the same way that Arrays, Maps, and Sets are. So in order to iterate over JavaScript objects, we have to transform them into something that is iterable. The way you approach it will change based on what exactly you want to do with that object. There is also a
plugin
that can be included, which will transform Objects to become iterable maps that can be dereferenced using the [key, value]
syntax.