Ember.js ,Think different ! (Etape 3)

Dans l’article précédent, le modèle manipulé restait assez basique et ne comportait notamment pas de relation. Hors quiconque a travaillé avec des outils de mapping sait bien que toute la subtilité des frameworks de mapping objets vient des possibilités offertes à ce niveau. Nous allons donc mettre en place une relation 1..n dans le modèle utilisé.

Cet article fait partie d’une série composée des posts suivants :

  • Introduction à Ember – Rappel rapide sur les mécanismes mis en place par ce framework,
  • Etape 1  – Application simple avec données ‘rack’ retournées directement par le modèle sous forme d’un tableau JSON. Cette étape sera complété par l’utilisation de addons Ember pour rendre l’application plus esthétique et ergonomique.
  • Etape 2  –  Passage sur un modèle géré par Ember Data. Etant donné qu’aucun back-office ne sera déployé, nous introduirons le framework de mock Mirage pour intercepter les appels HTTP.
  • Etape 3 (ce post) – Introduction d’une relation rack —> bottle, et enrichissement des données mockées.
  • Etape 4 –  Construction d’un composant graphique permettant une représentation d’un rack et simplifiant les templates d’affichage
  • Etape 5 – Mise en place d’un backoffice construit à l’aide de JHipster. Nous profiterons des capacités de cet outil pour obtenir rapidement un modèle persisté en base, les services Rest permettant de l’administrer ainsi qu’une interface d’administration. Nous verrons au passage quelques problématiques liées à la sécurité.
  • Etape 6 – Mise en place d’un backoffice sur services Liferay et packaging de l’application Ember au sein d’une portlet. Nous avons choisi un backoffice Liferay pour sa facilité à construire un modèle de persistance mais surtout pour bien démontrer l’utilisation potentielle des Adapters et des Serializers, même si un backoffice plus conforme aux standards Rest n’en sera que plus aisé à utiliser.

Le code de cet article est disponible sur mon gitlab dans les branches step_3_x.

Step 3 : Introduction de relations dans le modèle

Step 3.1 : mise en place du modèle et des associations

Ember Data est une solution de cache de persistance complète permettant de gérer les relations entre attributs. Sur ce point, Mirage est encore un peu à la traîne et certaines opérations demandent un peu de travail…

On commence par compléter la partie modèle par l’objet ‘bottle’, à la fois côté Ember Data et Mirage :

ember generate model bottle
ember g mirage-model bottle
ember g mirage-factory bottle

On ajoute la propriété suivante au modèle de rack :

// app/models/rack.js

  bottles : DS.hasMany('bottle')

Et on complète une définition de modèle pour bottle :

// app/models/bottle.js

import Ember from 'ember';

import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr('string'),
  createdAt: DS.attr('date', {defaultValue() { return new Date(); }}),
  yrow: DS.attr('number'),
  xcolumn: DS.attr('number'),
  flipped: DS.attr('boolean', {defaultValue: false}),

  rack: DS.belongsTo('rack')
});

On a ainsi établi une relation 1..n entre rack et bottles.

Côté Mirage, on indique les différentes relations introduites pour que le framework puisse les mocker :

// mirage/models/bottle.js
import { Model, belongsTo } from 'ember-cli-mirage';

export default Model.extend({
 rack: belongsTo()
});

// mirage/models/rack.js

import { Model, hasMany } from 'ember-cli-mirage';

export default Model.extend({
  bottles: hasMany()
});

On ajoute une nouvelle factory pour les objets de type bottle :

// mirage/factories/bottle.js

import { Factory, faker } from 'ember-cli-mirage';

export default Factory.extend({
  name: faker.list.cycle('Château Beychevelle',
   'Ribera del Duero',
   'Clos Fourtet St.-Emilion',
   'Romanée Conti',
   'Chambolle Musigny Guillon',
   'Tenet Syrah Columbia Valley',
   'Oddero Barolo',
   'Gigondas Lavau',
   'Duorum Douro',
   'Philippe Alliet Chinon',
   'Orin Swift Machete',
   'Château Lagrezette',
   'Château Puech Haut',
   'Château Haut Bellevue'),
  createdAt() { return new Date(); },
  yrow() { return faker.random.number({min:1, max:3}); },
  xcolumn() { return faker.random.number({min:3, max:20}); },
  flipped() { return faker.random.boolean; }
});

On configure également quelques routes complémentaires au niveau de Mirage pour permettre la récupération des bouteilles d’un rack en complétant le fichier config.js :

// mirage/config.js

export default function() {

 this.namespace = 'api';

  this.get('/racks',  function(schema) {
     return schema.racks.all();
  });
  this.get('/racks/:id',  (schema, request) => {
    return schema.racks.find(request.params.id);
  });
  this.get('/bottles',  function(schema) {
     return schema.bottles.all();
  });
  this.get('/bottles/:id', (schema, request) => {
    return schema.bottles.find(request.params.id);
  });
}
Reste à construire un scénario d’instanciation d’un jeu de test. Cela se passe au niveau du fichier default.js dans le répertoire ‘scenarios’ :
// mirage/scenarios/default.js
export default function(server) {
 /*
   Seed your development database using your factories.
   This data will not be loaded in your tests.

   Make sure to define a factory for each model you want to create.
 */
 let numberOfRacks = 4;
 let numberOfBottles = 0;

 let racks = server.createList('rack', numberOfRacks);
 for (let i=0; i<numberOfRacks; i++) {
    for (let col=0; col < racks[i].nbColumns; col++) {
       for (let lig=0; lig < racks[i].nbRows; lig++) {
          if (random(0,10)<5) {
            server.create('bottle', { yrow: lig, xcolumn: col, rack: racks[i] });
            numberOfBottles++;
         }
      }
    }
  }
  console.log(numberOfBottles+" bottles created in "+numberOfRacks+" racks");
 function random(min, max) {
   return Math.floor(Math.random() * (max - min)) + min;
 }
}

On va également demander à Ember Data de ramener directement les Bottles liées à un Rack en forçant la relation à la manière d’un eager Hibernate, le tout en modifiant le finder dans la route :

// app/routes/rack.js
import Ember from 'ember';

export default Ember.Route.extend({
  model() {
     return this.store.findAll('rack', {include: 'bottles'});   }
});

Lors du rechargement de la page, on constate le bon chargement de l’ensemble des ressources (ici 4 racks pour 185 bouteilles) :

Au passage, on va aussi introduire la colonne capacité dans le tableau des racks affichés en modifiant simplement le contrôleur (le code, assez évident sur ce point, est disponible dans le GitLab du projet)

Step 3.2 Affichage des pages de détail

Pour ajouter une page de détail de rack, on ajoute une route dans la description d’une des colonnes du tableau initialisé dans le contrôleur :

// app/controllers/racks

   Ember.Object.create({
     propertyName: "name",
     title: "Name",
     routeName: "rack"
   }),

Cette route doit également être ajoutée au fichier router.js pour l’ouvrir à la consultation en précisant qu’on passe l’id du rack dans l’URL :

// app/router.js
import Ember from 'ember';
import config from './config/environment';

const Router = Ember.Router.extend({
 location: config.locationType,
 rootURL: config.rootURL
});

Router.map(function() {
 this.route('racks');
 this.route('rack', { path: '/rack/:rack_id' });
});

export default Router;

Enfin, la route en question doit par ailleurs être créée :

ember g route rack

Et on complète ce fichier pour aller chercher le rack en question :

// app/route/rack.js
import Ember from 'ember';

export default Ember.Route.extend({
 model(params) {
   let pid = params.rack_id;

   let rack = this.store.peekRecord('rack', pid);
   if (rack == null) {
     this.transitionTo('racks');
   } else {
     return rack;
  }
 }
});

A noter qu’on utilise la méthode peekRecord pour aller chercher dans le cache du Store plutôt que sur le serveur.
Reste à écrire un template un poil plus évolué pour afficher les informations concernant le Rack sélectionné :

<div class="col-md-12 phase-container">
  <h4><strong>Rack Name:</strong> {{model.name}} ({{model.nbRows}} rows by {{model.nbColumns}} columns)</h4>
  <ul>
    {{#each model.bottles as |bottle|}}
      <li>{{bottle.name}} : row {{bottle.yrow}}, column {{bottle.xcolumn}}</li>
    {{/each}}
  </ul>
{{outlet}}
</div>

Et voilà !