BT

Ember.js - Des applications web bien faites

Écrit par Joachim Haagen Skeie , traduit par Eric Bellemon le 19 juin 2013 |

Introduction

L'an dernier, InfoQ a publié mon article "Ember.js – Rich Web Applications done right", à ce moment le code était basé sur la version 0.9.4 d'Ember.js et Ember.js était un projet clairement jeune.

Le framework a depuis parcouru un long chemin et il est temps de mettre à jour l'article avec l'état de l'art dans les technologies et pratiques de développement Ember.js, en particulier avec le routeur Ember.

L'API d'Ember.js s'est stabilisé avec la sortie de la Release Candidate 1 d'Ember.js 1.0. Cet article est basé sur un build récent de la branche master (24 mars 2013) pour Ember.js et Ember Data.

Mon livre "Ember.js in Action" va être édité par Manning Publications dans la seconde moitié de l'année. Il est possible d'accéder aux quatre premiers chapitres via l'avant première proposé par Manning, de nouveaux chapitres seront ajoutés au fur et à mesure que la date de sortie s'approchera.

Comment l'environnement d'Ember.js s'est-il amélioré ? Est-ce que Ember.js est toujours le framework à utiliser pour créer des applications web single-page ?

Il y a quelques questions auxquelles je répondrais au travers cet article. Nous avons beaucoup de choses à voir, donc commençons dès maintenant ! Le code source de l'application est disponible sur GitHub.

Qu'allons-nous faire ?

Nous allons construire une application d'album photo, simpliste, dans laquelle les utilisateurs seront présentés avec une rangée de photographies au bas de la page. Quand un utilisateur sélectionne une de ces photos, l'URL va être mise à jour pour représenter cet état pendant que la photographie sélectionnée va être affichée au-dessus de la liste des vignettes.

Nous allons aussi ajouter une fonction diaporama qui fera une transition toutes les 4 secondes vers la prochaine photo de la liste.

Une fois terminée, l'application ressemblera à la figure 1:

(Cliquer sur l'image pour l'agrandir)

img1

Figure 1 – L'application une fois terminé

Nous allons commencer avec une application toute simple, puis mettre en place les fonctionnalités dont nous avons besoin dans application pièce par pièce. Nous allons donc commencer par configurer notre application.

Structure du projet et initialisation

La version finale du code de notre application est disponible sur GitHub. Le code source est disponible dans le répertoire site. Vous devez lancer l'application de sorte qu'elle soit disponible sans context path. Il y a plusieurs moyens d'atteindre cet objectif, les solutions les plus simples sont:

  • Utiliser asdf-gem pour lancer un serveur dans le répertoire courant. Pour plus d'information, allez voir sur GitHub. Une fois le gem installé, lancez la commande "asdf" à partir du répertoire site
  • N'importe quel autra serveur web qui hébergera votre application a l'adresse http://localhost/

Le projet a la structure présentée avec la figure 2.

figure2

Figure 2 – Structure du projet

Toutes les librairies externes sont situées dans le répertoire site/scripts. Les dépendances obligatoires sont:

  • Ember.js, buid du 24 mars 2013
  • Ember Data, build du 16 février 2013
  • Handlebars.js 1.0 RC3
  • JQuery 1.9.1

Dans le répertoire img, il y a un total de 10 images, celles que nous allons utiliser pour créer notre album photo. Il y a un simple fichier master.css dans le dossier css.

Toute la logique de notre application va être séparée dans différents fichiers dans le répertoire app. Nous allons commencer par un unique fichier app.js. Cet article va vous indiquer lorsqu'il sera nécessaire d'ajouter de nouveaux fichiers dans l'application.

Introduction rapide aux Bindings

Dans Ember.js, les Bindings sont utilisés pour synchroniser les valeurs d'une variable entre objets. Examinez le code ci-dessous:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">

<html lang="en">
<head>
     <title>Ember.js Example Bindings</title>
     <link rel="stylesheet" href="css/master.css" type="text/css" charset="utf-8">
     <script src="scripts/jquery-1.9.1.min.js" type="text/javascript" charset="utf-8"></script>
     <script src="scripts/handlebars-1.0.0.rc.3.js" type="text/javascript" charset="utf-8"></script>
     <script src="scripts/ember.prod.20130324.js" type="text/javascript" charset="utf-8"></script>
     <script type="text/javascript">
         BindingsExample = Ember.Application.create();

         BindingsExample.person = Ember.Object.create({
             name: 'Joachim Haagen Skeie'
         });

         BindingsExample.car = Ember.Object.create({
             ownerBinding: 'BindingsExample.person.name'
         });
     </script>

     <script type="text/x-handlebars">
         <div id="mainArea">{{BindingsExample.car.owner}}</div>
     </script>

</head>
<body bgcolor="#555154">

</body>
</html>

Listing 1 - Exemple de bindings simple

Dans le code ci-dessous, nous commençons par charger le CSS avant d'ajouter les librairies JavaScript jQuery, Handlebars et Ember.js. L'application elle-même est définie de la ligne 10 à 25. On commence en définissant un namespace pour l'application, BindingsExemple dans ce cas. L'application a deux objets définis, un BindingsExample.person et un BindingsExample.car. Ici nous suivons les conventions de nommages d'Ember.js, en commençant les objets instanciés par une lettre minuscule.

L'attribut name de l'objet BindingsExample.person est fixé avec la chaine de caractère "Joachim Haagen Skeie". En regardant l'objet car, vous remarquerez qu'il y a un attribut appelé ownerBinding. Parce que le nom de l'attribut se termine par "Binding", Ember.js va automatiquement créer un attribut owner pour vous, qui est lié au contenu d'un autre attribut, dans ce cas, l'attribut BindingsExample.person.name.

Si vous ouvrez ce fichier HTML dans votre navigateur, il va simplement afficher la valeur de BindingsExample.car.owner, soit dans cet exemple "Joachim Haagen Skeie".

La beauté de cette approche est que chaque fois que BindingsExample.person.name va changer de valeur, le contenu du site web va aussi être mis à jour. Essayez de taper la commande suivante dans la console JavaScript de votre navigateur, et regardez le contenu du site changer:

BindingsExample.person.set('name', 'Some random dude')

Le contenu affiché sur la page web sera maintenant "Some random dude". Avec cela à l'esprit, il est temps de commencer notre application.

Commencez votre application Ember.js

Si vous voulez avoir une idée de l'application que nous construisons, vous pouvez jeter un oeil sur l'application une fois terminée.

Pour commencer, créez un fichier vide index.html, créez les répertoires app, css, img et scripts. Maintenant, créez ou copiez les fichiers suivants:

  • Dans le répertoire app, créez un fichier app.js.
  • Dans le répertoire css, copiez ce fichier.
  • Dans le répertoire scripts, copiez les fichiers à partir de GitHub.

Commençons par notre fichier index.html. Nous allons commencer avec un fichier plutôt vide, référençant juste nos scripts et fichiers CSS comme ci-dessous:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
        "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">

    <title>Ember.js Example Application</title>
    <link rel="stylesheet" href="css/master.css" type="text/css" charset="utf-8">
    <meta name="author" content="Joachim Haagen Skeie">
    <script src="scripts/jquery-1.9.1.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="scripts/handlebars-1.0.0.rc.3.js" type="text/javascript" charset="utf-8"></script>
    <script src="scripts/ember.prod.20130324.js" type="text/javascript" charset="utf-8"></script>
    <script src="scripts/ember-data-20130216.js" type="text/javascript" charset="utf-8"></script>
    <script src="app/app.js" type="text/javascript" charset="utf-8"></script>

    <script type="text/x-handlebars">

    </script>
</head>
<body bgcolor="#555154">

</body>
</html>

Listing 2 – Le fichier initial index.html

La première chose à faire est de créer un namespace pour notre application, que nous allons placer dans app.js. J'ai décidé de créer le namespace EME (Ember-Example). Ajoutez la ligne suivante dans votre fichier app.js:

EME = Ember.Application.create({});

Listing 3 – Ember-Example namespace (EME)

La dernière partie, avant de commencer à définir le modèle objet de l'application est d'ajouter du contenu dans nos CSS. Je souhaite que l'application ait un fond blanc, avec des bords arrondis, et j'aimerais que la section principale remplisse entièrement l'écran. Ajoutez ce qui suit dans le fichier css/master.css.

#mainArea {
    border-radius: 25px;
    -moz-bottomleft: 25px;
    padding: 15px;
    margin-top: 5px;
    background: #fff;
    text-align: left;
    position: absolute;
    top: 5px;
    left: 5px;
    right: 5px;
    bottom: 5px;
    z-index:1;
}

Listing 4 – CSS pour mainArea

Afin d'afficher un élément div avec un id "mainArea", nous allons étendre le template de l'application, que vous pouvez trouver dans index.html entre les deux tags script vides de type text/x-handlebars.

    <script type="text/x-handlebars">
       <div id="mainArea"></div>
    </script>

Listing 5 – Ajout d'un element div

Vous pouvez maintenant rafraichir votre page. Vous devriez voir un espace blanc vide avec des bords arrondis recouvrant une grande partie de l'écran. Avec cela en place, il est temps de commencer à définir le modèle de nos photographies.

Définir son modèle objet avec Ember Data

À cette étape, nous avons besoin d'ajouter Ember Data à la liste des scripts dans le fichier index.html, si vous ne l'avez pas déjà fait.

Pour commencer avec Ember.js, nous avons besoin d'initialiser le Datastore. Ouvrez app/app.js et ajoutez les lignes suivantes:

EME.store = DS.Store.create({
    adapter: "DS.RESTAdapter",
    revision: 11
});

Listing 6 – Creation du Datastore

Comme vous pouvez le voir, nous créons un nouvel objet DS.Store, appellé EME.store. Nous avons besoin d'indiquer deux types d'informations au datastore. La première est le numéro de révision d'Ember Data et la seconde est le nom de l'adaptateur que nous allons utiliser. Le numéro de révision est là pour s'assurer que vous serez notifié dans le cas où des modifications importantes ont lieu dans Ember Data. Puisque Ember Data n'a pas atteint une API 1.0 stable et finalisée, des incompatibilités dans l'API peuvent et vont arriver de temps à autre. Nous allons utiliser DS.RESTAdapter pour s'interfacer avec le backend.

Pour cette application, nous avons juste besoin d'un type de données, une photographie, qui va avoir 3 attributs: un id, un titre et une URL. Le modèle de données des photographies est défini en étendant DS.Model. Créez un fichier models/photo_model.js et ajoutez le code ci-dessous:

EME.Photo = DS.Model.extend({
    imageTitle: DS.attr('string'),
    imageUrl: DS.attr('string')
});

Listing 7 – Le modèle objet des photographies

Vous pouvez constater que l'on n'a pas défini d'attribut "id". Ceci vient du fait qu'Ember Data va en créer un pour nous. En réalité, il ne va même pas vous permettre d'en définir un manuellement. En plus, nous avons défini deux attributs "imageTitle" et "imageUrl", les deux étant définis comme des strings via DS.attr('string').

Maintenant, il est temps de penser à comment nous allons structurer notre application, et comment nous allons faire pour définir les URL.

Structurer l'application avec Ember Router

Dans l'article précédent, nous avons utilisé Ember StateManager pour structurer les différents états de l'application. Depuis, le StateManager a été remplacé par une implémentation plus robuste appellée Ember Router. Ember Router a le même but, mais il va générer automatiquement une grande partie du code standard pour vous au runtime, utilisant des valeurs par défauts saines qu'il est très simple de remplacer quand nécessaire.

La première chose que nous devons définir dans notre application sont les routes. Vous pouvez imaginer qu'une route est un état dans lequel l'utilisateur peut être. Chaque route va avoir une URL clairement définie. Ember.js va générer les contrôleurs, vues et templates automatiquement pour vous. Chaque fois que vous estimez avoir besoin de plus fonctionnalités que les fonctionnalités basiques prédéfinies, vous créez simplement votre propre implémentation et Ember.js va automatiquement remplacer le code généré par votre code.

Pour l'album photo, nous avons besoin de trois routes:

  • Une route index "/"
  • une route Photos qui répond à l'URL "/photos"
  • une route d'une Photo Sélectionné qui répond à l'URL "/photos/:photo_id"

Comme je l'ai mentionné, Ember.js va automatiquement générer tout cela pour vous, mais nous devons indiquer à Ember.js quelles routes nous voulons avoir. On fait cela simplement en implémentant EME.Router.map() comme ci-dessous. Créez un nouveau fichier router.js et ajoutez le code suivant:

EME.Router.map(function() {
    this.route("index", {path: "/"});
    this.resource("photos", {path: "/photos"}, function() {
        this.route("selectedPhoto", {path: ":photo_id"})
    });
});

Listing 8 – Définition des routes

Comme vous pouvez le voir, il y a 2 différents types de constructeurs que nous pouvons utiliser dans la fonction map(), route() représente une route finale et resource() représente une route qui peut avoir des sous-routes. Nous définissons 2 routes top-level, une appelée index qui répond à l'url "/", et une ressource appelée photos qui répond à l'URL "/photos". En plus nous créons une sous-route appelée selectedPhoto qui va répondre à l'URL "/photos/:photo_id". Ember Router utilise les attributs avec un deux-points comme préfixe pour indiquer les parties dynamiques. Ember Router remplacera la partie dynamique ultérieurement lorsqu'il mettra à jour les URL.

Remarquez aussi les conventions de nommage d'Ember Router: :photo_id fait référence à l'attribut "id" de l'objet EME.Photo.

Parce que nous n'avons rien à afficher dans la route index, nous allons simplement la rediriger vers la route photo dans le cas où l'utilisateur demande directement l'URL "/". Nous pouvons le faire en créant une classe EME.IndexRoute comme ci-dessous. Ajoutez cela au fichier router.js:

EME.IndexRoute = Ember.Route.extend({
    redirect: function() {
        this.transitionTo('photos');
    }
});

Listing 9 – Redirection de l'index

Dans le code ci-dessus, nous utilisons la fonction redirect afin de rediriger vers la route appellée photos en utilisant la fonction transitionTo().

Obtenir les photos du backend en utilisant Ember Data

La partie finale du code nécessaire pour notre routeur afin de récupérer des photos, est de dire quelle action sera nécessaire pour charger les données. Complétez router.js avec le code suivant:

EME.PhotosRoute = Ember.Route.extend({
    model: function() {
        return EME.Photo.find();
    }
});

Listing 9 – Redirection de l'index vers la route photos

Dans le code ci-dessus, nous indiquons à Ember Data de récupérer tous les objets EME.Photo à partir du serveur de manière asynchrone. Une fois que le serveur a répondu, EME.PhotosRoute va remplir automatiquement PhotosController avec les données récupérées. Notez que nous pouvons demander une photo spécifique au serveur en indiquant un id dans la fonction find(), EME.Photo.find(1), mais dans notre cas, récupérer toutes les photos est suffisant.

Parce que notre modèle est appelé EME.Photos, Ember Data va par défaut récupérer les données de ce modèle à l'URL "/photos". Le contenu est le suivant:

{ "photos": [
    { "id": 1, "image_title": "Bird", "image_url": "img/bird.jpg"},
    { "id": "2", "image_title": "Dragonfly", "image_url": "img/dragonfly.jpg"},
    { "id": "3", "image_title": "Fly", "image_url": "img/fly.jpg"},
    { "id": "4", "image_title": "Frog", "image_url": "img/frog.jpg"},
    { "id": "5", "image_title": "Lizard", "image_url": "img/lizard.jpg"},
    { "id": "6", "image_title": "Mountain 1", "image_url": "img/mountain.jpg"},
    { "id": "7", "image_title": "Mountain 2", "image_url": "img/mountain2.jpg"},
    { "id": "8", "image_title": "Panorama", "image_url": "img/panorama.jpg"},
    { "id": "9", "image_title": "Sheep", "image_url": "img/sheep.jpg"},
    { "id": "10", "image_title": "Waterfall", "image_url": "img/waterfall.jpg"}
]}

Listing 10 – Exemple de la liste de photos

Tout cela est bien, mais avoir les photos ne nous sert à rien si on ne peut pas les afficher. Commençons donc par définir des templates pour notre application.

Creation des templates

Ember.js utilise Handlebars.js comme moteur de template par défaut. Nous allons au total définir quatre templates. Nous avons déjà vu le template de l'application ci-dessus dans le listing 5, mais nous avons loupé un élément crucial, un endroit où on peut afficher le template de la route courante. Donc, commençons par modifier le template de l'application avec un "outlet", comme ci-dessous:

<script type="text/x-handlebars">
    <div id="mainArea">
        {{outlet}}
    </div>
</script>

Listing 11 – Modification du template de l'application pour ajouter un "outlet"

Un outlet est simplement défini par l'expression {{outlet}} et il peut être utilisé pour indiquer à Ember.js où le template doit afficher les sous-templates. Le contenu de ce template va changer en fonction de la route dans laquelle se trouve la vue. Pour la route index, cet outlet va contenir le template d'index alors que pour la ressource photo, cet outlet va afficher le template photo.

Maintenant que nous avons un lieu pour afficher la liste des aperçus des photos, définissons le template des photos:

   <script type="text/x-handlebars" id="photos">
       {{outlet}}

       <div class="thumbnailViewList">
           {{#each photo in controller}}
           <div class="thumbnailItem">
               {{#linkTo photos.selectedPhoto photo}}
                   {{view EME.PhotoThumbnailView
                        srcBinding="photo.imageUrl"
                        contentBinding="photo"}}
                   {{/linkTo}}
            </div>
            {{/each}}
       </div>
    </script>

Listing 12 – Definition du template photos

Il y a certaines choses nouvelles dans le code ci-dessus. La première nouveauté est le fait que le script text/x-handlerbars a un attribut "id". Cet attribut id indique à Ember.js le nom du template, et ce nom, photos, indique à Ember.js que le template appartient à PhotosRoute. Aussi longtemps que vous vous conformez à la convention de nommage, Ember.js est assez intelligent pour s'occuper du reste.

La première chose que nous définissons dans notre template est un {{outlet}} dans lequel sera affichée la photographie sélectionnée. Ensuite nous définissons un élément div dans lequel nous pouvons afficher chaque imagette. Pour itérer sur chacune des photos, chargées grâce au PhotosController, nous utilisons l'expression {{#each photo in controller}}. À l'intérieur du bloc {each}, nous utilisons l'expression {{#linkTo ..}} pour créer un lien vers la photographie avec la route selectedPhoto. Notez que nous définissons à la fois quelle route créer (via le constructeur parent-route.sub.route) ainsi que le contexte qu'Ember.js va passer à la route. Dans notre cas, le contexte est simplement la photographie sur laquelle l'utilisateur a cliqué.

Nous avons créé une vue personnalisée pour afficher chacune des imagettes. Créez le fichier views/photo_thumbnail_view.js avec comme contenu le code suivant:

EME.PhotoThumbnailView = Ember.View.extend({
    tagName: 'img',
    attributeBindings: ['src'],
    classNames: ['thumbnailItem'],
    classNameBindings: 'isSelected',

    isSelected: function() {
        return this.get('content.id') === 
this.get('controller.controllers.photosSelectedPhoto.content.id');
    }.property('controller.controllers.photosSelectedPhoto.content', 'content')
});

Listing 13 – Création d'une vue pour l'affichage des imagettes

PhotoThumbnailView commence par spécifier, via la propriété tagName, que notre vue sera affichée avec un tag img. Il indique ensuite la valeur de l'attribut src avant de spécifier la classe CSS thumbnailItem.

Nous voulons faire en sorte que la photographie sélectionnée soit plus grande que les autres afin d'indiquer clairement laquelle est sélectionnée. Afin d'ajouter une classe CSS is-selected à la valeur sélectionnée, nous utilisons la proriété classNameBindings. Cela va simplement apposer la classe CSS is-selected à la vue si la valeur de la propriété isSelected est true, ce qui est le cas uniquement si l'id de la vue photo est le même que l'id de la photo sélectionnée.

Le seul template manquant dans notre application est le template qui affiche la photo sélectionnée. Parce que notre route est une sous-route de la ressource photos, il va avoir comme id photos/selectedPhoto. Le code ci-dessous montre le template selectedPhoto:

<script type="text/x-handlebars" id="photos/selectedPhoto">
    <div id="selectedPhoto">
        <h1>{{imageTitle}}</h1>

        <div class="selectedPhotoItem">
            <img id="selectedImage" {{bindAttr src="imageUrl"}}/>
        </div>
    </div>
</script>

Listing 14 –Le template de selectedPhoto

Il n'y a rien de nouveau dans ce template, il affiche simplement un élément div pour afficher la photo sélectionnée avec son titre et la photo.

Vous pouvez maintenant recharger votre application. Le résultat final doit ressembler à quelque chose comme la figure 3 ci-dessous.

(Cliquez sur l'image pour l'aggrandir)

figure3

Figure 3 – L'album photo dans l'état actuel.

Vous pouvez noter que la photo actuellement sélectionnée n'est pas en surbrillance, ni plus grande que les autres imagettes. La raison pour cela est que nous avons besoin de spécifier un contrôleur pour à la fois la route photos et la route selectedPhotos. En plus nous avons besoin de faire un lien entre ces contrôleurs pour que PhotoThumbnailView puisse résoudre correctement la propriété isSelected. Donc, allons créer ces deux contrôleurs.

Définition des contrôleurs

Nous allons maintenant adapter nos contrôleurs pour les ressources photo et pour la route selectedPhoto. En plus nous allons avoir besoin d'un lien entre le contrôleur photo et le contrôleur selectedPhoto. Commençons par créer un nouveau fichier controllers/photos_controller.js avec le code ci-dessous:

EME.PhotosController = Ember.ArrayController.extend({
    needs: ['photosSelectedPhoto'],
});

Listing 15 –Le contrôlleur PhotosController

Comme vous pouvez le constater, il n'y a pas grand-chose pour le moment dans ce contrôleur. Parce que nous avons défini le contrôleur pour être un Ember.ArrayController, Ember.js prend en charge la mise en proxy de notre liste de photos dans le template, ainsi que la gestion des bindings et autres problématiques du contrôleur. L'unique chose que nous avons à définir est le fait que ce contrôleur a besoin du photosSelectedPhotoController. Puisque nous avons spécifié une relation explicite entre ces contrôleurs, nous avons aussi besoin de créer le contrôleur auquel nous faisons référence. Créez un nouveau fichier controllers/selected_photo_controller.js avec le contenu suivant:

EME.PhotosSelectedPhotoController = Ember.ObjectController.extend({});

Listing 16 –Le contrôlleur PhotosSelectedPhotoController

Puisque nous n'implémentons aucune logique dans ce contrôleur, il suffit simplement de le déclarer, en spécifiant que le contrôleur est un Ember.ObjectControlleur, signifiant qu'il va servir de proxy pour une unique photographie.

Rechargez votre application, et comme le montre la figure 4, observez que la photographie sélectionnée est maintenant mise en évidence comme voulu.

(Cliquez sur l'image pour l'agrandir)

figure4

Figure 4 – Résultat après avoir ajouté les contrôlleurs

La partie finale de notre application est de créer des boutons de contrôles qui permettent à l'utilisateur de naviguer entre la photo précédente et suivante, et aussi de démarrer et arrêter le diaporama.

Ajout des boutons de contrôles et de la fonction diaporama

Nous allons ajouter quatre nouveaux boutons sous la photo sélectionnée pour contrôler quelle photo est sélectionné. Le bouton "Play" va commencer le diaporama et changer la photo sélectionnée toute les 4 secondes. Le bouton "Stop" va arrêter le diaporama, alors que "Next" et "Prev" vont sélectionner respectivement la photo suivante ou précédente. Commençons par ajouter un template pour gérer les boutons et appelons le photoControls:

<script type="text/x-handlebars" id="photoControls">
    <div class="controlButtons">
        <button {{action playSlideshow}}>Play</button>
        <button {{action stopSlideshow}}>Stop</button>
        <button {{action prevPhoto}}>Prev</button>
        <button {{action nextPhoto}}>Next</button>
   </div>
</script> 

Listing 17 –Template photoControls

Ce template crée simplement quatre boutons. Chaque bouton est rattaché à une action que nous allons implémenter plus tard. Premièrement nous avons besoin de dire à l'application où afficher le template photoControls. Nous allons afficher ce template directement sous la photographie sélectionnée, donc il est judicieux de le spécifier juste après le {{outlet}} dans le template photos, comme ci-dessous:

<script type="text/x-handlebars" id="photos">
    <div>Disclamer: The photographs used for this example application is under 
    Copyright to Joachim Haagen Skeie.
    You are allowed to use the photographs while going through this example 
    application. The source code itself
    is released under the MIT licence.
    </div>
    {{outlet}}
    {{render photoControls}} 
    <div class="thumbnailViewList">
        {{#each photo in controller}}
        <div class="thumbnailItem">
            {{#linkTo photos.selectedPhoto photo}}
                {{view EME.PhotoThumbnailView
                    srcBinding="photo.imageUrl"
                    contentBinding="photo"}}
                {{/linkTo}}
        </div>
        {{/each}}
    </div>
</script>

Listing 18 –Ajout de photoControls dans l'application

Ensuite, nous avons besoin de définir les actions de nos quatre boutons. Puisque nous utilisons l'expression {{render}}, nous sommes capables de créer le contrôle PhotoControls et faire en sorte qu'Ember.js utilise automatiquement ce contrôleur comme contrôleur du template photoControls. Créez un nouveau fichier controllers/photo_controls_controller.js avec le contenu suivant:

EME.PhotoControlsController = Ember.Controller.extend({
    needs: ['photos', 'photosSelectedPhoto'],

    playSlideshow: function() {
        console.log('playSlideshow');
    },

    stopSlideshow: function() {
        console.log('stopSlideshow');
    },

    nextPhoto: function() {
        console.log('nextPhoto');
    },
    prevPhoto: function() {
        console.log('prevPhoto');
    }
}); 

Listing 19 –Ajout du contrôleur PhotoControlsController pour gérer les événements

Si vous rechargez votre application maintenant, et que vous allez dans la console de votre navigateur, vous devriez voir une ligne de log chaque fois qu'il y a un clic sur l'un des boutons.

Notez que nous avons spécifié que ce contrôleur a besoin à la fois de photosController et de photosSelectedPhoto. La raison est que nous ne voulons pas implémenter le contenu de ces actions dans le contrôleur. Nous voulons plutôt déléguer certaines d'entre elles à d'autres contrôleurs, où cette responsabilité a plus de sens. nextPhoto et prevPhoto sont plus adaptés à être dans PhotosController, donc mettons à jour le code pour déléguer les actions à ce contrôleur:

nextPhoto: function() {
    console.log('nextPhoto');
    this.get('controllers.photos').nextPhoto();
},

prevPhoto: function() {<
    console.log('prevPhoto');
    this.get('controllers.photos').prevPhoto();
}

Listing 19 – Delegation des événements à PhotosController

Comme vous pouvez le voir, nous déléguons simplement les fonctions à PhotosController. Mais avant que nous regardions l'implémentation de ces fonctions, finissons ce contrôleur en premier en implémentant la fonctionnalité de diaporama:

    playSlideshow: function() {
        console.log('playSlideshow');
        var controller = this;
        controller.nextPhoto();
        this.set('slideshowTimerId', setInterval(function() {
            Ember.run(function() {
                controller.nextPhoto();
            });
        }, 4000));
    },

    stopSlideshow: function() {
        console.log('stopSlideshow');
        clearInterval(this.get('slideshowTimerId'));
        this.set('slideshowTimerId', null);
    }

Listing 20 – Ajout de la fonctionnalité de diaporama

Il y a certaines choses à noter ici. Premièrement, lorsque l'utilisateur clique sur le bouton play, la fonction playSlideshow() va automatiquement appeler la fonction nextPhoto() afin de sélectionner la photo suivante. Ensuite elle lance un nouveau timer en utilisant setInterval(). Cette fonction retourne un id que nous pouvons utiliser plus tard pour retourner l'interval, donc nous faisons en sorte d'être certains de stocker cet id dans la propriété slideshowTimerId de PhotoControlsControllers.

Puisque nous ne contrôlons pas quand le timer exécute la fonction de callback, et parce que nous voulons nous assurer que nous exécutons ce callback dans la boucle d'exécution d'Ember.js, nous devons mettre le contenu de la callback dans Ember.run().

La pièce finale manquante dans notre application est l'implémentation des fonctions nextPhoto et prevPhoto. Je vais seulement montrer ici l'implémentation de nextPhoto, puisque leurs implémentations sont très similaires. Le listing 21 montre l'implémentation de nextPhoto:

    nextPhoto: function() {
        var selectedPhoto = null;
        if (!this.get('controllers.photosSelectedPhoto.content')) {
            this.transitionToRoute("photos.selectedPhoto", 
                this.get('content.firstObject'));
        } else {
            var selectedIndex = this.findSelectedItemIndex();

            if (selectedIndex >= (this.get('content.length') - 1)) {
                selectedIndex = 0;
            } else {
                selectedIndex++;
            }

            this.transitionToRoute("photos.selectedPhoto", 
               this.get('content').objectAt(selectedIndex))
        }
   }

Listing 21 –La fonction nextPhoto()

S'il n'y a pas de photo sélectionnée, la fonction va simplement afficher la première photographie de la liste. Ceci est réalisé en utilisant une transition vers la route photos.selectedPhoto en utilisant la propriété content.firstObject.

Si une photographie est déjà sélectionnée, nous devons trouver quel est l'index de la photo sélectionné dans le tableau content. Nous faisons cela en appelant la fonction findSelectedItemIndex(), le contenu de celle-ci est ci-dessous dans le listing 22.

Lorsque l'index de la photo vers laquelle nous voulons faire une transition a été déterminé, nous pouvons faire une transition vers photos.selectedPhoto en utilisant cette photo comme context.

    findSelectedItemIndex: function() {
        var content = this.get('content');
        var selectedPhoto = this.get('controllers.photosSelectedPhoto.content');

        for (index = 0; index < content.get('length'); index++) {
            if (this.get('controllers.photosSelectedPhoto.content') === 
content.objectAt(index)) {
                return index;
            }
        }

        return 0;
    }

Listing 22 –La fonction findSelectedItemIndex()

Il ne devrait pas y avoir de surprises dans le code de findSelectedItemIndex(). Il itère simplement le tableau content jusqu'à trouver la photographie sélectionnée. S'il n'est pas capable d'en trouver une, il retourne simplement l'index de la première photo du tableau content

Normalement, je ne devrais pas ajouter mon Adapter dans le fichier app.js mais plutôt dans son propre fichier .js. Cela permet de trouver plus facilement l'Adapter plus tard lorsque l'on souhaitera changer son implémentation.

Conclusion

Ce qu'Ember.js apporte sur la table est un modèle de développement d'application propre et consistant. Il permet de créer très facilement vos propres templates de vue qui sont simple à comprendre, à créer et à mettre à jour. Couplé avec une manière cohérente de gérer les bindings et les propriétés, Ember.js va générer une bonne partie du code nécessaire à un framework web, et parce qu'il est tellement évident de la technologie qui est utilisée et de comment l'arbre DOM est mis à jour qu'il est aussi très facile d'ajouter des plug-ins ou add-ons.

La convention de nommage d'Ember.js signifie que vous avez besoin de spécifier et d’implémenter des classes seulement lorsque celles par défauts d'Ember.js ne sont pas suffisantes. Vous vous retrouvez avec un code propre, facile à tester, et plus important encore, facile à maintenir.

J'espère que cet article vous a donné une meilleure compréhension du modèle de développement d'Ember.js et que vous avez maintenant une vue claire sur comment vous pouvez utiliser Ember.js sur vos prochains projets.

Tous les concepts et constructions utilisés dans cet article, et plus encore, vont être couvert en détail dans "Ember.js in Action". Ce livre est actuellement disponible via "Manning Publications Early Access Program", cela signifie que vous pouvez avoir le début du livre dès maintenant.

A propos de l'auteur

Joachim Haagen Skeie est le propriétaire de Haagen Software AS à Oslo en Norvège où il travaille comme consultant indépendant et formateur. Il a un vif intérêt à la fois pour le profiling d'application et les logiciels open source. À travers son entreprise, il est actuellement occupé à lancer EurekaJ Application Monitoring Tool. Il peut être contacté via son compte Twitter, ou par email joachim (at) haagen-software.no. Si vous être intéressé pour rencontrer des développeurs Ember.js à travers l'Europe, Joachim est à la tête de l'équipe derrière Ember Fest, une mini conférence à Munich en Allemagne en aout.

Bonjour étranger!

Vous devez créer un compte InfoQ ou cliquez sur pour déposer des commentaires. Mais il y a bien d'autres avantages à s'enregistrer.

Tirez le meilleur d'InfoQ

Donnez-nous votre avis

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

M'envoyer un email pour toute réponse à l'un de mes messages dans ce sujet
Commentaires de la Communauté

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

M'envoyer un email pour toute réponse à l'un de mes messages dans ce sujet

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

M'envoyer un email pour toute réponse à l'un de mes messages dans ce sujet

Discuter

Contenu Éducatif

Rien ne serait possible sans le soutien et la confiance de nos Sponsors Fondateurs:

AppDynamics   CloudBees   Microsoft   Zenika
Feedback Général
Bugs
Publicité
Éditorial
InfoQ.com et tous les contenus sont copyright © 2006-2013 C4Media Inc. InfoQ.com est hébergé chez Contegix, le meilleur ISP avec lequel nous ayons travaillé.
Politique de confidentialité
BT