Skip to content

Latest commit

 

History

History
495 lines (326 loc) · 18.4 KB

2011-08-14-BB-VIEWS.md

File metadata and controls

495 lines (326 loc) · 18.4 KB

#Backbone : "Dans l'ordre" - Part III : "Les Vues ... (Contrôleurs ?)"

##Introduction

Aujourd'hui, nous allons initialiser nos 1ère vue. Nous allons repartir de l'exemple "myLittleBrain" version "local storage". Donc vous pouvez continuer, même si vous n'avez pas de serveur http sous la main (par ex, un ingé en mission de dév MS-Access qui ne peut rien installer sur son poste et qui s'ennuie ...)

Avant toute chose : le concept de View dans Backbone diffère "légèrement" de ce que les développeurs web connaissent du MVC (particulièrement si vous faites du Struts, Play!Framework, ASP.Net MVC, ...). En ce qui me concerne, une vue, c'est essentiellement ma page html (ou jsp, gsp, asp, ...) avec ses composants html, des tags, etc. ... Et on se sert du contrôleur pour lui envoyer des infos (ou en récupérer) et déclencher le rendu de celle-ci.

C'est là qu'il va falloir adapter son mode de pensée, en effet BB propose un objet Backbone.View mais cet objet est plus un "wrapper" autour des "noeuds html" et/ou des lib js de templating utilisées, et il se chargera de déclencher le "rendu" de la vue html.

... Euh, ben alors, c'est un contrôleur ? ... Ben oui, en tous les cas ça y ressemble ;)

Donc vous l'appelez comme vous voulez, mais vous allez voir cela fonctionne pas mal du tout

##1ère vue : afficher un modèle dans une vue, c'est parti !

###Templating

La librairie Underscore utilisée par BB propose un mini moteur de template qui est largement suffisant pour ce que nous avons à faire (l'utilisation d'autres moteurs fera l'objet d'un prochain paragraphe).

Nous allons donc définir dans un 1er temps notre template d'affichage :

On retourne dans index.html.

    <body>

        <script type="text/template" id="doc-template">
            <span><%= id %></span>
            <span><%= title %></span>
            <span><%= text %></span>
            <span><%= keywords %></span>
        </script>


        <div id='doc-container'></div>

    </body>

Que vient-on de faire :

  • Dans le tag <script type="text/template" id="doc-template"> nous avons décrit le template qui permettra d'afficher les données d'un modèle.
    • Les <%= ...%> sont une convention du moteur de template d'Underscore
    • type="text/template" permet de ne pas afficher le template dans le navigateur car ce type n'est pas reconnu par le navigateur
    • id="doc-template" nous sert à identifier le template de manière unique (on pourrait aussi utiliser d'autres méthodes pour l'identifier, comme l'attribut name, mais c'est un autre débat. Aujourd'hui, le principe sera : "Toutes les instances de Backbone.view utilisent/partagent le même template unique) PS : il faut que je me colle à faire un article sur id versus name & co.
  • Le tag <div id='doc-container'></div> contiendra les données rendues à l'affichage

Maintenant nous allons écrire notre objet Backbone.view

###View

On retourne dans myLittleBrain.js, et on ajoute ceci dans le code :

    window.DocView = Backbone.View.extend({
        el : $('#doc-container'),
        initialize : function() {
            this.template = _.template($('#doc-template').html());
        },

        render : function() {
            var renderedContent = this.template(this.model.toJSON());
            $(this.el).html(renderedContent);
            return this;
        }

    });

Quelques explications :

  • el : $('#doc-container') : el est une référence à un élément du DOM, il y a différentes manières de le renseigner, là nous avons choisi d'utiliser le sélecteur de Zepto (ou jQuery). Chaque vu doit avoir une propriété el qui référence "l'endroit" où vont être affichées les données. Ici el correspond à notre code html : <div id='doc-container'></div>
  • ensuite dans initialize nous "expliquons" à l'objet view que l'on utilise le moteur de template d'Underscore : _.template et que le template est égal au contenu html de l'élément du dom ayant pour id doc-template : $('#doc-template').html()
  • ensuite nous écrivons la méthode render de notre vue :
    • le contenu html à afficher : on transforme les infos du modèle en une chaîne JSON (this.model.toJSON()) que l'on passe ensuite au moteur de template : this.template(this.model.toJSON())
    • puis l'on remplace le contenu html (innerHTML) du container par le résultat obtenu : $(this.el).html(renderedContent);
  • remarque : le return this; nous permettra de "chaîner" les méthodes de la vue le cas échéant.

ATTENTION : si nous laissons notre code en l'état, la vue n'affichera rien lors de l'appel de .render car window.DocView est créée avant le chargement complet de la page HTML, donc $('#doc-container') n'existera pas et la référence el sera vide. Il y a plusieurs solutions (pour le moment):

  1. faire le include de <script src="js/myLittleBrain.js"></script> après la fermeture du tag <body>
  2. utiliser $(document).ready(...) pour déclarer notre vue (elle ne sera déclarée que lorsque la page web sera complètement chargée)
  3. setter la propriété el dans la méthode initialize de la vue

Mon choix est l'option 1 (pour le moment), en plus les performances de chargement sont meilleures, tout particulièrement dans le cas de webapps "single page".

Procédez donc au changement suivant dans votre code html (dans index.html) :

Vous aviez donc ceci :

    <!DOCTYPE HTML PUBLIC>
    <html>
    <head>
        <title>My Little Brain</title>
        <script src="js/vendor/zepto.min.js"></script>
        <script src="js/vendor/underscore-min.js"></script>
        <script src="js/vendor/backbone-min.js"></script>
        <script src="js/vendor/backbone-localstorage.js"></script>
        <script src="js/myLittleBrain.js"></script>

        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

    </head>
    <body>

        <script type="text/template" id="doc-template">
            <span><%= id %></span>
            <span><%= title %></span>
            <span><%= text %></span>
            <span><%= keywords %></span>
        </script>

        <div id='doc-container'></div>

    </body>

    </html>

que vous allez transformer en cela :

    <!DOCTYPE HTML PUBLIC>
    <html>
    <head>
        <title>My Little Brain</title>
        <script src="js/vendor/zepto.min.js"></script>
        <script src="js/vendor/underscore-min.js"></script>
        <script src="js/vendor/backbone-min.js"></script>
        <script src="js/vendor/backbone-localstorage.js"></script>

        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

    </head>
    <body>

        <script type="text/template" id="doc-template">
            <span><%= id %></span>
            <span><%= title %></span>
            <span><%= text %></span>
            <span><%= keywords %></span>
        </script>


        <div id='doc-container'></div>

    </body>
    <script src="js/myLittleBrain.js"></script>
    </html>

A limite, vous pouvez déplacer tous les scripts vers le bas

###On joue avec

Maintenant, vous ouvrez la page index.html avec votre navigateur. Normalement, si vous avez bien tout suivi bien comme il faut le tuto précédent, vous devez avoir des modèles en mémoire cache (sinon relisez le tuto précédent)

Alt "bb_02_07.png"

Qu'avons nous fait ? :

  • nous avons créé une collection : docs = new Docs();
  • nous avons chargé les données du "local storage" : docs.fetch();
  • nous avons vérifié qu'il y avait bien des données : docs.each(function(doc){ console.log(doc.id); });
  • nous avons instancié une vue en lui précisant qu'on lui attachait le modèle '002' : docView = new DocView({ model : docs.get('002') });
  • et enfin nous lui "avons dit" d'afficher les informations dans la page html : docView.render();

Maintenant vous pouvez faire docView.model = docs.get('003') puis à nouveau docView.render(); pour afficher un autre modèle.

Vous pouvez aussi modifier le code de la vue de la façon suivante :

    window.DocView = Backbone.View.extend({
        el : $('#doc-container'),
        initialize : function() {
            this.template = _.template($('#doc-template').html());
        },

        setModel : function(model) {
            this.model = model;
            return this;
        },

        render : function() {
            var renderedContent = this.template(this.model.toJSON());
            $(this.el).html(renderedContent);
            return this;
        }

    });

ainsi vous pourrez faire : docView.setModel(docs.get('003')).render()

###Rafraîchir la vue à chaque changement de valeur du modèle

On parle aussi de binding : la vue "écoute", et à chaque changement des objets modèles elle se réactualise et déclenche automatiquement sa méthode render() :

On explique à la vue qu'elle doit écouter les changements du model associé et déclencher un render() en cas de changement, nous utiliserons le code suivant :

    _.bindAll(this, 'render');
    this.model.bind('change', this.render);

Modifions notre vue en ajoutant le code comme ci-dessous (la partie entre /*--- binding ---*/ et /*---------------*/) :

    window.DocView = Backbone.View.extend({
        el : $('#doc-container'),
        initialize : function() {
            this.template = _.template($('#doc-template').html());

            /*--- binding ---*/
            _.bindAll(this, 'render');
            this.model.bind('change', this.render);
            /*---------------*/
        },

        setModel : function(model) {
            this.model = model;
            return this;
        },

        render : function() {
            var renderedContent = this.template(this.model.toJSON());
            $(this.el).html(renderedContent);
            return this;
        }

    });

Alt "bb_02_08.png"

Donc maintenant, à chaque changement du modèle, la vue est rafraîchie. Je trouve ça assez magique. Vous n'êtes pas obligé "d'écouter tous les changements", par exemple, vous pouvez ne surveiller que la propriété title du modèle : this.model.bind('change:title ', this.render);

##View + Collection

Maintenant, nous souhaitons afficher le contenu de notre collection. Pour cela nous allons créer un nouveau template dans la page html et une nouvelle vue dans le code javascript.

Ajoutez ce template dans la page html (ainsi qu'un div "container") :

    <script type="text/template" id="docs-collection-template">
        <ol>
          <% _.each(docs, function(doc) { %>
            <li><%= doc.id %> : <%= doc.title %></li>
          <% }); %>
        </ol>
    </script>

    <div id='docs-collection-container'></div>

Puis, nous allons créer la nouvelle vue :

    window.DocsCollectionView = Backbone.View.extend({
        el : $('#docs-collection-container'),

        initialize : function() {
            this.template = _.template($('#docs-collection-template').html());

            /*--- binding ---*/
            _.bindAll(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({ docs : this.collection.toJSON() });
            $(this.el).html(renderedContent);
            return this;
        }

    });

J'attire votre attention sur : var renderedContent = this.template({ docs : this.collection.toJSON() }); de la méthode render(), le template html que nous avons défini, "attend" une collection de modèles "attachés" à la "racine" docs :

  • dans la vue { docs : ... }
  • dans le template : <% _.each(docs, function(doc) { %>

On essaye :

Pour initialiser notre vue, c'est un peu différent de la vue précédente, nous luis passons une collection au lieu d'un modèle : docsView = new DocsCollectionView({ collection : docs });

Alt "bb_02_09.png"

Et là une fois de plus c'est magique, la vue "réagit" aux modification effectuées sur la collection. Vous pouvez aussi tester la suppression d'éléments : docs.remove(docs.get("001"))

##View + events

Nous allons créer une nouvelle vue HTML (sans template cette fois-ci), dans index.html, qui va nous servir à ajouter des documents à la liste (en fait un formulaire de saisie) :

    <body>
        <!-- mon formulaire de saisie -->
        <div id="doc-form-container">
            <form action="/">
                <p>
                    <input type="text" class="id" placeholder="Identifiant"/>
                </p>
                <p>
                    <input type="text" class="title" placeholder="Titre"/>
                </p>
                <p>
                    <input type="text" class="text" placeholder="Texte"/>
                </p>
                <p>
                    <input type="text" class="keywords" placeholder="Mots Clés"/>
                </p>
                <input type="submit" class="submit" value="Ajouter Document" />
            </form>
        </div>

        <!-- etc. ... -->

Ensuite dans myLittleBrain.js, il faut créer un nouvel objet Backbone.View :

    window.DocFormView = Backbone.View.extend({
        el : $('#doc-form-container'),

        initialize : function() {
            //Nothing to do now
        },
        events : {
            'submit form' : 'addDoc'
        },
        addDoc : function(e) {
            e.preventDefault();

            this.collection.add({
                id : this.$('.id').val(),
                title : this.$('.title').val(),
                text : this.$('.text').val(),
                keywords : this.$('.keywords').val()
            }, { error : _.bind(this.error, this) });

            this.$('input[type="text"]').val(''); //on vide le form
        },
        error : function(model, error) {
            console.log(model, error);
            return this;
        }

    });

Remarques :

  • e.preventDefault(); sert à désactiver le "post" du formulaire lorsque l'on clique sur le bouton et permet de faire prendre la main à notre application
  • nous avons une propriété events dans notre objet Backbone.View (ce qui le fait ressembler de plus en plus à un contrôleur ... Non ?)
  • Donc le click sur le bouton du formulaire 'submit form' va déclencher l'appel de la méthode 'addDoc' de la vue
  • nous nous sommes ajouté un petit gestionnaire d'erreur pour la forme

Démonstration :

Ouvrez la page index.html, en mode console, saisissez ceci (copiez/collez) :

    docs = new Docs();
    docs.fetch();
    docFormView = new DocFormView({ collection : docs });
    docsView = new DocsCollectionView({ collection : docs });
    docsView.render();

Saisissez un document :

Alt "bb_02_10.png"

Il apparaît automatiquement dans la liste :

Alt "bb_02_11.png"

Essayez de saisir un document sans titre, vous aurez un message d'erreur dans la console :

Alt "bb_02_12.png"

Assez facile, non ?

##Utilisation d'autres moteurs de template

Vous n'êtes pas obligé d'utiliser le moteur de template d'Underscore. Il est tout à fait possible d'utiliser d'autres librairies comme Mustache, ou Tempo, ou d'autres.

Bien sûr il faudra adapter votre code. Je vous donne un exemple avec Tempo justement (mais rapide, perfectible, etc. ...) :

  1. Téléchargez la lib tempo (version minifiée par exemple) tempo.min.js
  2. Déclarez là dans votre page index.html : <script src="js/vendor/tempo.min.js"></script>

###Définition du template

Dans la page html :

    <ol id="documents-list">
        <li data-template>{{id}} : {{title}} ({{text}})</li>
    </ol>

Alors Tempo travaille directement dans le code html et cherche les flags data-templates. Cela implique aussi qu'il faudra gérer le fait de cacher ou d'afficher l'élément html <ol> car on ne l'a pas encapsulé dans un tag <script type="text/template" id="doc-template">, un simple $('#documents-list').hide() au chargement de la page et $('#documents-list').show() dans la méthode render() de la vue devraient suffire. Mais, là on s'en "fiche", c'est pour l'exemple.

###Définition d'un nouvel objet View

Ensuite, dans votre code javascript, créez une nouvelle vue :

    window.DocsCollectionViewTempo = Backbone.View.extend({

        initialize : function() {
            this.template = Tempo.prepare('documents-list');

            /*--- binding ---*/
            _.bindAll(this, 'render');
            this.collection.bind('change', this.render);
            this.collection.bind('add', this.render);
            this.collection.bind('remove', this.render);
            /*---------------*/

        },

        render : function() {
            this.template.render(this.collection.toJSON());
            return this;
        }

    });

###Jouez !

Chargez à nouveau votre page html, mais en utilisant la nouvelle vue DocsCollectionViewTempo : (toujours en mode console)

    docs = new Docs();
    docs.fetch();
    docFormView = new DocFormView({ collection : docs });
    docsView = new DocsCollectionViewTempo({ collection : docs });
    docsView.render();

Ensuite, le principe reste identique à l'exemple précédent.

##Conclusion

Il y a beaucoup d'autre chose à dire sur les vues BB, je vous laisse creuser par vous même. Le seul conseil que je donnerais : il est possible de faire des choses complexes (vues imbriquées, héritage de vue, etc. ...), mais restez simple, sinon cela peut très rapidement devenir une usine à gaz.

Vous trouverez le code de cette partie, ici : https://github.com/k33g/articles/tree/master/samples/backbone3/myLittleBrain-03.

La prochaine fois, nous parlerons de Backbone.Router. A la fin de cette partie, vous devriez avoir fait le tour des composants BB et donc être à même de faire une application BB complète.