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 :
- 03-controllers-test.js dans
tests/acceptance
.
Les contrôleurs Ember se limitent à deux responsabilités :
- maintenir l’état de l’application en fonction de la route courante en s’appuyant sur leur propriétés propres. Cet état (contexte) composé de ces propriétés ainsi que du ou des modèles récupéré(s) depuis la route sont mis à disposition du template.
- déclarer et implémenter des méthodes de traitement des actions et des évènements issus de la manipulation du template (et donc du DOM) par l’utilisateur.
La fonction des contrôleurs est donc très limitée et se réduit de plus en plus. Les futurs versions d’Ember verront disparaître les contrôleurs au profit de composants dès lors que ceux-ci seront routables ce qui n’est pas encore le cas (cf. RFC).
Routes, Contrôleurs & Modèles
La route définit donc, comme vu au chapitre précédent, une méthode model
permettant de retrouver le modèle.
Le contrôleur stocke, quant à lui, dans une propriété model
le résultat de cette méthode pour l’exposer au template.
L’initialisation de la propriété model
du contrôleur à partir du résultat de la méthode model
de la route, après résolution des éventuelles promesses par Ember, s’effectue de manière totalement automatique via la méthode setupController de la route.
setupController
est appelée lorsque la route ou le modèle change, après le hook afterModel
et peut donc être surchargée de manière à provisionner, dans le contrôleur associé à la route, des éléments de contexte supplémentaires dépendant de la route courante et de ses éventuels segments dynamiques :
setupController: function (controller, model) {
this._super(controller, model);
// custom behaviour
}
L’accès au(x) contrôleur(s) au sein d’une route peut s’effectuer de différentes manières :
-
via l’accès direct à la propriété controller définie et provisionnée par Ember au sein de la route :
this.get('controller');
-
via l’utilisation de la méthode controllerFor() qui, comme la méthode modelFor pour le modèle, permet d’accéder au contrôleur associé à une route donnée :
this.controllerFor('mere.fille'); this.controllerFor(this.routeName);
L’objet modèle peut donc être récupéré, manipulé et modifié, depuis le contrôleur, via l’accés à la propriété model
et depuis la route, en passant par le contrôleur :
// controller
this.get('model');
this.set('model', newModel);
// route
this.get('controller').get('model');
this.get('controller.model');
this.get('controller').set('model', newModel);
this.set('controller.model', newModel);
On remarque dans les exemples précédents, qu’Ember permet de faciliter l’accès à des objets et propriétés imbriqués via l’utilisation de la notation .
en lieu et place de getters / setters chaînés.
Cette syntaxe plus concise est à privilégier.
Elle est utilisable pour tous les objets Ember.
Actions
En Ember, le déclenchement d’évènements s’effectue grâce aux actions. Celles-ci sont déclarées au sein des templates et propagée au sein de l’application. L’action est donc traitée au niveau le plus bas où elle est interceptée : dans le composant, puis dans le contrôleur et, enfin, au sein de la hiérarchie de routes, de bas en haut. Une erreur est logguée dans le cas ou aucun gestionnaire n’est trouvé.
{{action}} helper
La déclaration d’une action s’effectue, au sein du template, par l’utilisation du helper {{action}}
au sein d’un élément HTML, d’un composant standard ou d’un composant personnalisé.
<div {{action "save"}}>confirm</div>
<button {{action "save"}}>confirm</button>
{{input value="confirm" enter="save"}}
<input type="text" value="confirm" {{action "save"}} />
En fonction du type de composant, le déclenchement de l’action s’effectue lors de certains évènements uniquement.
Par exemple, si l’on utilise le helper {{input}}
, celui-ci ne s’effectue qu’à la “validation” du champs.
C’est à dire lorsque l’on appuie sur Entrée
.
Pour les éléments HTML standard, il s’effectue au clic, ce qui explique que les comportements de {{input}}
et de <input/>
diffèrent.
Traitement d’une action
Une action déclenchée dans le template doit donc être traitée, dans le composant, le contrôleur correspondant, ou encore dans une des routes actives.
Quelque soit l’endroit de la déclaration du gestionnaire, celle-ci s’effectue de la manière suivante, nécessairement au sein du hash actions
:
// route, controller, component
actions: {
save() {
// perform some operations
}
}
Les gestionnaires d’actions définis dans le hash actions
sont hérités depuis les routes parentes et complétés / surchargés au sein de la route courante.
Bubbling
Une fois créée et lancée, une action est automatiquement propagée de bas en haut au sein de la hiérarchie de routes jusqu’à trouver un gestionnaire qui traitera l’action et stoppera sa propagation.
Il est néanmoins possible de forcer cette propagation, suite à un premier traitement en renvoyant true
dans le gestionnaire :
// route, controller, component
actions: {
save() {
// perform some operations
return true;
}
}
Paramètre
Il est possible de passer un paramètre à l’action qui sera déclenchée, de manière à pouvoir lire et utiliser ce paramètre dans le gestionnaire. Ce paramètre peut être un litéral ou un objet.
<div {{action "save" "value"}}>confirm</div>
<button {{action "save" model.id}}>confirm</button>
<input type="text" value="confirm" {{action "save" model}} />
On peut alors récupérer la valeur du paramètre dans l’action du contrôleur ou de la route :
actions: {
save (param) {
// perform some operations
}
}
- Déclencher et intercepter l’action
save
au clic sur le bouton.btn-submit
de manière à effectuer une simple transition vers la routecomic
- Définir dans le template
comic.edit
l’action ‘save’ comme déclenchée au clic sur le bouton.btn-submit
- Définir dans la route
comic.edit
la méthode d’interception de l’action ‘save’
Test : Les modifications doivent permettre de rendre le test 03 - Controller - 01 - Should save on edit submit passant.
NB : Il n’est pas nécessaire d’effectuer d’autre opération car les modifications effectuées onté déjà été répercutées automatiquement grâce au binding bidirectionnel.
{{!-- app/templates/comic/edit.hbs --}} <form> ... <div class="buttons"> <button type="submit" {{action "save"}} class="btn-submit"></button> <button type="reset" class="btn-cancel"></button> </div> ... </form>
// app/routes/comic/edit.js import Route from '@ember/routing/route'; export default Route.extend({ actions: { save () { this.transitionTo('comic'); } } });
- Définir dans le template
- Déclencher et intercepter l’action
cancel
au clic sur le bouton.btn-cancel
de manière à annuler les modifications effectuées sur le model courant puis effectuer une transition vers la routecomic
- Comme on ne dispose pas d’un
store
avancé comme cela sera le cas avec Ember Data, il est nécessaire d’effectuer en premier lieu une copie du modèle initial de manière à pouvoir le réinitialiser par la suite. Dans quelle méthode doit-on effectuer cette copie préalable ? -
De manière à réinitialiser proprement le modèle sans outil tel qu’Ember Data, implémenter la méthode
reset
suivante dansapp/model/comic.js
:... reset(comic) { this.set('title', comic.get('title')); this.set('scriptwriter', comic.get('scriptwriter')); this.set('illustrator', comic.get('illustrator')); this.set('publisher', comic.get('publisher')); }
- Réinitialiser le modèle en cas de
cancel
Test : Les modifications doivent permettre de rendre passant le test 03 - Controller - 02 - Should cancel on edit reset
{{!-- app/templates/comic/edit.hbs --}} <form> ... <div class="buttons"> <button type="submit" {{action "save"}} class="btn-submit"></button> <button type="reset" {{action "cancel"}} class="btn-cancel"></button> </div> ... </form>
// app/routes/comic/edit.js import Route from '@ember/routing/route'; import Comic from 'ember-training/models/comic'; export default Route.extend({ afterModel (model) { this.set('initialModel', Comic.create(model)); }, actions: { save () { this.transitionTo('comic'); }, cancel () { this.get('controller.model').reset(this.get('initialModel')); this.transitionTo('comic'); } } });
La copie initiale du modèle se fait évidement dans le hook
afterModel
puisque c’est dans celui-là seulement que l’on dispose du modèle initialisé. Cet objet est conservé dans une propriétéinitialModel
.La réinitialisation du modèle lui-même s’effectue en appelant la méthode
reset
avec le modèle conservé. On note l’utilisation de la notation chaînée.
- Comme on ne dispose pas d’un
- Intercepter et traiter les actions ‘save’ et ‘cancel’ pour la route
comics.create
- Rediriger vers la route
comic.index
du nouveau comic suite à validation. - Nettoyer la liste de comics et rediriger vers la route
comics
suite à annulation. Utiliser pour cela la fonction removeObject() deEmber.MutableArray
. - Transformer la propriété
slug
d’unComic
en computed propety de manière à ce que le slug corresponde à la valeur du titre transformée grâce à la fonction dasherize() et qu’il soit mis à jour à chaque modification du titre. Supprimer l’affectation de la valeur par défautnew
à la propriétéslug
lors de la création du comic dans la routecreate
. Attention cependant à bien conserver, dans la nouvelle propriété calculée, l’affectation de la valeur par défaut dans le cas outitle
n’est pas défini.
Test : Les modifications doivent permettre de rendre passants les tests 03 - Controller - 03 - Should save on create submit et 03 - Controller - 04 - Should reinit list on edit reset
// app/models/comic.js import EmberObject, { computed } from '@ember/object'; export default EmberObject.extend({ slug: computed('title', function() { const title = this.get('title') || 'new'; return title.dasherize(); }), title: '', scriptwriter: '', illustrator: '', publisher: '', reset (comic) { ... } });
// app/routes/comics/create.js import Route from '@ember/routing/route'; import Comic from 'ember-training/models/comic'; export default Route.extend({ templateName: 'comic/edit', model () {...}, actions: { save () { this.transitionTo('comic', this.get('controller.model')); }, cancel () { this.modelFor('comics').removeObject(this.get('controller.model')); this.transitionTo('comics'); } } });
On note le passage du model à la route
comic
lors de la transition suite ausave
puisque celui-ci vient d’être créé et était inconnu. Attention on remarque dans la console l’apparition de ligne deprecated, lié à la surcharge de la propriété calculé slug dans la route comics. Pour palier à ce problème il faut supprimer l’initialisation de la propriété slug qui sera traitée de manière automatique. On à également des lignes dépréciées pour la partie test, en effet il faut changer l’initialisation des comic via EmberObject et passer directement par le model Ember. - Rediriger vers la route
Evènements DOM
Lorsque l’on déclare une action, il est également possible de préciser explicitement le type d’évènement DOM que l’on souhaite lier à l’action de la manière suivante :
<div {{action "save" "value" on "doubleClick"}}>confirm</div>
<button {{action "save" model.id on "mouseUp"}}>confirm</button>
{{input enter=(action "save" model.id) value="confirm"}}
<input type="text" value="confirm" onclick={{action 'save' model}} />
- Les éléments html standards peuvent manipuler tout type d’évènement natif.
- Les {{action … on … }} peuvent gérer les évènements.
- Les évènements gérés par le helper
{{input}}
sont listés dans la documentation.
On remarque au passage, concernant l’utilisation du helper {{input}}
, l’utilisation d’une sous-expression Handlebars via la notation {{input ... (action ...)}}
.
Cette notation permet l’imbrication d’expressions au sein des helpers.
Types d’actions
Il existe en réalité aujourd’hui, pour des raisons historiques, deux types d’actions différentes pouvant être définies depuis un template.
Il s’agit des element space actions
d’un côté dont le fonctionnement s’appuie intégralement sur le bubbling et des closure actions
de l’autre qui doivent être intercéptées obligatoirement dans un contrôleur ou un composant et qui ne font pas intervenir de bubbling à ce niveau.
-
les element space actions sont les actions historiques d’Ember. Elles interviennent lors de l’utilisation des syntaxes standard telles que :
<div {{action "save" model}}>confirm</div> <div {{action "save" model on "mouseUp"}}>confirm</div> {{input enter="save" value="confirm"}}
Ces actions peuvent être indifféremment intercéptées dans un contrôleur, un composant ou une route.
-
les closure actions constituent un nouveau types d’actions. Elles interviennent lors de l’utilisation des syntaxes imbriquées ou précisant les évènements DOM telles que :
{{input enter=(action "save" model.id) value="confirm"}} <input type="text" value="confirm" onclick={{action "save" model}} />
Entre le template et le contrôleur / composant, ces actions ne se propagent pas via bubling et doivent impérativement être interceptées dans un contrôleur ou un composant et, éventuellement, propagées explicitement.
NB: Cette situation est problématique mais temporaire et les closure actions sont amenées à devenir le seul système de gestion des actions entre le template et le contrôleur / composant dans un avenir proche. Pour d’avantage de détails, se reporter à cette RFC.
Propagation explicite des actions
Les composants, contrôleurs et routes permettent donc de définir et de propager explicitement des actions via la méthode send(actionName, context).
Cette méthode permet de propager une action de nom actionName
associée éventuellement à un context
(objet, litéral, fonction, etc.) selon les mécanismes standards de bubbling décrits plus haut.
La recherche commence au sein même de l’objet courant et se propage en l’absence de gestionnaire local.
this.send("save", model);
Dans le cas des closure actions, c’est cette méthode qu’il est nécessaire d’utiliser pour permettre, si nécessaire, la propagation de l’action et de son contexte depuis le contrôleur ou le composant vers la route. L’action ainsi créée suit alors les règles de propagation et de bubbling standard définies plus haut.
Actions standards Ember
Ember fournit un certain nombre d’actions natives propagées automatiquement et interceptables au sein des routes.
Le traitement de ces actions se fait de la même manière que les actions vues précédement définies dans les templates, au sein du hash actions
:
-
error : Une action
error
est levée lorsqu’une promesse est rejetée au sein de l’un des hooks de la route (échec dans la récupération du modèle, etc.). La levée ainsi que la propagation de cette action via le bubbling permet la gestion de l’erreur à n’importe quel niveau de la hiérarchie de route.actions: { /* * @error: thrown error * @transition: failed transition */ error: function(error, transition) { ... } }
-
loading : Une action
loading
est levée lorsque l’un des hooks de la route retourne une promesse non encore résolue.actions: { /* * @transition: current transition * @route: route that triggered the event */ loading: function(transition, route) { ... } }
-
didTransition : Une action
didTransition
est levée lorsque la transition s’est effectuée complètement, c’est à dire après l’exécution des hooks d’entrée (beforeModel
,model
,afterModel
,setupController
). Cette action est courament utilisée pour des opération de tracking (visites, etc.).actions: { didTransition: function() { ... } }
-
willTransition : Une action
willTransition
est levée lorsqu’une tentative de transition est effectuée depuis cette route.actions: { /* * @transition: attempted transition */ willTransition: function(transition) { ... } }
- Modifier la route
comic.edit
pour gérer l’actionwillTransition
de manière à ce que si l’utilisateur change de route (en cliquant sur un autre comic par exemple) sans avoir sauvegardé, l’ensemble des modifications soient annulées.- L’annulation des modifications correspond aux mêmes opérations que celles effectuées lors d’un
cancel
- Conserver la propagation de l’action
willTransition
aux routes parentes.
Test : Les modifications doivent permettre de rendre passant le test 03 - Controller - 05 - Should cancel edit on transition
// app/routes/comic/edit.js afterModel (model) { ... }, resetComic () { this.get('controller.model').reset(this.get('initialModel')); }, actions: { save () { this.transitionTo('comic'); }, cancel () { this.resetComic(); this.transitionTo('comic'); }, willTransition () { this.resetComic(); return true; } }
On remarque que le save ne fonctionne plus (le test 03 - Controller - 01 - Should save on edit submit ne passe plus) et que les changements semblent être annulées systématiquement. L’action
willTransition
est en effet exécutée après les autres actions et notament lesave
qui déclenche une transition viatransitionTo
. De ce fait, quelques soient les opérations effectuées dans lesave
, les annulations effectuées parwillTransition
sont appliquées. - L’annulation des modifications correspond aux mêmes opérations que celles effectuées lors d’un
- Créer le contrôleur
app/controllers/comic/edit.js
et y intercepter les actionssave
etcancel
- Ces actions se contentent de positionner une propriété
hasUserSavedOrCancel
àtrue
dans le contrôleur de manière à signaler que l’utilisateur a délibérément effectué une opération. - Faire que ces actions continuent de se propager à la route.
- Modifier le gestionnaire de l’action
willTransition
de manière à n’effectuer les opérations d’annulation que si l’utilisateur n’a effectué aucune des deux actionssave
oucancel
- Comme on le verra plus tard, l’utilisation d’un outil tel qu’Ember Data permet, via les fonctions avancées de gestion de l’état des modèles et du
store
, d’éviter d’avoir à effectuer nous mêmes ces contrôles.
Test : Les modifications doivent permettre de rendre de nouveau passant le test 03 - Controller - 01 - Should save on edit submit
// app/controllers/comic/edit.js import Controller from '@ember/controller'; export default Controller.extend({ actions: { save() { this.set('hasUserSavedOrCancel', true); return true; }, cancel() { this.set('hasUserSavedOrCancel', true); return true; } } });
// app/routes/comic/edit.js actions: { save () { ... }, cancel () { ... }, willTransition () { if (!this.controller.get('hasUserSavedOrCancel')) { this.resetComic(); } return true; } }
- Ces actions se contentent de positionner une propriété
On remarque ici un autre effet de bord.
En effet, si les modifications semblent bien avoir permis de rentre le save
de nouveau opérationnel, willTransition
ne semble plus exécutée après un save
.
Pour le constater :
- Editer un premier comic et l’annuler via
willTransition
en cliquant sur un autre. Les modifications sont bien annulées. - Editer à nouveau un comic et sauvegarder les modifications. Celles-ci sont bien sauvegardées.
- Editer encore un comic et l’annuler via
willTransition
. Cette fois les modifications ne sont pas annulées.
Ceci est dû au fait que les contrôleurs Ember sont des singletons. Ainsi, pour une même route, le même contôleur est toujours réutilisé. Il est donc nécessaire de s’assurer, à chaque accès à la route, que l’état géré par ce contrôleur et éventuellement modifié lors du dernier accès est bien réinitialisé.
Dans notre cas, la propriété hasUserSavedOrCancel
a été conservée à true
laissant penser à willTransition
qu’une action utilisateur avait été effectuée.
Comme on l’a évoqué dans le chapitre précédent, les routes Ember disposent d’une méthode resetController appelée systématiquement lorsque la route ou le modèle change qui permet de laisser, en partant, le contrôleur dans un état stable, prêt pour une utilisation ultérieure.
-
Implémenter, dans la route
comic.edit
, le hookresetController
de manière à réinitialiser la propriétéhasUserSavedOrCancel
àfalse
Test : Les modifications doivent permettre de rendre passant le test 03 - Controller - 06 - Should call willTransition on edit despite an old save
// app/routes/comic/edit.js afterModel (model) { ... }, resetController (controller) { controller.set('hasUserSavedOrCancel', false); }, resetComic () { ... },
-
On souhaite enfin proposer une confirmation à l’utilisateur lors du
willTransition
avant d’annuler les changements.- Afficher une alerte javascript de confirmation lors du
willTransition
demandant confirmation que l’utilisateur souhaite abandonner ses changements - En cas de réponse positive, poursuivre les opérations du
willTransition
- En cas de réponse négative, annuler la transaction pour rester sur la route courante
Tests : Les modifications doivent permettre de rendre passants les tests 03 - Controller - 07 - Should cancel edit after confirm true et 03 - Controller - 08 - Should abort edit after confirm false
// app/routes/comic/edit.js actions: { save () { ... }, cancel () { ... }, willTransition (transition) { if (this.controller.get('hasUserSavedOrCancel')) { return true; } else if (confirm('Are you sure you want to abandon progress?')) { this.resetComic(); return true; } else { transition.abort(); } } }
- Afficher une alerte javascript de confirmation lors du
Contrôleurs
Association explicite de contrôleur
On a utilisé jusqu’à présent les controllers implicites d’Ember ou implémenté le contrôleur standard en respectant les conventions de nommage.
Dans certains cas, cependant, il peut être utile de spécifier explicitement le contrôleur que l’on souhaite associer à la route de manière à réutiliser un contrôleur existant.
Cela s’effectue grâce à la propriété controllerName de la route :
export default Ember.Route.extend({
controllerName: 'another/controller',
});
NB : On peut également utiliser la méthode this.controllerFor(‘another/route’) de manière à récupérer le controller d’une autre route et l’affecter explicitement à la route courante dans le hook setupController. Cependant cette méthode est moins élégante et peut générer des effets de bord. Elle n’est pas à privilégier. Noter également que le contrôleur en question doit impérativement avoir été créé (notamment parce que la route correspondante est active).
- Modifier la route
comics.create
pour implémenter le gestionnaire d’actionwillTransition
selon les mêmes principes que pourcomic.edit
- Réutiliser le contrôleur de
comic.edit
- Ne pas oublier de réinitialiser le contrôleur
Tests : Les modifications doivent permettre de rendre passants les tests 03 - Controller - 09 - Should cancel create on transition, 03 - Controller - 10 - Should call willTransition on create despite an old save, 03 - Controller - 11 - Should cancel create after confirm true et 03 - Controller - 12 - Should abort create after confirm false
export default Route.extend({ templateName: 'comic/edit', controllerName: 'comic/edit', model () { ... }, resetController (controller) { controller.set('hasUserSavedOrCancel', false); }, resetComic () { this.modelFor('comics').removeObject(this.get('controller.model')); }, actions: { save () { ... }, cancel () { this.resetComic(); this.transitionTo('comics'); }, willTransition (transition) { if (this.controller.get('hasUserSavedOrCancel')) { return true; } else if (confirm('Are you sure you want to abandon progress?')) { this.resetComic(); return true; } else { transition.abort(); } } } });
- Réutiliser le contrôleur de
Gestion de l’état et propriétés
Comme on l’a dit, la responsabilité principale des contrôleurs est de maintenir l’état de l’application à un instant donné. cela s’effectue au travers de la définition et de la manipulation de propriétés au sein du contrôleur. La valeur de ces propriétés est exposée au template qui peut ensuite provoquer des changements de valeur au travers des actions. L’utilisation de propriétés calculées au sein même du contrôleur permet ensuite de propager automatiquement ce changement partout où cela est nécessaire.
- Nous allons maintenant ajouter un champ permettant de filtrer la liste les comics ainsi qu’un bouton de tri permettant de trier les comics dans un ordre croissant ou décroissant.
- Créer le contrôleur
app/controllers/comics.js
en se basant sur le modèle proposé plus bas. - Implémenter le corps de l’action
sort
de manière à inverser la valeur de la propriétésortAsc
. Indice : utiliser pour cela une méthode de Controller qui permet d’inverser la valeur d’une propriété booléenne. - Compléter la propriété
filteredComics
afin que celle-ci se base sur la collection récupérée initialement. - Compléter la liste des propriétés observées par la propriété calculée
filteredComics
de manière à ce que celle-ci soit recalculée à chaque fois que la propriétéfilter
change, chaque fois que l’on ajoute ou supprime un comic dans la liste et enfin lorsque l’on modifie le titre de n’importe quel comic. - Compléter la propriété observée par
sortDefinition
de manière à ce que celle-ci soit recalculée chaque fois que la direction du tri est modifiée. - Compléter la propriété
sortedComics
afin que celle-ci se base sur la collection filtrée (filteredComics
). - On remarque l’utilisation de la méthode Ember.computed.filter qui permet de filtrer facilement une collection et de la méthode Ember.computed.sort qui permet, elle, de faciliter le tri.
Cette dernière s’appuie également sur une propriété calculée définissant les caractéristiques du tri (propriété, ordre). Ici
['title:asc']
ou['title:desc']
. - Modifier le template
app/templates/comics.hbs
en se basant sur le modèle proposé plus bas. - Ajouter avant la liste de comics un
input
permettant de modifier la valeur defilter
ainsi qu’un bouton permettant de déclencher l’actionsort
. Ce bouton doit porter les classes cssbtn-sort sort-asc
oubtn-sort sort-desc
en fonction de la valeur desortAsc
. - Modifier la collection parcourue par le
{{#each}}
de façon à utiliser la liste triée. - Enfin, modifier le span de classe
comics-number
afin d’afficher, en temps réel, le nombre de comics triés (ne pas modifier le contrôleur).
import Controller from '@ember/controller'; import { computed } from '@ember/object'; import { filter, sort } from '@ember/object/computed'; export default Controller.extend({ filter: "", sortAsc: true, filteredComics: filter(..., [???], function (model) { const title = model.get('title'); return !title || title.toLowerCase().match(new RegExp(this.get('filter').toLowerCase())); }), sortDefinition: computed(???, function () { return ["title:" + (this.get('sortAsc') ? 'asc' : 'desc')]; }), sortedComics: sort(???, 'sortDefinition'), actions: { sort () { // @TODO ??? } } });
<div class="comics"> <h2 class="comics-title">Comics list</h2> <div class="comics-filter"> {{input type=text value=... class="filter"}} <button ??? class="???"></button> </div> <ul class="comics-list"> {{#each ??? as |comic|...}} </ul> {{link-to '' 'comics.create' class="add-comic"}} <span class="comics-number">Number of comics: ???</span> </div> {{outlet}}
Tests : Les modifications doivent permettre de rendre passants les tests 03 - Controller - 13 - Should filter, 03 - Controller - 14 - Should sort ainsi que l’ensemble des tests unitaires du controller comics
// app/controllers/comics.js import Controller from '@ember/controller'; import { computed } from '@ember/object'; import { filter, sort } from '@ember/object/computed'; export default Controller.extend({ filter: "", sortAsc: true, filteredComics: filter('model', ['filter', 'model.[]', 'model.@each.title'], function (model) { const title = model.get('title'); return !title || title.toLowerCase().match(new RegExp(this.get('filter').toLowerCase())); }), sortDefinition: computed('sortAsc', function () { return ["title:" + (this.get('sortAsc') ? 'asc' : 'desc')]; }), sortedComics: sort('filteredComics', 'sortDefinition'), actions: { sort () { this.toggleProperty('sortAsc'); } } });
{{!-- app/templates/comics.hbs --}} <div class="comics"> <h2 class="comics-title">Comics list</h2> <div class="comics-filter"> <Input type="text" @value={{filter}} class="filter"/> <button {{action "sort"}} class="btn-sort {{if sortAsc "sort-asc" "sort-desc"}}"></button> </div> <ul class="comics-list"> {{#each sortedComics as |comic|}} <li class="{{if comic.scriptwriter "comic-with-scriptwriter" "comic-without-scriptwriter"}} comics-list-item"> {{#link-to "comic" comic}} {{comic.title}} by {{if comic.scriptwriter comic.scriptwriter "unknown scriptwriter"}} {{/link-to}} </li> {{else}} Sorry, no comic found {{/each}} </ul> {{link-to "" "comics.create" class="add-comic"}} <span class="comics-number">Number of comics: {{sortedComics.length}}</span> </div> {{outlet}}
- Créer le contrôleur
Il faut également changer le test Controller - 07 - Should cancel edit after confirm true qui devient non passant, car le tri est inversé. il faut donc remplacer await click(“.comics .comics-list > .comics-list-item:first-child > a”); par await click(“.comics .comics-list > .comics-list-item:last-child > a”);