Accueil > Java EE > Démarrer un projet Ember.js

Démarrer un projet Ember.js

Cet article est le second d’une série consacrée à la découverte d’Ember.js, framework javascript pour créer des applications web single page, que j’utilise quotidiennement avec bonheur depuis maintenant plus d’un an.

Après une introduction rapide aux concepts fondamentaux d’Ember dans le 1er article, passons à la pratique en ciblant une première application idéale : un site de commerce en ligne. La vente de vêtements flashy permettra d’exploiter une police d’icônes vectorielles sans se prendre la tête avec les illustrations graphiques (oui c’est fluo, c’est moche et je le revendique).

Obtenir le kit de démarrage rapide du projet

Cloner le projet Github:

git clone git@github.com:philcast/flashy-clothes-store.git

Autopsie du kit:

  • index.html: source html unique de notre appli single page
  • css
    • app.css: styles css de l’application
    • foundation: toolkit css avec grille responsive (version 5.x)
  • fonts
    • cloths: police d’icones vectorielles que l’on pourra utiliser en css en modifiant leur taille et leur couleur
  • js
    • app.js: source js de l’application
    • lib: les dépendances js
      Dépendance Version Description
      ember.js 1.3.2 (obligatoire)
      ember-data.js 1.0.0-beta.6 librairie de persistance de données pour Ember.js (facultatif – utilisé plus tard)
      handlebars.js 1.2.1 librairie de templating logic-less (obligatoire)
      foundation 5.x librairie js nécessaire aux plugins du toolkit css Foundation + dépendance Modernizr (facultatif)
      jquery 2.0.3 (obligatoire)
Sources du projet:

Le fichier app.js contient le minimum vital pour créer une application Ember.js. La variable App servira d’espace de nommage pour éviter de possibles conflits avec les librairies tierces.

var App = Ember.Application.create();

Le fichier index.html contient lui un script inline de type text/x-handlebars dans son body. Il s’agit du template Handlebars de l’application, qui sera compilé au runtime par Ember pour générer du html à insérer dans le DOM (avant la balise fermante du body).

<html>
...
<body>

<script type="text/x-handlebars" data-template-name="application">
    <header>
        <h1><a href="#">Flashy clothes store</a></h1>
        <h4>An Ember.js demo application</h4>
    </header>
</script>

<script src="js/lib/jquery-2.0.3.min.js"></script>
<script src="js/lib/modernizr-2.7.1.min.js"></script>
<script src="js/lib/foundation-5.0.3/foundation.min.js"></script>
<script src="js/lib/handlebars-1.2.1.js"></script>
<script src="js/lib/ember-1.3.2.js"></script>
<script src="js/app.js"></script>

</body>
</html>

Pour simplifier le tutoriel, tous les futurs templates seront regroupés dans cette même page html.

Conception des modèles

Nous concevons ici des modèles pour Ember Data, un store client permettant de s’interfacer avec des fournisseur de données tiers via des adaptateurs.

Ember Data fournit un adaptateurs DS.RESTAdapter reposant sur des conventions pour (dé)sérialiser et générer les requêtes Ajax nécessaires pour accéder à une couche de persistance exposée via une API Rest. Il suffit de spécialiser cet adaptateur pour prendre en charge une API existante proposant d’autres conventions, tant que celles-ci ne sont pas trop exotiques, bien sur.

Déclarer le store

Il n’y aura rien à coder dans cette partie du tutoriel.
Afin de ne pas perdre de temps sur cette tâche ingrate, un fichier store.js déjà fourni configure un store Ember Data utilisant un adaptateur bouchon DS.FixtureAdapter simulant des services REST distants jusqu’à en reproduire le comportement asynchrone.

App.Store = DS.Store.extend({
    adapter: DS.FixtureAdapter
});

Note: penser à rajouter les dépendances ember-data-1.0.0-beta.6.js et store.js dans le fichier index.html.

<html>
...
<script src="js/lib/jquery-2.0.3.min.js"></script>
<script src="js/lib/modernizr-2.7.1.min.js"></script>
<script src="js/lib/foundation-5.0.3/foundation.min.js"></script>
<script src="js/lib/handlebars-1.2.1.js"></script>
<script src="js/lib/ember-1.3.2.js"></script>
<script src="js/lib/ember-data-1.0.0-beta.6.js"></script>
<script src="js/app.js"></script>
<script src="js/store.js"></script>
</body>
</html>

Déclarer les modèles

Le fichier contient aussi la déclaration des deux modèles Category et Item:

App.Category = DS.Model.extend({
    name: DS.attr(),
    cssClass: DS.attr(),
    //Relation OneToMany asynchrone chargée à la demande via une requête ajax
    items: DS.hasMany('item', {async: true})
});

App.Item = DS.Model.extend({
    name: DS.attr(),
    description: DS.attr(),
    price: DS.attr('number'),
    cssClass: DS.attr(),
    //Relation inverse
    category: DS.belongsTo('category', {async: true})
});

Tout modèle spécialise la classe DS.Model, et supporte des attributs de type string, number, boolean ou date (string par défaut si non spécifié).

Nous pouvons aussi observer une relation OneToMany entre les modèles Category et Item.
Les autres types de relation supportés sont OneToOne et ManyToMany.
Ember détermine la relation inverse en fonction du type du modèle cible. En cas d’ambiguité, il est possible de spécifier explicitement la relation inverse.

Note:

L’attribut cssClass des modèle sera très utile plus tard. Il correspond aux classes CSS à appliquer pour illustrer la catégorie ou l’item:

  • Classe de l’icone vectorielle servant d’illustration (ex: icon-suit1)
  • Classe de coloration de l’icone (ex: black)

Initialisation des données bouchon

Le FixtureAdapter simule un repository distant par de simples tableaux Javascript contenant des données initialisés en dure:

App.Category.FIXTURES = [
    {
        id: 1,
        name: 'Suits',
        cssClass: 'icon-suit1 blue',
        items: [1, 2, 3, 4, 5, 6]
    },
    ...
];

App.Item.FIXTURES = [
    {
        id: 1,
        category: 1,
        name: 'Black two button slim suit',
        description: 'Black suit with slim two button jacket and slim fit trousers.',
        price: 80,
        cssClass: 'icon-suit1 black'
    },
    ...
];

Rechercher des enregistrements

Le store Ember Data propose des finder renvoyant des résultats sous forme de promesse résolue une fois la réponse de la requêtes ajax obtenue.
Exemples:

  • Recherche de la liste de tous les items (vêtements): store.findAll('item').
  • Recherche de l’item d’identifiant 3: store.find('item', 3).

Remarque: La sauvegarde de données et la gestion des transactions ne sont pas nécessaires dans ce tutoriel et feront l’objet d’un futur article.

Définition des routes de l’application

La hiérarchie des routes devant matcher celle des IHMs, il est préférable de commencer par une maquette telle que celle vue précédement:

  • Application(racine): vue titre + panier
    • Catalog: vue liste des rayons (costumes, tshirts, robes,…)
      • Category: vue liste des vêtements du rayon sélectionné
        • Item: vue détail du vêtement sélectionné dans une popup

Revenons maintenant au fichier app.js pour décrire le routeur de notre application:

App.Router.map(function () {
    this.resource('catalog', function () {
        this.resource('category', { path: '/category/:category_id' }, function () {
            this.resource('item', { path: '/item/:item_id' });
        });
    });
    this.route('checkout'); //Valider le panier et commander
});

Etudions l’arborescence:

  • Les feuilles sont des routes.
  • Les noeuds sont des ressources.

Toute ressource possède au moins une route index automatiquement générée par Ember car implicite.
Dans une vrai application, la resource item pourrait également regrouper des routes Résumé, Description complète ou avis client.

Notre application comporte 3 ressources et 5 routes:

URL Nom Route Controller Route Template
/ index App.IndexController App.IndexRoute index
N/A catalog App.CatalogController App.CatalogRoute catalog
/catalog catalog.index App.CatalogIndexController App.CatalogIndexRoute catalog/index
N/A category App.CategoryController App.CategoryRoute category
/category/:category_id category.index App.CategoryIndexController App.CategoryIndexRoute category/index
N/A item App.ItemController App.ItemRoute item
/item/:item_id item.index App.ItemIndexController App.ItemIndexRoute item/index
checkout checkout App.CheckoutController App.CheckoutRoute checkout

Pour chaque définition de route ou de ressource, Ember associe un template et instancie automatiquement une (un gestionnaire de ?) route et un controlleur selon les convention de nommage citées ci-dessus.
Si il ne trouve pas une définition de classe (App.IndexController par exemple), il en utilisera une par défaut.

Nous devons ici déclarer la classe App.IndexRoute pour ajouter une redirection automatique vers l’autre route catalog.index à l’ouverture de l’application:

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

Nous pouvons ouvrir le fichier index.html dans un navigateur récent et observer le fragment dynamique #/catalog dans l’url pour constater la redirection.

Note: La ressource category a été définie avec un paramètre optionnel { path: '/category/:category_id' } spécifiant un pattern d’url. Ce pattern comporte un segment dynamique :category_id. Ember.js sait par convention qu’il devra utiliser le modèle App.Category d’identifiant id spécifié dans l’url.
Inversement, il sait aussi générer l’url en sérialisant l’identifiant du modèle.

Prochainement…

Une fois nos routes définies, nous pourrons les implémenter, dans un prochain article, et réellement commencer à coder notre application.
L’entrée en matière peut paraitre longue mais est représentative d’Ember. Il s’agit d’un framework client fullstack qui nous contraint à un travail de conception en amont pour éviter des déboires et du refactoring par la suite. Nous verrons également que ces contraintes permettent à Ember de nous apporter beaucoup de magie pour une meilleur productivité.

Categories: Java EE Tags: , , , , ,


+ 7 = seize