theironcook / Backbone.ModelBinder

Simple, flexible and powerful Model-View binding for Backbone.
1.42k stars 159 forks source link

New Class to Bind Collections to Views #24

Open theironcook opened 12 years ago

theironcook commented 12 years ago

Hi All,

I've created a new class - CollectionViewBinder - that helps you bind views to collections.

It can help you bind a collection to a table - each row in the table will correspond to a model or bind a collection to any other type of html DOM element you'd like.

The CollectionViewBinder will help sync the collections models to the corresponding views via the add / remove / reset collection events. The dynamically created views can also be synchronized with their corresponding models view the ModelBinder.

theironcook commented 12 years ago

I've updated the CollectionViewBinder a bit and have added a wiki page on how it works.

https://github.com/theironcook/Backbone.ModelBinder/wiki/A-new-Class-to-Bind-Backbone-Collections-to-Views:-Javascript-Weekly-May-18th

theironcook commented 12 years ago

I'm finding several uses for the new collection binder. I have many places that show a <select> control and when the choices in the backing collection change the select needs to update. Here's how to hook this up with the collection binder...

initialize: function( ){
  var selectOptionBindings = {
    id: {selector: '', elAttribute: 'value'},
    name: {selector: ''}
  };

  var myFactory = new Backbone.CollectionBinder.ElManagerFactory('<option></option>', selectOptionBindings );
  this._myCollectionBinder = = new Backbone.CollectionBinder(myFactory);
},

render: function( ){
  ...
  this._myCollectionBinder .bind(this._myCollection, this.$('[name=mySelectControl]'));
  ...
}

Now when the choices in _myCollection change (adds/removes) or when the choices themselves change, the select options are automatically updated.

theironcook commented 12 years ago

I've renamed CollectionViewBinder to just CollectionBinder. I've renamed createBoundEls() to just bind()

brunoreis commented 12 years ago

It´s a very nice tool. Thanks.

Do you already think in a specific way to attach click and other event listeners to each "piece" of the view created by a ElManagerFactory?

It seems to me that a ViewManagerFactory would be better for this, since it uses the default backbone event registrations. But, since ElManagerFactory seems simpler, have you imagined how to deal with events on each row?

I need to have a list, with a selected item, where the user can navigate using the keyboard up and down and press space (or dbl-click) to edit one item. Do you see that it´s possible using ElMan... or it´s better to approach from the beggining with the ViewManagerFactory?

theironcook commented 12 years ago

Thank you.

Yes, your right. It's easier to handle events in nested views with the ViewManagerFactory because you can just use the events block in the nested View class.

But, you can handle events in nested views with the ElManagerFactory too. The CollectionBinder has a function getManagerForEl(el) that you can use to track down which elManager is related to the element. For example, if your outer view looks like this...

OuterView = Backbone.View.extend({
  events: {      
        'click tr': '_onRowClicked'
    },

    initialize: function() {
        // creates theRowTemplate and rowBindings here...
        var elFactory = new Backbone.CollectionBinder.ElManagerFactory(theRowTemplate, rowBindings);
        this._collectionBinder = new Backbone.CollectionBinder(elFactory );
    },

    onClose: function() {
        this._collectionBinder .unbind();
    },

     render: function() {
        // The template will contain a table with a tbody tag where the collection is rendered
        $(this.el).html(this.template());
        this._collectionBinder.bind(this.collection, this.$('tbody'));

        return this;
    },

    _onRowClicked: function(event){
        var el = $(event.target)[0];
        var theClickedModel = this._collectionBinder.getManagerForEl(el).getModel();
        this._theController.doSomething(theClickedModel);
    }
});

In the _onRowClicked function, we use the CollectionBinder which in turn uses the ElManagerFactory.getManagerForEl(el) to track down which ModelManager relates to the DOM element (if any are). This check is recursive so if the DOM element is deeply nested it will be found. (If the CollectionBinder is using a ViewManagerFactory the getManagerForEl(el) works the same way)

The ElManager then has the function getModel() to figure out which model was actually clicked.

Does that help answer your question?

brunoreis commented 12 years ago

Yes, thanks. I´ve figured out how to capture a click and identify the related model with your help. I will follow this path (with ElManager) and try more complex situations here like capturing and identifying clicks from buttons on the tr.

Thanks again for the code and response.

2012/6/7 Bart wood < reply@reply.github.com

Thank you.

Yes, your right. It's easier to handle events in nested views with the ViewManagerFactory because you can just use the events block in the nested View class.

But, you can handle events in nested views with the ElManagerFactory too. The CollectionBinder has a function getManagerForEl(el) that you can use to track down which elManager is related to the element. For example, if your outer view looks like this...

OuterView = Backbone.View.extend({
 events: {
       'click tr': '_onRowClicked'
   },

   initialize: function() {
       // creates theRowTemplate and rowBindings here...
       var elFactory = new
Backbone.CollectionBinder.ElManagerFactory(theRowTemplate, rowBindings);
       this._collectionBinder = new Backbone.CollectionBinder(elFactory );
   },

   onClose: function() {
       this._collectionBinder .unbind();
   },

    render: function() {
       $(this.el).html(this.template());
       this._collectionBinder.bind(this.collection, this.$('tbody'));

       return this;
   },

   _onRowClicked: function(event){
       var el = $(event.target)[0];
       var theClickedModel =
this._collectionBinder.getManagerForEl(el).getModel();
       this._theController.doSomething(theClickedModel);
   }
});

In the _onRowClicked function, we use the CollectionBinder which in turn uses the ElManagerFactory.getManagerForEl(el) to track down which ModelManager relates to the DOM element (if any are). This check is recursive so if the DOM element is deeply nested it will be found. (If the CollectionBinder is using a ViewManagerFactory the getManagerForEl(el) works the same way)

The ElManager then has the function getModel() to figure out which model was actually clicked.

Does that help answer your question?


Reply to this email directly or view it on GitHub:

https://github.com/theironcook/Backbone.ModelBinder/issues/24#issuecomment-6177865

brunoreis commented 12 years ago

Found another challnge here: now I´m using a collection´s comparator function. So the added elements are sorted each time. Is there a way to sort the view using the CollectionBinder? Should I need to render it all again? How can I do it?

theironcook commented 12 years ago

Hi Brunoreis,

That's a good point. Most of my views with the CollectionBinder are tables and I use the JQuery tablesorter plugin to allow the user to sort their own tables after the initial sort of the collection. But I can easily see wanting to sort lists/tables/sets of div tags automatically by the collections sorting order.

For now, you can call the CollectionBinder.unbind() and then bind() - this is a pretty wasteful and probably jarring solution for the end user if the views flickers etc depending on how many times you call it.

I'm thinking, maybe the CollectionBinder has an attribute called "autoSortViews" that will automatically ensure the nested views/elements maintain the sorting order - but if the collection is sorted too often this type of solution has the same issue.

Can you describe if your letting the user click something explicitly to change the sorting order or if it's something that the program does automatically?

brunoreis commented 12 years ago

I'm just adding a new item to the collection. Since I've defined a comparator for the Collection ( http://backbonejs.org/#Collection-comparator), the collection get sorted, but not the view.

I think that, in the case the user wants to change the way he views the table, It's better to use something like the tablesorter plugin. Maybe your solution will work for me either. I will have to think about it later because I'm not working with my project right now.

2012/6/15 Bart wood < reply@reply.github.com

Hi Brunoreis,

That's a good point. Most of my views with the CollectionBinder are tables and I use the JQuery tablesorter plugin to allow the user to sort their own tables after the initial sort of the collection. But I can easily see wanting to sort lists/tables/sets of div tags automatically by the collections sorting order.

For now, you can call the CollectionBinder.unbind() and then bind() - this is a pretty wasteful and probably jarring solution for the end user if the views flickers etc depending on how many times you call it.

I'm thinking, maybe the CollectionBinder has an attribute called "autoSortViews" that will automatically ensure the nested views/elements maintain the sorting order - but if the collection is sorted too often this type of solution has the same issue.

Can you describe if your letting the user click something explicitly to change the sorting order or if it's something that the program does automatically?


Reply to this email directly or view it on GitHub:

https://github.com/theironcook/Backbone.ModelBinder/issues/24#issuecomment-6358308

brunoreis commented 12 years ago

Let's say I have a user defined sort order using JQuery Tablesorter. How can I listen to a add event on the CollectionBinder and sort my view acording to user preferences?

2012/6/15 Bart wood < reply@reply.github.com

Hi Brunoreis,

That's a good point. Most of my views with the CollectionBinder are tables and I use the JQuery tablesorter plugin to allow the user to sort their own tables after the initial sort of the collection. But I can easily see wanting to sort lists/tables/sets of div tags automatically by the collections sorting order.

For now, you can call the CollectionBinder.unbind() and then bind() - this is a pretty wasteful and probably jarring solution for the end user if the views flickers etc depending on how many times you call it.

I'm thinking, maybe the CollectionBinder has an attribute called "autoSortViews" that will automatically ensure the nested views/elements maintain the sorting order - but if the collection is sorted too often this type of solution has the same issue.

Can you describe if your letting the user click something explicitly to change the sorting order or if it's something that the program does automatically?


Reply to this email directly or view it on GitHub:

https://github.com/theironcook/Backbone.ModelBinder/issues/24#issuecomment-6358308

theironcook commented 12 years ago

Sorry for the delay.

The CollectionBinder will trigger 2 events you can listen for - "elCreated" and "elRemoved". You can register for those events and then re-apply the jquery tablesorter. I do this in my code and it works great.

brunoreis commented 12 years ago

nice. I had figured out these events, bu for some reason the sort was not working when removing elements. The removed elements were apearing again on sort operations.

So I´ve found some events to trigger on the tablesorter to fix this (this fork: https://github.com/Mottie/tablesorter). Maybe it is cheaper to use:

render: function() { that = this; this._collectionBinder.on('elCreated',function(model,el){ $(that.el).find('table').trigger('addRows', [ $(el[0]), true] ); }) this._collectionBinder.on('elRemoved',function(model,el){ $(that.el).find('table').trigger('update'); }) this._collectionBinder.bind(this.collection, this.$('tbody')); this.sort(); return this; },

2012/6/18 Bart wood < reply@reply.github.com

Sorry for the delay.

The CollectionBinder will trigger 2 events you can listen for - "elCreated" and "elRemoved". You can register for those events and then re-apply the jquery tablesorter. I do this in my code and it works great.


Reply to this email directly or view it on GitHub:

https://github.com/theironcook/Backbone.ModelBinder/issues/24#issuecomment-6397154

theironcook commented 12 years ago

I've added an option to the CollectionBinder named "autoSort". If this option is set, the collection binder keeps it's nested views in the same sorting order as the collection.

viniciuscb commented 12 years ago

Is it possible to specify a different parentEl for each model in the collection, based in the model's attributes?

My program has to have the following functionality: I have to render a collection in a matrix, a html table of several rows and columns. Each model has a view that is only a

boxxxie commented 12 years ago

don't use a matrix, represent the matrix in your object as an attribute.

do you think this would work for you?

viniciuscb commented 12 years ago

Hello, I have decided to implement this behavior in another way, instead of putting the elements inside a table. I will draw them inside div's absolute positioned.

However, I made an implementation, it is not elegant but here it goes:

the template

<table class="table"><tbody>
   <tr>
              <td id="spot_4_1" ></td>
              <td id="spot_4_2" ></td>
              <td id="spot_4_3" ></td>
              <td id="spot_4_4" ></td>
              <td id="spot_4_5" ></td>
        </tr>
   <tr>
              <td id="spot_3_1" ></td>
              <td id="spot_3_2" ></td>
              <td id="spot_3_3" ></td>
              <td id="spot_3_4" ></td>
              <td id="spot_3_5" ></td>
        </tr>
...

the code

    elManagerFactory = new Backbone.CollectionBinder.ViewManagerFactory( (model) -> new Application.Views.ParkingSpots.SpotButton({model: model}));

    elManagerFactory.findParentEl = (parking_spot) ->
      $(this._parentEl).find('#spot_'+parking_spot.get('position_y')+'_'+parking_spot.get('position_x'))

    elManagerFactory.createEl = ->
      @_view = @_viewCreator(@_model);
      @findParentEl(@_model).append(@_view.render(@_model).el);

      this.trigger('elCreated', @_model, @_view);

    @_collectionBinder = new Backbone.CollectionBinder(elManagerFactory);
    @_collectionBinder.bind(@collection, @$('table'))
theironcook commented 12 years ago

@viniciuscb

That will work - interesting solution. I guess another possibility would be to use a collection binder per table row and have a collection for each row of parking spots - so spot_4 would be it's own collection. The CollectionBinder has an autoSort option that will keep your nested views sorted in the same order as the collection it's bound too. But that way works too.

platinumazure commented 9 years ago

@theironcook Since this is long since implemented and released (including auto-sort capabilities!), should this issue be closed?

platinumazure commented 9 years ago

Ah-- now I see one possible reason this has been left open. There are no unit tests! (My company is totally using that code too, yikes!). I can try to chip in but there's a fair bit of ground to cover I think! If anyone else wants to help, reply here or ping me and let's coordinate!