Le modèle objet de Backbone est puissant, mais ATTENTION! (ceci est un cri)

Je ne présente plus Backbone, cette petite librairie javascript MVC survitaminée. Un des aspects importants de Backbone, c’est son modèle objet que l’on peut deviner à travers ses différents composants (Model, Collection, View, Router). Ce modèle est séduisant car il nous permet de reproduire une logique “orientée classe” dans notre code javascript, mais attention les raccourcis peuvent être dangereux, car le modèle objet javascript est lui très différent d’un modèle objet tel celui de Java, .Net, etc. … Au passage je vous conseille cette excellente lecture : “Le point sur Javascript et l’héritage prototypal” par Naholyr.

Je me suis fait avoir pas plus tard qu’hier, donc voici une piqûre de rappel à travers 2 composants de Backbone (Modèles et Vues) sur quelques pièges à éviter. Les concepts sont applicables à l’ensemble des composants.

Les modèles

Rappel

Un modèle en Backbone est la structure de données qui va donc porter les informations. Généralement, il se définit de la façon suivante :

var Human = Backbone.Model.extend({

});

var bob = new Human({firstName:"Bob", lastName:"Morane"});

Donc bob est une instance de Human qui lui-même “hérite” de Backbone.Model, et sa structure ressemble à ceci :

Alt "BBOM-01.png"

Nous avons passé les fields (properties) et leurs valeurs directement dans le constructeur de Human et maintenant nous pouvons accéder à ces propriétés de la façon suivante :

//Qui es tu ?
console.log(bob.get("firstName"), bob.get("lastName"))

//Je veux mon nom en majuscule
bob.set("lastName","MORANE")

//Ou bien
bob.set({firstName:"BOB", lastName:"MORANE"})

Les méthodes d’instance de Model, donc get et set sont les équivalents de “getter” et “setter”, et ils permettent d’accéder et modifier la propriété attributes qui contient l’ensemble des données de la structure.

Le piège : les valeurs par défaut

Dans les modèles Backbone, il est possible de définir des valeurs par défaut, de la façon suivante :

var Human = Backbone.Model.extend({
    defaults : {
        firstName:"John",
        lastName:"Doe",
        friends:[]
    }
});

C’est très pratique. Vous pouvez ensuite instancier vos modèles et ajouter des friends à bob:

var bob = new Human({firstName:"Bob", lastName:"Morane"});
var john = new Human()
var jane = new Human({firstName:"Jane"})

bob.get("friends").push(john, jane)

Et là c’est le drame. Allons voir les “copains” de jane et de john (mais euh … tu ne parlais de bob ?)

Alt "BBOM-02.png"

Mais que s’est-il passé!? En fait, le champ friends est partagé entre toutes les instances!

Alors quand on vous dit de lire la documentation, ce n’est pas pour rien :) Mais dans ce cas là, ils auraient pu l’afficher en rouge et en gras. Sur http://backbonejs.org/# Model-defaults, il est dit en petit et en italique ceci :

Remember that in JavaScript, objects are passed by reference, so if you include an object as a default value, it will be shared among all instances. Instead, define defaults as a function.

Solution

Donc pour la faire simple et rapide, définissez vos valeurs par défaut avec une fonction de la manière suivante :

var Human = Backbone.Model.extend({
    defaults : function() {
      return {
        firstName:"John",
        lastName:"Doe",
        friends:[]
      }
    }
});

Et là, cela va tout de suite mieux :

Alt "BBOM-03.png"

Passons donc à la problématique suivante (dont les origines sont identiques) à l’aide des vues.

Les vues

La problématique qui va suivre est aussi valable pour les modèles, collections et routeurs mais on la trouve plus souvent dans le cas des vues.

Très rapide petit rappel

La vue dans Backbone (Backbone.View) est la structure qui va faire le lien entre vos modèles et votre affichage “HTML” dans le navigateur. … C’est donc … un contrôleur (private joke).

Elle se définit généralement de la manière suivante :

var Humans = Backbone.Collection.extend({
  model : Human
})

var HumansView = Backbone.View.extend({
  initialize : function () {
    this.$el = $("ul");
    this.template = _.template(
      "<% _.each(humans ,function(human){ %>"+
      "<li><%= human.firstName %> - <%= human.lastName %></li>"+
      "<% }); %>"
    );
  },
  render : function () {
    var renderedContent = this.template({humans:this.collection.toJSON()});
    this.$el.html(renderedContent);
  }
});

var humansView = new HumansView({collection:new Humans([bob,john,jane])})
humansView.render()

Côté HTML, j’ai juste à ajouter <ul></ul> dans ma page, et le contenu de ma vue ira gentiment s’insérer au milieu.

Et là tout va bien :

Alt "BBOM-04.png"

Le piège : encore une histoire de partage!

Ce cas de figure voudrait que l’on utilise des “subviews” (une vue par modèle), comme cela chaque sous-vue possède son propre modèle d’événements, méthodes etc. … ce qui permet d’interagir de manière unitaire sur les éléments de la liste, de ne pas être obligé de tout recharger pour rafraîchir un seul élément, etc. … etc. …

Donc notre code devrait ressembler à ceci :

var HumanItemView = Backbone.View.extend({
  tagName:"li",
  initialize : function () {
    this.template = _.template(
        "<li><%= firstName %> - <%= lastName %></li>"
    );
  },
  render : function () {
    var renderedContent = this.template(this.model.toJSON());
    this.$el.html(renderedContent);
    return this;
  }
});

var HumansView = Backbone.View.extend({
  el  : "ul",
  subViews : [],
  initialize : function () {
    this.collection.each(function(model){
        this.subViews.push(new HumanItemView({model:model}));
    },this)
  },
  render : function () {
    // Render each child view
    this.$el.empty();
    _(this.subViews).each(function (view) {
        this.$el.append(view.render().el);
    }, this);
  }
});

var humansView = new HumansView({collection:new Humans([bob,john,jane])})
humansView.render()

Donc, dans ma vue principale HumansView j’ai ajouté une propriété subViews. A l’initialisation je parcours la collection et j’instancie autant de sous-vues que de modèles.

Maintenant, je souhaite instancier une 2ème fois HumansView avec une nouvelle collection ayant un contenu différent pour pouvoir afficher un contenu de ma liste différent selon les cas :

var humansView = new HumansView({collection:new Humans([bob,john,jane])});
var humansView2 = new HumansView({collection:new Humans([john,jane])});

humansView.render();

Alt "BBOM-05.png"

Arghhh! … :) subViews est partagée entre toutes les instances de HumansView, donc les vues s’ajoutent pour toutes les instances.

Solution

En fait, il suffit de déclarer subViews dans la méthode initialize de cette manière :

var HumansView = Backbone.View.extend({
  el  : "ul",
  initialize : function () {
    this.subViews = [];
    this.collection.each(function(model){
      this.subViews.push(new HumanItemView({model:model}));
    },this)
  },
  render : function () {
    // Render each child view
    this.$el.empty();
    _(this.subViews).each(function (view) {
      this.$el.append(view.render().el);
    }, this);
  }
});

De la même manière …

J’aimerais pouvoir afficher mes 2 instances de HumansView en même temps et à 2 endroits différents dans ma page HTML :

<ul id="list1"></ul>
<hr>
<ul id="list2"></ul>

Dans ce cas là, il suffit de supprimer la référence el : "ul" qui elle aussi, je le rappelle est partagée avec toutes les instances de HumansView :

var HumansView = Backbone.View.extend({
  initialize : function () {
    this.subViews = [];
    this.collection.each(function(model){
      this.subViews.push(new HumanItemView({model:model}));
    },this)
  },
  render : function () {
    // Render each child view
    this.$el.empty();
    _(this.subViews).each(function (view) {
      this.$el.append(view.render().el);
    }, this);
  }
});

Et d’instancier nos 2 vues de cette façon :

var humansView = new HumansView({el:"# list1",collection:new Humans([bob,john,jane])});
var humansView2 = new HumansView({el:"# list2",collection:new Humans([john,jane])});

humansView.render();
humansView2.render();

Et là tout va bien :

Alt "BBOM-06.png"

Conclusion

Faites attention à la manière dont vous déclarez les propriétés de vos objets. Pensez d’abord à l’utilisation que vous allez en faire : le partage entre instance est aussi un aspect intéressant, mais il ne faut jamais l’oublier, car on reprend facilement des “vieux” réflexes “à la Java” qui ne fonctionnent pas forcément pareil en JavaScript.

J’espère que cet article servira à certains d’entre-vous.

Prochain épisode, nous verrons comment “détourner” le modèle objet de Backbone et se l’approprier à d’autres fins.

Bon WE à tous.

blog comments powered by Disqus

Related posts