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.

blog comments powered by Disqus

Mes flux :