2012-02-14 10 views
132

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.Cómo renderizar y anexar sub-vistas en Backbone.js

Aquí hay un par 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: usted no tiene que preocuparse por mantener el orden DOM derecha con anexar. 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 ves obligado a volver a delegarEvents(), ¿cuál 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 el tagName de los elementos, por lo que la plantilla necesita mantener los nombres de etiqueta correctos.

Otra forma:

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: usted no tiene eventos de re-delegado. 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 las 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 subvista está ahora separada del método render() de la vista.

Con una onRender evento:

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

He tipo de mezclar y emparejar un montón de diferentes prácticas en todos estos ejemplos (tanto lo de eso), pero lo que son los que se mantendría o ¿añadir? y que no harias?

Resumen de las prácticas:

  • subvistas instanciar en initialize o en render?
  • ¿Realiza toda la lógica de representación de subvista en render o en onRender?
  • Utilice setElement o append/appendTo?
+0

Tendría cuidado con lo nuevo sin eliminar, hay pérdida de memoria allí. – vimdude

+1

No se preocupe, tengo un método 'close' y un' onClose' que limpia a los niños, pero tengo curiosidad acerca de cómo crear instancias y representarlos en primer lugar. –

+3

@abdelsaid: en JavaScript, el GC maneja la desasignación de memoria. 'delete' en JS no es lo mismo que' delete' de C++. Es una palabra clave mal nombrada si me preguntas. –

Respuesta

30

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 eventos reales (es decir, no un evento artificial "onRender"), valdría la pena vincular esos eventos al propio render. Si encuentra que render se vuelve difícil de manejar y complejo, tiene muy pocas subvistas.

Volver a su segundo ejemplo, que es probablemente el menor de los tres males. Aquí es código de ejemplo levantada de Recipes With Backbone, que se encuentra en la página 42 de mi edición de 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); 
} 

Ésta es sólo una configuración ligeramente más sofisticado que el segundo ejemplo: que specifiy un conjunto de funciones, addAll y addOne, que lo 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 le gustaría tener un elemento marcador de posición (por ejemplo, un div vacío o ul) que luego puede replaceWith un nuevo elemento (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 utilizando un Backbone listo para usar. 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 recent debates en problemas similares a estos.

+3

La penúltima línea - 'model.bind ('remove', view.remove);' - ¿no debería hacer eso en la función de inicialización de la cita para mantenerlos separados? – ash

+2

¿Qué sucede cuando una vista no se puede volver a crear instancia cada vez que se procesa el elemento principal porque mantiene un estado? – mor

+0

Detenga toda esta locura y solo use el complemento [Backbone.subviews] (https://github.com/rotundasoftware/backbone.subviews)! –

58

que he visto en general/utilizó un par de diferentes soluciones:

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 al primer ejemplo, con algunos cambios:

  1. El orden en el que se agregan los subelementos importa
  2. La vista exterior no contiene n los elementos html que se establecerán en las vistas internas (lo que significa que todavía puede especificar tagName en la vista interna)
  3. render() se llama DESPUÉS de que el elemento de la vista interna se haya colocado en el DOM, lo que es útil si su interior método render() de vista está poniendo/dimensionamiento sí en la página de la base de otros elementos posición/tamaño (que es un caso de uso común, 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); 
    } 
}); 

Solución 2 puede parecer más limpio, pero ha causado un poco de st rango cosas en mi experiencia y ha afectado el rendimiento negativamente.

Yo generalmente uso Solución 1, por un par de razones:

  1. Muchos de mis puntos de vista se basan en estar ya en el DOM en su render() método
  2. Cuando se vuelve a representar el punto de vista exterior, puntos de vista no tienen que ser re-inicializado, que re-inicialización puede causar pérdidas de memoria y también puede causar problemas extraños con fijaciones existentes

Tenga en cuenta que si va a inicializar un new View() cada vez es render() llamado, esa inicialización va a llamar al delegateEvents() de todos modos. Entonces, eso no debería ser necesariamente una "estafa", como expresaste.

+3

+1 para la solución 1 –

+1

Ninguna de estas soluciones funciona en el árbol de sub vista llamando a View.remove, que puede ser vital para realizar una limpieza personalizada en la vista, lo que de otra manera evitaría la recolección de basura –

2

Control hacia fuera este mixin para la creación y representación subvistas:

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

es una solución minimalista que aborda muchos de los temas tratados en este hilo, incluyendo el orden de representación, y no tener que volver a delegado eventos, etc. Tenga en cuenta que el caso de una vista de colección (donde cada modelo de la colección se representa con una subvista) es un tema diferente. La mejor solución general que conozco para ese caso es el CollectionView in Marionette.

4

Tengo, lo que creo que es, una solución bastante completa a 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 
}); 
5

Sorprendido esto no se ha mencionado todavía, pero me gustaría considerar seriamente el uso de Marionette.

que hace valer un poco más estructura a aplicaciones Backbone, incluyendo los tipos específicos de vista (ListView, ItemView, Region y Layout), añadiendo adecuados Controller s y mucho más.

Aquí está the project on Github y un gran guide by Addy Osmani in the book Backbone Fundamentals para empezar.

+2

Esto no responde la pregunta. –

+2

@CeasarBautista No entiendo cómo usar Marionette para lograr esto, pero Marionette sí resuelve el problema anterior –

0

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 puede ser una función u objeto devolver un objeto de definiciones de vista
  • Cuando se llama de .remove un padre, el .remove de los niños anidadas de la orden más baja hasta debería ser llamado (hasta el final de sub-sub -sub views)
  • De forma predeterminada, la vista principal pasa su propio modelo y colección, pero las opciones se pueden agregar y anular.

He aquí 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' 
      }; 
     } 
    } 
} 
0

Backbone fue construido intencionadamente de manera que no hay una práctica "común" en lo que respecta a este y muchos otros temas. 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 render de una vista para cambiar manualmente todos los datos en la vista. Para hacerlo más extremo, ni siquiera necesita una función específica de render. 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 al uso de plantillas y simplemente destruir y reconstruir la vista completa y sus subvistas en cada llamada 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.

0

También podría insertar las subcategorías representadas como variables en la plantilla principal como variables.

primera rendir los subvistas y los convierte a HTML como este:

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

(de esa manera se podría también dinámicamente cadena de concatenar los puntos de vista como subview1 + subview2 cuando se utiliza en bucles) y luego pasarlo al maestro plantilla que se parece a esto: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

e inyectar finalmente así:

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

En cuanto a los eventos dentro de las subvistas: Lo más probable es que tengan que estar conectados en el principal (masterView) con este enfoque no dentro de las subvistas.

0

Me gusta utilizar el siguiente enfoque, que también se asegura de eliminar correctamente las vistas secundarias. 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() { 
    } });