Un guide pour créer votre première application Ember.js
Publié: 2022-03-11Alors que les applications Web modernes fonctionnent de plus en plus côté client (le fait même que nous les appelions désormais «applications Web» par opposition à «sites Web» est assez révélateur), il y a eu un intérêt croissant pour les frameworks côté client. . Il y a beaucoup d'acteurs dans ce domaine mais pour les applications avec beaucoup de fonctionnalités et de nombreuses pièces mobiles, deux d'entre eux se démarquent particulièrement : Angular.js et Ember.js.
Nous avons déjà publié un [tutoriel Angular.js][https://www.toptal.com/angular-js/a-step-by-step-guide-to-your-first-angularjs-app] complet, donc nous ' Nous allons nous concentrer sur Ember.js dans cet article, dans lequel nous allons créer une application Ember simple pour cataloguer votre collection de musique. Vous découvrirez les principaux éléments constitutifs du cadre et aurez un aperçu de ses principes de conception. Si vous souhaitez voir le code source pendant la lecture, il est disponible sous forme de rock-and-roll sur Github.
Qu'allons-nous construire ?
Voici à quoi ressemblera notre application Rock & Roll dans sa version finale :
Sur la gauche, vous verrez que nous avons une liste d'artistes et, sur la droite, une liste de chansons de l'artiste sélectionné (vous pouvez également voir que j'ai de bons goûts en musique, mais je m'égare). De nouveaux artistes et chansons peuvent être ajoutés simplement en tapant dans la zone de texte et en appuyant sur le bouton adjacent. Les étoiles à côté de chaque chanson servent à la noter, à la iTunes.
Nous pourrions décomposer les fonctionnalités rudimentaires de l'application en les étapes suivantes :
- Cliquer sur « Ajouter » ajoute un nouvel artiste à la liste, avec un nom spécifié par le champ « Nouvel artiste » (il en va de même pour les chansons d'un artiste donné).
- Vider le champ "Nouvel artiste" désactive le bouton "Ajouter" (il en va de même pour les chansons d'un artiste donné).
- Cliquer sur le nom d'un artiste liste ses chansons sur la droite.
- Cliquer sur les étoiles évalue une chanson donnée.
Nous avons un long chemin à parcourir pour que cela fonctionne, alors commençons.
Routes : la clé de l'application Ember.js
L'une des caractéristiques distinctives d'Ember est l'accent mis sur les URL. Dans de nombreux autres frameworks, avoir des URL séparées pour des écrans séparés fait défaut ou est ajouté après coup. Dans Ember, le routeur - le composant qui gère les URL et les transitions entre elles - est la pièce centrale qui coordonne le travail entre les blocs de construction. Par conséquent, c'est aussi la clé pour comprendre le fonctionnement interne des applications Ember.
Voici les itinéraires pour notre application :
App.Router.map(function() { this.resource('artists', function() { this.route('songs', { path: ':slug' }); }); });
Nous définissons une route de ressources, artists
et une route de songs
imbriquées à l'intérieur. Cette définition nous donnera les itinéraires suivants :
J'ai utilisé le super plugin Ember Inspector (il existe à la fois pour Chrome et Firefox) pour vous montrer les routes générées de manière facilement lisible. Voici les règles de base pour les routes Ember, que vous pouvez vérifier pour notre cas particulier à l'aide du tableau ci-dessus :
Il existe une route d'
application
implicite.Ceci est activé pour toutes les demandes (transitions).
Il existe une route d'
index
implicite.Celui-ci est saisi lorsque l'utilisateur accède à la racine de l'application.
Chaque route de ressource crée une route avec le même nom et crée implicitement une route d'index en dessous.
Cet itinéraire d'index est activé lorsque l'utilisateur navigue vers l'itinéraire. Dans notre cas,
artists.index
est déclenché lorsque l'utilisateur accède à/artists
.Une route imbriquée simple (sans ressource) aura son nom de route parent comme préfixe.
La route que nous avons définie comme
this.route('songs', ...)
auraartists.songs
comme nom. Il se déclenche lorsque l'utilisateur navigue vers/artists/pearl-jam
ou/artists/radiohead
.Si le chemin n'est pas donné, il est supposé être égal au nom de la route.
Si le chemin contient un
:
, il est considéré comme un segment dynamique .Le nom qui lui est attribué (dans notre cas,
slug
) correspondra à la valeur dans le segment approprié de l'url. Le segmentslug
ci-dessus aura la valeurpearl-jam
,radiohead
ou toute autre valeur extraite de l'URL.
Afficher la liste des artistes
Dans un premier temps, nous allons construire l'écran qui affiche la liste des artistes sur la gauche. Cet écran doit être montré aux utilisateurs lorsqu'ils naviguent vers /artists/
:
Pour comprendre comment cet écran est rendu, il est temps d'introduire un autre principe de conception global d'Ember : la convention plutôt que la configuration . Dans la section ci-dessus, nous avons vu que /artists
active la route des artists
. Par convention, le nom de cet objet route est ArtistsRoute
. C'est la responsabilité de cet objet route de récupérer les données pour que l'application s'affiche. Cela se produit dans le crochet du modèle de la route :
App.ArtistsRoute = Ember.Route.extend({ model: function() { var artistObjects = []; Ember.$.getJSON('http://localhost:9393/artists', function(artists) { artists.forEach(function(data) { artistObjects.pushObject(App.Artist.createRecord(data)); }); }); return artistObjects; } });
Dans cet extrait, les données sont récupérées via un appel XHR depuis le back-end et, après conversion en un objet de modèle, poussées vers un tableau que nous pouvons ensuite afficher. Cependant, les responsabilités de l'itinéraire ne s'étendent pas à la fourniture d'une logique d'affichage, qui est gérée par le contrôleur. Nous allons jeter un coup d'oeil.
Hum, en fait, nous n'avons pas besoin de définir le contrôleur à ce stade ! Ember est suffisamment intelligent pour générer le contrôleur en cas de besoin et définir l'attribut M.odel
du contrôleur sur la valeur de retour du crochet de modèle lui-même, à savoir la liste des artistes. (Encore une fois, c'est le résultat du paradigme de la "convention sur la configuration".) Nous pouvons descendre d'une couche et créer un modèle pour afficher la liste :
<script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> {{#each model}} {{#link-to "artists.songs" this class="list-group-item artist-link"}} {{name}} <span class="pointer glyphicon glyphicon-chevron-right"></span> {{/link-to}} {{/each}} </div> </div> <div class="col-md-8"> <div class="list-group"> {{outlet}} </div> </div> </script>
Si cela vous semble familier, c'est parce qu'Ember.js utilise des modèles Handlebars, qui ont une syntaxe et des aides très simples mais ne permettent pas une logique non triviale (par exemple, ORing ou ANDing termes dans un conditionnel).
Dans le modèle ci-dessus, nous parcourons le modèle (configuré précédemment par la route vers un tableau contenant tous les artistes) et pour chaque élément qu'il contient, nous rendons un lien qui nous amène à la route artists.songs
pour cet artiste. Le lien contient le nom de l'artiste. L'assistant #each
dans Handlebars change la portée à l'intérieur de l'élément actuel, donc {{name}}
fera toujours référence au nom de l'artiste qui est actuellement en cours d'itération.
Itinéraires imbriqués pour les vues imbriquées
Un autre point d'intérêt dans l'extrait ci-dessus : {{outlet}}
, qui spécifie les emplacements dans le modèle où le contenu peut être rendu. Lors de l'imbrication de routes, le modèle de la route de ressource externe est rendu en premier, suivi de la route interne, qui restitue son contenu de modèle dans le {{outlet}}
défini par la route externe. C'est exactement ce qui se passe ici.
Par convention, toutes les routes restituent leur contenu dans le modèle du même nom. Ci-dessus, l'attribut data-template-name
du modèle ci-dessus est artists
ce qui signifie qu'il sera rendu pour la route externe, artists
. Il spécifie une sortie pour le contenu du panneau de droite, dans lequel la route interne, artists.index
rend son contenu :
<script type="text/x-handlebars" data-template-name="artists/index"> <div class="list-group-item empty-list"> <div class="empty-message"> Select an artist. </div> </div> </script>
En résumé, un itinéraire ( artists
) restitue son contenu dans la barre latérale gauche, son modèle étant la liste des artistes. Un autre itinéraire, artists.index
rend son propre contenu dans l'emplacement fourni par le modèle d' artists
. Il pourrait récupérer des données pour lui servir de modèle, mais dans ce cas, tout ce que nous voulons afficher est du texte statique, nous n'en avons donc pas besoin.
Créer un artiste
Partie 1 : Liaison de données
Ensuite, nous voulons pouvoir créer des artistes, pas seulement regarder une liste ennuyeuse.
Quand j'ai montré ce modèle d' artists
qui affiche la liste des artistes, j'ai un peu triché. J'ai découpé la partie supérieure pour me concentrer sur ce qui est important. Maintenant, je rajoute ça :
<script type="text/x-handlebars" data-template-name="artists"> <div class="col-md-4"> <div class="list-group"> <div class="list-group-item"> {{input type="text" class="new-artist" placeholder="New Artist" value=newName}} <button class="btn btn-primary btn-sm new-artist-button" {{action "createArtist"}} {{bind-attr disabled=disabled}}>Add</button> </div> < this is where the list of artists is rendered > ... </script>
Nous utilisons un assistant Ember, input
, avec le type text pour rendre une entrée de texte simple. Dans celui-ci, nous lions la valeur de l'entrée de texte à la propriété newName
du contrôleur qui sauvegarde ce modèle, ArtistsController
. Par conséquent, lorsque la propriété value de l'entrée change (en d'autres termes, lorsque l'utilisateur y tape du texte), la propriété newName
sur le contrôleur sera synchronisée.
Nous indiquons également que l'action createArtist
doit être déclenchée lorsque le bouton est cliqué. Enfin, nous lions la propriété disabled du bouton à la propriété disabled
du contrôleur. Alors, à quoi ressemble le contrôleur ?
App.ArtistsController = Ember.ArrayController.extend({ newName: '', disabled: function() { return Ember.isEmpty(this.get('newName')); }.property('newName') });
newName
est défini sur vide au début, ce qui signifie que la saisie de texte sera vide. (Vous vous souvenez de ce que j'ai dit à propos des liaisons ? Essayez de modifier newName
et voyez-le se refléter sous forme de texte dans le champ de saisie.)

disabled
est implémenté de telle sorte que lorsqu'il n'y a pas de texte dans la zone de saisie, il va retourner true
et donc le bouton sera désactivé. L'appel .property
à la fin en fait une "propriété calculée", une autre tranche délicieuse du gâteau Ember.
Les propriétés calculées sont des propriétés qui dépendent d'autres propriétés, qui peuvent elles-mêmes être « normales » ou calculées. Ember met en cache la valeur de ceux-ci jusqu'à ce que l'une des propriétés dépendantes change. Il recalcule ensuite la valeur de la propriété calculée et la met à nouveau en cache.
Voici une représentation visuelle du processus ci-dessus. Pour résumer : lorsque l'utilisateur saisit le nom d'un artiste, la propriété newName
se met à jour, suivie de la propriété disabled
et, enfin, le nom de l'artiste est ajouté à la liste.
Détour : une seule source de vérité
Penses-y un moment. À l'aide de liaisons et de propriétés calculées, nous pouvons établir des données (modèles) comme source unique de vérité . Ci-dessus, une modification du nom du nouvel artiste déclenche une modification de la propriété du contrôleur, qui à son tour déclenche une modification de la propriété désactivée. Lorsque l'utilisateur commence à taper le nom du nouvel artiste, le bouton s'active, comme par magie.
Plus le système est grand, plus nous tirons parti du principe de la « source unique de vérité ». Il garde notre code propre et robuste, et nos définitions de propriétés, plus déclaratives.
Certains autres frameworks mettent également l'accent sur le fait que les données du modèle soient la seule source de vérité, mais ne vont pas aussi loin qu'Ember ou ne parviennent pas à faire un travail aussi approfondi. Angular, par exemple, a des liaisons bidirectionnelles, mais n'a pas de propriétés calculées. Il peut « émuler » des propriétés calculées à l'aide de fonctions simples ; le problème ici est qu'il n'a aucun moyen de savoir quand actualiser une "propriété calculée" et recourt donc à une vérification sale et, à son tour, entraîne une perte de performances, particulièrement notable dans les applications plus importantes.
Si vous souhaitez en savoir plus sur le sujet, je vous recommande de lire le billet de blog d'eviltrout pour une version plus courte ou cette question Quora pour une discussion plus longue dans laquelle les principaux développeurs des deux côtés interviennent.
Partie 2 : Gestionnaires d'action
Revenons en arrière pour voir comment l'action createArtist
est créée après son déclenchement (après l'appui sur le bouton) :
App.ArtistsRoute = Ember.Route.extend({ ... actions: { createArtist: function() { var name = this.get('controller').get('newName'); Ember.$.ajax('http://localhost:9393/artists', { type: 'POST', dataType: 'json', data: { name: name }, context: this, success: function(data) { var artist = App.Artist.createRecord(data); this.modelFor('artists').pushObject(artist); this.get('controller').set('newName', ''); this.transitionTo('artists.songs', artist); }, error: function() { alert('Failed to save artist'); } }); } } });
Les gestionnaires d'action doivent être encapsulés dans un objet d' actions
et peuvent être définis sur la route, le contrôleur ou la vue. J'ai choisi de le définir sur la route ici car le résultat de l'action n'est pas confiné au contrôleur mais plutôt "global".
Il n'y a rien d'extraordinaire ici. Une fois que le back-end nous a informés que l'opération de sauvegarde s'est terminée avec succès, nous faisons trois choses, dans l'ordre :
- Ajoutez le nouvel artiste au modèle du modèle (tous les artistes) afin qu'il soit restitué et que le nouvel artiste apparaisse comme dernier élément de la liste.
- Effacez le champ de saisie via la liaison
newName
, ce qui nous évite d'avoir à manipuler directement le DOM. - Transition vers un nouvel itinéraire (
artists.songs
), en passant l'artiste fraîchement créé comme modèle pour cet itinéraire.transitionTo
est le moyen de se déplacer entre les routes en interne. (Lelink-to
assistant sert à le faire via l'action de l'utilisateur.)
Afficher les chansons d'un artiste
Nous pouvons afficher les chansons d'un artiste soit en cliquant sur le nom de l'artiste. On y passe aussi l'artiste qui va devenir le modèle du nouveau parcours. Si l'objet de modèle est ainsi transmis, le hook de model
de la route ne sera pas appelé puisqu'il n'est pas nécessaire de résoudre le modèle.
La route active ici est artists.songs
et donc le contrôleur et le modèle seront respectivement ArtistsSongsController
et artists/songs
. Nous avons déjà vu comment le modèle est rendu dans le point de vente fourni par le modèle des artists
afin que nous puissions nous concentrer uniquement sur le modèle à portée de main :
<script type="text/x-handlebars" data-template-name="artists/songs"> (...) {{#each songs}} <div class="list-group-item"> {{title}} {{view App.StarRating maxRating=5}} </div> {{/each}} </script>
Notez que j'ai supprimé le code pour créer une nouvelle chanson car il serait exactement le même que celui pour créer un nouvel artiste.
La propriété songs
est configurée dans tous les objets artiste à partir des données renvoyées par le serveur. Le mécanisme exact par lequel cela se fait n'a que peu d'intérêt dans la discussion actuelle. Pour l'instant, il nous suffit de savoir que chaque chanson a un titre et une note.
Le titre est affiché directement dans le modèle et la note est représentée par des étoiles, via la vue StarRating
. Voyons cela maintenant.
Widget d'évaluation par étoiles
La note d'une chanson est comprise entre 1 et 5 et est présentée à l'utilisateur via une vue, App.StarRating
. Les vues ont accès à leur contexte (dans ce cas, la chanson) et à leur contrôleur. Cela signifie qu'ils peuvent lire et modifier ses propriétés. Cela contraste avec un autre bloc de construction Ember, les composants, qui sont des contrôles isolés et réutilisables avec un accès uniquement à ce qui leur a été transmis. (Nous pourrions également utiliser un composant de classement par étoiles dans cet exemple.)
Voyons comment la vue affiche le nombre d'étoiles et définit la note de la chanson lorsque l'utilisateur clique sur l'une des étoiles :
App.StarRating = Ember.View.extend({ classNames: ['rating-panel'], templateName: 'star-rating', rating: Ember.computed.alias('context.rating'), fullStars: Ember.computed.alias('rating'), numStars: Ember.computed.alias('maxRating'), stars: function() { var ratings = []; var fullStars = this.starRange(1, this.get('fullStars'), 'full'); var emptyStars = this.starRange(this.get('fullStars') + 1, this.get('numStars'), 'empty'); Array.prototype.push.apply(ratings, fullStars); Array.prototype.push.apply(ratings, emptyStars); return ratings; }.property('fullStars', 'numStars'), starRange: function(start, end, type) { var starsData = []; for (i = start; i <= end; i++) { starsData.push({ rating: i, full: type === 'full' }); }; return starsData; }, (...) });
rating
, fullStars
et numStars
sont des propriétés calculées dont nous avons discuté précédemment avec la propriété disabled
du ArtistsController
. Ci-dessus, j'ai utilisé une macro de propriété dite calculée, dont environ une douzaine sont définies dans Ember. Ils rendent les propriétés calculées typiques plus succinctes et moins sujettes aux erreurs (à écrire). J'ai défini la rating
comme étant la note du contexte (et donc de la chanson), tandis que j'ai défini les propriétés fullStars
et numStars
afin qu'elles se lisent mieux dans le contexte du widget de notation par étoiles.
La méthode des stars
est l'attraction principale. Il renvoie un tableau de données pour les étoiles dans lequel chaque élément contient une propriété de rating
(de 1 à 5) et un indicateur ( full
) pour indiquer si l'étoile est pleine. Il est donc extrêmement simple de les parcourir dans le modèle :
<script type="text/x-handlebars" data-template-name="star-rating"> {{#each view.stars}} <span {{bind-attr data-rating=rating}} {{bind-attr class=":star-rating :glyphicon full:glyphicon-star:glyphicon-star-empty"}} {{action "setRating" target=view}}> </span> {{/each}} </script>
Cet extrait contient plusieurs points à noter :
- Tout d'abord,
each
assistant indique qu'il utilise une propriété de vue (par opposition à une propriété sur le contrôleur) en préfixant le nom de la propriété avecview
. - Deuxièmement, l'attribut
class
de la balise span a des classes mixtes dynamiques et statiques attribuées. Tout ce qui est préfixé par un:
devient une classe statique, tandis que lafull:glyphicon-star:glyphicon-star-empty
est comme un opérateur ternaire en JavaScript : si la propriété full est truey, la première classe doit être affectée ; sinon, le deuxième. - Enfin, lorsque vous cliquez sur la balise, l'action
setRating
doit être déclenchée, mais Ember la recherchera sur la vue, et non sur la route ou le contrôleur, comme dans le cas de la création d'un nouvel artiste.
L'action est ainsi définie sur la vue :
App.StarRating = Ember.View.extend({ (...) actions: { setRating: function() { var newRating = $(event.target).data('rating'); this.set('rating', newRating); } } });
Nous obtenons la note à partir de l'attribut de données de rating
que nous avons attribué dans le modèle, puis nous le définissons comme rating
pour la chanson. Notez que la nouvelle note n'est pas conservée sur le back-end. Il ne serait pas difficile d'implémenter cette fonctionnalité en fonction de la façon dont nous avons créé un artiste et est laissée en exercice au lecteur motivé.
Envelopper le tout
Nous avons goûté plusieurs ingrédients du gâteau Ember susmentionné :
- Nous avons vu comment les routes sont au cœur des applications Ember et comment elles servent de base aux conventions de nommage.
- Nous avons vu comment les liaisons de données bidirectionnelles et les propriétés calculées font de nos données de modèle la seule source de vérité et nous permettent d'éviter la manipulation directe du DOM.
- Et nous avons vu comment déclencher et gérer des actions de plusieurs manières et créer une vue personnalisée pour créer un contrôle qui ne fait pas partie de notre code HTML.
Beau, n'est-ce pas ?
Pour en savoir plus (et regarder)
Il y a bien plus à Ember que je ne pourrais tenir dans ce post seul. Si vous souhaitez voir une série de screencasts sur la façon dont j'ai construit une version un peu plus développée de l'application ci-dessus et/ou en savoir plus sur Ember, vous pouvez vous inscrire à ma liste de diffusion pour obtenir des articles ou des conseils sur une base hebdomadaire.
J'espère que j'ai aiguisé votre appétit pour en savoir plus sur Ember.js et que vous allez bien au-delà de l'exemple d'application que j'ai utilisé dans cet article. Alors que vous continuez à en savoir plus sur Ember.js, assurez-vous de jeter un coup d'œil à notre article sur Ember Data pour apprendre à utiliser la bibliothèque ember-data. Amusez-vous à construire !