Attention!!! Cette formation n'est plus maintenue depuis ember 3.12 et ne correspond pas aux dernières évolutions du framework. Je vous invite à vous reporter sur les guides officiels d'ember pour des tutoriaux complets d'introduction.

NB : Les exercices de cette section seront validés par le passage des cas de tests associés. Il est donc nécessaire, en premier lieu, de copier ce ou ces fichiers de test dans le projet :

Composants

Les composants Ember constituent une part importante de la structure du framework. Ember revendique en effet depuis toujours une approche composants ainsi que la volonté de converger vers les Web components et les Custom elements en particulier.

Ember permet ainsi de définir - sous la forme de composants - des éléments évolués offrant une réutilisation maximales à la fois de structures d’affichage et de structures logiques au sein d’une application. Voire, via le packaging de ces composants au sein d’addons, au sein de plusieurs applications Ember.

Le futures versions d’Ember doivent même voir la notion de contrôleur totalement remplacée par l’utilisation de composants routables.

Définition

Un composant Ember se définit par les éléments suivants :

  • un template dans app/templates/components/my-component.hbs définissant le template spécifique du composant si il existe
  • une sous classe de Components dans app/components/my-component.js qui permet d’implémenter l’intégralité de la logique propre du composant

Comme à son habitude, Ember ne nécessite pas que ces deux éléments soient explicitement déclarés si ce n’est pas nécessaire. Ainsi il est possible qu’un composant soit pleinement définit par son template (aucune logique spécifique) ou par son fichier js (aucun rendu spécifique). Dans ce cas, l’élément manquant est généré de manière transparente par Ember sous la forme, respectivement, d’un object vide héritant d’Components ou d’un template vide.

A noter qu’Ember fournit des outils permettant d’effectuer des tests d’intégration sur nos composants (en plus d’éventuels tests unitaires standards dans le cas de composants implémentant une logique propre complexe).

Nommage

Conformément à la spécification W3C, le nom d’un composant (et donc le nom des fichiers .js et .hbs associés doit impérativement comporter un tiret -. Dans le cas contraire, le composant ne sera pas détécté par Ember.

Ainsi my-component est valide, myComponent ou mycomponent ne le sont pas.

Utilisation

Une fois créé, un composant s’utilise de la même manière qu’un helper via la notation < ... > ou {{ ... }}.

Il peut s’agir :

  • de composants de type inline : Ces composants n’embarquent pas de contenu externe et se suffisent à eux-même. Il peut s’agir de composants très simples ou très complexes. Les informations et propriétés de l’expérieur leur sont exclusivement passées via des paramètres. Ainsi un composant invoqué de cette manière :

    <UserName class="admin" @user={{model}}>
    

    avec le template suivant :

    <dl>
      <dt>First name: </dt><dd>{{user.firstName}}</dd>
      <dt>Last name: </dt><dd>{{user.lastName}}</dd>
    </dl>
    

    et le modèle suivant :

    {
      firstName: "Franck",
      lastName: "Underwood"
    }
    

    donnera l’HTML suivant :

    <div class="admin">
      <dl>
        <dt>First name: </dt><dd>Franck</dd>
        <dt>Last name: </dt><dd>Underwood</dd>
      </dl>
    </div>
    
  • de composants de type block encapsulant du contenu. Ces composants fonctionnent exactement comme les premiers à la différence près qu’ils permettent d’y insérer du contenu externe. Ce contenu s’insèrera dans le template du composant en lieu et place de l’expression {{yield}}. Ainsi un composant invoqué de cette manière :

    <FullArticle class="article" title={{model.title}}>
      {{#each model.paragraphs as |paragraph|}}
        <p>{{paragraph}}</p>
      {{/each}}
    </FullArticle>
    

    avec le template suivant :

    <article>
      <h2>{{title}}</h2>
      <div class="content">{{yield}}</div>
    </article>
    

    et le modèle suivant :

    {
      title: "Lorem ipsum ...",
      paragraphs: [
        "Lorem ipsum dolor sit amet",
        "Consectetur adipiscing elit"
      ]
    }
    

    donnera l’HTML suivant :

    <div class="article">
      <article>
        <h2>Lorem ipsum ...</h2>
        <div class="content">
          <p>Lorem ipsum dolor sit amet</p>
          <p>consectetur adipiscing elit</p>
        </div>
      </article>
    </div>
    

Passage de propriétés

Il est bien entendu possible de passer des propriétés - dynamiques ou non - aux composants afin qu’ils puissent les afficher et/ou les manipuler. Ce passage de propriétés se fait tout naturellement selon la syntaxe habituelle name=value. Ainsi la déclaration suivante :

{{custom-user title='My title' user=model}}  => ancienne facon de faire

ou via la syntax angle bracket :

<CustomUser title='My title' @user={{model}}/>

permet la manipulation suivante dans le template du composant :

<h4>{{title}}</h4>
<dl>
    <dt>First name :</dt><dd>{{user.firstname}}</dd> 
    <dt>Last name :</dt><dd>{{user.lastname}}</dd> 
</dl>

Les deux propriétés title (litéral) et user ont donc été passées au composant qui peut alors les manipuler. Dans le cas précis il effectue un simple affichage

  1. On souhaite afficher l’image de couverture pour chaque comic juste après le titre. Comme on anticipe que l’on aura besoin de réutiliser cet élément (dans une future notion d’album ?), on va en faire un composant.
    • Créer un composant image-cover très simple (template uniquement) affichant la couverture du comic dans une image de classe cover.
    • Copier les images de couverture en copiant ce repertoire vers public/assets/images
    • Pour le moment, se contenter d’afficher, pour tous les comics, la couverture par défaut (public/assets/images/comics/covers/default.jpg)
    • Mettre à jour les templates app/templates/comic/index.hbs et app/templates/comic/edit.hbs pour ajouter l’appel du composant juste après le titre
    {{!-- app/templates/components/image-cover.hbs --}}
    <img class="cover" src="/assets/images/comics/covers/default.jpg"/>
    
    {{!-- app/templates/comic/index.hbs --}}
    ... 
    <div class="comic-header">...</h3>
    {{image-cover name=model.slug}}
    <dl class="comic-description">
    ...
    
    {{!-- app/templates/comic/edit.hbs --}}
    ... 
    <div class="comic-header">...</h3>
    
    {{image-cover name=model.slug}}
    
    <div class="comic-description">
    ...
    
  2. On souhaite maintenant dynamiser ce composant pour afficher l’image de couverture correspondant au comic sélectionné.
    • Modifier le template app/templates/comic/index.hbs pour ajouter les passage d’un paramètre au composant lui permettant d’accéder à l’identifiant (slug) du comic.
    • Modifier le template du composant pour remplacer “default” par la valeur de ce slug

    Test : Ces modifications doivent permettre de rendre passant le test image-cover - renders image-cover

    {{!-- app/templates/comic/index.hbs --}}
    ... 
    <div class="comic-header">...</h3>
    {{image-cover name=model.slug}}
    <dl class="comic-description">
    ...
    
    {{!-- app/templates/components/image-cover.hbs --}}
    <img class="cover" src="/assets/images/comics/covers/{{name}}.jpg"/>
    
    • Inspecter ensuite le DOM au niveau de l’image. Quel est le code qui a été généré ? Que constate-t-on ?

    Le code généré est le suivant :

    <div id="ember483" class="ember-view"><img class="cover" src="/assets/images/comics/covers/blacksad.jpg"></div>
    

    On retrouve bien le code de notre template et on constate qu’il a été encapsulé dans un élément div englobant attaché par Ember à notre composant.

Personalisation du rendu d’un composant

Le rendu des composants Ember peut être très largement personalisé en créant une sous classe de Components dans app/components. Il est alors possible de configurer différentes choses :

Elément HTML

On a vu plus haut qu’Ember encapsule par défaut les composants dans des div englobantes. Il est facilement possible de modifier ce comportement grâce à la propriété tagName du composant. Cette propriété attend une chaîne de caractère contenant le type de l’élément :

export default Component.extend({
  tagName: 'li'
});

Résultat :

<li id="ember123" class="ember-view"></li>

Classes

De la même manière il est possible de spécifier le ou les noms de classe(s) associés au composant via la propriété classNames. Cette propriété attend soit une chaîne de caractère avec le nom de la classe unique à ajouter au composant soit un tableau de chaînes de caractères dans le cas de classes multiples :

export default Component.extend({
  classNames: ['btn', 'success']
});

Résultat :

<div id="ember123" class="ember-view btn success"></div>

Il est également possible de positionner des classes sur l’élément racine d’un composant en fonction de critères applicatifs - de la valeur d’une propriété booléenne en l’occurrence. Cela s’effectue grâce à la propriété classNameBindings. La présence d’une classe sur le composant dépend ainsi de la valeur de la propriété booléenne associée sur le format <prop>:<classIfTrue>:<classIfFalse>.

export default Component.extend({
  classNameBindings: 'isSuccess:success:error',
  isSuccess: true
});

Résultat :

<div id="ember123" class="ember-view success"></div>

Tout comme la propriété classNames, cette propriété accepte aussi bien une chaîne unique (une seule classe) qu’un tableau de chaînes.

Attributs

Il est également posssible de positionner et de modifier différents attributs sur l’élément racine du composant via la propriété attributeBindings. Celle-ci fonctionne comme la précédente et autorise également la présence d’un seul identifiant d’attribut (chaîne) ou d’une liste d’identifiants. Elle permet de positionner l’attribut spécifié à la valeur de la propriété de même nom.

export default Component.extend({
  attributeBindings: 'name',
  name: "username"
});

Il est également possible de spécifier explicitement le nom de la propriété :

export default Component.extend({
  attributeBindings: 'userName:name',
  userName: "username"
});

Résultat :

<div id="ember123" class="ember-view" name="username"></div>

Cela permet notamment de définir des valeurs d’attributs à partir de valeurs de propriétés passés au composant.

Interactions

Suivant le principe DDAU (Data Down Actions Up), les principales formes d’interactions des composants Ember avec leur environnement sont les suivantes :

  • Depuis les éléments parents vers les enfants. Les éléments parents sont ceux qui déclarent un composant - généralement en incluant sa définition au sein de leur template. Cette communication descendante se fait via l’utilisation de propriétés dynamiques.
  • Depuis le DOM suite à des actions de l’utilisation via l’interception d’évènements.
  • Depuis les éléments enfants vers les parents. Les composants peuvent ainsi informer leurs parents de la survenue d’évènements extérieurs. Cela s’effectue via des actions.

Interactions parents -> enfants (propriétés)

Les composants suivent les principes de communication standard d’Ember et cette forme de communication descendante s’appuie sur la manipulation de propriétés dynamiques bindées. En effet, de manière générale, Ember n’utilise pas de mécanismes de type bus d’évènement ou de broadcasting à proprement parler pour communiquer. A la place, un état est partagé entre les différents composants sous la forme de propriétés dynamiques. Ces propriétés sont ainsi passées par les parents aux enfants sous la forme de paramètres classiques (name=value ou pour la partie angle bracket @name={{value}}) comme vu plus haut. Tout évènement de changement de valeur de cette propriété sera ainsi disponible pour les composants enfants qui souhaitent l’écouter, leur permettant ainsi de réagir à ce changement en adaptant leur comportement et/ou leur rendu.

Les binding de classes ou d’attributs peuvent faire directement référence à ces propriétés passées au composant. Ainsi, si un composant est invoqué de la manière suivante :

{{MyComponent selected=true}}

Il peut parfaitement déclarer le binding suivant :

export default Component.extend({
  classNameBindings: 'selected'
});

Par convention, la propriété booléenneselected est automatiquement écoutée pour décider du positionnement de la classe de même nom.

Il est cependant nécessaire de rappeler explicitement que les propriétés passées dynamiquement aux composants ne sont, par définition, pas disponibles au moment de la déclaration des propriétés du composant. Ainsi, la syntaxe suivante (où user est passé au composant par le parent) n’affichera jamais l’attribut name qui restera toujours null :

export default Component.extend({
  attributeBindings: 'userName:name',
  userName: user.get('fullName')
});

En effet, au moment de la déclaration de userName, user n’est pas défini et sa valeur ne serait, à fortiori pas mise à jour lors du changement de la valeur user.fullName. Il est donc nécessaire d’utiliser une computed property. Dans ce cas, dès que la propriété dynamique userName passée par le parent changera de valeur, l’attribut name sera automatiquement mis à jour :

export default Component.extend({
  attributeBindings: 'userName:name',
  userName: computed('user.fullName', function() {
      return this.get('user.fullName');
  }
});

Ces mécanismes permettent donc de propager naturellement aux composants, via leurs parents, des changements d’états intervenus à d’autres endroits de l’application.

  1. Modifier le composant image-cover pour passer sur une version full javascript
    • Supprimer le fichier de tempates et créer le composant javascript
    • Faire en sorte de supprimer la div englobante tout en conservant le fonctionnement du composant

    Tests : Ces modifications doivent conserver passant le test image-cover-test - renders image-cover et rendre passant le test renders image-cover - root is image

    // app/components/image-cover.js
    
    import Component from '@ember/component';
    import { computed } from '@ember/object';
    
    export default Component.extend({
      tagName: 'img',
      classNames: 'cover',
      attributeBindings: 'src',
      src: computed('name', function () {
        return `/assets/images/comics/covers/${this.get('name')}.jpg`;
      })
    });
    

    On note au passage l’utilisation des littéraux de gabarits (template literals) ES6. cf. MDN

Evènements utilisateurs (DOM)

Une autre forme évidente d’interaction consiste à demander à un composant de réagir à différents évènements DOM le concernant (c’est à dire intervenant sur la portion d’arbre DOM qu’il gère).

Cela se fait simplement en déclarant dans le composant une fonction du même nom que l’évènement auquel on souhaite que le composant réagisse. La liste des évènements gérés nativement est disponible dans la documentation officielle.

Un paramètre est passé automatiquement à la function. Il contient l’évènement d’origine afin de permettre la récupération d’informations complémentaires (data, origine, etc.). L’évènement n’est pas consommmé et continue à être propagé au sein de l’arbre d’appel. Il est possible de stopper cette propagation en renvoyant false.

export default Component.extend({
  click(event) {
      // do whatever you want
      ...
      
      // stop event propagation if you want
      return false; 
  }
});

Il est possible de permettre explicitement à une application Ember de gérer des évènements personnalisés via la propriété customEvents. De manière plus générale, cette propriété permet de définir de nouveaux gestionnaires pour des évènements non pris en charge nativement mais également de neutraliser la gestion de certains évènements. Les évènements non pris en charge peuvent être des évènements DOM standard non pris en charge ou même des évènements plus métiers.

NB :

  • La déclaration de la prise en charge de ces nouveaux évènements se fait au niveau de l’application et non du composant.
  • Les évènements DOM doivent être des Bubble events. Les autres évènements ne peuvent être interceptés.

Ainsi le code suivant ajoute un gestionnaire pour l’évènement paste et supprime celui du doubleClick utilisateur. Le label associé à l’évènement correspond au nom du gestionnaire qui sera invoqué lors de la survenue de l’évènement :

export default Application.extend({
  customEvents: {
      // add support for the paste event
      paste : 'paste',
      
      // remove support for click event
      doubleClick: null
  }
});

Ainsi, chaque composant pourra déclarer un gestionnaire paste de cette manière :

export default Component.extend({
  paste() {
      // ...
  }
});
  1. Créer un composant fav-btn qui va mettre en place un bouton permettant de sélectionner / désélectionner un comic en favori
    • il doit porter la classe btn-fav
    • le comic est considéré favori si sa propriété isFavorite est à true
    • le clic sur le bouton doit changer l’affichage en positionnant / enlevant la classe selected sur ce composant
    • le clic doit inverser la valeur de la propriété isFavorite du comic
    • modifier les templates comic/index.hbs et comic/edit.hbs pour intégrer ce composant juste en dessous de l’élément racine. L’appel doit être de cette forme :

      {{fav-btn selected=...}}
      

    Tests : Ces modifications doivent rendre passant les tests renders fav-btn, update fav-btn after external change et update fav-btn after click

    // app/components/fav-btn.js
    
    import Component from '@ember/component';
    
    export default Component.extend({
      tagName: 'span',
      classNames: 'btn-fav',
      classNameBindings: 'selected',
    
      click: function () {
        this.toggleProperty('selected');
      }
    });
    
    {{fav-btn selected=model.isFavorite}}
    

Interactions enfants -> parents (actions)

Enfin, la dernière forme d’interaction concerne la communication ascendante, c’est à dire depuis un composant enfant vers son ou ses parents. Cette mécanique s’appuie sur des actions. Ainsi chaque composant peut, tout comme les contrôleurs, définir des gestionnaires d’actions via le hash actions: {}. Mais ils peuvent également déclencher ou exécuter des actions pour communiquer avec leurs parents.

Pour des raisons historiques, il existe deux modes de déclaration et de gestion des actions. Les actions peuvent ainsi être définies :

  • Sous forme de libellés par le composant (element space actions). Ce libellé, ainsi que d’éventuels paramètres peuvent être levés par le composant et propagés. L’action est exécutée par le parent avec les paramètres passés par le composant.
  • Sous forme de fonctions (closure actions) définies et implémentées par le ou les parents. La fonction est alors passée en propriété du composant. L’action est exécutée par l’enfant à qui l’on a passé l’action.

Element space actions

Dans cette première forme, à l’issue d’un traitement (après la gestion d’un évènement DOM par exemple), un composant peut appeler la méthode sendAction pour propager une action et avertir ainsi ses parents.

Cette méthode prend en premier paramètre le nom de l’action. Sans paramètre, c’est le nom par défaut “action” qui est pris. Tous les paramètres suivants seront vu comme des paramètres, le contexte d’exécution de l’action est remonté en même temps que le nom de l’action.

export default Component.extend({
  ...

  click() {
    ...
    this.sendAction(); // === this.sendAction('action');
    this.sendAction('other', argument);
  }
});

Il est important de comprendre que cette méthode ne propage pas l’action 'action' au travers de l’arbre des composants mais l’action définie lors de la définition du composant. Ainsi, le composant définit de la manière suivante :

{{my-component action='customAction' onSubmit='save'}}

… entraînera l’exécution de l’action 'customAction' lors d’un this.sendAction() et de l’action 'save' lors d’un this.sendAction('onSubmit', args). Ces deux actions sont à définir dans l’un des parents du composant (autre composant, controlleur, routes). L’action est propagée au travers de la hiérarchie, jusqu’à trouver un gestionnaire.

// route
actions: {
    save() {
      ...
    },
    customAction(args) {
      ...
    }
  }

Closure actions

Dans cette seconde forme, l’élément parent a passé au composant l’action elle-même, c’est à dire une fonction javascript. Le composant est donc en mesure d’exécuter directement cette méthode en lui ajoutant les paramètres dont il dispose localement.

Ainsi la définition s’effectue de la manière suivante :

{{my-component action=(action 'customAction') onSubmit=(action 'save')}}

… et l’exécution :

export default Component.extend({
  ...

  click() {
    ...
    this.get('action')();
    this.get('onSubmit')(argument);
  }
});

Tout comme dans le cas des closure actions vues au chapitre précédent, ces actions ne bubblent pas et doivent être explicitement définies dans le composant ou le contrôleur le plus proche du composant. Si nécessaire, elles peuvent être propagées via l’utilisation d’autres actions au travers de l’appel à la méthode send :

cancel() {
  this.send('onCancel');
}

Les deux formes coexistent et sont partiellement compatibles mais il semble que la seconde soit celle qui doive perdurer.

A noter qu’il est possible de mixer certaines notations même si ce n’est pas l’option la plus lisible et donc pas celle à privilégier. Par exemple :

{{my-component action='customAction' onSubmit=(action 'save')}}
export default Component.extend({
  mouseOver() {
    this.get('action')();
  },
  click() {
    this.sendAction('onSubmit');
  }
});
  1. Modifier le composant fav-btn de manière à propager une action en fin de méthode click()
    • le gestionnaire d’action doit simplement permettre de logger, dans la route, le message suivant : <comic.slug> - favorite: <comic.isFavorite>
    • cette action doit être exécutée aussi bien en consultation qu’en édition
    • les deux typologies d’actions définies plus haut peuvent être utilisées mais les closure actions sont à priliégier sauf en cas de besoin de bubbling
    • utiliser impérativement [console.log] pour cette opération

    Test : Ces modifications doivent rendre passant les tests 04 - Components - 01 - Should log on index et 04 - Components - 02 - Should log on edit

    Element space actions

    // app/components/btn-fav.js
    export default Component.extend({
      tagName: 'span',
      classNames: 'btn-fav',
      classNameBindings: 'selected',
    
      click() {
        this.toggleProperty('selected');
        // eslint-disable-next-line ember/closure-actions
        this.sendAction();
      }
    });
    
    {{!-- app/templates/comic/index.hbs --}}
    ...
    {{fav-btn selected=model.isFavorite action="favorize"}}
    ...
    
    {{!-- app/templates/comic/edit.hbs --}}
    ...
    {{fav-btn selected=model.isFavorite action="favorize"}}
    ...
    
    // app/routes/comic.js
    
    export default Route.extend({
      ...
      actions: {
        favorize () {
          const model = this.modelFor(this.routeName);
          // eslint-disable-next-line no-console
          console.log(model.get('slug'), '- favorite:', model.get('isFavorite'));
        }
      }
    });
    

    Closure actions

    // app/components/btn-fav.js
    
    export default Component.extend({
      tagName: 'span',
      classNames: 'btn-fav',
      classNameBindings: 'selected',
    
      click() {
        this.toggleProperty('selected');
        this.get('favorize')();
      }
    });
    
    {{!-- app/templates/comic/index.hbs --}}
    ...
    {{fav-btn selected=model.isFavorite favorize=(action "favorize")}}
    ...
    
    {{!-- app/templates/comic/edit.hbs --}}
    ...
    {{fav-btn selected=model.isFavorite favorize=(action "favorize")}}
    ...
    
    // app/controllers/comic/index.js
    
    export default Controller.extend({
    
      actions: {
        favorize() {
          this.send('onFavorize');
        }
      }
    });
    
    // app/controllers/comic/edit.js
    
    export default Controller.extend({
    
      actions: {
        ...
        favorize() {
          this.send('onFavorize');
        }
      }
    });
    
    // app/routes/comic.js
    export default Route.extend({
      ...
      actions: {
        onFavorize () {
          const model = this.modelFor(this.routeName);
          // eslint-disable-next-line no-console
          console.log(model.get('slug'), '- favorite:', model.get('isFavorite'));
        }
      }
    });
    

Cycle de vie des composants

Les opération de création, de rendu, de mise à jour et de destruction des composants obéissent à un cycle de vie complet constitué des différentes méthodes appelées à chaque étape. Ces méthodes sont autant de hook qu’il est possible d’étendre pour enrichir le composant et effectuer des opérations complémentaires.

Lorsque l’on souhaite surcharger l’une de ces méthodes pour y greffer nos opérations, il est généralement nécessaire d’appeler la méthode originale même si toutes ces méthodes n’ont pas nécessairement d’implémentation par défaut :

didInsertElement(args) {
  this._super(args);
  ...
}

Les cycles de vie liés au rendu initial et aux rendus ultérieurs (mises à jour) sont sensiblement différents :

Rendu initial

  1. init : Initialisation du composant, initialisation des attributs, etc.
  2. didReceiveAttrs : Appelé juste après init et à chaque mise à jour des attributs. Ce hook peut être utilisé pour effectuer des opérations complémentaires sur les attributs avant les opérations de rendu.
  3. willRender : Appelé à chaque fois que le template va être rendu, quelqu’en soit la raison. Mais avant le rendu lui même.
  4. didInsertElement : Appelé aprés le rendu (initial uniquement), une fois que le template a été totalement rendu et inséré dans le DOM. A ce moment, le composant est accessible via la notation $(). Ce hook est trés fréquemment exploité pour interagir avec des éléments issus de librairies third-party qui nécessitent d’être insérées dans le DOM avant d’être manipulés (datePicker, etc.)
  5. didRender : Appelé après l’ensemble des opérations de rendu et de mise à jour du DOM.

Rendus ultérieurs

  1. didUpdateAttrs : Appelé quand les attributs du composant sont mise à jour mais pas lors des changements de valeur des propriétés passées au compoosant. Ce hook n’est pas appelé non plus lors d’un rerender explicite.
  2. didReceiveAttrs : cf. plus haut
  3. willUpdate : Appelé à chaque fois que le template va être rendu, quelqu’en soit la raison.
  4. willRender : cf. plus haut
  5. didUpdate : Appelé lors que le DOM a été pleinement mis à jour.
  6. didRender : cf. plus haut

Des hooks sont également disponible lors de la phase de destruction :

Destruction

  1. willDestroyElement : Appelé lorsqu’un composant détecte qu’il doit être supprimé, avant sa suppression. Ce hook permet notamment de supprimer d’éventuels listeners.
  2. willClearRender : Appelé lorsque la vue contenant le composant va être renrendue.
  3. didDestroyElement : Appelé après la destruction de l’élément du composant.

La très grand majorité de ces hook est très rarement utilisée. Les plus fréquents sont didInsertElement, willDestroyElement et moins fréquement didReceiveAttrs.

  1. On souhaite désormais enrichir le composant image-cover afin qu’il affiche une image par défaut si aucune jaquette n’est disponible pour le comic. Notamment lors de la création.
    • pour cela on doit se baser sur l’évènement onerror de l’élément img racine
    • comme l’évènement onerror n’est pas un évènement qui se propage (de même que onload etc.), il n’est pas possible de s’appuyer sur les customEvents
    • on doit donc installer, via le hook approprié, un listener sur l’évènement onerror via jQuery (attention au this)
    • implémenter ce listener de manière à changer la source de l’image pour default.jpg en cas d’erreur
    • ne pas oublier de supprimer le listener avant la destruction du composant pour éviter les fuites mémoire

    Tests : Ces modifications doivent rendre passant les tests 04 - Components - 03 - Image cover should fallback et 04 - Components - 04 - Image cover should change if model changes

    //app/components/image-cover.js
    export default Component.extend({
      tagName: 'img',
      classNames: 'cover',
      attributeBindings: 'src',
        src: computed('name', function () {
          return this.getImagePath(this.get('name'));
        }),
        currentNode: computed('elementId', function() {
          return $('#' + this.get('elementId'));
        }),
      
        getImagePath(name) {
          return `/assets/images/comics/covers/${name}.jpg`;
        },
      
        didInsertElement(...args) {
          this._super(...args);
          this.get('currentNode').on('error', () => {
            return this.onError();
          });
        },
      
        willDestroyElement(){
          this.get('currentNode').off('error');
        },
      
        onError() {
          this.get('currentNode').attr('src', this.getImagePath('default'));
        }
    });