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:
initialize
ou dans render
?render
ou dans onRender
?setElement
ou append/appendTo
?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:
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:
render()
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.
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.
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 Controller
s 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.
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
});
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 .
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() {
} });
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.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)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'
};
}
}
}
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);
}
});
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.
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.