Ember.js - Question « Un adaptateur modèle <=> template ? »

Problématique

Depuis quelques mois, j'utilise Ember.js dans certain de mes projets.

Dernièrement, j'ai été confronté au problème présenté ci-dessous.

Par exemple, j'ai cette structure de données (couche modèle) :

* Lundi
    * Évenement 1
    * Évenement 2
    * Evenement 3

* Mardi
    * Évenement 4
    * Évenement 5

* Mercredi
    * Évenement 6
    * Évenement 7
    * Évenement 8

Avec deux boucles, je peux facilement réaliser le tableau suivant :

Lundi Évenement 1 Évenement 2 Évenement 3
Mardi Évenement 4 Évenement 5  
Mercredi Évenement 6 Évenement 7 Évenement 8

Pour faire ce tableau, je peux utiliser un template du style :

<table>
    {{#each day in App.day_list}}
    <tr>
        <td>
            {{ day.name }}
        </td>
        {{#each event in day.events}}
        <td>
            {{ event.name }}
        </td>
        {{/each}}
    </tr>
    {{/each}}
</table>

Par contre, il est bien plus difficile (sans changer la structure de données) de réaliser le tableau suivant :

Lundi Mardi Mercredi
Évenement 1 Évenement 4 Évenement 6
Évenement 2 Évenement 5 Évenement 7
Évenement 3   Évenement 8

Ici le tableau est inversé… je ne peux donc pas réaliser ce tableau avec les deux mêmes boucles utilisées dans l'exemple précédent.

La seul solution que j'ai trouvé c'est de fournir un objet de type "vue" qui adapte mon modèle au besoin de ma vue. Seulement ce n'est pas si simple… car :

  • il faut que si les données sont modifiées, que le template soit automatiquement mis à jour… il faut donc une gestion d'évenement entre les objets de la couche modèles et l'objet "vue"
  • en cas d'interactions au niveau de l'interface utilisateur il faut que les données soient automatiquement mis à jour.

Premier exemple, simple

Il est possible de tester le premier exemple simple via ce lien jsFiddle.

Ce qu'il est possible de voir et tester :

  • cliquez sur les liens « Step 1 », « Step 2 », « Step 3 », « Step 4 »
  • ces liens modifient le contenu de la couche modèle
  • dès la première étape, vous pouvez voir deux tableaux identiques… qui affichent le contenu de la couche modèle
  • à chaque étape, vous pouvez modifier la valeur des cellules des tableaux
  • lorsque la valeur d'une cellule est modifiée dans un des deux tableaux, la cellule dans le second tableau est aussi modifiée

Voici ci-dessous un petit screencast de ce premier exemple simple :

Recherche d'une solution pour le « Tableau pivoté »

Comme indiqué plus haut, je cherche une solution pour afficher le même tableau mais pivoté (voir exemple au dessus).

J'ai un début de solution dans ce jsFiddle.

Vous pouvez constater que les étapes 1, 2, 3 fonctionnent.
L'étape 4 ne fonctionne pas…

Au niveau du code source, vous pouvez voir cette ligne commentée à plusieurs endroits :

// App.planning.rows.sync();

C'est cette fonction sync qui synchronise mon objet planning avec les données de la couche modèle dont l'objet de racine est day_list. Cette méthode est automatiquement exécutée par un observateur quand il y a des modifications au niveau de l'objet App.day_list (les sync en commentaire sont simplement là pour voir le comportement que devrait avoir le programme si cette méthode était bien exécutée) :

Ember.addObserver(
    App.day_list,
    '@each.content.@each',
    App.planning.rows,
    'sync'
);

C'est cet observateur qui fonctionne bien pour l'étape 1, 2 et 3 mais pas pour l'étape 4.

Mes questions

  • Avez vous une solution pour que sync soit bien exécuté lors de l'étape 4 ?
  • Est-ce que mon approche est la bonne ?
  • Quel est le nom standard de l'objet que j'appelle "vue" ? est-ce que je suis dans le vrai en disant que c'est un objet de type "vue" ?
  • Est-ce qu'il existe un pattern classique pour ce type de besoin en Ember.js ?
Read and Post Comments

Ember.js - Épisode 2 - Déclarer un tableau dans une classe

Après un premier billet intitulé « Ember.js - Épisode 1 - Présentation de l'application feuille de matchs » voici un second billet à propos de Ember.js où je vais présenter comment résoudre le problème assez trivial que j'ai rencontré lors de la déclaration d'une propriété de type tableau dans une classe.

Note : je ne sais pas si j'ai bien réussi à doser le niveau de ce billet… je pense que par moment j'ai expliqué des concepts trop basiques de Ember.js. J'attends vos remarques à ce sujet dans les commentaires.

Les fonctions "create" et "extend" de Ember.js

Tout d'abord, voyons les différences entre Ember.Object.create et Ember.Object.extend.

Pour créer un objet avec Ember.js, vous devez utiliser la fonction suivante :

MyApp.contact1 = Ember.Object.create({
    firstname: 'Stéphane',
    lastname: 'Klein'
});

Pour créer une classe, vous devez utiliser la fonction suivante :

// Création de la classe
MyApp.Contact = Ember.Object.extend({
    sayHello: function() {
        alert('Hello');
    }
});

// Création de deux objets de type "Contact"
MyApp.contact1 = MyApp.Contact.create(
    firstname: 'Stéphane',
    lastname: 'Klein'
);
MyApp.contact2 = MyApp.Contact.create(
    firstname: 'Thomas',
    lastname: 'Petit'
);

MyApp.contact1.sayHello();

Vous l'aurez compris :

  • create permet de créer un objet
  • extend permet de créer une classe

Pour plus d'information à ce sujet, je vous invite à consulter la documentation de Ember.js.

Dans l'exemple de ce billet, j'ai besoin d'utilser la commande "Ember.Object.extend" pour déclarer une nouvelle classe.

Description de la classe que je souhaite créer

Je souhaite créer une classe nommée "Game" qui représente un match dans la feuille de matchs (vous pouvez retrouver la description de l'application dans le premier billet).

Cette classe "Game" doit remplir les objectifs suivants :

  • stocker les scores des sets du match
  • calculer le nombre total de points du match (ceci n'a en fait aucune utilité dans mon application mais c'est une fonction utile dans l'exemple de ce billet… car il illustre très bien la problématique)

Je veux pouvoir faire des choses comme :

MyApp.game_1 = MyApp.Game.create({});

MyApp.game_1.sets[0] = 5; // Attention ici "sets" correspond à des sets d'un match
MyApp.game_1.sets[1] = 2;

console.log(MyApp.game_1.total()); // retourne 7

Première implémentation qui ne fonctionne pas comme souhaité

Voici une première version de ma classe "Game" qui ne fonctionne pas comme souhaité.

Dans main.js :

MyApp = Ember.Application.create();

MyApp.Game = Ember.Object.extend({
    sets: [ // Attention, ici "sets" correpsond à des sets d'un match
        Ember.Object.create({ value: '' }),
        Ember.Object.create({ value: '' }),
        Ember.Object.create({ value: '' }),
        Ember.Object.create({ value: '' }),
        Ember.Object.create({ value: '' })
    ],
    total: function() {
        var total = 0;
        this.sets.forEach(function(item) {
            if (
                (item.value != '') &&
                (!isNaN(item.value))
            ) {
                total += parseInt(item.value);
            }
        });
        return total;
    }.property('sets.@each.value')
});

MyApp.games = [
    MyApp.Game.create(),
    MyApp.Game.create(),
    MyApp.Game.create()
]

Explication du code source :

  • ligne 1 : je déclare l'application
  • ligne 3 : je déclare la nouvelle classe nommée "Game"
  • lignes 4-10 : déclaration de la propriété sets avec des scores de sets vides (attention, ici un sets correspond à des sets de matchs)
  • lignes 11 à 22 : déclaration de la méthode total. Cette méthode parcourt tous les éléments de la propriété sets et fait la somme de ses valeurs en transformant la valeur des sets en entier…
  • ligne 22 : "l'annotation" property, indique que la valeur de la méthode total change dès qu'une proriété value de la propriété sets est modifiée
  • lignes 25-29 : création de 3 objet de type Game dans un tableau nommé games

Voici ci-dessous, le template qui utilise la couche modèle qui vient d'être défini (les instances dans "MyApp.games" et la classe "MyApp.Game").

Dans index.html :

<body>
<script type="text/x-handlebars">
  <h1>Game 1</h1>
    <ul>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.0.sets.0.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.0.sets.1.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.0.sets.2.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.0.sets.3.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.0.sets.4.value"}}
      </li>
    </ul>

  <p>Total : {{MyApp.games.0.total}}</p>

  <h1>Game 2</h1>
    <ul>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.1.sets.0.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.1.sets.1.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.1.sets.2.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.1.sets.3.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.1.sets.4.value"}}
      </li>
    </ul>

  <p>Total : {{MyApp.games.1.total}}</p>

  <h1>Game 3</h1>

    <ul>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.2.sets.0.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.2.sets.1.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.2.sets.2.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.2.sets.3.value"}}
      </li>
      <li>
        {{view Ember.TextField valueBinding="MyApp.games.2.sets.4.value"}}
      </li>
    </ul>

  <p>Total : {{MyApp.games.2.total}}</p>
</script></body>

Explication :

  • ligne 2 : balise d'ouverture un bloc template de Ember.js
  • ligne 6 : cette "view" crée une balise du type <input type="text"…, ce champ input est directement connecté avec l'objet MyApp.games.0.sets.0.value de la couche modèle. Ce qui veut dire que lorsque cet objet est mise à jour du coté de la couche modèle alors le champ input est directement mise à jour. Inversement, si je champ input est mise à jour via l'Interface Utilisateur alors la couche modèle est mise à jour automatiquement.
  • ligne 22 : la valeur retournée par la méthode MyApp.games.0.total est affichée et elle est automatiquement mise à jour… grâce à l'annotation property('sets.@each.value')
  • lignes 22-42 : affichage de la seconde instance MyApp.games.1
  • lignes 45-65 : affichage de la troisième instance MyApp.games.2

Vous pouvez tester cette implémentation en live sur jsFiddle.

Qu'est ce qu'il ne fonctionne pas dans cette implémentation ?

Dans cette implémentation, lorsque l'on modifie la valeur des sets d'une instance, par exemple « Game 1 », les champs des autres matchs « Game 2 » et « Game 3 » sont eux aussi mise à jour avec les mêmes valeurs.

Toutes les instances de classe de type "Game" partagent les mêmes données !

Pourquoi ?

Parcequ'un objet de type tableau est directement affecté à la propriété sets de la classe Game lors de sa déclaration… ce tableau est créé une fois… et est donc identique et partagé par toutes les instances de type Game.

La bonne solution : déclarer le tableau dans le constructeur

Voici maintenant l'implémentation qui fonctionne comme voulu.

Dans main.js :

MyApp = Ember.Application.create();

MyApp.Game = Ember.Object.extend({
    init: function() {
        this._super();
        this.set('sets', [
            Ember.Object.create({ value: '' }),
            Ember.Object.create({ value: '' }),
            Ember.Object.create({ value: '' }),
            Ember.Object.create({ value: '' }),
            Ember.Object.create({ value: '' })
        ]);
    },
    total: function() {
        var total = 0;
        this.sets.forEach(function(item) {
            if (
                (item.value != '') &&
                (!isNaN(item.value))
            ) {
                total += parseInt(item.value);
            }
        });
        return total;
    }.property('sets.@each.value')
});

MyApp.games = [
    MyApp.Game.create(),
    MyApp.Game.create(),
    MyApp.Game.create()
]

Le fichier index.html est exactement identique à la première implémentation.

Tester cette version en live sur jsFiddle.

Explications :

  • lignes 4-13: cette fois, un constructeur (méthode init) est déclaré
  • ligne 5 : cette ligne importante pour le bon fonctionnement d'une classe Ember.js
  • ligne 6 : ici est la subtilité, il faut absolument passer par la méthode set pour ajouter une nouvelle propriété à la classe
  • ligne 7-11 : ajout de 5 objets directement dans le tableau

Cette fois, cette implémentation fonctionne comme voulu car chaque instance de type Game contient un tableau différent.

Attention à ne pas utiliser la méthode "push" mais bien "pushObject"

Voici une autre subtilité, l'exemple ci-dessous ne fonctionne pas comme souhaité car il utilise la méthode push de la classe Array.

Dans main.js :

MyApp = Ember.Application.create();

MyApp.Game = Ember.Object.extend({
    init: function() {
        this._super();
        this.set('sets', Ember.A([]));
        this.get('sets').push(Ember.Object.create({ value: '' }));
        this.get('sets').push(Ember.Object.create({ value: '' }));
        this.get('sets').push(Ember.Object.create({ value: '' }));
        this.get('sets').push(Ember.Object.create({ value: '' }));
        this.get('sets').push(Ember.Object.create({ value: '' }));
    },
    total: function() {
        var total = 0;
        this.sets.forEach(function(item) {
            if (
                (item.value != '') &&
                (!isNaN(item.value))
            ) {
                total += parseInt(item.value);
            }
        });
        return total;
    }.property('sets.@each.value')
});

MyApp.games = [
    MyApp.Game.create(),
    MyApp.Game.create(),
    MyApp.Game.create()
];

Tester l'exemple en live sur jsFiddle.

Exemple qui fonctionne avec l'utilisation de pushObject :

MyApp = Ember.Application.create();

MyApp.Game = Ember.Object.extend({
    init: function() {
        this._super();
        this.set('sets', Ember.A([]);
        this.get('sets').pushObject(Ember.Object.create({ value: '' }));
        this.get('sets').pushObject(Ember.Object.create({ value: '' }));
        this.get('sets').pushObject(Ember.Object.create({ value: '' }));
        this.get('sets').pushObject(Ember.Object.create({ value: '' }));
        this.get('sets').pushObject(Ember.Object.create({ value: '' }));
    },
    total: function() {
        var total = 0;
        this.sets.forEach(function(item) {
            if (
                (item.value != '') &&
                (!isNaN(item.value))
            ) {
                total += parseInt(item.value);
            }
        });
        return total;
    }.property('sets.@each.value')
});

MyApp.games = [
    MyApp.Game.create(),
    MyApp.Game.create(),
    MyApp.Game.create()
];

Tester l'exemple en live sur jsFiddle.

Explication :

  • Lorsque l'on utilise un tableau qui doit être observé par Ember.js, il est important d'utiliser la méthode pushObject et non pas push sinon property('sets.@each.value') ne fonctionne plus.

Dans le cadre de l'application « Feuille de matchs »

L'application « Feuille de matchs » met en oeuvre ce que l'on a vu ci-dessus.

J'ai perdu pas mal de temps avec la subtilité d'utiliser this.set dans le constructeur. J'ai aussi perdu du temps car j'ai utilisé par erreur la méthode push à la place de pushObject.

J'espère que ce billet vous sera utile et qui vous évitera de perdre du temps là où j'en ai perdu.

Read and Post Comments

Ember.js - Épisode 1 - Présentation de l'application feuille de matchs

Je souhaite écrire une série de billets à propos de Ember.js, un framework Javascript qui permet d'écrire des applications webs.

J'ai commencé à étudier et à expérimenter Ember.js il y a quelques semaines.

C'est le billet « Une bien belle mise à jour » sur le blog du service Capitaine Train (n'hésitez pas à me contacter si vous souhaitez recevoir une invitation à ce super service en ligne) qui m'a donné envie d'étudier Ember.js.

J'avais déjà croisé cet outil par le passé… mais j'avais vite zapé car je n'avais pas vu l'intérêt de ce énième framework javascript.

Une série de billets sur le développement d'une application web ?

Je souhaite réaliser ma série de billets à propos de Ember.js sous la forme d'un récit du développement d'une petite application web, mettant en oeuvre entre autres : Ember.js (Javascript coté client) et Pyramid (en Python coté serveur).

Pourquoi sous cette forme ? Et bien parce que je pense qu'un exemple complet de réalisation d'une application (que je souhaite dans tous les cas réaliser) peut être une bonne source d'information pour ceux qui veulent se lancer avec Ember.js et Pyramid.

À noter… que vous pourrez apprendre des choses uniquement sur Ember.js sans avoir l'obligation d'étudier Pyramid… et inversement.

Mes premiers contacts avec Ember.js

Ma rencontre avec Ember.js a suivi ces étapes :

  1. j'ai regardé vite fait… je n'ai pas vu son potentiel
  2. j'ai regardé un peu plus la documentation… et c'est la partie suivante de la documentation qui a attiré mon attention « Computed Properties (Getters) »
  3. j'ai fait un petit exemple rapide… j'ai été impressionné par la fonctionnalité suivante de Ember.js (qui est son cœur… sa raison d'être) :
    • lorsque l'on modifie les données au niveau de la couche modèle… le rendu de la page est automatiquement mis à jour (en rejouant le ou les templates présents dans la page)
    • lorsqu'il y a une interaction utilisateur au niveau de l'UI… les données de la couche modèle sont automatiquements mis à jour
  4. j'ai commencé à réaliser un premier projet plus concret… et je suis très vite resté bloqué sur des problèmes… que j'ai maintenant réussi à résoudre mais après beaucoup de recheches

Ma conclusion :

  • Ember.js semble être un super outil…
  • Ember.js manque encore de documentation… et surtout des exemples

À travers le récit du développement de l'application « Feuille de matchs » je souhaite justement partager les difficultés que j'ai rencontré et indiquer des solutions.

Avertissement

Je n'ai pas beaucoup d'expérience sur Ember.js, dans ce billet et dans les billets suivants… je ne vous dis pas qu'il faut absolument que vous utilisiez cet outil.

Peut être que dans six mois je vais m'apercevoir et conclure que cet outil a plein d'inconvénients, plein de limitations… mais pour le moment j'adore… tout va bien à part le manque de documentation.

Je pourrai vous en dire plus… quand j'aurai développé une ou deux applications webs basées sur Ember.js.

Présentation rapide de l'application

Je fais du Tennis de Table en compétition depuis plusieurs années.

Voici la feuille de match d'une rencontre de Tennis de Table par équipe :

Mon objectif… est de créer un service web permettant de :

  • saisir d'une manière conviviale une feuille de match
  • partager facilement une feuille de match sur le web
Là où Ember.js est intéressant dans cette application web, c'est qu'il y a beaucoup d'intéraction au niveau des champs de la feuille de match.
Par exemple, lors de la saisie des noms des joueurs dans les deux cadres en haut du document… ces noms sont automatiquement mis à jour dans la liste des matchs.
Le total des sets… des matchs doit être automatiquemnt mis à jour lors de la saisie des scores des sets.

Code source et démonstration de l'application

Voici une démonstration de la première version de l'application. Pour le moment il n'y a qu'une petite partie de l'application Javascript qui est réalisée.

Je ferai un screencast de la démonstration pour le prochain billet.

Par la suite… je vais mettre à jour ce billet pour qu'il continue à être à jour avec les évolutions de l'application… du code source…

Read and Post Comments

Mes flux :