如何使Backbone.js集合项独特?

时间:2011-06-20 20:17:58

标签: javascript backbone.js underscore.js

说我有这些Backbone.js模型:

var Truck = Backbone.Model.extend({});

var truck1 = new Truck();
var truck2 = new Truck();

truck1.set("brand", "Ford");
truck2.set("brand", "Toyota");
truck3.set("brand", "Honda");
truck4.set("brand", "Ford");

然后,假设我们有一个Backbone.js集合:

var TruckList = Backbone.Collection.extend({
  model: Truck,
  comparator: function(truck) {
     return truck.get("brand");
  };

});

我是一名汽车收藏家,所以有时间将每辆车添加到我的收藏中:

Trucks = new TruckList();
Trucks.add(truck1);
Trucks.add(truck2);
Trucks.add(truck3);
Trucks.add(truck4);

只关注品牌属性,truck4是truck1的副本。我的收藏中不能有重复项。我需要我的收藏品具有独特的价值观。

我的问题是,如何从Backbone.js集合中删除重复的项目?

我应该使用Underscore.js吗?如果是这样,有人可以提供一个工作/可运行的如何执行此操作的示例。

假设如下:

1.Collection未排序

  1. 必须对品牌属性值进行删除

  2. Ajax调用以填充Truck的每个实例。这意味着在添加到集合时,您无权访问Truck属性。

12 个答案:

答案 0 :(得分:19)

我会覆盖您的TruckList集合中的add方法,并使用下划线检测那里的重复项并拒绝重复项。像。的东西。

TruckList.prototype.add = function(truck) {
    // Using isDupe routine from @Bill Eisenhauer's answer
    var isDupe = this.any(function(_truck) { 
        return _truck.get('brand') === truck.get('brand');
    });

    // Up to you either return false or throw an exception or silently ignore
    // NOTE: DEFAULT functionality of adding duplicate to collection is to IGNORE and RETURN. Returning false here is unexpected. ALSO, this doesn't support the merge: true flag.
    // Return result of prototype.add to ensure default functionality of .add is maintained. 
    return isDupe ? false : Backbone.Collection.prototype.add.call(this, truck);
}

答案 1 :(得分:7)

实现此目的的最简单方法是确保您添加的模型具有唯一ID。默认情况下,Backbone集合不会添加具有重复ID的模型。

test('Collection should not add duplicate models', 1, function() {
    var model1 = {
        id: "1234"
    };
    var model2 = {
        id: "1234"
    };

    this.collection.add([model1, model2]);

    equal(1, this.collection.length, "collection length should be one when trying to add two duplicate models");
});

答案 2 :(得分:5)

试试这个。它使用任何下划线方法来检测潜在的重复,然后转出(如果是这样)。当然,你可能想要打扮一下,除了更加健壮之外:

TruckList.prototype.add = function(newTruck) {
  var isDupe = this.any(function(truck) { 
    return truck.get('brand') === newTruck.get('brand');
  }
  if (isDupe) return;
  Backbone.Collection.prototype.add.call(this, truck);
}

顺便说一句,我可能会在卡车上编写一个函数来进行欺骗检查,以便收集对这种情况不太了解。

答案 3 :(得分:5)

var TruckList = Backbone.Collection.extend({
model : Truck,

// Using @Peter Lyons' answer
add : function(truck) {
    // Using isDupe routine from @Bill Eisenhauer's answer
    var isDupe = this.any(function(_truck) {
        return _truck.get('brand') === truck.get('brand');
    });
    if (isDupe) {
        // Up to you either return false or throw an exception or silently
        // ignore
        return false;
    }
    Backbone.Collection.prototype.add.call(this, truck);
},

comparator : function(truck) {
    return truck.get("brand");
} });

VassilisB的答案很有效,但它会覆盖Backbone Collection的add()行为。因此,当您尝试执行此操作时可能会出现错误:

var truckList = new TruckList([{brand: 'Ford'}, {brand: 'Toyota'}]); 

所以,我添加了一些检查以避免这些错误:

var TruckList = Backbone.Collection.extend({
    model : Truck,

    // Using @Peter Lyons' answer
    add : function(trucks) {
        // For array
        trucks = _.isArray(trucks) ? trucks.slice() : [trucks]; //From backbone code itself
        for (i = 0, length = trucks.length; i < length; i++) {
            var truck = ((trucks[i] instanceof this.model) ? trucks[i]  : new this.model(trucks[i] )); // Create a model if it's a JS object

            // Using isDupe routine from @Bill Eisenhauer's answer
            var isDupe = this.any(function(_truck) {
                return _truck.get('brand') === truck.get('brand');
            });
            if (isDupe) {
                // Up to you either return false or throw an exception or silently
                // ignore
                return false;
            }
            Backbone.Collection.prototype.add.call(this, truck);
       }
    },

    comparator : function(truck) {
        return truck.get("brand");
    }});

答案 4 :(得分:3)

我正在使用同样的问题做FileUpload事情,这就是我的做法(coffeescript):

File = Backbone.Model.extend
    validate: (args) ->
        result
        if !@collection.isUniqueFile(args)
            result = 'File already in list'
        result

Files = Backbone.Collection.extend
    model: File

    isUniqueFile: (file) ->
        found
        for f in @models
            if f.get('name') is file.name
                found = f
                break
        if found
            false
        else
            true

......就是这样。在File中自动引用集合对象,并在集合上调用操作时自动调用Validation,在本例中为Add。

答案 5 :(得分:2)

Underscore.js是backbone.js的预先要求,为此提供了一个函数:http://documentcloud.github.com/underscore/#uniq

示例:

_.uniq([1,1,1,1,1,2,3,4,5]); // returns [1,2,3,4,5]

答案 6 :(得分:1)

不确定这是对Backbone还是下划线的更新,但where()函数在Backbone 0.9.2中可以为您进行匹配:

TruckList.prototype.add = function(truck) {
    var matches = this.where({name: truck.get('brand')});

    if (matches.length > 0) {
        //Up to you either return false or throw an exception or silently ignore
        return false;
    }

    Backbone.Collection.prototype.add.call(this, truck);
}

答案 7 :(得分:1)

我更喜欢像这样覆盖add方法。

var TruckList = Backbone.Collection.extend({
model : Truck,

// Using @Peter Lyons' answer
add : function(truck) {
    // Using isDupe routine from @Bill Eisenhauer's answer
    var isDupe = this.any(function(_truck) {
        return _truck.get('brand') === truck.get('brand');
    });
    if (isDupe) {
        // Up to you either return false or throw an exception or silently
        // ignore
        return false;
    }
    Backbone.Collection.prototype.add.call(this, truck);
},

comparator : function(truck) {
    return truck.get("brand");
} });

答案 8 :(得分:0)

试试这个......

var TruckList = Backbone.Collection.extend({
  model: Truck,
  comparator: function(truck) {
     return truck.get("brand");
  },
  wherePartialUnique: function(attrs) {
    // this method is really only tolerant of string values.  you can't do partial
    // matches on arrays, objects, etc.  use collection.where for that
      if (_.isEmpty(attrs)) return [];
      var seen = [];
      return this.filter(function(model) {
        for (var key in attrs) {
          // sometimes keys are empty. that's bad, so let's not include it in a unique result set
          // you might want empty keys though, so comment the next line out if you do.
          if ( _.isEmpty(model.get(key).trim()) ) return false;
          // on to the filtering...
          if (model.get(key).toLowerCase().indexOf(attrs[key].toLowerCase()) >= 0) {
            if (seen.indexOf( model.get(key) ) >= 0 ) return false;
            seen.push(model.get(key));
            return true;
          } else {
            return false;
          }
        }
        return true;
      });
    }
});

要记住的一些事情:

  1. 这是基于backbone.collection.where方法,与该方法不同,它将尝试对集合中的模型属性进行部分匹配。如果你不想这样,你需要修改它才能完全匹配。只是模仿你在原始方法中看到的内容。

  2. 它应该能够接受多个属性匹配,所以如果你有foo和bar的模型属性,你应该可以做collection.wherePartialUnique({foo:“你”,bar:“dude”} )。我没有测试过。 :)我只做了一个键/值对。

  3. 我还从考虑中删除了空模型属性。我不关心他们,但你可能会这样。

  4. 此方法不需要比较器所依赖的唯一模型属性的集合。它更像是一个sql不同的查询,但我不是一个sql人,所以如果这是一个坏的例子,请不要开枪:)

  5. 您的收藏品是通过比较器功能进行分类的,因此您对其中某个未被排序的假设是不正确的。

  6. 我相信这也符合你的所有目标:

      
        
    1. 收集未排序
    2.   
    3. 必须对品牌属性值进行删除
    4.   
    5. Ajax调用以填充Truck的每个实例。这意味着在添加到集合时,您无权访问Truck属性。
    6.   

答案 9 :(得分:0)

似乎一个优雅的解决方案是使用_.findWhere只要你有一些独特的属性(在你的情况下品牌)。 _.findWhere将返回一个匹配,这是一个JavaScript对象,因此是真实的或未定义的,这是假的。这样您就可以使用单个if语句。

var TruckList = Backbone.Collection.extend({
  model: Truck,

  add: function (truck) {
    if (!this.findWhere({ brand: truck.get('brand') })) {
      Backbone.Collection.prototype.add.call(this, truck);
    }
  }
});

答案 10 :(得分:0)

我真的不满意这个解决方案的公认答案。它包含许多错误。我已经编辑了原始解决方案以突出我的顾虑,但我提出以下解决方案,假设您可以弄脏副本的id / cid属性:

TruckList.prototype.add = function(truckToAdd, options) {
    //  Find duplicate truck by brand:
    var duplicateTruck = this.find(function(truck){
        return truck.get('brand') === truckToAdd.get('brand');
    });

    //  Make truck an actual duplicate by ID: 
    //  TODO: This modifies truckToAdd's ID. This could be expanded to preserve the ID while also taking into consideration any merge: true options.
    if(duplicateTruck !== undefined){
        if(duplicateTruck.has('id')){
            truckToAdd.set('id', duplicateTruck.get('id'), { silent: true });
        }
        else {
            truckToAdd.cid = duplicateTruck.cid;
        }
    }

    //  Allow Backbone to handle the duplicate instead of trying to do it manually.
    return Backbone.Collection.prototype.add.call(this, truckToAdd, options);
}

这个唯一的缺陷就是不保留truckToAdd的ID / cid。但是,这确实保留了将项添加到集合的所有预期功能,包括传递merge:true。

答案 11 :(得分:0)

由于以下几个原因,我对提供的答案不满意:

  • 修改add的返回值是意料之外的。
  • 不支持{ merge: true }是意外的。

我提供了一个我认为更强大的解决方案。这个解决方案克隆了给定模型,如果它们在集合中有重复,则更新克隆&#39;用于匹配重复项ID的ID,然后将重复项和非重复项列表传递到原始的add方法,以便它可以发挥其魔力。据我所知,没有意外的副作用。

add: function (models, options) {
    var preparedModels;

    if (models instanceof Backbone.Collection) {
        preparedModels = models.map(this._prepareModelToAdd.bind(this));
    }
    else if (_.isArray(models)) {
        preparedModels = _.map(models, this._prepareModelToAdd.bind(this));
    } else if (!_.isNull(models) && !_.isUndefined(models)) {
        preparedModels = this._prepareModelToAdd(models);
    } else {
        preparedModels = models;
    }

    //  Call the original add method using preparedModels which have updated their IDs to match any existing models.
    return Backbone.Collection.prototype.add.call(this, preparedModels, options);
},

//  Return a copy of the given model's attributes with the id or cid updated to match any pre-existing model.
//  If no existing model is found then this function is a no-op.
//  NOTE: _prepareModel is reserved by Backbone and should be avoided.
_prepareModelToAdd: function (model) {
    //  If an existing model was not found then just use the given reference.
    var preparedModel = model;
    var existingModel = this._getExistingModel(model);

    //  If an existing model was found then clone the given reference and update its id.
    if (!_.isUndefined(existingModel)) {
        preparedModel = this._clone(model);
        this._copyId(preparedModel, existingModel);
    }

    return preparedModel;
},

//  Try to find an existing model in the collection based on the given model's brand.
_getExistingModel: function (model) {
    var brand = model instanceof Backbone.Model ? model.get('brand') : model.brand;
    var existingModel = this._getByBrand(brand);
    return existingModel;
},

_getByBrand: function (brand) {
    return this.find(function (model) {
        return model.get('brand') === brand;
    });
},

_clone: function (model) {
    //  Avoid calling model.clone because re-initializing the model could cause side-effects.
    //  Avoid calling model.toJSON because the method may have been overidden.
    return model instanceof Backbone.Model ? _.clone(model.attributes) : _.clone(model);
},

//  Copy the model's id or cid onto attributes to ensure Backbone.Collection.prototype.add treats attributes as a duplicate.
_copyId: function (attributes, model) {
    if (model.has('id')) {
        attributes.id = model.get('id');
    } else {
        attributes.cid = model.cid;
    }
}