Les custom element, c'est un peu l'équivalent des directives de Angular. Il s'agit d'élements qui se matérialisent sous la forme d'un tag html et qui peuvent prendre en compte des attributs. On va par exemple faire un custom element qui affiche une liste d'items pour une todo list :
<require
from="todo-item"></require>
<todo-item
repeat.for="item of todo.items"
item="item"></todo-item>
Nous allons alors créer une classe pour gérer cet élément. Pour la déclarer en tant que CustomElement, on peut utiliser des meta données mais le plus simple est de la suffixer par CustomElement
:
export class TodoItemCustomElement {}
On aurait pu aussi déclarer le custom element grâce aux meta data :
import {Behavior} from 'aurelia-templating';
export class TodoItem {
static metadata(){
return Behavior
.customElement('todo-item');
}
}
Behavior peut définir pas mal de choses comme par exemple l'utilisation de Shadow DOM, l'emplacement de la vue si elle n'est pas à l'endroit conventionnel (même nom que le fichier js, mais html, dans notre exemple todo-item.js et todo-item.html) ou, les attributs de l'élément.
Properties
.withPropery
permet donc de spécifier quels sont les attributs que l'élément prendra en compte. On pourra alors définir une méthode qui sera appellée à chaque fois que cette valeur changera (l'équivalent d'un watch chez Angular, mais clairement plus light).
export class TodoItem {
static metadata(){
return Behavior
.customElement('todo-item')
.withProperty('item')
.withProperty('foo');
}
itemChanged () {
this.item.doSomething();
}
fooChanged () {
this.bar = this.foo.getBar();
}
}
Imaginons maintenant un élément todo
qui prendra un objet todo en attribut et qui lui même intègrera des élements todo-item
:
// index.html
<require
from="todo"></require>
<todo
list="list"></todo>
// todo.html
<template>
<import
from="todo-item"></import>
<h2>${title}</h2>
<p
if.bind="loading">…wait for it…</p>
<ul
if.bind="!loading">
<todo-item
repeat.for="item of items"
item="item"></todo-item>
</ul>
</template>
// todo.html
<template>
<li>
${content}
</li>
</template>
Avec les classes javascript suivantes :
// todo.js
import {Behavior} from 'aurelia-templating';
export class TodoCustomElement {
static metadata(){
return Behavior
.withProperty('list');
}
listChanged () {
this.loading = true;
this.list.loadItems()
.then(items => this.items = items)
.catch(err => console.error(err))
.finally(() => this.loading = false);
}
}
// todo-item.js
import {Behavior} from 'aurelia-templating';
export class TodoItemCustomElement {
static metadata(){
return Behavior
.withProperty('item');
}
}
Avancé
On peut spécifier une propriété plus finement grâce à .and()
. Par exemple, pour indiquer que l'attribut est one way binded :
// todo-item.js
import {Behavior} from 'aurelia-templating';
export class TodoItemCustomElement {
static metadata(){
return Behavior
.withProperty('item').and(x => x.bindingIsOneWay())
.withProperty('pwet').and(x => x.bindingIsOneTime())
.withProperty('lol').and(x => x.bindingIsTwoWay())
;
}
}
Exemple
Un petit exemple sympa que je viens de faire pour afficher un gravatar. Tout va partir de :
<require from="ui/gravatar"></require>
<gravatar
email.bind="user.email"
size="sm"></gravatar>
Ce qui aura pour but d'afficher le gravatar de l'utilisateur, c'est à dire une image qui pointe vers gravatar avec le hash md5 de l'email.
Voici donc le template du custom element situé dans ui/gravatar.html
:
<template>
<img
src.bind="src">
</template>
Et son contrôleur situé sur ui/gravatar.js
:
import {Behavior} from 'aurelia-templating';
import md5 from 'js-md5';
const DEFAULT_WIDTH = 64;
export class GravatarCustomElement {
static metadata(){
return Behavior
.withProperty('email').and(x => x.bindingIsOneWay())
.withProperty('size')
.useShadowDOM();
}
constructor () {
this.width = DEFAULT_WIDTH;
}
sizeChanged (size) {
if (+size > 0) {
this.width = +size;
} else {
switch (size) {
case 'lg':
this.width = 100;
break;
case 'sm':
this.width = 32;
break;
case 'xs':
this.width = 16;
break;
default:
case 'md':
this.width = 64;
break;
}
}
}
emailChanged (email) {
let hash = md5(email),
width = this.width;
this.src = `//secure.gravatar.com/avatar/${hash}?s=${width}&d=blank`;
}
}
js-md5 s'installe via jspm install npm:js-md5
. Et donc on voit deux attributs, l'un étant un one way binding qui va juste lire la valeur de l'email, et l'autre est un string, donc sans binding. On différencie le binding du string simplement en ne mettant pas .bind
lors de l'implémentation de l'élément. C'est l'équivalent de size="'sm'"
ou du scope: {size: '@'}
de Angular, mais en bien plus facile à lire. On a aussi un constructeur qui va initialiser les valeurs par défaut. Et donc, la source de l'image est mise à jour à chaque changement de la valeur de l'email.