2013-08-10 1 views
11

Diversi posti nella mia applicazione Backbone Mi piacerebbe avere una ricerca istantanea su una raccolta, ma sto avendo difficoltà a trovare il modo migliore per implementare esso.Backbone.js - Best practice per l'implementazione della ricerca "istantanea"

Ecco una rapida implementazione. http://jsfiddle.net/7YgeE/ Ricorda che la mia collezione potrebbe contenere fino a 200 modelli.

var CollectionView = Backbone.View.extend({ 

    template: $('#template').html(), 

    initialize: function() { 

    this.collection = new Backbone.Collection([ 
     { first: 'John', last: 'Doe' }, 
     { first: 'Mary', last: 'Jane' }, 
     { first: 'Billy', last: 'Bob' }, 
     { first: 'Dexter', last: 'Morgan' }, 
     { first: 'Walter', last: 'White' }, 
     { first: 'Billy', last: 'Bobby' } 
    ]); 
    this.collection.on('add', this.addOne, this); 

    this.render(); 
    }, 

    events: { 
    'keyup .search': 'search', 
    }, 

    // Returns array subset of models that match search. 
    search: function(e) { 

    var search = this.$('.search').val().toLowerCase(); 

    this.$('tbody').empty(); // is this creating ghost views? 

    _.each(this.collection.filter(function(model) { 
     return _.some(
     model.values(), 
     function(value) { 
      return ~value.toLowerCase().indexOf(search); 
     }); 
    }), $.proxy(this.addOne, this)); 
    }, 

    addOne: function(model) { 

    var view = new RowView({ model: model }); 
    this.$('tbody').append(view.render().el); 
    }, 

    render: function() { 

    $('#insert').replaceWith(this.$el.html(this.template)); 
     this.collection.each(this.addOne, this); 
    } 
}); 

E una piccola vista per ogni modello ...

var RowView = Backbone.View.extend({ 

    tagName: 'tr', 

    events: { 
    'click': 'click' 
    }, 

    click: function() { 
    // Set element to active 
    this.$el.addClass('selected').siblings().removeClass('selected'); 

    // Some detail view will listen for this. 
    App.trigger('model:view', this.model); 
    }, 

    render: function() { 

    this.$el.html('<td>' + this.model.get('first') + '</td><td>' + this.model.get('last') + '</td>'); 
     return this; 
    } 
}); 

new CollectionView; 

Domanda 1

Su ogni keydown, a filtrare la collezione, vuoto il tbody, e rendere il risultati, creando così una nuova vista per ogni modello. Ho appena creato una vista fantasma, sì? Sarebbe meglio distruggere adeguatamente ogni vista? O dovrei tentare di gestire il mio RowView s ... creando ognuno una sola volta e scorrendo attraverso di essi per rendere solo i risultati? Un array nel mio CollectionView forse? Dopo aver svuotato il tbody, il dovrebbe ancora avere il valore el oppure ora è nullo e deve essere nuovamente sottoposto a rendering?

Domanda 2, Selezione del modello

Noterete sto innescando un evento personalizzato nel mio RowView. Mi piacerebbe avere una vista dettagliata da qualche parte per gestire quell'evento e visualizzare l'interezza del mio modello. Quando cerco la mia lista, se il mio modello selezionato rimane nei risultati di ricerca, voglio mantenere quello stato e lasciarlo nella mia vista di dettaglio. Una volta che non è più nei miei risultati, svuoterò la vista di dettaglio. Quindi dovrò sicuramente gestire una serie di viste, giusto? Ho considerato una struttura doppiamente collegata in cui ogni vista indica il suo modello e ogni modello per la sua vista ... ma se devo implementare una fabbrica singleton sui miei modelli in futuro, non posso imporlo sul modello. :/

Quindi qual è il modo migliore per gestire queste visualizzazioni?

risposta

19

ho avuto un po 'portato via mentre gioca con la tua domanda.

In primo luogo, vorrei creare una raccolta dedicata per contenere i modelli filtrati e un "modello di stato" per gestire la ricerca. Ad esempio,

var Filter = Backbone.Model.extend({ 
    defaults: { 
     what: '', // the textual search 
     where: 'all' // I added a scope to the search 
    }, 
    initialize: function(opts) { 
     // the source collection 
     this.collection = opts.collection; 
     // the filtered models 
     this.filtered = new Backbone.Collection(opts.collection.models); 
     //listening to changes on the filter 
     this.on('change:what change:where', this.filter); 
    }, 

    //recalculate the state of the filtered list 
    filter: function() { 
     var what = this.get('what').trim(), 
      where = this.get('where'), 
      lookin = (where==='all') ? ['first', 'last'] : where, 
      models; 

     if (what==='') { 
      models = this.collection.models;    
     } else { 
      models = this.collection.filter(function(model) { 
       return _.some(_.values(model.pick(lookin)), function(value) { 
        return ~value.toLowerCase().indexOf(what); 
       }); 
      }); 
     } 

     // let's reset the filtered collection with the appropriate models 
     this.filtered.reset(models); 
    } 
}); 

che sarebbe un'istanza come

var people = new Backbone.Collection([ 
    {first: 'John', last: 'Doe'}, 
    {first: 'Mary', last: 'Jane'}, 
    {first: 'Billy', last: 'Bob'}, 
    {first: 'Dexter', last: 'Morgan'}, 
    {first: 'Walter', last: 'White'}, 
    {first: 'Billy', last: 'Bobby'} 
]); 
var flt = new Filter({collection: people}); 

Poi creerebbe viste separate per l'elenco e campi di immissione: facile da mantenere e per muoversi

var BaseView = Backbone.View.extend({ 
    render:function() { 
     var html, $oldel = this.$el, $newel; 

     html = this.html(); 
     $newel=$(html); 

     this.setElement($newel); 
     $oldel.replaceWith($newel); 

     return this; 
    } 
}); 
var CollectionView = BaseView.extend({ 
    initialize: function(opts) { 
     // I like to pass the templates in the options 
     this.template = opts.template; 
     // listen to the filtered collection and rerender 
     this.listenTo(this.collection, 'reset', this.render); 
    }, 
    html: function() { 
     return this.template({ 
      models: this.collection.toJSON() 
     }); 
    } 
}); 
var FormView = Backbone.View.extend({ 
    events: { 
     // throttled to limit the updates 
     'keyup input[name="what"]': _.throttle(function(e) { 
      this.model.set('what', e.currentTarget.value); 
     }, 200), 

     'click input[name="where"]': function(e) { 
      this.model.set('where', e.currentTarget.value); 
     } 
    } 
}); 

BaseView consente di modificare il DOM in posizione, vedere Backbone, not "this.el" wrapping per dettagli

Le istanze sarebbe simile

var inputView = new FormView({ 
    el: 'form', 
    model: flt 
}); 
var listView = new CollectionView({ 
    template: _.template($('#template-list').html()), 
    collection: flt.filtered 
}); 
$('#content').append(listView.render().el); 

e una demo della ricerca in questa fase http://jsfiddle.net/XxRD7/2/

Infine, vorrei modificare CollectionView di innestare il punto di vista di fila nella mia funzione di rendering, qualcosa come

var ItemView = BaseView.extend({ 
    events: { 
     'click': function() { 
      console.log(this.model.get('first')); 
     } 
    } 
}); 

var CollectionView = BaseView.extend({ 
    initialize: function(opts) { 
     this.template = opts.template; 
     this.listenTo(this.collection, 'reset', this.render); 
    }, 
    html: function() { 
     var models = this.collection.map(function (model) { 
      return _.extend(model.toJSON(), { 
       cid: model.cid 
      }); 
     }); 
     return this.template({models: models}); 
    }, 
    render: function() { 
     BaseView.prototype.render.call(this); 

     var coll = this.collection; 
     this.$('[data-cid]').each(function(ix, el) { 
      new ItemView({ 
       el: el, 
       model: coll.get($(el).data('cid')) 
      }); 
     }); 

     return this; 
    } 
}); 

Another Fiddle http://jsfiddle.net/XxRD7/3/

+0

Grazie, questo è estremamente utile. Mi piace molto quello che hai fatto con il filtro. Nei miei primi tentativi avevo anche lo scope, ma era difficile da codificare e in qualche modo mi mancava la funzione 'pick' nei documenti. Inoltre, non ho mai saputo della funzione 'throttle', anche molto utile. –

+0

Sto ancora spostando la testa nel modo in cui hai reso le cose, e non sono abbastanza venduto sull'uso di 'setElement'. Mi sembra inelegante essere ri-legare gli eventi ad ogni rendering. Non ho mai visto questa tecnica di innesto, dove si rendono gli elementi della lista in CollectionView e si innestano sulle ItemViews ... Non sono abituato a ItemView che non è responsabile del rendering stesso, e da un lato sembra una separazione di preoccupazioni che non dovrebbero verificarsi, ma d'altra parte è sorprendentemente semplice in quanto è sempre più facile avere il modello iterato sulle nostre collezioni. –

+0

@savinger 'setElement' è per lo più per ragioni estetiche e per" autosufficienza "dei modelli, questa tecnica sarebbe più utile se fosse necessario rieseguire il rerender delle righe, ad esempio. Questa risposta può aiutarti a capire il mio punto di vista http://stackoverflow.com/questions/12004534/backbonejs-rendering-problems/12006179#12006179 – nikoshr

4

La raccolta associata a CollectionView deve essere coerente con ciò che si sta visualizzando, oppure si incontrano problemi. Non dovresti dover svuotare manualmente il tuo corpo. È necessario aggiornare la raccolta e ascoltare gli eventi emessi dalla raccolta in CollectionView e utilizzarli per aggiornare la vista. Nel tuo metodo di ricerca, devi solo aggiornare la tua raccolta e non il tuo CollectionView. Questo è un modo è possibile implementare nel CollectionView metodo initialize:


initialize: function() { 
    //... 

    this.listenTo(this.collection, "reset", this.render); 
    this.listenTo(this.collection, "add", this.addOne); 
} 

E nel vostro metodo di ricerca, si può solo reimpostare la raccolta e la vista renderà automaticamente:


search: function() { 
    this.collection.reset(filteredModels); 
} 

dove filteredModels è una serie di modelli che corrispondono alla query di ricerca. Tieni presente che, una volta ripristinata la raccolta con modelli filtrati, perderai l'accesso agli altri modelli che erano originariamente presenti prima della ricerca. Dovresti avere un riferimento a una collezione principale che contenga tutti i tuoi modelli, indipendentemente dalla ricerca. Questa "raccolta principale" non è associata alla vista di per sé, ma è possibile utilizzare il filtro su questa raccolta principale e aggiornare la raccolta della vista con i modelli filtrati.

Per quanto riguarda la seconda domanda, non è necessario avere un riferimento alla vista dal modello.Il modello dovrebbe essere completamente indipendente dalla vista: solo la vista dovrebbe fare riferimento al modello.

Il tuo metodo addOne potrebbe essere riscritta in questo modo per migliorare le prestazioni (sempre utilizzare $ el allegare subviews):


var view = new RowView({ model: model }); 
this.$el.find('tbody').append(view.render().el); 
+0

Grazie per la risposta. Prima domanda ... Qual è la differenza tra 'this.listenTo (this.collection," reset ", this.render)' e 'this.collection.on (" reset ", this.render, this)'? –

+0

Seconda domanda. Mi piace quello che hai detto su una collezione master e collezione CollectionView ... ma non hai indirizzato le sottoview. Va bene creare nuovi 'RowView's con ogni' addOne'? –

+2

@savinger Fondamentalmente realizzano la stessa cosa: ascoltano gli eventi. Tuttavia, 'this.listenTo' associa l'ascolto alla vista mentre' this.collection.on' associa l'ascolto alla raccolta. Questa non sembra una grande differenza, ma tieni presente che se usi 'this.collection.on', la raccolta continuerà ad ascoltare anche se rimuovi la vista, il che può causare perdite di memoria e rallentare notevolmente l'applicazione. D'altra parte, se usi 'this.listenTo', non ascolterà l'evento dopo aver rimosso la vista. – hesson