Skip to content

Commit

Permalink
Implement Model.mixin and Collection.mixin (#4024)
Browse files Browse the repository at this point in the history
Combine implementation of collection and model underscore methods
  • Loading branch information
megawac authored and akre54 committed Apr 18, 2017
1 parent 539c9af commit e524804
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 71 deletions.
159 changes: 88 additions & 71 deletions backbone.js
Expand Up @@ -68,54 +68,6 @@
// form param named `model`.
Backbone.emulateJSON = false;

// Proxy Backbone class methods to Underscore functions, wrapping the model's
// `attributes` object or collection's `models` array behind the scenes.
//
// collection.filter(function(model) { return model.get('age') > 10 });
// collection.each(this.addView);
//
// `Function#apply` can be slow so we use the method's arg count, if we know it.
var addMethod = function(length, method, attribute) {
switch (length) {
case 1: return function() {
return _[method](this[attribute]);
};
case 2: return function(value) {
return _[method](this[attribute], value);
};
case 3: return function(iteratee, context) {
return _[method](this[attribute], cb(iteratee, this), context);
};
case 4: return function(iteratee, defaultVal, context) {
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
default: return function() {
var args = slice.call(arguments);
args.unshift(this[attribute]);
return _[method].apply(_, args);
};
}
};
var addUnderscoreMethods = function(Class, methods, attribute) {
_.each(methods, function(length, method) {
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
});
};

// Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
var cb = function(iteratee, instance) {
if (_.isFunction(iteratee)) return iteratee;
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
return iteratee;
};
var modelMatcher = function(attrs) {
var matcher = _.matches(attrs);
return function(model) {
return matcher(model.attributes);
};
};

// Backbone.Events
// ---------------

Expand Down Expand Up @@ -790,14 +742,6 @@

});

// Underscore methods that we want to implement on the Model, mapped to the
// number of arguments they take.
var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
omit: 0, chain: 1, isEmpty: 1};

// Mix in each Underscore method as a proxy to `Model#attributes`.
addUnderscoreMethods(Model, modelMethods, 'attributes');

// Backbone.Collection
// -------------------

Expand Down Expand Up @@ -1335,21 +1279,6 @@
return {value: void 0, done: true};
};

// Underscore methods that we want to implement on the Collection.
// 90% of the core usefulness of Backbone Collections is actually implemented
// right here:
var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0,
foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3,
select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3};

// Mix in each Underscore method as a proxy to `Collection#models`.
addUnderscoreMethods(Collection, collectionMethods, 'models');

// Backbone.View
// -------------

Expand Down Expand Up @@ -1518,6 +1447,94 @@

});

// Proxy Backbone class methods to Underscore functions, wrapping the model's
// `attributes` object or collection's `models` array behind the scenes.
//
// collection.filter(function(model) { return model.get('age') > 10 });
// collection.each(this.addView);
//
// `Function#apply` can be slow so we use the method's arg count, if we know it.
var addMethod = function(base, length, method, attribute) {
switch (length) {
case 1: return function() {
return base[method](this[attribute]);
};
case 2: return function(value) {
return base[method](this[attribute], value);
};
case 3: return function(iteratee, context) {
return base[method](this[attribute], cb(iteratee, this), context);
};
case 4: return function(iteratee, defaultVal, context) {
return base[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
default: return function() {
var args = slice.call(arguments);
args.unshift(this[attribute]);
return base[method].apply(base, args);
};
}
};

var addUnderscoreMethods = function(Class, base, methods, attribute) {
_.each(methods, function(length, method) {
if (base[method]) Class.prototype[method] = addMethod(base, length, method, attribute);
});
};

// Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`.
var cb = function(iteratee, instance) {
if (_.isFunction(iteratee)) return iteratee;
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
return iteratee;
};
var modelMatcher = function(attrs) {
var matcher = _.matches(attrs);
return function(model) {
return matcher(model.attributes);
};
};

// Underscore methods that we want to implement on the Collection.
// 90% of the core usefulness of Backbone Collections is actually implemented
// right here:
var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0,
foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3,
select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3};


// Underscore methods that we want to implement on the Model, mapped to the
// number of arguments they take.
var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
omit: 0, chain: 1, isEmpty: 1};

// Mix in each Underscore method as a proxy to `Collection#models`.

_.each([
[Collection, collectionMethods, 'models'],
[Model, modelMethods, 'attributes']
], function(config) {
var Base = config[0],
methods = config[1],
attribute = config[2];

Base.mixin = function(obj) {
var mappings = _.reduce(_.functions(obj), function(memo, name) {
memo[name] = 0;
return memo;
}, {});
addUnderscoreMethods(Base, obj, mappings, attribute);
};

addUnderscoreMethods(Base, _, methods, attribute);
});

// Backbone.sync
// -------------

Expand Down
21 changes: 21 additions & 0 deletions test/collection.js
Expand Up @@ -711,6 +711,27 @@
assert.equal(coll.findWhere({a: 4}), void 0);
});

QUnit.test('mixin', function(assert) {
Backbone.Collection.mixin({
sum: function(models, iteratee) {
return _.reduce(models, function(s, m) {
return s + iteratee(m);
}, 0);
}
});

var coll = new Backbone.Collection([
{a: 1},
{a: 1, b: 2},
{a: 2, b: 2},
{a: 3}
]);

assert.equal(coll.sum(function(m) {
return m.get('a');
}), 7);
});

QUnit.test('Underscore methods', function(assert) {
assert.expect(21);
assert.equal(col.map(function(model){ return model.get('label'); }).join(' '), 'a b c d');
Expand Down
22 changes: 22 additions & 0 deletions test/model.js
Expand Up @@ -1395,6 +1395,28 @@
assert.ok(!model.set('valid', false, {validate: true}));
});

QUnit.test('mixin', function(assert) {
Backbone.Model.mixin({
isEqual: function(model1, model2) {
return _.isEqual(model1, model2.attributes);
}
});

var model1 = new Backbone.Model({
a: {b: 2}, c: 3
});
var model2 = new Backbone.Model({
a: {b: 2}, c: 3
});
var model3 = new Backbone.Model({
a: {b: 4}, c: 3
});

assert.equal(model1.isEqual(model2), true);
assert.equal(model1.isEqual(model3), false);
});


QUnit.test('#1179 - isValid returns true in the absence of validate.', function(assert) {
assert.expect(1);
var model = new Backbone.Model();
Expand Down

0 comments on commit e524804

Please sign in to comment.