Como renderizar e anexar sub-visualizações no Backbone.js

133

Eu tenho uma configuração de exibição aninhada que pode se aprofundar um pouco no meu aplicativo. Há várias maneiras pelas quais eu poderia pensar em inicializar, renderizar e anexar as sub-visualizações, mas estou me perguntando o que é uma prática comum.

Aqui estão alguns que eu pensei:

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

Prós: você não precisa se preocupar em manter a ordem DOM correta ao anexar. As visualizações são inicializadas desde o início, portanto, não há muito o que fazer de uma vez na função de renderização.

Contras: Você é forçado a delegar novamente novamenteEvents (), o que pode ser caro? A função de renderização da visualização pai está cheia de toda a renderização da subvisão que precisa acontecer? Você não tem a capacidade de definir os tagNameelementos, portanto, o modelo precisa manter os tagsNames corretos.

Outra maneira:

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

Prós: você não precisa delegar novamente os eventos. Você não precisa de um modelo que contenha apenas espaços reservados vazios e os seus tagName voltem a ser definidos pela exibição.

Contras: agora você precisa anexar as coisas na ordem certa. A renderização da visualização pai ainda é confusa pela renderização da subvisão.

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

Prós: a lógica da subvisão agora está separada do render()método da visualização .

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

Eu meio que misturei e combinei várias práticas diferentes em todos esses exemplos (desculpe por isso), mas quais são as que você gostaria de manter ou adicionar? e o que você não faria?

Resumo das práticas:

  • Instanciar sub-visualizações dentro initializeou dentro render?
  • Executar toda a lógica de renderização de sub-visualização em renderou dentro onRender?
  • Use setElementou append/appendTo?
Ian Storm Taylor
fonte
Eu teria cuidado com o novo sem excluir, você tem vazamento de memória lá.
vimdude
1
Não se preocupe, eu tenho um closemétodo e um onCloseque limpa crianças, mas só estou curioso sobre como instanciar e renderizá-las em primeiro lugar.
Ian tempestade Taylor
3
@abdelsaid: No JavaScript, o GC lida com a desalocação de memória. deleteem JS não é o mesmo que deleteem C ++. É uma palavra-chave com um nome muito ruim, se você me perguntar.
31512 Mike
@MikeBantegui conseguiu, mas é o mesmo que em java, exceto que em JS, para liberar memória, você só precisa atribuir nulo. Para esclarecer o que quero dizer, tente criar um loop com um novo objeto dentro e monitorar a memória. É claro que o GC chegará a ele, mas você perderá memória antes que ele chegue. Nesse caso, Render que pode ser chamado várias vezes.
vimdude
3
Sou desenvolvedor iniciante de Backbone. Alguém pode explicar por que o exemplo 1 nos obriga a delegar novamente os eventos? (Ou devo fazer isso em sua própria pergunta?) Obrigado.
Pilau

Respostas:

58

Eu geralmente vi / usei algumas soluções diferentes:

Solução 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();
    }
});

Isso é semelhante ao seu primeiro exemplo, com algumas alterações:

  1. A ordem na qual você anexa os subelementos é importante
  2. A visualização externa não contém os elementos html a serem definidos nas visualizações internas (o que significa que você ainda pode especificar tagName na visualização interna)
  3. render()é chamado APÓS o elemento da visualização interna ter sido colocado no DOM, o que é útil se o render()método da visualização interna estiver se colocando / dimensionando-se na página com base na posição / tamanho de outros elementos (que é um caso de uso comum, na minha experiência)

Solução 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);
    }
});

A solução 2 pode parecer mais limpa, mas causou algumas coisas estranhas na minha experiência e afetou negativamente o desempenho.

Geralmente uso a Solução 1, por alguns motivos:

  1. Muitas das minhas visualizações dependem de já estar no DOM em seu render()método
  2. Quando a visualização externa é renderizada novamente, as visualizações não precisam ser reinicializadas, o que pode causar vazamentos de memória e também problemas esquisitos com as ligações existentes.

Lembre-se de que, se você estiver inicializando uma chamada new View()toda vez render(), ela será iniciada delegateEvents()assim mesmo. Portanto, isso não deve ser necessariamente um "golpe", como você já expressou.

Lukas
fonte
1
Nenhuma dessas soluções funcionar até a árvore sub vista chamando View.remove, que pode ser vital em fazer a limpeza personalizado na vista, que de outra forma impedir a coleta de lixo
Dominic
31

Este é um problema constante com o Backbone e, na minha experiência, não há realmente uma resposta satisfatória para esta pergunta. Eu compartilho sua frustração, especialmente porque há tão pouca orientação, apesar do quão comum é esse caso de uso. Dito isto, costumo usar algo semelhante ao seu segundo exemplo.

Antes de tudo, eu descartaria de imediato tudo o que exigisse que você delegasse novamente os eventos. O modelo de visualização orientada a eventos do Backbone é um dos componentes mais cruciais e perder essa funcionalidade simplesmente porque seu aplicativo não é trivial deixaria um gosto ruim na boca de qualquer programador. Então, arranhe o número um.

Em relação ao seu terceiro exemplo, acho que é apenas uma conclusão final da prática de renderização convencional e não acrescenta muito significado. Talvez se você estiver ativando um evento real (" onRender" não é um evento artificial ), valeria a pena vincular esses eventos a rendersi mesmo. Se você renderse sentir pesado e complexo, terá poucas subvisões.

Volte ao seu segundo exemplo, que provavelmente é o menor dos três males. Aqui está um exemplo de código retirado de Recipes With Backbone , encontrado na página 42 da minha edição em 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);
}

Essa é apenas uma configuração um pouco mais sofisticada que o seu segundo exemplo: eles especificam um conjunto de funções addAlle addOnefazem o trabalho sujo. Eu acho que essa abordagem é viável (e certamente a uso); mas ainda deixa um sabor bizarro. (Perdoe todas essas metáforas da língua.)

Ao seu ponto de anexar na ordem correta: se você estiver anexando estritamente, com certeza, isso é uma limitação. Mas certifique-se de considerar todos os esquemas de modelos possíveis. Talvez você realmente queira um elemento de espaço reservado (por exemplo, um vazio divou ul) que possa ser replaceWithum novo elemento (DOM) que contém as subvisões apropriadas. Anexar não é a única solução, e você certamente pode contornar o problema de pedidos se você se importa muito com isso, mas eu imagino que você tenha um problema de design, se estiver enganando você. Lembre-se de que as subviews podem ter subviews, e deveriam, se for apropriado. Dessa forma, você tem uma estrutura semelhante a uma árvore, o que é bastante agradável: cada subvisão adiciona todas as suas subvisões, em ordem, antes que a visualização pai adicione outra, e assim por diante.

Infelizmente, a solução 2 é provavelmente a melhor que você pode esperar para usar o Backbone pronto para uso. Se você estiver interessado em verificar bibliotecas de terceiros, uma que eu procurei (mas ainda não tive tempo para brincar) é o Backbone.LayoutManager , que parece ter um método mais saudável de adicionar sub-visualizações. No entanto, mesmo eles tiveram debates recentes sobre questões semelhantes a essas.

Josh Leitzel
fonte
4
A penúltima linha - model.bind('remove', view.remove);- você não deveria simplesmente fazer isso na função de inicialização do Compromisso para mantê-los separados?
Atp
2
E quando uma visão não pode ser re-instanciada toda vez que é gerada pelos pais porque mantém um estado?
mor
Pare com toda essa loucura e use o plug-in Backbone.subviews !
Admirável Dave
6

Surpreendeu que isso ainda não tenha sido mencionado, mas eu consideraria seriamente usar Marionette .

Ele aplica um pouco mais estrutura para aplicações de backbone, incluindo visão específica tipos ( ListView, ItemView, Regione Layout), adicionando adequados Controllers e muito mais.

Aqui está o projeto no Github e um ótimo guia de Addy Osmani no livro Backbone Fundamentals para você começar.

Dana Woodman
fonte
3
Isso não responde à pergunta.
Ceasar Bautista 02/02
2
@CeasarBautista eu não entrar em como usar Marionette para alcançar este objetivo, mas não Marionette de fato resolver o problema acima
Dana Woodman
4

Eu tenho, o que acredito ser, uma solução bastante abrangente para esse problema. Ele permite que um modelo dentro de uma coleção seja alterado e tenha apenas sua exibição renderizada novamente (em vez de toda a coleção). Ele também lida com a remoção de visualizações de zumbis através dos 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
fonte
2

Confira este mixin para criar e renderizar subviews:

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

É uma solução minimalista que aborda muitos dos problemas discutidos neste encadeamento, incluindo ordem de renderização, não sendo necessário delegar novamente eventos etc. Observe que o caso de uma exibição de coleção (em que cada modelo da coleção é representado por um subview) é um tópico diferente. A melhor solução geral que conheço nesse caso é o CollectionView in Marionette .

Brave Dave
fonte
0

Eu realmente não gosto de nenhuma das soluções acima. Eu prefiro para essa configuração que cada visualização precise trabalhar manualmente no método render.

  • views pode ser uma função ou objeto retornando um objeto de definições de visualização
  • Quando os pais .removesão chamados, os .removefilhos aninhados da ordem mais baixa para cima devem ser chamados (desde as visualizações sub-sub-sub)
  • Por padrão, a visualização principal passa seu próprio modelo e coleção, mas as opções podem ser adicionadas e substituídas.

Aqui está um exemplo:

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
fonte
0

O backbone foi construído intencionalmente para que não houvesse prática "comum" em relação a esse e a muitos outros problemas. Ele deve ser o mais unopinionated possível. Teoricamente, você nem precisa usar modelos com o Backbone. Você pode usar javascript / jquery na renderfunção de uma exibição para alterar manualmente todos os dados na exibição. Para torná-lo mais extremo, você nem precisa de uma renderfunção específica . Você poderia ter uma função chamada renderFirstNameque atualiza o primeiro nome no dom e renderLastNameque atualiza o sobrenome no dom. Se você adotasse essa abordagem, seria muito melhor em termos de desempenho e nunca mais precisaria delegar eventos manualmente. O código também faria total sentido para alguém que o estivesse lendo (embora fosse um código mais longo / mais confuso).

No entanto, geralmente não há desvantagem em usar modelos e simplesmente destruir e reconstruir a exibição inteira e suas subvisões em cada chamada de renderização, pois nem sequer ocorreu ao questionador fazer outra coisa. Então é isso que a maioria das pessoas faz em praticamente todas as situações em que se deparam. E é por isso que estruturas opinativas apenas tornam esse o comportamento padrão.

Nick Manning
fonte
0

Você também pode injetar as subvisões renderizadas como variáveis ​​no modelo principal como variáveis.

primeiro renderize as subvisões e converta-as em html assim:

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

(dessa forma, você também pode concatenar dinamicamente as visualizações como subview1 + subview2quando usadas em loops) e depois passá-las para o modelo principal que se parece com isso: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

e injete-o finalmente assim:

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

Em relação aos Eventos nas subvisões: É provável que eles tenham que estar conectados no pai (masterView) com essa abordagem, não nas subvisões.

B Piltz
fonte
0

Eu gosto de usar a abordagem a seguir, que também remove as visualizações filho corretamente. Aqui está um exemplo do livro 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
fonte
0

Não há necessidade de delegar novamente os eventos, pois é caro. Ver abaixo:

    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
fonte