Skip to content

Latest commit

 

History

History
1715 lines (1303 loc) · 48.7 KB

07-VUES-ET-TEMPLATING.md

File metadata and controls

1715 lines (1303 loc) · 48.7 KB

#Vues & Templating

Sommaire

  • 1ère vue
  • Mise à jour automatique de l’affichage
  • Sous-vues
  • Templating
  • évènements

Nous avons joué avec les données dans le chapitre précédent, nous allons maintenant voir comment les afficher dynamiquement dans notre page web.

Le composant View de Backbone est peut-être celui qui génère le plus de polémiques. Est-ce vraiment une vue ? Ne serait-ce pas plutôt un contrôleur ? Il se trouve que dans une version plus ancienne de Backbone, le composant Controller existait, aujourd’hui il est le devenu le composant Router que nous verrons par la suite … Cependant, un routeur est-il réellement un contrôleur ?... Mais, rappelez-vous que l’on est dans un contexte client (navigateur) et que le concept MVC « classique » n’est pas forcément « portable » en l’état. L’essentiel est que cela fonctionne, et si les contrôleurs vous manquent à ce point, nous verrons comment en créer quelques chapitres plus loin.

##Préparons le terrain

Pour repartir sur de bonnes bases, nous allons supprimer la base de données avec laquelle nous avons déjà bien joué. Donc supprimez le fichier blog.db de la racine de votre application. Ensuite, modifiez le code javascript de la page index.html dans le répertoire /public, donc dans la partie <script></script>, pour instancier une collection : (on ajoute : window.blogPosts = new Posts();)

Instancier une collection :

$(function() {

  window.Post = Backbone.Model.extend({
  urlRoot: "/blogposts"

  });

  window.Posts = Backbone.Collection.extend({
  model: Post,
  all: function() {
    this.url = "/blogposts";
    return this;
  },
  query: function(query) {
    this.url = "/blogposts/query/" + query;
    return this;
  }
  });

  window.blogPosts = new Posts();
});

Sauvegardez, puis relancez votre application (node app.js ou nodemon app.js), dans le navigateur accédez à la page principale (http://localhost:3000/), pour enfin ouvrir la console de votre navigateur. Nous allons créer des modèles, que nous ajouterons à la collection blogposts.

###Création et sauvegarde des modèles

Commencez par saisir ceci dans la console du navigateur :

Ajouter des modèles à la collection :

  var messages = [
  "Maecenas sed diam eget risus varius blandit sit amet non magna.",
  "Integer posuere erat a ante venenatis dapibus posuere velit aliquet.",
  "Donec id elit non mi porta gravida at eget metus.",
  "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
  "Cras mattis consectetur purus sit amet fermentum.",
  "Nulla vitae elit libero, a pharetra augue."];

  blogPosts.add([
  new Post({
  title: "Premier Message",
  message: messages[0],
  date: new Date(2012, 10, 23, 7, 4, 0, 0),
  author: "bob"
  }),
  new Post({
  title: "Backbone ???",
  message: messages[1],
  date: new Date(2012, 10, 23, 7, 5, 0, 0),
  author: "bob"
  }),
  new Post({
  title: "Les Modèles",
  message: messages[2],
  date: new Date(2012, 10, 23, 7, 6, 0, 0),
  author: "sam"
  }),
  new Post({
  title: "Les Vues",
  message: messages[3],
  date: new Date(2012, 10, 23, 7, 7, 0, 0),
  author: "sam"
  }),
  new Post({
  title: "Les Routes",
  message: messages[4],
  date: new Date(2012, 10, 23, 7, 8, 0, 0),
  author: "bob"
  }),
  new Post({
  title: "Mais où sont les contrôleurs ?",
  message: messages[5],
  date: new Date(2012, 10, 23, 7, 9, 0, 0),
  author: "bob"
  })

  ])

Remarque : en javascript, pour les dates, le chiffre 10 correspond à Novembre (faire +1)

Nous avons donc maintenant 5 Posts dans notre collection. Pour ne pas avoir à tout re-saisir à chaque fois, sauvegardez vos posts (toujours dans la console du navigateur) :

Sauvegarder les modèles en base :

blogPosts.each(function(post) {
  post.save({}, {
  success: function(post) {
    console.log(post.get("title"), " sauvegardé");
  },
  error: function() {
    console.log("Oupss");
  }
  });
})

Vous devriez au final obtenir ceci :

BB\

Pour vérifier que la sauvegarde a bien fonctionné, raffraichissez votre page et lancez ce code dans la console du navigateur :

Charger la collection avec les modèles sauvegardés en base :

blogPosts.all()
  .fetch({
  success: function(result) {
  console.log(result);
  }
})

Si tout va bien (il n’y a pas de raison), vous devriez obtenir ceci :

BB\

Ainsi, quoiqu’il se passe, vous disposez de tous vos messages et ne serez plus obligés de les ressaisir pour la suite des exercices. Nous pouvons donc entrer dans le vif du sujet.

##1ère vue

Un objet Vue dans Backbone (Backbone.View) et généralement composé (au minimum) par convention de :

  • une propriété el : c'est l'élément du DOM (la partie de votre page html à laquelle on rattache l'objet View)
  • une méthode initialize (déclenchée à l’instanciation de la vue)
  • une méthode render (chargée d’afficher les données liées à la vue)

Remarque : Libre à vous de vous faire vos propres bonnes pratiques concernant les responsabilités de l’objet View afin de rendre votre code lisible et maintenable … Vous trouverez toujours quelqu’un pour les discuter mais c’est comme cela que l’on apprend et s’améliore … Et vous pouvez aussi avoir raison :-).

Dans notre page index.html nous allons ajouter un tag <div id="posts_list"></div> comme ceci :

<div class="container">
  <div class="hero-unit">
  <h1>Backbone rocks !!!</h1>
  </div>
  <div id="posts_list"></div>
</div>

Et modifier le code javascript de la manière suivante : entre la définition de la collection et son instanciation, ajoutez le code de notre première vue :

1ère vue pour afficher le contenu de la collection :

window.PostsListView = Backbone.View.extend({
  el: $("#posts_list"),
  initialize: function(data) {
  this.collection = data;
  },
  render: function() {
  var html = "";
  $(this.el).html(""); //on vide le div
  this.collection.each(function(model) {

    html += [
    '<h1>' + model.get("title") + '</h1><hr>',
    '<b>par : ' + model.get("author") + '</b> le : ' + model.get("date") + '<br>',
    '<p>' + model.get("message") + '</p>'].join("");

  });

  $(this.el).append(html);

  }
});

###Explications & utilisation

Notre vue PostsListView est reliée au tag <div id="posts_list"></div> par la propriété el qui n’est ni plus ni moins un objet jQuery. La méthode initialize (qui sera appelée à l’instanciation de la vue), prend en paramètre les données que nous souhaitons afficher, et les affecte à la propriété collection de la vue. La méthode render, vide le contenu du tag <div id="posts_list"></div>, parcourt la collection de données pour construire le code html, et enfin affiche celui-ci par la commande $(this.el).append(html). Mais utilisons directement notre code, ce sera plus « parlant ».

Sauvegardez, rafraichissez la page et en mode console, passez les étapes qui suivent :

1] Chargez les données de la collection :

blogPosts.all()
  .fetch({
  success: function(result) {
  console.log(result);
  }
})

2] Instanciez la vue en lui passant la collection en paramètre :

postsListView = new PostsListView(blogPosts)

3] Appelez la méthode render de la vue :

postsListView.render()

Et vous obtenez la liste de vos messages :

BB\

Souvenez vous, dans les chapitres précédents nous avions « donné » aux collections la possibilité de faire des requêtes sur les données avant de lancer un fetch. Essayez donc ceci dans la console de votre navigateur :

Je ne veux que les posts de l’auteur "Sam" :

blogPosts.query('{"author" : "sam"}')
  .fetch({
  success: function(result) {
  console.log(result);
  }
})

Puis faite à nouveau un :

postsListView.render()

Et là, l’affichage s’actualise automatiquement :

BB\

##Maintenant, un peu de magie ...

###S'abonner aux événements

Modifions une nouvelle fois notre vue en ajoutans le code suivant à la méthode initialize :

_.bindAll(this, 'render');
this.collection.bind('reset', this.render);

Nous venons d’expliquer que tous les évènement déclarés déclencheront la méthode render de la vue. Et ensuite nous avons expliqué que la méthode reset de la collection déclenchera la méthode render de la vue.

Remarque : Une collection Backbone déclenche un reset lors de l’appel d’un fetch. La méthode reset vide la collection.

//TODO: faire un chapitre à part sur _.bindAll

Le code de notre vue doit donc ressembler à ceci :

PostListView :

window.PostsListView = Backbone.View.extend({
  el: $("#posts_list"),
  initialize: function(data) {
  this.collection = data;

  _.bindAll(this, 'render');
  this.collection.bind('reset', this.render);

  },
  render: function() {
  var html = "";
  $(this.el).html(""); //on vide le div
  this.collection.each(function(model) {

    html += [
    '<h1>' + model.get("title") + '</h1><hr>',
    '<b>par : ' + model.get("author") + '</b> le : ' + model.get("date") + '<br>',
    '<p>' + model.get("message") + '</p>'].join("");

  });

  $(this.el).append(html);

  }
});

Sauvegardez ensuite la page, puis retournez dans le navigateur, rafraichissez la page et retournez dans la console du navigateur pour instancier une nouvelle vue :

postsListView = new PostsListView(blogPosts)

Puis essayez ceci :

blogPosts.all()
  .fetch({
  success: function(result) {
  console.log(result);
  }
})

et cela :

blogPosts.query('{"author" : "sam"}')
  .fetch({
  success: function(result) {
  console.log(result);
  }
})

Vous remarquez que votre vue se met à jour automatiquement à chaque changement, sans avoir à rappeler la méthode render de la vue. Mais il est possible de faire ceci aussi avec les changements sur les modèles.

###S’abonner à d’autres évènements (modèles)

Toujours dans la méthode initialize de la vue, ajoutez le code suivant :

this.collection.bind('change', this.render);
this.collection.bind('add', this.render);
this.collection.bind('remove', this.render);

Maintenant, si vous changez la valeur d'un attribut d'un modèle, que vous ajoutez ou supprimez un modèle de la collection, la vue sera réactualisée à chaque fois.

Sauvegardez la page, puis retournez dans le navigateur, rafraichissez la page et retournez dans la console du navigateur pour instancier une nouvelle vue et charger les données de la collection :

postsListView = new PostsListView(blogPosts)
blogPosts.all()
  .fetch({
  success: function(result) {
  console.log(result);
  }
})

Puis changez le titre du 1er post :

blogPosts.at(0).set("title","BACKBONE ???!!!")

ou ajoutez un post :

blogPosts.add(new Post({title:"HELLO",message : "salut", author : "k33g"}))

ou encore supprimez un post :

blogPosts.remove(blogPosts.at(0));

Là encore, votre page s’actualise instantanément.

###Amélioration & Finalisation du code

Avant de passer à l’utilisation des templates dans les vue, nous allons apporter quelques modifications et améliorer un peu notre code pour nous préparer à la suite.

Dans ses dernières versions, Backbone a hérité d’un raccourcis concernant la propriété el de la vue qui consiste à remplacer (avec pour objectif l’optimisation d’exécution de code) le sélecteur $(this.el) par this.$el.

De plus, vous devez savoir qu’il n’est pas obligatoire de déclarer l’affectation de la collection dans la méthode initialize de la vue, mais que l’on peut faire ceci directement en paramètre du constructeur à l’instanciation de la vue. Comme ceci :

new PostsListView({collection : blogPosts})

On pourrait faire de même avec el, et utiliser ceci :

new PostsListView({el : $("#posts_list"), collection : blogPosts})

En fait tout dépend de vos besoins (et de vos habitudes).

En ce qui nous concerne, modifions le code de notre vue de la manière suivante :

PostsListView :

window.PostsListView = Backbone.View.extend({
  el: $("#posts_list"),
  initialize: function() {

  _.bindAll(this, 'render');
  this.collection.bind('reset', this.render);
  this.collection.bind('change', this.render);
  this.collection.bind('add', this.render);
  this.collection.bind('remove', this.render);

  },
  render: function() {
  var html = "";
  this.$el.html(""); //on vide le div
  this.collection.each(function(model) {

    html += [
    '<h1>' + model.get("title") + '</h1><hr>',
    '<b>par : ' + model.get("author") + '</b> le : ' + model.get("date") + '<br>',
    '<p>' + model.get("message") + '</p>'].join("");

  });

  this.$el.append(html);

  }
});

Puis à la fin du code javascript, ajoutez le code qui instancie la vue, ainsi que le code qui « charge » la collection (on se souvient que le render de la vue sera déclenché automatiquement une fois le fetch de la collection terminé.) :

window.postsListView = new PostsListView({
  collection: blogPosts
})

blogPosts.all().fetch({
  success: function(result) {
  console.log(result);
  }
});

Le code final du script dans la page devrait ressembler à ceci :

Code final :

<!-- === code applicatif === -->
<script>
$(function() {

  window.Post = Backbone.Model.extend({
  urlRoot: "/blogposts"

  });

  window.Posts = Backbone.Collection.extend({
  model: Post,
  all: function() {
    this.url = "/blogposts";
    return this;
  },
  query: function(query) {
    this.url = "/blogposts/query/" + query;
    return this;
  }

  });

  window.PostsListView = Backbone.View.extend({
  el: $("#posts_list"),
  initialize: function() {

    _.bindAll(this, 'render');
    this.collection.bind('reset', this.render);
    this.collection.bind('change', this.render);
    this.collection.bind('add', this.render);
    this.collection.bind('remove', this.render);

  },
  render: function() {
    var html = "";
    this.$el.html(""); //on vide le div
    this.collection.each(function(model) {

    html += [
      '<h1>' + model.get("title") + '</h1><hr>',
      '<b>par : ' + model.get("author") + '</b> le : ' + model.get("date") + '<br>',
      '<p>' + model.get("message") + '</p>'].join("");

    });

    this.$el.append(html);

  }
  });

  window.blogPosts = new Posts();

  window.postsListView = new PostsListView({
  collection: blogPosts
  })

  blogPosts.all().fetch({
  success: function(result) {
    //ça marche !!!
  }
  });
});
</script>

Nous avons fait un peu de magie, passons donc à la sorcellerie ;) ...

##Utilisation du templating ... 1ère fois

Vous vous souvenez ? Je vous avez parlé d'underscore avec les templates ? Eh bien il est temps de les mettre en œuvre.

###Définition de notre 1er template

Dans la partie HTML de notre page, juste avant <div id="posts_list"></div>, ajoutez le code ci-dessous (ce sera notre template) :

<!-- template pour les posts -->
<script type="text/template" id="posts_list_template">

<% _.each(posts ,function(post){ %>
  <h1><%= post.get("title") %></h1><hr>
  <b>par : <%= post.get("author") %></b> le : <%= post.get("date") %><br>
  <p><%= post.get("message") %></p>
<% }); %>

</script>

Remarque : le fait de définir le template à l'intérieur de <script type="text/template"></script> fait que le modèle de template ne sera pas affiché dans la page.

En fait (grace à underscore), nous venons de définir le template dont la vue backbone va se servir pour afficher les données. Il faudra lui passer pour cela un tableau de posts. Modifions donc notre vue de la façon suivante :

window.PostsListView = Backbone.View.extend({
  el: $("#posts_list"),
  initialize: function() {
  this.template = _.template($("#posts_list_template").html());

  _.bindAll(this, 'render');
  this.collection.bind('reset', this.render);
  this.collection.bind('change', this.render);
  this.collection.bind('add', this.render);
  this.collection.bind('remove', this.render);

  },
  render: function() {
  var renderedContent = this.template({
    posts: this.collection.models
  });

  this.$el.html(renderedContent);
  }
});

Nous avons inséré dans la méthode initialize : this.template = _.template($("#posts_list_template").html());, où nous expliquons que la définition du template se trouve dans la zone ayant posts_list_template pour id. Nous avons aussi simplifié grandement le contenu de la méthode render, où nous faisons générer le contenu HTML à partir du template et des données.

Remarque : notez bien que this.collection.models est un tableau de modèles.

Vous pouvez sauvegarder et rafraichir, les résultats sont identiques aux précédent, mais il est beaucoup plus facile de créer et modifier vos templates html.

BB\

##Sous-vue(s)

Il est possible de faire des sous vues pour gérer différentes parties de votre page web. En fait il s’agit de vues encapsulées dans une autre vue, ce qui peut être pratique en termes d’organisation, mais aussi dans les cas où les comportements de chacunes des vues dépendent les uns des autres.

Nous allons donc profiter des possibilités de Twitter Bootstrap pour revoir un peu la mise en page de notre « site » et du même coup mettre en œuvre le concept de sous-vue.

###Réorganisation du code html

Modifions notre code html de la manière suivante :

<div class="navbar navbar-fixed-top">
  <div class="navbar-inner">
    <div class="container">
      <a class="brand">Mon Blog</a>
    </div>
  </div>
</div>

<div class="container-fluid">

  <div class="row-fluid">

    <div class="span3">
      <script type="text/template" id="blog_sidebar_template">
        <h2>Les 3 derniers :</h2>
        <ul>
        <% _.each(posts ,function(post){ %>
          <li><%= post.get("title") %></li>
        <% }); %>
        </ul>
      </script>
      <div class="sidebar" id="blog_sidebar">
        <!-- Last 3 posts -->
      </div>
    </div>

    <div class="span9">
      <div class="hero-unit">
        <h1>Backbone rocks !!!</h1>
      </div>

      <!-- template pour les posts -->
      <script type="text/template" id="posts_list_template">

        <% _.each(posts ,function(post){ %>
          <h1><%= post.get("title") %></h1><hr>
          <b>par : <%= post.get("author") %></b> le : <%= post.get("date") %><br>
          <p><%= post.get("message") %></p>
        <% }); %>

      </script>
      <div class="row-fluid" id="posts_list"></div>
    </div>
  </div>
</div>

Explications : nous avons donc 2 templates, un pour afficher les 3 derniers posts (id="blog_sidebar_template"), un pour afficher tous les posts (id="posts_list_template").

###Création & Modification des vues

Nous allons créer une vue principale (MainView) qui se chargera de "piloter" 2 sous-vues (SidebarView et PostsListView) :

1] Simplifions PostsListView :

window.PostsListView = Backbone.View.extend({
  el: $("#posts_list"),
  initialize: function() {
    this.template = _.template($("#posts_list_template").html());
  },
  render: function() {
    var renderedContent = this.template({
      posts: this.collection.models
    });
    this.$el.html(renderedContent);
  }
});

2] Création de SidebarView :

window.SidebarView = Backbone.View.extend({
  el: $("#blog_sidebar"),
  initialize: function() {
    this.template = _.template($("#blog_sidebar_template").html());
  },
  render: function() {
    var renderedContent = this.template({
      posts: this.collection.models
    });
    this.$el.html(renderedContent);
  }
});

3] Création de la vue principale :

window.MainView = Backbone.View.extend({
  initialize: function() {

    _.bindAll(this, 'render');
    this.collection.bind('reset', this.render);
    this.collection.bind('change', this.render);
    this.collection.bind('add', this.render);
    this.collection.bind('remove', this.render);

    this.sidebarView = new SidebarView();
    this.postsListView = new PostsListView({
      collection: blogPosts
    });

  },
  render: function() {
    this.sidebarView.collection = new Posts(this.collection.first(3));
    this.sidebarView.render();
    this.postsListView.render();
  }
});

C'est donc maintenant la vue MainView qui s'abonne aux changements de la collection et déclenche le rendu des 2 autres vues.

Et enfin instancions la collection ainsi que la vue principale (qui se chargera d’instancier les deux sous-vues). Donc à la place de :

window.blogPosts = new Posts();
window.postsListView = new PostsListView({
  collection: blogPosts
})

Nous aurons ceci :

window.blogPosts = new Posts();
window.mainView = new MainView({
  collection: blogPosts
});

Vous pouvez sauvegarder votre code et raffraichir votre page :

BB\

Et si vous faites ceci en mode console :

blogPosts.at(0).set("title","Bonjour à tous !")

Vous verrez que les modifications sont bien propagées dans les 2 vues simultanément.

###Un dernier petit réglage : tri des collections

Nous souhaitons avant d’aller plus loin trier la collection de posts pour avoir les message en ordre décroissant. Pour cela nous allons créer ce que l’on appelle un « comparator » dans la méthode initialize de la vue principale MainView :

Trier la collection par ordre décroissant de date :

this.collection.comparator = function(model) {
  return -(new Date(model.get("date")).getTime());
}

Donc le code final de MainView sera celui-ci :

Vue principale :

window.MainView = Backbone.View.extend({
  initialize: function() {

    this.collection.comparator = function(model) {
      return -(new Date(model.get("date")).getTime());
    }

    _.bindAll(this, 'render');
    this.collection.bind('reset', this.render);
    this.collection.bind('change', this.render);
    this.collection.bind('add', this.render);
    this.collection.bind('remove', this.render);

    this.sidebarView = new SidebarView();
    this.postsListView = new PostsListView({
      collection: this.collection
    });

  },
  render: function() {

    this.sidebarView.collection = new Posts(this.collection.first(3));
    this.sidebarView.render();
    this.postsListView.render();
  }
});

Et le rendu dans le navigateur devrait vous donner ceci :

BB\

//TODO : faire un paragraphe sur le comparator dans le chapitre sur les collections

##Utilisation d’autre(s) moteur(s) de template

Vous lirez souvent que Backbone est "framwork agnostic", donc vous pouvez par exemple l'utiliser avec zepto, plutôt que jQuery en ce qui concerne la gestion du DOM et des appels Ajax. Il en est de même avec le moteur de template. Rien ne vous oblige à utiliser celui d’Underscore. Un des plus utilisé est Mustache.js (http://mustache.github.com/). Vous pouvez récupérer le code ici : https://github.com/janl/mustache.js/. En fait, plus précisément, enregistrez le fichier https://raw.github.com/janl/mustache.js/master/mustache.js dans votre répertoire public/libs/vendors. Puis faites y référence dans votre page index.html :

<script src="libs/vendors/mustache.js"></script>

Et nous allons une fois de plus "casser" notre code html.

###Redéfinissons donc nos templates

1] Avant pour la partie concernant la vue SidebarView nous avions ceci :

<script type="text/template" id="blog_sidebar_template">
  <h2>Les 3 derniers :</h2>
  <ul>
  <% _.each(posts ,function(post){ %>
      <li><%= post.get("title") %></li>
  <% }); %>
  </ul>
</script>

2] Que vous allez remplacer par ceci :

<script type="text/template" id="blog_sidebar_template">
  <h2>Les 3 derniers :</h2>

  <ul>{{#posts}}
    <li>{{title}}</li>
  {{/posts}}</ul>

</script>

3] En ce qui concerne le template lié à la vue PostsListView, remplacez :

<script type="text/template" id="posts_list_template">

  <% _.each(posts ,function(post){ %>
    <h1><%= post.get("title") %></h1><hr>
    <b>par : <%= post.get("author") %></b> le : <%= post.get("date") %><br>
    <p><%= post.get("message") %></p>
  <% }); %>

</script>

4] Par :

<script type="text/template" id="posts_list_template">

  {{#posts}}
    <h1>{{title}}</h1>
    <b>par : {{author}}</b> le : {{date}}<br>
    <p>{{message}}</p>
  {{/posts}}

</script>

Nous obtenons donc des templates html plus lisibles, utilisable moyennant une petite modification de nos vues :

5] Avant (avec le moteur de template d’underscore) :

window.PostsListView = Backbone.View.extend({
  el: $("#posts_list"),
  initialize: function() {
    this.template = _.template($("#posts_list_template").html());
  },
  render: function() {
    var renderedContent = this.template({
      posts: this.collection.models
    });
    this.$el.html(renderedContent);
  }
});

window.SidebarView = Backbone.View.extend({
  el: $("#blog_sidebar"),
  initialize: function() {
    this.template = _.template($("#blog_sidebar_template").html());
  },
  render: function() {
    var renderedContent = this.template({
      posts: this.collection.models
    });
    this.$el.html(renderedContent);
  }
});

6] Après (en utilisant Mustache) nous aurons ceci :

window.PostsListView = Backbone.View.extend({
  el: $("#posts_list"),
  initialize: function() {
    this.template = $("#posts_list_template").html();
  },
  render: function() {
    var renderedContent = Mustache.to_html(
    this.template, {
      posts: this.collection.toJSON()
    });

    this.$el.html(renderedContent);
  }
});

window.SidebarView = Backbone.View.extend({
  el: $("#blog_sidebar"),
  initialize: function() {
    this.template = $("#blog_sidebar_template").html();
  },
  render: function() {
    var renderedContent = Mustache.to_html(
    this.template, {
      posts: this.collection.toJSON()
    });

    this.$el.html(renderedContent);
  }
});

Vous noterez l'utilisation de this.collection.toJSON() plutôt que this.collection.models. En effet Mustache a besoin d’objets au format JSON, et (cela tombe bien), les collections Backbone dispose d’une méthode d’exportation/mise en forme au format JSON.

Sauvegardez, lancez, il n’y a pas de changement, l'affichages est identique (heureusement), vous avez juste utilisé une autre façon de travailler.

##Gestion des événements dans les vues

Les objets de type Backbone.View peuvent aussi gérer les évènements (mouseover, click, etc. …). Nous allons donc profiter de ce paragraphe pour mettre en œuvre un système d’authentification dans notre application, qui utilisera donc cette possibilité. Il est temps de retourner travailler côté serveur quelques instants.

Si vous voulez en savoir plus sur les événements dans les vues Backbone, je vous engage fortement à lire la documentation : http://backbonejs.org/#View-delegateEvents.

Donc ...

##Authentification (côté serveur) : les utilisateurs

Ce paragraphe ne parle pas des vues, mais est nécessaire pour la mise en place des paragraphes suivants.

Nous aurons besoin d’une liste des utilisateurs connectés (je vous le rappelle, nous sommes côté serveur, donc dans le fichier app.js) que nous représenterons sous la forme d’un tableau de variables (ou d’objets) :

var connectedUsers = [];

Nous aurons besoin d’ajouter des utilisateurs dans notre base de données, donc nous allons nous créer de quoi rajouter au moins une fois quelques utilisateurs pour notre application :

Ajouter un utilisateur en base :

function addUser(user) {
  users.save(null, user, function(err, key) {
    if (err) {
      console.log("Erreur : ", err);
    } else {
      user.id = key;
      console.log(user);
    }
  });
}

Nous appellerons n fois cette fonctions pour ajouter des utilisateurs :

Ajouter des utilisateurs :

function addUsers() {
  addUser({
    email     : "[email protected]",
    password  : "backbone",
    isAdmin   : true,
    firstName : "Bob",
    lastName  : "Morane"
  });
  addUser({
    email     : "[email protected]",
    password  : "underscore",
    isAdmin   : false,
    firstName : "Sam",
    lastName  : "Le Pirate"
  });

  //etc. ...
}

Et pour déclencher l’ajout des utilisateurs, nous créeons une « route » addusers :

app.get('/addusers', function(req, res) {
  addUsers();
  res.json({
    MESSAGE: "Users added."
  });
});

Qu’il suffira d’appeler comme ceci dans le navigateur : http://localhost:3000/addusers/

Remarque : notez bien que mon système d’authentification est très « léger ». En production, il vous faudrait quelque chose de plus abouti, mais ce n’est pas le propos de cet ouvrage. Nous avions besoin de quelque chose de simple.

###S’authentifier – Se déconnecter

Nous aurons besoin de nous authentifier. Il nous faut donc d’abord une fonction « utilitaire » qui nous permette de vérifier si l’email de l’utilisateur n’est pas déjà pris (utilisateur déjà connecté sous une autre session) :

Vérifier si un utilisateur est déjà connecté :

function findUserByMail(email) {
  /*
    Permet de vérifier si un utilisateur est déjà loggé
  */
  return connectedUsers.filter(function(user) {
    return user.email == email;
  })[0];
}

Nous allons donc créer une route authenticate avec le code suivant :

Code pour s’authentifier :

app.post('/authenticate', function(req, res) {
  console.log("POST authenticate ", req.body);
  //Je récupère les information de connexion de l'utilisateur
  var user = req.body;

  //est ce que l'email est déjà utilisé ?
  if (findUserByMail(user.email)) {
    res.json({
      infos: "Utilisateur déjà connecté"
    })
  } else { //si l'email n'est pas utilisé
    //Je cherche l'utilisateur dans la base de données
    users.find({
      email: user.email,
      password: user.password
    },

    function(err, results) {
      if (err) {
        res.json({
          error: "Oups, Houson, on a un problème"
        });
      } else {
        //J'ai trouvé l'utilisateur
        var key = Object.keys(results)[0],
          authenticatedUser = results[key];

        //Je rajoute l'id de session à l'objet utilisateur

        authenticatedUser.key = key;
        authenticatedUser.sessionID = req.sessionID;

        //Ajouter l'utilisateur authentifié à la liste des utilisateurs connectés
        connectedUsers.push(authenticatedUser);

        //Je renvoie au navigateur les informations de l'utilisateur
        // ... sans le mot de passe bien sûr
        res.json({
          email: authenticatedUser.email,
          firstName: authenticatedUser.firstName,
          lastName: authenticatedUser.lastName,
          isAdmin: authenticatedUser.isAdmin
        });
      }
    });
  }

});

Remarque : La « bienséance » (d’un point de vue architecture) voudrait que ne mette pas tout ce code au niveau de la route mais dans la méthode d’un contrôleur qui serait appelée par la route. Une fois de plus je vais au plus court, mais gardez à l’esprit : toujours un code lisible et maintenable.

Il faudra aussi pouvoir se déconnecter. Nous ajoutons donc une route logoff qui nous permettra de déconnecter l’utilisateur.

Nous avons tout d’abord besoin d’une fonction nous permettant de retrouver un utilisateur par son id de session parmi les utilisateurs connectés :

function findUserBySession(sessionID) {
  /*
    Permet de retrouver un utilisateur par son id de session
  */
  return connectedUsers.filter(function(user) {
    return user.sessionID == sessionID;
  })[0];

}

Que nous allons utiliser ensuite dans notre route logoff :

Se déconnecter :

app.get('/logoff', function(req, res) {

  //Je recherche l'utilisateur courant parmi les utilisateurs connectés
  var alreadyAuthenticatedUser = findUserBySession(req.sessionID);

  if (alreadyAuthenticatedUser) {
    //Je l'ai trouvé, je le supprime de la liste des utilisateurs connectés
    var posInArray = connectedUsers.indexOf(alreadyAuthenticatedUser);
    connectedUsers.splice(posInArray, 1);
    res.json({
      state: "disconnected"
    });
  } else {
    res.json({});
  }

});

Nous aurons aussi besoin d’un moyen pour savoir (côté client) si un utilisateur est déjà connecté. Donc nous allons créer une route alreadyauthenticated que la page web pourra « appeler » pour vérification (par exemple au rechargement de la page) :

Est-ce que je suis déjà authentifié ?

app.get('/alreadyauthenticated', function(req, res) {

  var alreadyAuthenticatedUser = findUserBySession(req.sessionID);

  //Si je suis déjà authentifié, renvoyer les informations utilisateur
  if (alreadyAuthenticatedUser) {
    res.json({
      email: alreadyAuthenticatedUser.email,
      firstName: alreadyAuthenticatedUser.firstName,
      lastName: alreadyAuthenticatedUser.lastName,
      isAdmin: alreadyAuthenticatedUser.isAdmin
    });
  } else {
    res.json({});
  }

});

Nous somme maintenant prêts à utiliser tout cela côté client. Le code définitif de app.js devrait ressembler à ceci :

/*--------------------------------------------
  Déclaration des librairies
--------------------------------------------*/
var express = require('express'),
  nStore = require('nstore'),
  app = module.exports = express.createServer();

nStore = nStore.extend(require('nstore/query')());

/*--------------------------------------------
  Paramétrages de fonctionnement d'Express
--------------------------------------------*/
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.static(__dirname + '/public'));
app.use(express.cookieParser('ilovebackbone'));
app.use(express.session({
  secret: "ilovebackbone"
}));

/*--------------------------------------------
  Définition des "bases" posts & users
--------------------------------------------*/
var posts, users;

posts = nStore.new("blog.db", function() {
  users = nStore.new("users.db", function() {
    Routes();
    app.listen(3000);
    console.log('Express app started on port 3000');

  });
});


/*======= Authentification =======*/

var connectedUsers = [];

function addUser(user) {
  users.save(null, user, function(err, key) {
    if (err) {
      console.log("Erreur : ", err);
    } else {
      user.id = key;
      console.log(user);
    }
  });
}

function addUsers() {
  addUser({
    email: "[email protected]",
    password: "backbone",
    isAdmin: true,
    firstName: "Bob",
    lastName: "Morane"
  });
  addUser({
    email: "[email protected]",
    password: "underscore",
    isAdmin: false,
    firstName: "Sam",
    lastName: "Le Pirate"
  });

  //etc. ...
}

function findUserBySession(sessionID) {
  /*
    Permet de retrouver un utilisateur par son id de session
  */
  return connectedUsers.filter(function(user) {
    return user.sessionID == sessionID;
  })[0];

}

function findUserByMail(email) {
  /*
    Permet de vérifier si un utilisateur est déjà loggé
  */
  return connectedUsers.filter(function(user) {
    return user.email == email;
  })[0];
}


function Routes() {
  /*======= Routes pour authentification =======*/

  app.get('/addusers', function(req, res) {
    addUsers();
    res.json({
      MESSAGE: "Users added."
    });
  });

  app.get('/alreadyauthenticated', function(req, res) {

    var alreadyAuthenticatedUser = findUserBySession(req.sessionID);

    /*  Si je suis déjà authentifié,
      renvoyer les informations utilisateur
      sans le mot de passe bien sûr
    */
    if (alreadyAuthenticatedUser) {
      res.json({
        email: alreadyAuthenticatedUser.email,
        firstName: alreadyAuthenticatedUser.firstName,
        lastName: alreadyAuthenticatedUser.lastName,
        isAdmin: alreadyAuthenticatedUser.isAdmin
      });
    } else {
      res.json({});
    }

  });

  app.post('/authenticate', function(req, res) {
    console.log("POST authenticate ", req.body);
    //Je récupère les information de connexion de l'utilisateur
    var user = req.body;

    //est ce que l'email est déjà utilisé ?
    if (findUserByMail(user.email)) {
      res.json({
        infos: "Utilisateur déjà connecté"
      })
    } else { //si l'email n'est pas utilisé
      //Je cherche l'utilisateur dans la base de données
      users.find({
        email: user.email,
        password: user.password
      }, function(err, results) {
        if (err) {
          res.json({
            error: "Oups, Houson, on a un problème"
          });
        } else {
          //J'ai trouvé l'utilisateur
          var key = Object.keys(results)[0],
            authenticatedUser = results[key];

          //Je rajoute l'id de session à l'objet utilisateur

          authenticatedUser.key = key;
          authenticatedUser.sessionID = req.sessionID;

          //J'ajoute l'utilisateur authentifié à la liste des utilisateurs connectés
          connectedUsers.push(authenticatedUser);

          //Je renvoie au navigateur les informations de l'utilisateur
          // ... sans le mot de passe bien sûr
          res.json({
            email: authenticatedUser.email,
            firstName: authenticatedUser.firstName,
            lastName: authenticatedUser.lastName,
            isAdmin: authenticatedUser.isAdmin
          });
        }
      });
    }

  });

  app.get('/logoff', function(req, res) {

    //Je recherche l'utilisateur courant parmi les utilisateurs connectés
    var alreadyAuthenticatedUser = findUserBySession(req.sessionID);

    if (alreadyAuthenticatedUser) {
      //Je l'ai trouvé, je le supprime de la liste des utilisateurs connectés
      var posInArray = connectedUsers.indexOf(alreadyAuthenticatedUser);
      connectedUsers.splice(posInArray, 1);
      res.json({
        state: "disconnected"
      });
    } else {
      res.json({});
    }

  });

  /*======= Fin des routes "authentification" =======*/

  /*
    Obtenir la liste de tous les posts lorsque
    l'on appelle http://localhost:3000/blogposts
    en mode GET
  */
  app.get('/blogposts', function(req, res) {
    console.log("GET (ALL) : /blogposts");
    posts.all(function(err, results) {

      if (err) {
        console.log("Erreur : ", err);
        res.json(err);
      } else {
        var posts = [];
        for (var key in results) {
          var post = results[key];
          post.id = key;
          posts.push(post);
        }
        res.json(posts);
      }
    });

  });

  /*
    Obtenir la liste de tous les posts correspondant à un critère
    lorsque l'on appelle http://localhost:3000/blogposts/ en
    mode GET avec une requête en paramètre
    ex : query : { "title" : "Mon 1er post"} }
  */
  app.get('/blogposts/:query', function(req, res) {
    console.log("GET (QUERY) : /blogposts/" + req.params.query);

    posts.find(JSON.parse(req.params.query), function(err, results) {
      if (err) {
        console.log("Erreur : ", err);
        res.json(err);
      } else {
        var posts = [];
        for (var key in results) {
          var post = results[key];
          post.id = key;
          posts.push(post);
        }
        res.json(posts);
      }
    });

  });

  /*
    Retrouver un post par sa clé unique lorsque
    l'on appelle http://localhost:3000/blogpost/identifiant_du_post
    en mode GET
  */

  app.get('/blogpost/:id', function(req, res) {
    console.log("GET : /blogpost/" + req.params.id);
    posts.get(req.params.id, function(err, post, key) {
      if (err) {
        console.log("Erreur : ", err);
        res.json(err);

      } else {
        post.id = key;
        res.json(post);
      }
    });
  });

  /*
    Créer un nouveau post lorsque
    l'on appelle http://localhost:3000/blogpost
    avec en paramètre le post au format JSON
    en mode POST
  */
  app.post('/blogpost', function(req, res) {
    console.log("POST CREATE ", req.body);

    var d = new Date(),
      model = req.body;
    model.saveDate = (d.valueOf());

    posts.save(null, model, function(err, key) {
      if (err) {
        console.log("Erreur : ", err);
        res.json(err);
      } else {
        model.id = key;
        res.json(model);
      }
    });
  });


  /*
    Mettre à jour un post lorsque
    l'on appelle http://localhost:3000/blogpost
    avec en paramètre le post au format JSON
    en mode PUT
  */
  app.put('/blogpost/:id', function(req, res) {
    console.log("PUT UPDATE", req.body, req.params.id);

    var d = new Date(),
      model = req.body;
    model.saveDate = (d.valueOf());

    posts.save(req.params.id, model, function(err, key) {
      if (err) {
        console.log("Erreur : ", err);
        res.json(err);
      } else {
        res.json(model);
      }
    });
  });

  /*
    supprimer un post par sa clé unique lorsque
    l'on appelle http://localhost:3000/blogpost/identifiant_du_post
    en mode DELETE
  */
  app.delete('/blogpost/:id', function(req, res) {
    console.log("DELETE : /delete/" + req.params.id);

    posts.remove(req.params.id, function(err) {
      if (err) {
        console.log("Erreur : ", err);
        res.json(err);
      } else {
        //petit correctif de contournement (bug ds nStore) :
        //ré-ouvrir la base lorsque la suppression a été faite
        posts = nStore.new("blog.db", function() {
          res.json(req.params.id);
          //Le modèle est vide si on ne trouve rien
        });
      }
    });
  });

}

##Authentification (côté client)

Nous repassons enfin au code client et nous allons pouvoir vérifier comment sont gérés les évènements dans une vue en implémentant l’authentification côté client.

###Formulaire d’authentification

Nous allons donc commencer par créer le template du formulaire d’authentification dans notre page index.html (je choisis de le placer juste après la liste des 3 derniers posts) :

<div class="sidebar" id="blog_sidebar">
  <!-- Last 3 posts -->
</div>

<!-- /*======= Formulaire d'authentification =======*/ -->
<script type="text/template" id="blog_login_form_template">
  <h3>Login :</h3>
  <input name="email" type="text" placeholder="email"/><br>
  <input name="password" type="password" placeholder="password"/><br>
  <a href="#" class="btn btn-primary">Login</a>
  <a href="#" class="btn btn-inverse">Logoff</a><br>
  <b>{{message}} {{firstName}} {{lastName}}</b>

</script>
<form class="container" id="blog_login_form">

</form>

###L’objet Backbone.View : Login.View

Notre composant d’authentification aura 2 zones de saisie (email et mot de passe), un bouton de login, un bouton pour se déconnecter, une zone pour afficher un message (bienvenue, erreur, …). Le composant devra aussi pouvoir vérifier si l’utilisateur est toujours connecté en cas de rafraîchissement de la page.

/*======= Authentification =======*/
window.LoginView = Backbone.View.extend({
  el: $("#blog_login_form"),

  initialize: function() {
    var that = this;
    this.template = $("#blog_login_form_template").html();

    //on vérifie si pas déjà authentifié
    $.ajax({
      type: "GET",
      url: "/alreadyauthenticated",
      error: function(err) {
        console.log(err);
      },
      success: function(dataFromServer) {

        if (dataFromServer.firstName) {
          that.render("Bienvenue", dataFromServer);
        } else {
          that.render("???", {
            firstName : "John",
            lastName  : "Doe"
          });
        }
      }
    })

  },

  render: function(message, user) {

    var renderedContent = Mustache.to_html(this.template, {
      message: message,
      firstName : user ? user.firstName : "",
      lastName  : user ? user.lastName : ""
    });
    this.$el.html(renderedContent);
  }

});

window.loginView = new LoginView();
/*======= Fin authentification =======*/

A l’initialisation (initialize) la vue va vérifier si l’utilisateur en cours est déjà authentifié (par exemple vous vous êtes signé, mais vous avez rafraîchi la page), en appelant la route /alreadyauthenticated, si l’utilisateur est déjà authentifié, la méthode render de la vue est appelée avec un message de bienvenue et les informations de l’utilisateur ( that.render("Bienvenue",dataFromServer); ) dans le cas contraire la méthode render est aussi appelée mais avec un message signifiant que l’utilisateur n’est pas connecté ( that.render("???",{firstName:"John", lastName:"Doe"}); ).

###Ajoutons une gestion des évènements

Une propriété de l’objet Backbone.View permet de gérer les évènements spécifiques à la vue. Si vous vous souvenez, notre template de formulaire ressemble à ceci :

<!-- /*======= Formulaire d'authentification =======*/ -->
<script type="text/template" id="blog_login_form_template">
  <h3>Login :</h3>
  <input name="email" type="text" placeholder="email"/><br>
  <input name="password" type="password" placeholder="password"/><br>
  <a href="#" class="btn btn-primary">Login</a>
  <a href="#" class="btn btn-inverse">Logoff</a><br>
  <b>{{message}} {{firstName}} {{lastName}}</b>
</script>

Je souhaite pouvoir déclencher des évènements lorsque je clique sur les boutons du formulaire. Pour cela il suffit d’ajouter à l’objet Backbone.View la propriété events :

events: {
  "click  .btn-primary": "onClickBtnLogin",
  "click  .btn-inverse": "onClickBtnLogoff"
},

En fait je demande à mon objet Backbone.View d’intercepter tous les évènements de type click sur les éléments html (de la vue considérée) dont la classe css est .btn-primary ou .btn-inverse et de déclencher respectivement les méthodes onClickBtnLogin ou onClickBtnLogoff.

Remarque : nous aurions très bien pu affecter des id aux boutons :

<a href="#" id="btnLogIn" class="btn btn-primary">Login</a>
<a href="#" id="btnLogOff" class="btn btn-inverse">Logoff</a><br>

et relier les évènements aux ids :

events: {
  "click  #btnLogIn"  : "onClickBtnLogin",
  "click  #btnLogOff" : "onClickBtnLogoff"
},

Il ne nous reste plus qu’à écrire les méthodes onClickBtnLogin ou onClickBtnLogoff au sein de notre objet de type Backbone.View qui vont respectivement appeler les routes que nous avions définies précédement :

onClickBtnLogin: function(domEvent) {

  var fields = $("#blog_login_form :input"),
    that = this;

  $.ajax({
    type: "POST",
    url: "/authenticate",
    data: {
      email: fields[0].value,
      password: fields[1].value
    },
    dataType: 'json',
    error: function(err) {
      console.log(err);
    },
    success: function(dataFromServer) {

      if (dataFromServer.infos) {
        that.render(dataFromServer.infos);
      } else {
        if (dataFromServer.error) {
          that.render(dataFromServer.error);
        } else {
          that.render("Bienvenue", dataFromServer);
        }
      }

    }
  });
},
onClickBtnLogoff: function() {

  var that = this;
  $.ajax({
    type: "GET",
    url: "/logoff",
    error: function(err) {
      console.log(err);
    },
    success: function(dataFromServer) {
      console.log(dataFromServer);
      that.render("???", {
        firstName: "John",
        lastName: "Doe"
      });
    }
  })
}

###Vérification

Si vous avez bien suivi, nous devons avoir au moins 2 utilisateurs en base de données :

function addUsers() {
  addUser({
    email     : "[email protected]",
    password  : "backbone",
    isAdmin   : true,
    firstName : "Bob",
    lastName  : "Morane"
  });
  addUser({
    email     : "[email protected]",
    password  : "underscore",
    isAdmin   : false,
    firstName : "Sam",
    lastName  : "Le Pirate"
  });

  //etc. ...
}
```javascript

Lançons donc notre page web :

![BB](RSRC/07_08_VIEWS.png)\


Authentifiez vous en tapant n’importe quoi :

![BB](RSRC/07_09_VIEWS.png)\


Vous obtenez le message **"Ouups loupé !!!"**

Authentifiez vous en utilisant un des utilisateurs existant :

![BB](RSRC/07_10_VIEWS.png)\


Vous obtenez un message de bienvenue.

Vous pouvez essayer de raffraîchir la page, vous restez connecté.

Si vous ouvrez un autre navigateur (une autre marque de navigateur pour être sûr de ne pas partager la session), vous vous apercevez qu’il ne considère pas que vous êtes authentifié :

![BB](RSRC/07_11_VIEWS.png)\


Essayez de vous connecter avec un utilisateur déjà loggé sur une autre session :

![BB](RSRC/07_12_VIEWS.png)\


Vous obtenez le message **"Utilisateur déjà connecté"**

Vous disposez maintenant de suffisament d'élément pour jouer avec les vues. Nous allons pouvoir passer au composant `Backbone.Router`. Nous reviendrons ensuite sur la sécurisation de notre application dans le chapitre sur l'organisation du code.