Custom element avec Aurelia

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.

Hadrien

Hi, I'm a french Javascript Lead Developer, Web Architect from Toulouse, France. I've worked for 12 years for many projects with YUI, AngularJS, Aurelia.io and now React and React native.

Toulouse, France https://hadrien.eu