EmberJs – De l’intérêt d’utiliser les Namespaces Ember ou comment connaître le nom de la classe d’une instance d’Ember.Object

Certaines parties de Ember/Ember-Data sont assez obscures pour les nouveaux venus. Parfois, les comportements du framework peuvent paraître relever de la magie aussi longtemps qu’on ne se plonge pas dans le code pour en isoler certains mécanismes.

C’était pour moi le cas concernant les requêtes émises par EmberData en fonction des modèles définit dans votre application.

Considérons le bout de code suivant :

MyApp.User = DS.Model.extend({
    firstname: DS.attr('string'),
    lastname: DS.attr('string')
    // ...
});

MyApp.User.find();

Lorsqu’on exécute un find sur ce modèle, EmberData est suffisamment intelligent pour lancer une requête GET sur /url/de/votre/api/user.

Mais comment ?

Quand on décortique l’adapter, on constate qu’au moment de construire la requête Ajax, ce dernier fait appel à la méthode rootForType du Serializer :

rootForType: function(type) {
    var typeString = type.toString();
    // [...]
    var parts = typeString.split(".");
    var name = parts[parts.length - 1];
    return name.replace(/([A-Z])/g, '_$1').toLowerCase().slice(1);
}

C’est en fait tout bête, le Serializer fait un toString sur notre classe du modèle.

MyApp.User.toString() // => MyApp.User

Et pour obtenir le même résultat depuis une instance, EmberData utilise tout simplement l’attribut constructor de l’objet :

MyApp.User.find(someId).then(function(someUser) {
    someUser.contructor.toString(); // => MyApp.User
});

On continue néanmoins à nager en pleine magie, comment Ember est-il capable d’afficher ce résultat sur un toString() d’un objet quelconque.
Ember redéfinie de manière intelligente la méthode toString de la manière suivante :

Ember.Mixin.prototype.toString = function() {
  if (!Ember.BOOTED && !this[NAME_KEY]) {
    processAllNamespaces();
  }

  var ret;
  if (this[NAME_KEY]) {
    ret = this[NAME_KEY];
  } else {
    var str = superClassString(this);
    if (str) {
      ret = "(subclass of " + str + ")";
    } else {
      ret = "(unknown mixin)";
    }
    this.toString = makeToString(ret);
  }

  return ret;
};

En lisant ce bout de code, on constate qu’un traitement particulier est fait à l’initialisation de l’application Ember pour parser tous les Namespaces.
Ember parcourt de manière récursive l’ensemble des Namespaces déclarés et surcharge pour chacune des classes appartenant à ces namespaces la méthode toString, de manière à renvoyer un path de la forme MyNameSpace.MySubNameSpace.MyAmazingClass.

Tout simplement !

Et je dois dire que ce comportement est bien pratique pour calculer certaines propriétés de manière générique, en se basant justement sur le nom de notre classe via la méthode toString.

Exemple :

// Au sein d'une classe héritant de Ember.Object et appartenant à un Namespace quelconque
myPropertyWhichReallyNeedMyClassname: function() {
    var parts = this.constructor.toString().split('.'),
        name = parts[parts.length - 1];

    return 'Something' + name;
}.property(),

Plus d’infos ici : http://www.emberist.com/2012/04/09/naming-conventions.html
Et dans le code évidemment : https://github.com/emberjs/ember.js/blob/master/packages/ember-runtime/lib/system/namespace.js

EmberJs – Construire une application autour de panels et d’onglets

Le router de emberjs se base massivement sur des conventions de nommage qui rendent ultra rapide le développement d’une application, notamment si celle-ci est basée sur un workflow standard de type CRUD.
Je liste mes entités, je peux voir une entité, je peux éditer une entité…
Exemples d’URLs de ce type :

  • /posts
  • /posts/1/show
  • /posts/1/edit
  • /authors
  • /authors/1/show
  • /authors/1/edit

Dans ce genre de cas, il n’est presque jamais nécessaire de surcharger les routes générées en mémoire par ember.

En revanche, quand une application devient un peu plus complexe dans sa structure, il devient indispensable de définir des routes spécifiques.

Aujourd’hui, je vais présenter une approche permettant de construire une application emberjs autour d’un layout découpé en panneaux (panels), chacun de ces panneaux étant capable d’afficher un ou plusieurs onglets. Evidemment, en ayant comme objectif que l’état de ces différents onglets soit géré par le router pour pouvoir le conserver via l’URL sur un reloading de la page.

Ci-dessous un petit schéma pour expliciter visuellement le contexte :

Application ember panes with tabs

Commençons par définir notre application et le layout associé :

// main.js
App = Ember.Application.create({});
// index.html
<script type="text/x-handlebars" data-template-name="application">
  <h1>This is a sample app</h1>
  {{outlet leftPanel}}
  {{outlet rightPanel}}
</script>

Pour savoir en permanence quels sont les onglets sélectionnés dans chacun des panneaux de gauche et de droite, il nous faut les stocker quelque part. L’endroit idéal est le contrôleur d’application qui se trouve hiérarchiquement au dessus de nos deux panels.
On lui ajoute donc deux propriétés leftTab et rightTab auxquelles on associe les onglets à afficher par défaut.
Ensuite, il nous faut définir les contrôleurs pour le panneau de gauche et le panneau de droite. Ils restent vides dans un premier temps mais ce sont eux qui s’occuperont de notifier le router lorsque l’utilisateur souhaite changer l’un ou l’autre des onglets sélectionnés.

App.ApplicationController = Ember.Controller.extend({
    leftTab: 'leftPanelTab1',
    rightTab: 'rightPanelTab1'
});
 
App.LeftPanelController = Ember.Controller.extend({
    // ...
});
 
App.RightPanelController = Ember.Controller.extend({
    // ...
});

Ajoutons les templates associés à nos panneaux. Ces derniers contiennent les liens vers les différents onglets et un outlet dans lequel notre router va injecter l’onglet sélectionné :

<script type="text/x-handlebars" data-template-name="leftPanel">
<div>Left panel
    <a href="#" {{action selectTab "leftPanelTab1"}}>Left panel tab 1</a>
    <a href="#" {{action selectTab "leftPanelTab2"}}>Left panel tab 2</a>
    {{outlet}}
</div>
</script>

<script type="text/x-handlebars" data-template-name="rightPane">
<div>Right panel
    <a href="#" {{action selectTab "rightPanelTab1"}}>Right panel tab 1</a>
    <a href="#" {{action selectTab "rightPanelTab2"}}>Right panel tab 2</a>
    {{outlet}}
</div>
</script>

Pour injecter nos panneaux au sein de notre layout, il est nécessaire d’implémenter la route ApplicationRoute :

App.ApplicationRoute = Ember.Route.extend({
    renderTemplate: function() {
        this.render();
        this.render('leftPanel', {
            outlet: 'leftPanel',
            into: 'application'
        });
 
        this.render('rightPanel', {
            outlet: 'rightPanel',
            into: 'application'
        });
    }
});

Pour notifier notre application que l’utilisateur souhaite changer l’onglet de l’un ou l’autre des panneaux, on utilise ici un helper action pour déclencher une action au niveau de nos contrôleurs, en lui spécifiant en paramètre l’onglet cliqué.

Il nous faut donc maintenant intercepter ces actions dans les contrôleurs et remonter l’information au niveau de notre router en les modifiant ainsi :

App.LeftPanelController = Ember.Controller.extend({
    selectTab: function(tab) {
        this.send('leftTabChanged', tab);
    }
});
 
App.RightPanelController = Ember.Controller.extend({
    selectTab: function(tab) {
        this.send('rightTabChanged', tab);
    }
});

Pour représenter l’état de nos panneaux (quels onglets sont affichés), nous allons implémenter une route prenant en paramètres l’onglet à afficher dans chaque zone.

App.PanelsRoute = Ember.Route.extend({
    serialize: function(params, paramNames) {
        return params;
    },
    renderTemplate: function(controller, params) {
 
        this.render(params.leftTab, {
            into: 'leftPanel'
        });
 
 
        this.render(params.rightTab, {
            into: 'rightPanel'
        });
    }
});

Et pour obtenir une belle URL associée, nous pouvons définir le mapping suivant :

App.Router.map(function() {
    this.resource('panels', { path: '/:leftTab/:rightTab' });
});

Enfin, pour terminer et lier l’ensemble, nous allons modifier notre ApplicationRoute afin d’y gérer les évènements associés aux changements d’onglets.
Quand un évènement nous notifiant du changement d’onglet dans le panneau de gauche remonte jusqu’au router, on redirige vers la route panels en lui injectant les nouveaux onglets sélectionnés.

App.ApplicationRoute = Ember.Route.extend({
    events: {
        leftTabChanged: function(tab) {
            this.controllerFor('application').set('leftTab', tab);
            this.transitionTo('panels', {
                leftTab: tab,
                rightTab: this.controllerFor('application').get('rightTab')
            });
 
        },
        rightTabChanged: function(tab) {
            this.controllerFor('application').set('rightTab', tab);
            this.transitionTo('panels', {
                leftTab:this.controllerFor('application').get('leftTab'),
                rightTab:tab
            });
        }
    },

    renderTemplates: // ...
}

Une autre solution pour répondre à ce même problème serait d’injecter dans nos controlers de panels l’onglet sélectionné dans le panneau opposé. On peut dans ce cas simplifier le code et utiliser directement des helpers linkTo. La solution présentée ici offre néanmoins une grande souplesse pour opérer des traitements intermédiaires lors de la selection par l’utilisateur d’un onglet dans un ou l’autre des panneaux.

Ci-dessous le code complet de cet exemple :

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

App.Router.map(function() {
    this.resource('panels', { path: '/:leftTab/:rightTab' });
});

App.ApplicationRoute = Ember.Route.extend({
    events: {
        leftTabChanged: function(tab) {
            this.controllerFor('application').set('leftTab', tab);
            this.transitionTo('panels', {
                leftTab: tab,
                rightTab: this.controllerFor('application').get('rightTab')
            });

        },
        rightTabChanged: function(tab) {
            this.controllerFor('application').set('rightTab', tab);
            this.transitionTo('panels', {
                leftTab:this.controllerFor('application').get('leftTab'),
                rightTab:tab
            });
        }
    },

    renderTemplate: function() {
        this.render();
        this.render('leftPanel', {
            outlet: 'leftPanel',
            into: 'application'
        });

        this.render('rightPanel', {
            outlet: 'rightPanel',
            into: 'application'
        });
    }
});

App.IndexRoute = Ember.Route.extend({
    redirect: function() {
        this.transitionTo('panels', {
            leftTab: this.controllerFor('application').get('leftTab'),
            rightTab: this.controllerFor('application').get('rightTab')
        });
    }
});

App.PanelsRoute = Ember.Route.extend({
    serialize: function(params, paramNames) {
        return params;
    },
    renderTemplate: function(controller, params) {

        this.render(params.leftTab, {
            into: 'leftPanel'
        });


        this.render(params.rightTab, {
            into: 'rightPanel'
        });
    }
});

App.ApplicationController = Ember.Controller.extend({
    leftTab: 'leftPanelTab1',
    rightTab: 'rightPanelTab1'
});

App.LeftPanelController = Ember.Controller.extend({
    selectTab: function(tab) {
        this.send('leftTabChanged', tab);
    }
});

App.LeftPanelView = Ember.View.extend({
    classNames: ['pane']
});

App.RightPanelController = Ember.Controller.extend({
    selectTab: function(tab) {
        this.send('rightTabChanged', tab);
    }
});

App.RightPanelView = Ember.View.extend({
    classNames: ['pane']
});
<script type="text/x-handlebars" data-template-name="application">
  <h1>This is a sample app</h1>
  {{outlet leftPanel}}
  {{outlet rightPanel}}
</script>

<script type="text/x-handlebars" data-template-name="leftPanel">
    <a href="#lefttab1" {{action selectTab "leftPanelTab1"}}>Left pane tab 1</a>
    <a href="#lefttab2" {{action selectTab "leftPanelTab2"}}>Left pane tab 2</a>
    <div class="tab-container">{{outlet}}</div>
</script>

<script type="text/x-handlebars" data-template-name="leftPanelTab1">
Left Panel Tab1
</script>

<script type="text/x-handlebars" data-template-name="leftPanelTab2">
Left Panel Tab2
</script>

<script type="text/x-handlebars" data-template-name="rightPanel">
    <a href="#righttab1" {{action selectTab "rightPanelTab1"}}>Right pane tab 1</a>
    <a href="#righttab2" {{action selectTab "rightPanelTab2"}}>Right pane tab 2</a>
    <div class="tab-container">{{outlet}}</div>
</script>

<script type="text/x-handlebars" data-template-name="rightPanelTab1">
    Right Panel Tab1
</script>

<script type="text/x-handlebars" data-template-name="rightPanelTab2">
Right Panel Tab2
</script>

Le résultat est visible sur ce jsfiddle : http://jsfiddle.net/obalais/L78yu
Et le gist associé ci-besoin : https://gist.github.com/bobey/5118200

Symfony2 MopaBoostrapBundle – Les problèmes fréquents

Ce bundle est parfait pour se lancer dans la réalisation d’une application basée sur le Twitter Boostrap. En revanche, c’est à chaque fois une vrai galère pour l’installer sur un projet sf2 existant.
Si vous partez sur une application toute neuve, je vous suggère de jeter un oeil à l’édition Symfony Boostrap hébergée sur ce repo github : https://github.com/phiamo/symfony-bootstrap. Elle intègre nativement les bundles suivants pré-configurés :

  • Symfony2
  • bootstrap
  • MopaBootstrapBundle
  • MopaBootstrapSandboxBundle

Si vous souhaitez intégrer ce même bundle avec vos petites mains, vous risquez de rencontrer quelques problèmes facilement solvables. Je ne vais pas refaire la documentation du projet qui est tout de même assez complète. Vous la trouverez à cette adresse : https://github.com/phiamo/MopaBootstrapBundle/blob/master/Resources/doc/index.md

Le fail le plus courant, c’est la compilation des fichiers less. Première chose à faire, vérifier qu’une version récente de nodejs est installée.
Sinon, sur Ubuntu :

sudo add-apt-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs npm
sudo npm install -g less

Plus d’infos à ce sujet ici : http://blog.overnetcity.com/19-05-2011/premiers-pas-avec-node-js-installation-de-node-et-express/

Une fois node, npm et less installés, il faut indiquer au MopaBoostrapBundle où se situent les binaires de node et du compilateur less.

Rajoutez dans votre fichier de config les lignes suivantes :

node: /usr/bin/node
node_paths: [/opt/lessc/lib, /usr/lib/node_modules]

Si le chargement de votre fichier css échoue et que vous vous choppez une stacktrace de ce type, c’est vraisemblablement que les chemins indiqués vers node ou lessc ne sont pas les bons :

Error Output:

module.js:340
throw err;
^
Error: Cannot find module less
at Function.Module._resolveFilename (module.js:338:15)
at Function.Module._load (module.js:280:25)
at Module.require (module.js:362:17)
at require (module.js:378:17)
at Object.<anonymous> (/tmp/assetic_lessnn6vMt:1:74)
at Module._compile (module.js:449:26)
at Object.Module._extensions..js (module.js:467:10)
at Module.load (module.js:356:32)
at Function.Module._load (module.js:312:12)
at Module.runMain (module.js:492:10)

Faites un petit whereis lessc et whereis node pour trouver les chemins qui vont bien !

Autre problème fréquent au moment de la compilation :

Error Output:
[ERROR] /path/to/project/src/YourProject/MainBundle/Resources/public/less/../img/glyphicons-halflings.png (No such file or directory)

Pour le coup, une petite recherche Google remonte pas mal de résultats à ce sujet. C’est un problème avec cssembed qui fonctionne mal avec les chemins relatifs. Il suffit en fait de recréer l’arborescence attendue par le boostrap twitter, soit en copiant le sprite vers le dossier img/, juste au dessus de votre fichier less principal, soit en utilisant un lien symbolique.
Plus d’infos ici : https://github.com/phiamo/MopaBootstrapBundle/blob/master/Resources/doc/assetic-configuration.md

Voilà c’est tout pour le moment. Je tâcherai de mettre à jour cet article en fonction des nouveaux points bloquants que je rencontrerai éventuellement.

Ember Data Serialization Process – Hooks

Le Serializer de Ember Data s’occupe de transformer une instance d’un modèle Ember dans le format accepté par votre API.
Pour cela, Ember Data met à la disposition du développeur toute une série de Hooks permettant de s’adapter à chaque API, aussi complexe et peu consistante soit-elle.

Le code de Ember Data est extrêmement bien documenté mais comme on dit qu’une image vaut toujours mieux qu’un long discours, j’en ai extrait ce graphique me permettant de trouver plus rapidement où me brancher selon les cas :

Ember Data Serialization Process

Peut-être qu’il pourra être utile à certains.

Et pour plonger dans le code du Serializer : https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/serializer.js