4

I have a problem I'm playing with here, I have a collection of BackboneJS models, each model has a 'ordinal' property that tracks its order in the collection.

Here is my play-data

var ex_group_test_data = [{

            title: 'PRE EXERCISE',
            id:         0,
            ordinal:    1,

            group_items: [{
                id:         0,
                ordinal:    0,
                title: 'item 1'
            },{
                id:         1,
                ordinal:    1,
                title: 'item 2'
            }]

        },{

            title: 'MAIN PART',
            id:         1,
            ordinal:    0,

            group_items: [{
                id:             2,
                ordinal:        0,
                title:          'item 3',
                description:    'testing descrip'
            },{
                id:         3,
                ordinal:    1,
                title: 'item 4'
            }]

        },{

            title: 'POST EXERCISE BS',
            id:         2,
            ordinal:    2,

            group_items: [{
                id:             2,
                ordinal:        0,
                title:          'item 5',
                description:    'testing descrip'
            },{
                id:         3,
                ordinal:    1,
                title: 'item 6'
            }]

        }];

And here is the gist of my backbone collection

        Collections.Exercise_Groups = Backbone.Collection.extend({
            model: Models.Exercise_Group,
            comparator: function(model){
                return model.get('ordinal');
            },
            initialize: function(){

                return this;
            }

Starting simple, I want to be able to take a model and move it +1 or -1 ordinal and maintain 0-indexing of all models in the collection.

Eventually I want to bring this to a level where I can drop in models or remove them from any position and still maintain my 0-indexing, or take a model and move it +/- X positions.

Anyone have an recommended ways of accomplishing this?

EDIT 1

I've worked out a solution, I might want to optimize this tomorrow after I actually get some sleep. It maintains the 0-indexing of the 'ordinals' of my models in my collection, whether i'm moving a model forwards or backwards relative to it's original position.

EDIT 2 Err actually it has errors on fringe-cases.

/**
             * Move model to a specified location. 
             * 
             * @param   int [model id]
             * @param   int [mew item position]
             * @return  this
             */
            move_to: function(m_id, new_pos){

                //keep within range
                if(new_pos < 0)
                    new_pos = 0;
                else if(new_pos > (this.length - 1))
                    new_pos = this.length - 1;


                var model = this.get(m_id),
                    old_pos = model.get('ordinal');


                model.set({
                    ordinal: new_pos
                });
                if(new_pos == old_pos){
                    //trigger associated events
                    this.sort();
                    return this;
                }

                //update indexes of affected models
                this.each(function(m){

                    //ordinal of current model in loop
                    var m_ordinal = m.get('ordinal');

                    //skip if this is the model we just updated
                    if(m.get('id') == m_id)
                        return;

                    if(old_pos < new_pos){
                        //moving down, ordinal is increasing

                        if(m_ordinal <= new_pos && m_ordinal != 0){
                            //this is in the range we care about
                            m.set({
                                ordinal: m.get('ordinal') - 1
                            });

                        }

                    }else if(old_pos > new_pos){
                        //moving up, ordinal is decreasing                  


                        if(m_ordinal >= new_pos && (m_ordinal != (this.length - 1))){
                            //this is in the range we care about
                            m.set({
                                ordinal: m.get('ordinal') + 1
                            });

                        }

                    }

                });

                this.sort();
                return this;

            }

EDIT 3 Okay I think I've fixed all the problems, some simple scope stuff. Here's some code I've tested pretty thoroughly and I believe it works.

/**
                 * Move model to a specified location. 
                 * 
                 * @param   int [model id]
                 * @param   int [mew item position]
                 * @return  this
                 */
                move_to: function(m_id, new_pos){

                    //keep within range
                    if(new_pos < 0)
                        new_pos = 0;
                    else if(new_pos > (this.length - 1))
                        new_pos = this.length - 1;


                    var model = this.get(m_id),
                        old_pos = model.get('ordinal');

                    log('old_pos ' + old_pos);
                    log('new_pos ' + new_pos);


                    model.set({
                        ordinal: new_pos
                    });
                    if(old_pos == new_pos){
                        //trigger associated events
                        this.sort();
                        return this;
                    }

                    var _this = this;
                    //update indexes of affected models
                    this.each(function(m){

                        //ordinal of current model in loop
                        var m_ordinal = m.get('ordinal');

                        //skip if this is the model we just updated
                        if(m.get('id') == m_id)
                            return;

                        if(old_pos < new_pos){
                            //moving down, ordinal is increasing

                            if(m_ordinal <= new_pos && !(m_ordinal <= 0)){
                                //this is in the range we care about
                                m.set({
                                    ordinal: m.get('ordinal') - 1
                                });

                            }

                        }else if(old_pos > new_pos){
                            //moving up, ordinal is decreasing                  
                             log('p1');

                            if(m_ordinal >= new_pos && !(m_ordinal >= (_this.length - 1))){
                                //this is in the range we care about
                                m.set({
                                    ordinal: m.get('ordinal') + 1
                                });

                            }

                        }

                    });

                    this.sort();                    
                    return this;

                }

EDIT 4

Found another bug, patched it.

Backbone.Collection.prototype.move_to = function(m_id, new_pos) {

    //keep within range
    if(new_pos < 0)
        new_pos = 0;
    else if(new_pos > (this.length - 1))
        new_pos = this.length - 1;


    var model = this.get(m_id),
        old_pos = model.get('ordinal');

    log('old_pos ' + old_pos);
    log('new_pos ' + new_pos);

    model.set({
        ordinal: new_pos
    });
    if(old_pos == new_pos){
        //trigger associated events
        this.sort();
        return this;
    }

    var _this = this;
    //update indexes of affected models
    this.each(function(m){

        log(m.id);

        //ordinal of current model in loop
        var m_ordinal = m.get('ordinal');

        //skip if this is the model we just updated
        if(m.get('id') == m_id)
            return;

        if(old_pos < new_pos){
            //moving down, ordinal is increasing

            if(m_ordinal <= new_pos && m_ordinal >= old_pos && !(m_ordinal <= 0)){
                //this is in the range we care about
                m.set({
                    ordinal: m.get('ordinal') - 1
                }, {
                    silent: true
                });

            }

        }else if(old_pos > new_pos){
            //moving up, ordinal is decreasing                  
             log('p1');

            if(m_ordinal >= new_pos && m_ordinal <= old_pos && !(m_ordinal >= (_this.length - 1))){
                //this is in the range we care about
                m.set({
                    ordinal: m.get('ordinal') + 1
                }, {
                    silent: true
                });

            }

        }

    });

    this.sort();                    
    return this;
};
alex
  • 479,566
  • 201
  • 878
  • 984
Casey Flynn
  • 13,654
  • 23
  • 103
  • 194
  • So you want to say `collection.move_up(id)` or `collection.move_down(id)`? What sort of events do you expect to be generated? Just `"change:ordinal"` on the two models involved in the swaps or something on the collection as well? – mu is too short Jul 26 '12 at 04:52
  • I'd say a collection.move_up/down set of methods would be a start. I'm still a bit shaky on backbone. My view listens for the 'reset' event and re-renders itself accordingly already. The up/down methods would not wrap the items from the start to end of list/vice-versa. – Casey Flynn Jul 26 '12 at 05:56

1 Answers1

23

If you look at the backbone.js source code, you'll find out that for example the add method supports adding models to certain indexes with

collectionName.add(model, {at: index});

removing from position might need you to make a custom function to the collection, like:

// Your Collection

removeAt: function(options) {
  if (options.at) {
    this.remove(this.at(options.at));
  }
}

for +1 / -1 you can make a custom function and make use of the built-in underscore indexOf-function

// Your Collection

moveUp: function(model) { // I see move up as the -1
  var index = this.indexOf(model);
  if (index > 0) {
    this.remove(model, {silent: true}); // silence this to stop excess event triggers
    this.add(model, {at: index-1});
  }
}

moveDown: function(model) { // I see move up as the -1
  var index = this.indexOf(model);
  if (index < this.models.length) {
    this.remove(model, {silent: true}); // silence this to stop excess event triggers
    this.add(model, {at: index+1});
  }
}

This way you can also implement the moveUp and moveDown to the models themselves for more easily readable code!

// Your Model

moveUp: function() {
  this.collection.moveUp(this);
}

// And the same for moveDown

But now the index property is not saved in the models themselves. To read the index just use collection.indexOf(model), but if you want to store that information in the models at all times, you could bind to the add and remove events to update all indexes when changes to the collection are made:

// Your collection

initialize: function(options?) {
  ...
  this.on('add remove', this.updateModelOrdinals);
  ...
},

...

updateModelOrdinals: function() {
  this.each(function(model, index) {
    this.model.set('ordinal', index);
  });
}

et voilà! Now you should have the functionality you need without reinventing the wheel and still keeping the 0-indexing in place with Backbone's own functionality! Sorry for getting a bit carried off, ask if I went over your head. And read the backbone.js source, you can find seriously useful stuff there!

Hope this helps!

jakee
  • 18,486
  • 3
  • 37
  • 42
  • now those functions are examples, they're not tested and i can see that some of the need some additional if-clauses to make them waterproof, but you should get the idea – jakee Jul 26 '12 at 08:19
  • Your answer helped a lot, thanks! One problem I ran into with this solution, is that the add method of the collection will do a sort if a comparator is defined, in my case resetting the order before the updateModelOrdinals method gets a chance to run. I had to name my comparator function something else and sort the collection manually. Not a big deal, but it would be nice if Backbone had the possibility to also silence the "sort" in this case. – stian Nov 06 '12 at 08:49
  • Points for the Model's own version of this. I've done this kind of thing a lot for things like determining the next model in a collection, and it's really handy to just be able to grab a model and call find out things about its context in a collection without having to explicitly get the collection. – Tom Aug 08 '13 at 16:30
  • Thanks this is awesome. For those trying this code, you don't need the 'this.model' in updateModelOrdinals each loop because model is passed in already. Just use 'model' instead. Also, I didn't have to do removeAt because remove accepted the 'at' object like add does. – milesaron Apr 07 '15 at 21:26