javascript - Cómo renderizar y agregar sub-vistas en Backbone.js
model-view-controller (10)
Tengo una configuración de Vista anidada que puede ser algo profunda en mi aplicación. Hay muchas maneras en que podría pensar en inicializar, renderizar y agregar las sub-vistas, pero me pregunto qué práctica común es.
Aquí hay una pareja en la que he pensado:
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();
}
Pros: No tiene que preocuparse por mantener el orden DOM correcto con anexos. Las vistas se inicializan desde el principio, por lo que no hay tanto que hacer de una sola vez en la función de renderizado.
Contras: ¿ Te obligan a volver a delegarEventos (), que puede ser costoso? ¿La función de representación de la vista principal está saturada con toda la representación de subvista que debe suceder? No tiene la capacidad de establecer tagName
de los elementos, por lo que la plantilla necesita mantener los tagNames correctos.
De otra manera:
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);
}
Pros: no es necesario volver a delegar eventos. No necesita una plantilla que solo contenga marcadores de posición vacíos y los tagName vuelvan a estar definidos por la vista.
Contras: ahora debe asegurarse de agregar cosas en el orden correcto. El procesamiento de la vista principal aún está desordenado por la representación de la subvista.
Con un evento 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();
}
Pros: la lógica de la subvista ahora está separada del método render()
de la vista.
Con un evento 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();
}
Combiné y combiné un montón de prácticas diferentes en todos estos ejemplos (lo siento mucho) pero, ¿cuáles serían los que mantendría o agregaría? y que no harias?
Resumen de prácticas:
- ¿Crear instancias de las subvistas en
initialize
o enrender
? - ¿Realiza toda la lógica de
render
onRender
enrender
o enonRender
? - Use
setElement
oappend/appendTo
?
Echa un vistazo a este mixin para crear y representar subvistas:
Es una solución minimalista que aborda muchos de los problemas discutidos en este hilo, incluido el orden de representación, no tener que volver a delegar eventos, etc. Tenga en cuenta que el caso de una vista de colección (donde cada modelo de la colección se representa con uno subvista) es un tema diferente. La mejor solución general que conozco para ese caso es CollectionView en Marionette .
El backbone se construyó intencionalmente para que no haya una práctica "común" con respecto a este y muchos otros problemas. Está destinado a ser lo menos sofocado posible. Teóricamente, ni siquiera tiene que usar plantillas con Backbone. Puede usar javascript / jquery en la función de render
de una vista para cambiar manualmente todos los datos en la vista. Para hacerlo más extremo, ni siquiera necesitas una función de render
específica. Podría tener una función llamada renderFirstName
que actualiza el primer nombre en el dom y renderLastName
que actualiza el apellido en el dom. Si tomas este enfoque, sería mucho mejor en términos de rendimiento y nunca más tendrías que volver a delegar eventos manualmente. El código también tendría sentido para alguien que lo leyera (aunque sería un código más largo / desordenado).
Sin embargo, generalmente no hay desventaja en el uso de plantillas y simplemente se destruye y se reconstruye toda la vista y sus subvistas en todas y cada una de las llamadas de renderizado, ya que ni siquiera se le ocurrió al interlocutor hacer otra cosa. Entonces eso es lo que hace la mayoría de las personas en casi todas las situaciones que encuentran. Y es por eso que los frameworks con opiniones obstinadas solo hacen que este sea el comportamiento predeterminado.
En general, he visto / usado un par de soluciones diferentes:
Solución 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();
}
});
Esto es similar a su primer ejemplo, con algunos cambios:
- El orden en el que anexas los elementos secundarios importa
- La vista exterior no contiene los elementos html que se establecerán en las vistas internas (lo que significa que aún puede especificar tagName en la vista interna)
-
render()
se llama DESPUÉS de que el elemento de la vista interna se haya colocado en el DOM, lo cual es útil si el métodorender()
su vista interna se ubica / dimensiona en la página basándose en la posición / tamaño de otros elementos (que es un uso común caso, en mi experiencia)
Solución 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 solución 2 puede parecer más limpia, pero ha causado algunas cosas extrañas en mi experiencia y ha afectado negativamente el rendimiento.
Generalmente uso la Solución 1, por un par de razones:
- Muchos de mis puntos de vista se basan en que ya están en DOM en su método
render()
- Cuando se vuelve a renderizar la vista exterior, no es necesario reinicializar las vistas, lo que puede provocar fugas de memoria y también causar problemas extraños con las vinculaciones existentes.
Tenga en cuenta que si está inicializando una new View()
cada vez que se render()
, esa inicialización va a llamar a delegateEvents()
todos modos. Entonces, eso no debería ser necesariamente una "estafa", como expresaste.
Este es un problema perenne con Backbone y, en mi experiencia, no hay realmente una respuesta satisfactoria a esta pregunta. Comparto tu frustración, especialmente porque hay muy poca guía a pesar de lo común que es este caso de uso. Dicho eso, suelo ir con algo parecido a tu segundo ejemplo.
En primer lugar, descartaría cualquier cosa que requiera que vuelva a delegar eventos. El modelo de vista basado en eventos de Backbone es uno de sus componentes más cruciales, y perder esa funcionalidad simplemente porque su aplicación no es trivial dejaría un mal sabor en la boca de cualquier programador. Así que rasca el número uno.
En cuanto a su tercer ejemplo, creo que es solo un final en torno a la práctica de representación convencional y no agrega mucho significado. Tal vez si está activando un evento real (es decir, no un evento artificial " onRender
"), valdría la pena simplemente vincular esos eventos para render
sí mismo. Si considera que el render
vuelve difícil de manejar y complejo, tiene muy pocas subvistas.
Volviendo a tu segundo ejemplo, que es probablemente el menor de los tres males. Aquí hay un código de ejemplo extraído de Recipes With Backbone , que se encuentra en la página 42 de mi edición en 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);
}
Esta es solo una configuración un poco más sofisticada que su segundo ejemplo: especifican un conjunto de funciones, addAll
y addOne
, que hacen el trabajo sucio. Creo que este enfoque es viable (y ciertamente lo uso); pero todavía deja un regusto extraño. (Perdón todas estas metáforas de la lengua.)
A su punto de agregar en el orden correcto: si se agrega estrictamente, claro, eso es una limitación. Pero asegúrese de considerar todos los esquemas de plantillas posibles. Tal vez realmente le gustaría un elemento de marcador de posición (por ejemplo, un div
o ul
vacío) que luego puede replaceWith
un elemento nuevo (DOM) que contiene las subvistas apropiadas. Anexar no es la única solución, y sin duda puede solucionar el problema de pedidos si le importa tanto, pero me imagino que tiene un problema de diseño si lo está haciendo tropezar. Recuerde, las subvistas pueden tener subvistas, y deberían hacerlo si es apropiado. De esta manera, tiene una estructura bastante similar a un árbol, que es bastante agradable: cada subvista agrega todas sus subvistas, en orden, antes de que la vista principal agregue otra, y así sucesivamente.
Desafortunadamente, la solución n. ° 2 es probablemente lo mejor que se puede esperar para usar Backbone fuera de la caja. Si está interesado en consultar bibliotecas de terceros, una de las que he estudiado (pero que aún no he tenido tiempo para jugar) es Backbone.LayoutManager , que parece tener un método más saludable para agregar subvistas. Sin embargo, incluso ellos han tenido debates recientes sobre cuestiones similares a estos.
Me gusta utilizar el siguiente enfoque, que también me asegura de eliminar las vistas secundarias correctamente. Aquí hay un ejemplo del book de 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() {
} });
No es necesario volver a delegar eventos, ya que es costoso. Vea abajo:
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);
}
});
Realmente no me gusta ninguna de las soluciones anteriores. Prefiero que esta configuración sobre cada vista tenga que trabajar manualmente en el método de renderizado.
-
views
pueden ser una función u objeto que devuelve un objeto de las definiciones de vista - Cuando se llama a
.remove
de un padre, se.remove
llamar al.remove
de hijos anidados desde el orden más bajo hacia arriba (todo desde las vistas sub-sub-sub) - De forma predeterminada, la vista principal pasa su propio modelo y colección, pero las opciones se pueden agregar y anular.
Aquí hay un ejemplo:
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''
};
}
}
}
Sorprendido de que no se haya mencionado aún, pero consideraría seriamente usar Marionette .
Impone un poco más de estructura a las aplicaciones de Backbone, incluidos los tipos de vista específicos ( ListView
, ItemView
, Region
y Layout
), agregando Controller
apropiados y mucho más.
Aquí está el proyecto de Github y una gran guía de Addy Osmani en el libro Backbone Fundamentals para que comiences.
También puede inyectar las subvistas representadas como variables en la plantilla principal como variables.
primero represente las subvistas y conviértalas a html de la siguiente manera:
var subview1 = $(subview1.render.el).html(); var subview2 = $(subview2.render.el).html();
(De esta forma, también podría encadenar dinámicamente las vistas como subview1 + subview2
cuando se usa en bucles) y luego pasarlo a la plantilla maestra que se ve así: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...
e inyectarlo finalmente así:
this.$el.html(_.template(MasterTemplate, { sub1: subview1, sub2: subview2 } ));
Con respecto a los Eventos dentro de las subvistas: Lo más probable es que tengan que estar conectados en el padre (masterView) con este enfoque no dentro de las subvistas.
Tengo, lo que creo que es, una solución bastante completa para este problema. Permite que un modelo dentro de una colección cambie, y solo se vuelve a renderizar su vista (en lugar de la colección completa). También maneja la eliminación de vistas de zombies a través de los métodos 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");
});
}
});
Uso:
var PartView = SubView.extend({
tagName: "tr",
className: "part",
template: _.template($("#part-row-template").html())
});
var PartListView = ViewCollection.extend({
el: $("table#parts"),
subViewClass: PartView
});