Cómo renderizar y agregar sub-vistas en Backbone.js

133

Tengo una configuración de Vista anidada que puede ser algo profunda en mi aplicación. Hay muchas maneras en las que podría pensar en inicializar, renderizar y agregar las vistas secundarias, pero me pregunto qué es una práctica común.

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 los anexos. Las vistas se inicializan desde el principio, por lo que no hay tanto que hacer a la vez en la función de renderizado.

Contras: ¿ Estás obligado a volver a delegar eventos (), lo que puede ser costoso? ¿La función de representación de la vista principal está abarrotada con toda la representación de subvista que debe suceder? No tiene la capacidad de establecer los tagNameelementos, por lo que la plantilla debe mantener los nombres de etiqueta 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 tiene que volver a delegar eventos. No necesita una plantilla que solo contenga marcadores de posición vacíos y la etiqueta vuelve a definir su nombre de etiqueta.

Contras: ahora debe asegurarse de agregar las cosas en el orden correcto. El renderizado de la vista principal todavía está abarrotado por el renderizado de la subvista.

Con un onRenderevento:

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 render()método de la vista .

Con un onRenderevento:

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 mezclado y comparado un montón de prácticas diferentes en todos estos ejemplos (lo siento mucho), pero ¿cuáles son las que mantendría o agregaría? y que no harias

Resumen de prácticas:

  • ¿Instanciar subvistas en initializeo en render?
  • ¿Realiza toda la lógica de representación de subvista en rendero en onRender?
  • Usar setElemento append/appendTo?
Ian Storm Taylor
fuente
Tendría cuidado con lo nuevo sin eliminar, tiene una pérdida de memoria allí.
vimdude
1
No se preocupe, tengo un closemétodo y un método onCloseque limpia a los niños, pero tengo curiosidad acerca de cómo crear instancias y renderizarlos en primer lugar.
Ian Storm Taylor
3
@abdelsaid: en JavaScript, el GC maneja la desasignación de memoria. deleteen JS no es lo mismo que deletedesde C ++. Es una palabra clave muy mal nombrada si me preguntas.
Mike Bailey
@MikeBantegui lo consiguió, pero es lo mismo que en Java, excepto que en JS para liberar memoria solo necesita asignar nulo. Para aclarar lo que quiero decir, intente crear un bucle con un nuevo objeto dentro y monitorear la memoria. Por supuesto, GC lo alcanzará, pero perderá memoria antes de que llegue. En este caso, renderice lo que podría llamarse muchas veces.
vimdude
3
Soy un desarrollador novato de Backbone. ¿Alguien puede explicar por qué el ejemplo 1 nos obliga a volver a delegar eventos? (¿O debería hacer esto en su propia pregunta?) Gracias.
pilau

Respuestas:

58

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:

  1. El orden en el que agrega los subelementos es importante
  2. La vista externa 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)
  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 vista interna render() método de está colocando / dimensionando en la página en función de la posición / tamaño de otros elementos (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);
    }
});

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:

  1. Muchos de mis puntos de vista dependen de que ya estén en el DOM en su render() método
  2. Cuando se vuelve a representar la vista externa, las vistas no tienen que reinicializarse, lo que puede reiniciar la memoria y causar problemas extraños con los enlaces existentes.

Tenga en cuenta que si está iniciando una llamada new View()cada vez render(), esa inicialización llamará de delegateEvents()todos modos. Así que eso no necesariamente debe ser una "estafa", como has expresado.

Lukas
fuente
1
Ninguna de estas soluciones funciona en el árbol de subvista que llama a View.remove, que puede ser vital para realizar una limpieza personalizada en la vista, que de lo contrario evitaría la recolección de basura
Dominic
31

Este es un problema permanente con Backbone y, en mi experiencia, no hay realmente una respuesta satisfactoria a esta pregunta. Comparto su frustración, especialmente porque hay muy poca orientación a pesar de lo común que es este caso de uso. Dicho esto, generalmente voy con algo similar a tu segundo ejemplo.

En primer lugar, descartaría de antemano cualquier cosa que requiera que vuelvas a delegar eventos. El modelo de vista basada 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.

Con respecto a su tercer ejemplo, creo que es solo una ejecución final en torno a la práctica de renderizado convencional y no agrega mucho significado. Quizás si está activando un evento real (es decir, no un " onRender" evento artificial ), valdría la pena vincular esos eventos a rendersí mismo. Si tu encuentrasrender difícil de manejar y complejo, tiene muy pocas subvistas.

Volvamos a su segundo ejemplo, que probablemente sea el menor de los tres males. Aquí hay un código de ejemplo levantado de Recetas con Backbone , que se encuentra en la página 42 de mi edición 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, addAllyaddOne hacen el trabajo sucio. Creo que este enfoque es viable (y ciertamente lo uso); pero aún deja un sabor extraño. (Perdone todas estas metáforas de la lengua).

A su punto de agregar en el orden correcto: si está agregando estrictamente, eso es una limitación. Pero asegúrese de considerar todos los posibles esquemas de plantillas. Quizás desee un elemento de marcador de posición (p. Ej., Un vacío divo ul) que luego pueda replaceWithincluir un nuevo elemento (DOM) que contenga las subvistas apropiadas. Anexar no es la única solución, y ciertamente puede solucionar el problema de los pedidos si se preocupa tanto por eso, pero me imagino que tiene un problema de diseño si lo está tropezando. Recuerde, las subvistas pueden tener subvistas, y deberían tenerlas si es apropiado. De esa manera, tiene una estructura bastante similar a un árbol, lo cual 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 la mejor que puede esperar para usar Backbone listo para usar. Si está interesado en consultar bibliotecas de terceros, una que he examinado (pero que todavía 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 temas similares a estos.

Josh Leitzel
fuente
44
La penúltima línea ... model.bind('remove', view.remove);¿no deberías hacer eso en la función de inicialización de la cita para mantenerlos separados?
atp
2
¿Qué pasa cuando una vista no se puede volver a instanciar cada vez que se procesa el padre porque mantiene un estado?
mor
¡Detén toda esta locura y solo usa el complemento Backbone.subviews !
Valiente Dave el
6

Sorprendido, esto no se ha mencionado todavía, pero consideraría seriamente usar Marionette .

Se impone un poco más estructura para aplicaciones de red troncal, incluyendo tipos específicos de vista ( ListView, ItemView, Regiony Layout), añadiendo adecuados Controllers y mucho más.

Aquí está el proyecto en Github y una gran guía de Addy Osmani en el libro Backbone Fundamentals para comenzar.

Dana Woodman
fuente
3
Esto no responde la pregunta.
Ceasar Bautista
2
@CeasarBautista No entiendo cómo usar Marionette para lograr esto, pero Marionette sí resuelve el problema anterior
Dana Woodman
4

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 representar 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
});
sarink
fuente
2

Mira este mixin para crear y renderizar subvistas:

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

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 la CollectionView en Marionette .

Valiente Dave
fuente
0

Realmente no me gusta ninguna de las soluciones anteriores. Prefiero esta configuración en lugar de que cada vista tenga que trabajar manualmente en el método de renderizado.

  • views puede ser una función u objeto que devuelve definiciones de un objeto de vista
  • Cuando .removese llama a un padre , el.remove hijos anidados desde el orden más bajo (desde vistas sub-sub-sub)
  • De forma predeterminada, la vista principal pasa su propio modelo y colección, pero se pueden agregar y anular opciones.

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'
            };
        }
    }
}
Dominic
fuente
0

La columna vertebral fue construida intencionalmente para que no hubiera una práctica "común" con respecto a este y muchos otros temas. Está destinado a ser tan discreto como sea posible. Teóricamente, ni siquiera tiene que usar plantillas con Backbone. Puede usar javascript / jquery en la renderfunción de una vista para cambiar manualmente todos los datos en la vista. Para hacerlo más extremo, ni siquiera necesita una renderfunción específica . Podría tener una función llamada renderFirstNameque actualiza el primer nombre en el dom yrenderLastName que actualiza el apellido en el dom. Si adoptara este enfoque, sería mucho mejor en términos de rendimiento y nunca más tendría que delegar eventos manualmente. El código también tendría sentido para alguien que lo lea (aunque sería un código más largo / más desordenado).

Sin embargo, generalmente no hay inconvenientes en usar plantillas y simplemente destruir y reconstruir toda la vista y sus subvistas en todas y cada una de las llamadas de renderizado, ya que ni siquiera se le ocurrió al interrogador hacer nada de otra manera. Así que eso es lo que la mayoría de la gente hace en casi todas las situaciones que encuentran. Y es por eso que los marcos obstinados simplemente hacen de este el comportamiento predeterminado.

Nick Manning
fuente
0

También puede inyectar las subvistas renderizadas como variables en la plantilla principal como variables.

primero renderice las subvistas y conviértalas a html de esta manera:

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

(de esa manera, también podría concatenar dinámicamente las vistas como subview1 + subview2cuando 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.

B Piltz
fuente
0

Me gusta usar el siguiente enfoque que también se asegura de eliminar las vistas secundarias correctamente. Aquí hay un ejemplo del libro 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() {
    } });
FlintOff
fuente
0

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);            
    }
});
Soham Joshi
fuente