web-dev-qa-db-fra.com

Comment rendre et ajouter des sous-vues dans Backbone.js

J'ai une configuration imbriquée-View qui peut aller un peu en profondeur dans mon application. Je pourrais penser à différentes méthodes pour initialiser, restituer et ajouter les vues secondaires, mais je me demande quelle est la pratique courante.

Voici un couple auquel j'ai pensé:

initialize : function () {

    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});
},

render : function () {

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

    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

Avantages: Vous n'avez pas à vous soucier de maintenir le bon ordre de DOM avec l'ajout. Les vues sont initialisées très tôt, il n’ya donc pas beaucoup à faire en même temps dans la fonction de rendu.

Inconvénients: Vous êtes obligé de re-delegateEvents (), ce qui pourrait être coûteux? La fonction de rendu de la vue parent est encombrée de tous les rendus de sous-vues nécessaires? Vous n'avez pas la possibilité de définir le tagName des éléments, le modèle doit donc conserver les noms de balises corrects.

Autre moyen:

initialize : function () {

},

render : function () {

    this.$el.empty();

    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});

    this.$el.append(this.subView1.render().el, this.subView2.render().el);
}

Avantages: Vous n'avez pas à redéléguer des événements. Vous n'avez pas besoin d'un modèle qui ne contient que des espaces réservés vides et vos balises sont de nouveau définies par la vue.

Inconvénients: Vous devez maintenant vous assurer d’ajouter les éléments dans le bon ordre. Le rendu de la vue parent est toujours encombré par le rendu de la sous-vue.

Avec un événement onRender:

initialize : function () {
    this.on('render', this.onRender);
    this.subView1 = new Subview({options});
    this.subView2 = new Subview({options});
},

render : function () {

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

    //other stuff

    return this.trigger('render');
},

onRender : function () {

    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

Avantages: La logique de sous-vue est maintenant séparée de la méthode render() de la vue.

Avec un événement onRender:

initialize : function () {
    this.on('render', this.onRender);
},

render : function () {

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

    //other stuff

    return this.trigger('render');
},

onRender : function () {
    this.subView1 = new Subview();
    this.subView2 = new Subview();
    this.subView1.setElement('.some-el').render();
    this.subView2.setElement('.some-el').render();
}

J'ai un peu mélangé et assorti un tas de pratiques différentes dans tous ces exemples (désolé pour ça), mais quels sont ceux que vous voudriez garder ou ajouter? et que ne feriez-vous pas?

Résumé des pratiques:

  • Instanciez les sous-vues dans initialize ou dans render?
  • Effectuer toute la logique de rendu de sous-vue dans render ou dans onRender?
  • Utilisez setElement ou append/appendTo?
133
Ian Storm Taylor

J'ai généralement vu/utilisé plusieurs solutions différentes:

Solution 1

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.inner = new InnerView();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.$el.append(this.inner.$el);
        this.inner.render();
    }
});

var InnerView = Backbone.View.extend({
    render: function() {
        this.$el.html(template);
        this.delegateEvents();
    }
});

Ceci est similaire à votre premier exemple, avec quelques modifications:

  1. L'ordre dans lequel vous ajoutez les sous-éléments est important
  2. La vue externe ne contient pas les éléments HTML à définir sur la (les) vue (s) interne (vous pouvez donc toujours spécifier tagName dans la vue interne).
  3. render() est appelé APRÈS que l'élément de la vue interne ait été placé dans le DOM, ce qui est utile si la méthode render() de votre vue interne se place/se redimensionne elle-même sur la page en fonction de la position des autres éléments./taille (ce qui est un cas d'utilisation courant, d'après mon expérience)

Solution 2

var OuterView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

    render: function() {
        this.$el.html(template); // or this.$el.empty() if you have no template
        this.inner = new InnerView();
        this.$el.append(this.inner.$el);
    }
});

var InnerView = Backbone.View.extend({
    initialize: function() {
        this.render();
    },

    render: function() {
        this.$el.html(template);
    }
});

La solution 2 peut sembler plus propre, mais cela a provoqué des choses étranges dans mon expérience et a affecté négativement les performances.

J'utilise généralement la solution 1 pour plusieurs raisons:

  1. Beaucoup de mes points de vue reposent sur le fait d'être déjà dans le DOM dans leur méthode render()
  2. Lorsque la vue externe est restituée, il n'est pas nécessaire de réinitialiser les vues, ce qui peut provoquer des fuites de mémoire ainsi que des problèmes épineux avec les liaisons existantes.

Gardez à l'esprit que si vous initialisez un new View() à chaque appel de render(), cette initialisation appellera de toute façon delegateEvents(). Donc, cela ne devrait pas nécessairement être un "con", comme vous l'avez dit.

58
Lukas

C'est un problème récurrent avec Backbone et, d'après mon expérience, il n'y a pas vraiment de réponse satisfaisante à cette question. Je partage votre frustration, d’autant plus qu’il existe très peu de conseils, malgré la fréquence de ce cas d’utilisation. Cela dit, je vais généralement avec quelque chose qui ressemble à votre deuxième exemple.

Tout d’abord, j’écarterais immédiatement tout ce qui vous obligerait à déléguer des événements. Le modèle d'affichage basé sur les événements de Backbone est l'un de ses composants les plus cruciaux. Perdre cette fonctionnalité simplement parce que votre application n'est pas anodine laisserait un mauvais goût dans la bouche de tout programmeur. Alors grattez numéro un.

En ce qui concerne votre troisième exemple, je pense qu’il ne s’agit que d’aboutir à la pratique du rendu classique et n’ajoute pas beaucoup de sens. Peut-être que si vous déclenchez un événement réel (c'est-à-dire, pas un événement "onRender" artificiel), il vaudrait la peine de lier ces événements à render lui-même. Si vous trouvez que render devient lourd et complexe, vous avez trop peu de sous-vues.

Revenons à votre deuxième exemple, qui est probablement le moindre des trois maux. Voici un exemple de code extrait de Recipes With Backbone , figurant à la page 42 de my PDF = édition:

...
render: function() {
    $(this.el).html(this.template());
    this.addAll();
    return this;
},
  addAll: function() {
    this.collection.each(this.addOne);
},
  addOne: function(model) {
    view = new Views.Appointment({model: model});
    view.render();
    $(this.el).append(view.el);
    model.bind('remove', view.remove);
}

Ceci est seulement une configuration légèrement plus sophistiquée que votre deuxième exemple: ils spécifient un ensemble de fonctions, addAll et addOne, qui font le sale boulot. Je pense que cette approche est réalisable (et je l’utilise certainement); mais il reste encore un arrière-goût bizarre. (Pardon à toutes ces métaphores de la langue.)

Pour ce qui est d’ajouter dans le bon ordre: si vous ajoutez strictement, c’est une limitation. Mais assurez-vous de prendre en compte tous les schémas possibles. Peut-être voudriez-vous réellement un élément d’espace réservé (par exemple, un div ou un ul vide) que vous pourrez ensuite replaceWith un nouveau (DOM) élément qui contient les sous-vues appropriées. L'ajout n'est pas la seule solution, et vous pouvez certainement contourner le problème de la commande si vous vous en souciez autant, mais j'imagine que vous avez un problème de conception s'il vous fait perdre la tête. N'oubliez pas que les sous-vues peuvent avoir des sous-vues et qu'elles devraient l'être si cela est approprié. De cette façon, vous avez une structure plutôt arborescente, ce qui est très agréable: chaque sous-vue ajoute toutes ses sous-vues, dans l'ordre, avant que la vue parent n'en ajoute une autre, etc.

Malheureusement, la solution n ° 2 est probablement la meilleure solution que vous puissiez espérer utiliser avec Backbone. Si vous souhaitez consulter des bibliothèques tierces, celle que j'ai consultée (mais que je n'ai pas encore eu le temps de jouer) est Backbone.LayoutManager , qui semble avoir un méthode plus saine d'ajouter des sous-vues. Cependant, même ils ont eu débats récents sur des questions similaires à celles-ci.

31
Josh Leitzel

Surpris, cela n'a pas encore été mentionné, mais je réfléchirais sérieusement à l'utilisation de Marionette .

Il impose un peu plus de structure aux applications Backbone, y compris des types de vues spécifiques (ListView, ItemView, Region et Layout), en ajoutant le correct Controllers et beaucoup plus.

Voici le projet sur Github et un excellent guide par Addy Osmani dans le livre Backbone Fundamentals pour vous aider à démarrer.

5
Dana Woodman

J'ai ce que je crois être une solution assez complète à ce problème. Cela permet à un modèle dans une collection de changer et de ne restituer que sa vue (plutôt que la collection entière). Il gère également la suppression des vues de zombies via les méthodes close ().

var SubView = Backbone.View.extend({
    // tagName: must be implemented
    // className: must be implemented
    // template: must be implemented

    initialize: function() {
        this.model.on("change", this.render, this);
        this.model.on("close", this.close, this);
    },

    render: function(options) {
        console.log("rendering subview for",this.model.get("name"));
        var defaultOptions = {};
        options = typeof options === "object" ? $.extend(true, defaultOptions, options) : defaultOptions;
        this.$el.html(this.template({model: this.model.toJSON(), options: options})).fadeIn("fast");
        return this;
    },

    close: function() {
        console.log("closing subview for",this.model.get("name"));
        this.model.off("change", this.render, this);
        this.model.off("close", this.close, this);
        this.remove();
    }
});
var ViewCollection = Backbone.View.extend({
    // el: must be implemented
    // subViewClass: must be implemented

    initialize: function() {
        var self = this;
        self.collection.on("add", self.addSubView, self);
        self.collection.on("remove", self.removeSubView, self);
        self.collection.on("reset", self.reset, self);
        self.collection.on("closeAll", self.closeAll, self);
        self.collection.reset = function(models, options) {
            self.closeAll();
            Backbone.Collection.prototype.reset.call(this, models, options);
        };
        self.reset();
    },

    reset: function() {
        this.$el.empty();
        this.render();
    },

    render: function() {
        console.log("rendering viewcollection for",this.collection.models);
        var self = this;
        self.collection.each(function(model) {
            self.addSubView(model);
        });
        return self;
    },

    addSubView: function(model) {
        var sv = new this.subViewClass({model: model});
        this.$el.append(sv.render().el);
    },

    removeSubView: function(model) {
        model.trigger("close");
    },

    closeAll: function() {
        this.collection.each(function(model) {
            model.trigger("close");
        });
    }
});

Usage:

var PartView = SubView.extend({
    tagName: "tr",
    className: "part",
    template: _.template($("#part-row-template").html())
});

var PartListView = ViewCollection.extend({
    el: $("table#parts"),
    subViewClass: PartView
});
4
sarink

Découvrez ce mixin pour la création et le rendu des sous-vues:

https://github.com/rotundasoftware/backbone.subviews

Il s’agit d’une solution minimaliste qui aborde de nombreux problèmes abordés dans ce fil de discussion, notamment l’ordre de rendu, le fait de ne pas devoir redéléguer des événements, etc. Notez que le cas d’une vue de collection (où chaque modèle de la collection est représenté sous-vue) est un sujet différent. La meilleure solution générale que je connaisse pour ce cas est le CollectionView in Marionette .

2
Brave Dave

J'aime utiliser l'approche suivante, qui permet également de supprimer correctement les vues enfants. Voici un exemple tiré du livre par Addy Osmani.

Backbone.View.prototype.close = function() {
    if (this.onClose) {
        this.onClose();
    }
    this.remove(); };

NewView = Backbone.View.extend({
    initialize: function() {
       this.childViews = [];
    },
    renderChildren: function(item) {
        var itemView = new NewChildView({ model: item });
        $(this.el).prepend(itemView.render());
        this.childViews.Push(itemView);
    },
    onClose: function() {
      _(this.childViews).each(function(view) {
        view.close();
      });
    } });

NewChildView = Backbone.View.extend({
    tagName: 'li',
    render: function() {
    } });
0
FlintOff

Je n'aime pas vraiment l'une des solutions ci-dessus. Je préfère pour cette configuration que chaque vue devant travailler manuellement dans la méthode de rendu.

  • views peut être une fonction ou un objet renvoyant une définition d'objet de vue
  • Quand un parent est .remove s'appelle, le .remove des enfants imbriqués de l'ordre le plus bas doivent être appelés (à partir des sous-sous-sous-vues)
  • Par défaut, la vue parent transmet ses propres modèles et collections, mais des options peuvent être ajoutées et remplacées.

Voici un exemple:

views: {
    '.js-toolbar-left': CancelBtnView, // shorthand
    '.js-toolbar-right': {
        view: DoneBtnView,
        append: true
    },
    '.js-notification': {
        view: Notification.View,
        options: function() { // Options passed when instantiating
            return {
                message: this.state.get('notificationMessage'),
                state: 'information'
            };
        }
    }
}
0
Dominic

Il n'est pas nécessaire de déléguer à nouveau des événements car cela coûte cher. Voir ci-dessous:

    var OuterView = Backbone.View.extend({
    initialize: function() {
        this.inner = new InnerView();
    },

    render: function() {
        // first detach subviews            
        this.inner.$el.detach(); 

        // now can set html without affecting subview element's events
        this.$el.html(template);

        // now render and attach subview OR can even replace placeholder 
        // elements in template with the rendered subview element
        this.$el.append(this.inner.render().el);

    }
});

var InnerView = Backbone.View.extend({
    render: function() {
        this.$el.html(template);            
    }
});
0
soham joshi

Vous pouvez également injecter les sous-vues rendues en tant que variables dans le modèle principal en tant que variables.

commencez par afficher les sous-vues et convertissez-les en HTML comme ceci:

var subview1 = $(subview1.render.el).html(); var subview2 = $(subview2.render.el).html();

(De cette façon, vous pouvez également concaténer de manière dynamique les vues telles que subview1 + subview2 lorsqu'elles sont utilisées dans des boucles), puis le transmettre au modèle maître qui ressemble à ceci: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

et l'injecter enfin comme ça:

this.$el.html(_.template(MasterTemplate, { sub1: subview1, sub2: subview2 } ));

En ce qui concerne les événements dans les sous-vues: Ils devront probablement être connectés dans le parent (masterView) avec cette approche, mais pas dans les sous-vues.

0
B Piltz

Backbone a été construit de manière intentionnelle, de sorte qu’il n’y ait pas de pratique "courante" en ce qui concerne cette question et de nombreuses autres. Il est censé être le moins avisé possible. Théoriquement, vous n'avez même pas besoin d'utiliser des modèles avec Backbone. Vous pouvez utiliser javascript/jquery dans la fonction render d'une vue pour modifier manuellement toutes les données de la vue. Pour le rendre plus extrême, vous n’avez même pas besoin d’une fonction spécifique render. Vous pourriez avoir une fonction appelée renderFirstName qui met à jour le prénom dans le dom et renderLastName qui met à jour le nom de famille dans le dom. Si vous adoptiez cette approche, les performances seraient nettement meilleures et vous n'auriez plus jamais à déléguer manuellement des événements. Le code aurait également un sens total pour quelqu'un qui le lirait (bien qu'il s'agisse d'un code plus long/plus compliqué).

Cependant, il n’ya généralement aucun inconvénient à utiliser des modèles et à simplement détruire et reconstruire la vue entière et ses sous-vues à chaque appel de rendu, car le questionneur n’a même pas eu l’idée de faire autre chose. C'est donc ce que la plupart des gens font dans presque toutes les situations qu'ils rencontrent. Et c’est la raison pour laquelle les cadres d’opinion en font simplement le comportement par défaut.

0
Nick Manning