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

133

J'ai une configuration de vue imbriquée qui peut être un peu plus profonde dans mon application. Il y a un tas de façons dont je pourrais penser à l'initialisation, au rendu et à l'ajout des sous-vues, mais je me demande quelle est la pratique courante.

En voici quelques-uns auxquels 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 DOM avec l'ajout. Les vues sont initialisées très tôt, il n'y a donc pas grand chose à faire en même temps dans la fonction de rendu.

Inconvénients: Vous êtes obligé de re-déléguer les événements (), ce qui peut être coûteux? La fonction de rendu de la vue parente est encombrée de tout le rendu de sous-vue qui doit se produire? Vous n'avez pas la possibilité de définir les tagNameéléments, le modèle doit donc conserver les bons tagNames.

Autrement:

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);
}

Pour: Vous n'avez pas à déléguer à nouveau des événements. Vous n'avez pas besoin d'un modèle contenant uniquement des espaces réservés vides et votre tagName est de nouveau défini par la vue.

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

Avec un onRenderévénement:

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 désormais séparée de la render()méthode de la vue .

Avec un onRenderévénement:

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 en quelque sorte mélangé et mis en correspondance un tas de pratiques différentes dans tous ces exemples (désolé à ce sujet) mais quelles sont celles que vous conserveriez ou ajouteriez? et que ne feriez-vous pas?

Résumé des pratiques:

  • Instancier des sous-vues dans initializeou dans render?
  • Exécuter toute la logique de rendu de sous-vue dans renderou dans onRender?
  • Utilisez setElementou append/appendTo?
Ian Storm Taylor
la source
Je ferais attention au nouveau sans suppression, vous avez une fuite de mémoire là-dedans.
vimdude le
1
Ne vous inquiétez pas, j'ai une closeméthode et une onClosequi nettoie les enfants, mais je suis juste curieux de savoir comment les instancier et les rendre en premier lieu.
Ian Storm Taylor
3
@abdelsaid: En JavaScript, le GC gère la désallocation de la mémoire. deletedans JS n'est pas le même que celui deletede C ++. C'est un mot-clé très mal nommé si vous me demandez.
Mike Bailey
@MikeBantegui l'a compris mais c'est la même chose qu'en java sauf que dans JS pour libérer de la mémoire, il vous suffit d'attribuer null. Pour clarifier ce que je veux dire, essayez de créer une boucle avec un nouvel objet à l'intérieur et de surveiller la mémoire. Bien sûr, GC y arrivera, mais vous perdrez de la mémoire avant d'y arriver. Dans ce cas, Render qui peut être appelé plusieurs fois.
vimdude
3
Je suis un développeur novice de Backbone. Quelqu'un peut-il expliquer pourquoi l'exemple 1 nous oblige à re-déléguer des événements? (Ou devrais-je poser cela dans sa propre question?) Merci.
pilau

Réponses:

58

J'ai généralement vu / utilisé quelques 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 ou les vues intérieures (ce qui signifie que vous pouvez toujours spécifier tagName dans la vue intérieure)
  3. render()est appelé APRÈS que l'élément de la vue intérieure a été placé dans le DOM, ce qui est utile si la render()méthode de votre vue intérieure se place / se redimensionne sur la page en fonction de la position / taille des autres éléments (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 paraître plus propre, mais elle a causé 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 opinions reposent sur le fait d'être déjà dans le DOM dans leur render()méthode
  2. Lorsque la vue externe est de nouveau rendue, les vues n'ont pas besoin d'être réinitialisées, ce qui peut provoquer des fuites de mémoire et également causer des problèmes bizarres avec les liaisons existantes

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

Lukas
la source
1
Aucune de ces solutions ne fonctionne dans l'arborescence de sous-vue appelant View.remove, ce qui peut être vital pour effectuer un nettoyage personnalisé de la vue, ce qui empêcherait autrement le garbage collection
Dominic
31

C'est un problème éternel 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 y a si peu d'indications 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, je rejetterais d'emblée tout ce qui vous oblige à re-déléguer des événements. Le modèle de vue événementielle de Backbone est l'un de ses composants les plus cruciaux, et perdre cette fonctionnalité simplement parce que votre application n'est pas triviale laisserait un mauvais goût dans la bouche de tout programmeur. Donc zéro numéro un.

En ce qui concerne votre troisième exemple, je pense que c'est juste une fin autour de la pratique de rendu conventionnelle et n'ajoute pas beaucoup de sens. Peut-être que si vous déclenchez un événement réel (c'est-à-dire pas un onRenderévénement artificiel " "), il vaudrait la peine de simplement lier ces événements à renderlui-même. Si vous trouvez que vous renderdevenez 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 , trouvé à la page 42 de mon édition PDF:

...
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);
}

Ce n'est qu'une configuration légèrement plus sophistiquée que votre deuxième exemple: elles spécifient un ensemble de fonctions, addAllet addOne, qui font le sale boulot. Je pense que cette approche est réalisable (et je l'utilise certainement); mais cela laisse toujours un arrière-goût bizarre. (Pardonnez toutes ces métaphores de la langue.)

À votre point sur l'ajout dans le bon ordre: si vous ajoutez strictement, bien sûr, c'est une limitation. Mais assurez-vous de considérer tous les schémas de modèles possibles. Peut-être aimeriez-vous en fait un élément d'espace réservé (par exemple, un vide divou ul) que vous pouvez ensuite replaceWithcréer un nouvel élément (DOM) contenant les sous-vues appropriées. L'ajout n'est pas la seule solution, et vous pouvez certainement contourner le problème de commande si vous vous en souciez autant, mais j'imagine que vous avez un problème de conception si cela vous fait trébucher. N'oubliez pas que les sous-vues peuvent avoir des sous-vues, et elles le devraient si cela est approprié. De cette façon, vous avez une structure plutôt arborescente, ce qui est assez sympa: chaque sous-vue ajoute toutes ses sous-vues, dans l'ordre, avant que la vue parente n'en ajoute une autre, et ainsi de suite.

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

Josh Leitzel
la source
4
L'avant-dernière ligne - model.bind('remove', view.remove);- ne devriez-vous pas simplement faire cela dans la fonction d'initialisation du rendez-vous pour les garder séparés?
atp
2
Qu'en est-il lorsqu'une vue ne peut pas être ré-instanciée à chaque fois que son parent est rendu car elle conserve un état?
mor
Arrêtez toute cette folie et utilisez simplement le plugin Backbone.subviews !
Brave Dave
6

Surpris que cela n'ait pas encore été mentionné, mais j'envisagerais sérieusement d'utiliser Marionette .

Il applique un peu plus de structure pour des applications de Backbone, y compris les types de vue spécifique ( ListView, ItemView, Regionet Layout), ajoutant appropriés Controllers et beaucoup plus.

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

Dana Woodman
la source
3
Cela ne répond pas à la question.
Ceasar Bautista
2
@CeasarBautista Je ne vais pas expliquer comment utiliser Marionette pour y parvenir mais Marionette résout effectivement le problème ci-dessus
Dana Woodman
4

J'ai, ce que je crois être, une solution assez complète à ce problème. Il permet à un modèle au sein d'une collection de changer et d'avoir uniquement sa vue restituée (plutôt que la collection entière). Il gère également la suppression des vues 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
});
couler
la source
2

Découvrez ce mixin pour créer et rendre des sous-vues:

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

C'est une solution minimaliste qui résout un grand nombre des problèmes abordés dans ce fil, y compris l'ordre de rendu, ne pas avoir à re-dé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é avec un subview) est un sujet différent. La meilleure solution générale que je connaisse pour ce cas est la CollectionView dans Marionette .

Brave Dave
la source
0

Je n'aime vraiment aucune des solutions ci-dessus. Je préfère pour cette configuration que chaque vue doit effectuer manuellement un travail dans la méthode de rendu.

  • views peut être une fonction ou un objet renvoyant un objet de définitions de vue
  • Lorsqu'un parent .removeest appelé, les .removeenfants imbriqués de l'ordre le plus bas doivent être appelés (depuis les sous-sous-sous-vues)
  • Par défaut, la vue parent transmet son propre modèle et sa propre collection, 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'
            };
        }
    }
}
Dominique
la source
0

Backbone a été construit intentionnellement pour qu'il n'y ait pas de pratique "commune" à ce sujet et à bien d'autres problèmes. Il est censé être aussi sans opinion que possible. Théoriquement, vous n'avez même pas besoin d'utiliser des modèles avec Backbone. Vous pouvez utiliser javascript / jquery dans la renderfonction 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 renderfonction spécifique . Vous pourriez avoir une fonction appelée renderFirstNamequi met à jour le prénom dans le dom et renderLastNamequi met à jour le nom de famille dans le dom. Si vous adoptiez cette approche, ce serait bien meilleur en termes de performances 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 que ce soit un code plus long / plus compliqué).

Cependant, il n'y a 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 il n'est même pas venu à l'esprit de l'interrogateur de faire autre chose. C'est donc ce que font la plupart des gens dans pratiquement toutes les situations qu'ils rencontrent. Et c'est pourquoi les cadres d'opinion en font simplement le comportement par défaut.

Nick Manning
la source
0

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

rendre d'abord les sous-vues et les convertir 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 dynamiquement les vues comme subview1 + subview2lorsqu'elles sont utilisées dans des boucles), puis les transmettre au modèle principal qui ressemble à ceci: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

et injectez-le enfin comme ceci:

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

Concernant les événements dans les sous-vues: Ils devront probablement être connectés dans le parent (masterView) avec cette approche et non dans les sous-vues.

B Piltz
la source
0

J'aime utiliser l'approche suivante qui m'assure également de supprimer correctement les vues enfants. Voici un exemple du livre d'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() {
    } });
FlintOff
la source
0

Il n'est pas nécessaire de redéléguer les é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);            
    }
});
soham joshi
la source