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 :
- 04-components-test.js dans
tests/acceptance
. - fav-btn-test.js dans
tests/integration/components
. - image-cover-test.js dans
tests/integration/components
.
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
- 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 classecover
. - 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
etapp/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"> ...
- Créer un composant
- 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. - Modifier le template
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.
- 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() {
// ...
}
});
- 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
etcomic/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}}
- il doit porter la classe
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');
}
});
- Modifier le composant
fav-btn
de manière à propager une action en fin de méthodeclick()
- 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')); } } });
- le gestionnaire d’action doit simplement permettre de logger, dans la route, le message suivant :
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
init
: Initialisation du composant, initialisation des attributs, etc.didReceiveAttrs
: Appelé juste aprèsinit
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.willRender
: Appelé à chaque fois que le template va être rendu, quelqu’en soit la raison. Mais avant le rendu lui même.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.)didRender
: Appelé après l’ensemble des opérations de rendu et de mise à jour du DOM.
Rendus ultérieurs
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.didReceiveAttrs
: cf. plus hautwillUpdate
: Appelé à chaque fois que le template va être rendu, quelqu’en soit la raison.willRender
: cf. plus hautdidUpdate
: Appelé lors que le DOM a été pleinement mis à jour.didRender
: cf. plus haut
Des hooks sont également disponible lors de la phase de destruction :
Destruction
willDestroyElement
: Appelé lorsqu’un composant détecte qu’il doit être supprimé, avant sa suppression. Ce hook permet notamment de supprimer d’éventuels listeners.willClearRender
: Appelé lorsque la vue contenant le composant va être renrendue.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
.
- 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émentimg
racine - comme l’évènement
onerror
n’est pas un évènement qui se propage (de même queonload
etc.), il n’est pas possible de s’appuyer sur lescustomEvents
- on doit donc installer, via le hook approprié, un listener sur l’évènement
onerror
via jQuery (attention authis
) - 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')); } });
- pour cela on doit se baser sur l’évènement