Backbone.ModelBinder icon indicating copy to clipboard operation
Backbone.ModelBinder copied to clipboard

New Class to Bind Collections to Views

Open theironcook opened this issue 12 years ago • 19 comments

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 avatar May 10 '12 15:05 theironcook

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 avatar May 14 '12 20:05 theironcook

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 avatar May 21 '12 19:05 theironcook

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

theironcook avatar May 29 '12 19:05 theironcook

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?

brunoreis avatar Jun 07 '12 11:06 brunoreis

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?

theironcook avatar Jun 07 '12 14:06 theironcook

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 < [email protected]

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 avatar Jun 08 '12 00:06 brunoreis

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?

brunoreis avatar Jun 15 '12 11:06 brunoreis

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?

theironcook avatar Jun 15 '12 14:06 theironcook

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 < [email protected]

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 avatar Jun 15 '12 14:06 brunoreis

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 < [email protected]

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 avatar Jun 15 '12 17:06 brunoreis

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.

theironcook avatar Jun 18 '12 14:06 theironcook

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 < [email protected]

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

brunoreis avatar Jun 18 '12 22:06 brunoreis

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.

theironcook avatar Jul 31 '12 15:07 theironcook

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

Suggestions?

viniciuscb avatar Aug 23 '12 18:08 viniciuscb

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

do you think this would work for you?

boxxxie avatar Aug 23 '12 19:08 boxxxie

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'))

viniciuscb avatar Aug 23 '12 20:08 viniciuscb

@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.

theironcook avatar Aug 30 '12 02:08 theironcook

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

platinumazure avatar Feb 09 '15 03:02 platinumazure

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!

platinumazure avatar Feb 16 '15 05:02 platinumazure