javascript - Como renderizar e anexar subvisualizações no Backbone.js




model-view-controller (9)

Eu tenho uma configuração de visão aninhada que pode ficar um pouco profunda no meu aplicativo. Há várias formas de pensar em inicializar, renderizar e anexar as subvisualizações, mas estou me perguntando qual é a prática comum.

Aqui está um par 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 o pedido DOM correto com o acréscimo. As visualizações são inicializadas no início, portanto, não há muito o que fazer de uma vez na função de renderização.

Contras: Você é forçado a re-delegateEvents (), o que pode ser caro? A função de renderização da exibição pai está cheia de toda a renderização de subvisualização que precisa acontecer? Você não tem a capacidade de definir o tagName dos elementos, portanto, o modelo precisa manter os tagNames 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 eventos. Você não precisa de um modelo que apenas contenha espaços reservados vazios e que seus tagName estejam de volta à definição pela exibição.

Contras: Agora você tem que se certificar de acrescentar as coisas na ordem correta. A renderização da exibição principal ainda está desordenada pela renderização de subvisualização.

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

Prós: A lógica da subview agora está separada do método render() da visão.

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

Eu meio que misturei e combinei um monte de práticas diferentes em todos esses exemplos (desculpe por isso), mas quais são os que você manteria ou adicionaria? e o que você não faria?

Resumo das práticas:

  • Instantiate subviews em initialize ou render ?
  • Executar toda a lógica de render onRender no render ou no onRender ?
  • Use setElement ou append/appendTo ?

Answers

Confira este mixin para criar e renderizar subvisualizações:

Backbone.subviews

É uma solução minimalista que aborda muitos dos problemas discutidos neste tópico, incluindo a ordem de renderização, não ter que re-delegar eventos, etc. Observe que o caso de uma visualização de coleta (em que cada modelo na coleção é representado por um subview) é um tópico diferente. A melhor solução geral que conheço nesse caso é o CollectionView em Marionette .


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

Primeiro de tudo, eu descartaria qualquer coisa que exigisse que você re-delegasse eventos. O modelo de visão orientada a eventos do Backbone é um dos seus 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, risque o número um.

Em relação ao seu terceiro exemplo, acho que é apenas um fim em torno da prática de renderização convencional e não acrescenta muito significado. Talvez, se você estiver realizando um evento real (isto é, não um evento " onRender " planejado), valeria a pena vincular esses eventos para se render . Se você achar que a render se tornando difícil e complexa, você tem muito poucas subvisualizações.

De volta ao seu segundo exemplo, que é provavelmente o menor dos três males. Aqui está um exemplo de código extraído de Receitas com 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);
}

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

Para o seu ponto em acrescentar na ordem certa: se você está anexando estritamente, com certeza, isso é uma limitação. Mas certifique-se de considerar todos os possíveis esquemas de modelos. Talvez você realmente queira um elemento de espaço reservado (por exemplo, um div ou ul vazio) que possa ser replaceWith um novo elemento (DOM) que contenha as subvisualizações apropriadas. O acréscimo não é a única solução, e você certamente pode contornar o problema de ordenação se você se importar muito com isso, mas eu imagino que você tem um problema de design se ele está tropeçando em você. Lembre-se de que as subvisualizações podem ter subvisualizações, e deveriam, se isso for apropriado. Dessa forma, você tem uma estrutura bastante semelhante a uma árvore, o que é bastante interessante: cada subview adiciona todas as suas subvisualizações, em ordem, antes que a view 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 examinei (mas ainda não tive tempo para brincar) é o Backbone.LayoutManager , que parece ter um método mais saudável de adicionar subvisualizações. No entanto, mesmo eles tiveram debates recentes sobre questões semelhantes a estes.


Eu tenho, o que eu acredito ser, uma solução bastante abrangente para este problema. Ele permite que um modelo dentro de uma coleção seja alterado e tenha apenas sua exibição re-renderizada (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
});

A espinha dorsal foi intencionalmente construída de forma que não houvesse uma prática "comum" em relação a isso e a muitas outras questões. Pretende-se ser o mais unpolionado possível. Teoricamente, você não precisa nem usar modelos com o Backbone. Você poderia usar javascript / jquery na função de render de uma exibição para alterar manualmente todos os dados na exibição. Para torná-lo mais extremo, você nem precisa de uma função de render específica. Você poderia ter uma função chamada renderFirstName que atualiza o primeiro nome no dom e renderLastName que atualiza o último nome no dom. Se você adotasse essa abordagem, seria muito melhor em termos de desempenho e nunca precisaria delegar manualmente os eventos novamente. O código também faria todo o sentido para alguém que o estivesse lendo (embora fosse um código mais longo / confuso).

No entanto, normalmente não há desvantagem em usar modelos e simplesmente destruir e reconstruir toda a visualização e suas subvisualizações em todas as chamadas de renderização, já que nem sequer ocorreu ao questionador fazer qualquer outra coisa. Então é isso que a maioria das pessoas faz para praticamente todas as situações que encontram. E é por isso que os frameworks opinativos apenas fazem disso o comportamento padrão.


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

primeiro renderize as subvisualizações e converta-as para html assim:

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

(dessa forma você também pode dinamicamente encadear concatenar as visualizações como subview1 + subview2 quando usado em loops) e então passá-las para o template mestre que se parece com isto: ... some header stuff ... <%= sub1 %> <%= sub2 %> ... some footer stuff ...

e injetá-lo finalmente assim:

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

Quanto aos Eventos nas subvisualizações: Eles provavelmente terão que estar conectados no pai (masterView) com essa abordagem, não dentro das subvisualizações.


Não há necessidade de re-delegar 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);            
    }
});

Eu gosto de usar a seguinte abordagem, que também se esqueça de remover as exibições filho corretamente. Aqui está um exemplo do 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() {
    } });

Surpreso isso ainda não foi mencionado, mas eu consideraria seriamente usar Marionette .

Ele reforça um pouco mais a estrutura dos aplicativos do Backbone, incluindo tipos de visualização específicos ( ListView , ItemView , Region e Layout ), adicionando os Controller apropriados e muito mais.

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


Ajudaria se você fosse um pouco mais descritivo no que você está tentando fazer. Se você está tentando gerar dados paginados, há algumas opções em como você faz isso. Você pode gerar links separados para cada página que você deseja obter diretamente.

<a href='/path-to-page?page=1' class='pager-link'>1</a>
<a href='/path-to-page?page=2' class='pager-link'>2</a>
<span class='pager-link current-page'>3</a>
...

Observe que a página atual no exemplo é tratada de maneira diferente no código e com CSS.

Se você quiser que os dados paginados sejam alterados por meio do AJAX, é aqui que o jQuery entraria. O que você faria é incluir um manipulador de cliques em cada uma das tags de âncora correspondentes a uma página diferente. Este manipulador de cliques invocaria algum código jQuery que vai e busca a próxima página via AJAX e atualiza a tabela com os novos dados. O exemplo abaixo pressupõe que você tenha um serviço da Web que retorne os novos dados da página.

$(document).ready( function() {
    $('a.pager-link').click( function() {
        var page = $(this).attr('href').split(/\?/)[1];
        $.ajax({
            type: 'POST',
            url: '/path-to-service',
            data: page,
            success: function(content) {
               $('#myTable').html(content);  // replace
            }
        });
        return false; // to stop link
    });
});




javascript model-view-controller backbone.js