Skip to content

Commit

Permalink
Implements the JavaScript Iterable protocol.
Browse files Browse the repository at this point in the history
This introduces new methods related to Iterators on Backbone.Collection to mirror those found on Array: `values`, `keys`, `entries`, and `@@iterator`. Each of these methods will return a JavaScript Iterator, which has a `next` method, yielding the models or ids of models contained in the Collection.

The CollectionIterator is careful to use the `at()` and `modelId()` methods on the host collection rather than direct access to the `models` property, which should ensure it is resilient to creative subclassing of Backbone.Collection and future feature addition.

The [`@@iterator`](http://www.ecma-international.org/ecma-262/6.0/#sec-well-known-symbols) method is defined using `Symbol.iterator` if it exists in the JavaScript runtime (modern browsers/node.js) and falls back to the string `"@@iterator"` which was popularized by older versions of Firefox and has become the standard fallback behavior for other third-party libraries. This ensures that Backbone can still be used across all browsers, even with use of these new methods.

Supporting Iterable allows better integration between Backbone and the most recent additions to the JavaScript language, including `for of` loops and data-collection constructor functions, as well as better integration with other third-party libraries that accept Iterables instead of only Arrays.

Fixes #3954
  • Loading branch information
leebyron committed Jul 21, 2016
1 parent 760ad49 commit be67b73
Show file tree
Hide file tree
Showing 2 changed files with 132 additions and 0 deletions.
78 changes: 78 additions & 0 deletions backbone.js
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,21 @@
return attrs[this.model.prototype.idAttribute || 'id'];
},

// Get an iterator of all models in this collection.
values: function() {
return new CollectionIterator(this, ITERATOR_VALUES);
},

// Get an iterator of all model IDs in this collection.
keys: function() {
return new CollectionIterator(this, ITERATOR_KEYS);
},

// Get an iterator of all [ID, model] tuples in this collection.
entries: function() {
return new CollectionIterator(this, ITERATOR_KEYSVALUES);
},

// Private method to reset all internal state. Called when the collection
// is first initialized or reset.
_reset: function() {
Expand Down Expand Up @@ -1202,6 +1217,69 @@

});

// Defining an @@iterator method implements JavaScript's Iterable protocol.
// In modern ES2015 browsers, this value is found at Symbol.iterator.
/* global Symbol */
var $$iterator = typeof Symbol === 'function' && Symbol.iterator;
if ($$iterator) {
Collection.prototype[$$iterator] = Collection.prototype.values;
}

// CollectionIterator
// ------------------

// A CollectionIterator implements JavaScript's Iterator protocol, allowing the
// use of `for of` loops in modern browsers and interoperation between
// Backbone.Collection and other JavaScript functions and third-party libraries
// which can operate on Iterables.
var CollectionIterator = function(collection, kind) {
this._collection = collection;
this._kind = kind;
this._index = 0;
};

var ITERATOR_VALUES = 1;
var ITERATOR_KEYS = 2;
var ITERATOR_KEYSVALUES = 3;

// All Iterators should themselves be Iterable.
if ($$iterator) {
CollectionIterator.prototype[$$iterator] = function() {
return this;
};
}

CollectionIterator.prototype.next = function() {
if (this._collection) {

// Only continue iterating if the iterated collection is long enough.
if (this._index < this._collection.length) {
var model = this._collection.at(this._index);
this._index++;

// Construct a value depending on what kind of values should be iterated.
var value;
if (this._kind === ITERATOR_VALUES) {
value = model;
} else {
var id = this._collection.modelId(model.attributes);
if (this._kind === ITERATOR_KEYS) {
value = id;
} else {
value = [id, model];
}
}
return {value: value, done: false};
}

// Once exhausted, remove the reference to the collection so future
// calls to the next method always return done.
this._collection = void 0;
}

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:
Expand Down
54 changes: 54 additions & 0 deletions test/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -1758,6 +1758,60 @@
assert.equal(c2.modelId(m.attributes), void 0);
});

QUnit.test('Collection implements Iterable, values is default iterator function', function(assert) {
/* global Symbol */
var $$iterator = typeof Symbol === 'function' && Symbol.iterator;
// This test only applies to environments which define Symbol.iterator.
if (!$$iterator) {
assert.expect(0);
return;
}
assert.expect(2);
var collection = new Backbone.Collection([]);
assert.strictEqual(collection[$$iterator], collection.values);
var iterator = collection[$$iterator]();
assert.deepEqual(iterator.next(), {value: void 0, done: true});
});

QUnit.test('Collection.values iterates models in sorted order', function(assert) {
assert.expect(4);
var one = new Backbone.Model({id: 1});
var two = new Backbone.Model({id: 2});
var three = new Backbone.Model({id: 3});
var collection = new Backbone.Collection([one, two, three]);
var iterator = collection.values();
assert.strictEqual(iterator.next().value, one);
assert.strictEqual(iterator.next().value, two);
assert.strictEqual(iterator.next().value, three);
assert.strictEqual(iterator.next().value, void 0);
});

QUnit.test('Collection.keys iterates ids in sorted order', function(assert) {
assert.expect(4);
var one = new Backbone.Model({id: 1});
var two = new Backbone.Model({id: 2});
var three = new Backbone.Model({id: 3});
var collection = new Backbone.Collection([one, two, three]);
var iterator = collection.keys();
assert.strictEqual(iterator.next().value, 1);
assert.strictEqual(iterator.next().value, 2);
assert.strictEqual(iterator.next().value, 3);
assert.strictEqual(iterator.next().value, void 0);
});

QUnit.test('Collection.entries iterates ids and models in sorted order', function(assert) {
assert.expect(4);
var one = new Backbone.Model({id: 1});
var two = new Backbone.Model({id: 2});
var three = new Backbone.Model({id: 3});
var collection = new Backbone.Collection([one, two, three]);
var iterator = collection.entries();
assert.deepEqual(iterator.next().value, [1, one]);
assert.deepEqual(iterator.next().value, [2, two]);
assert.deepEqual(iterator.next().value, [3, three]);
assert.strictEqual(iterator.next().value, void 0);
});

QUnit.test('#3039 #3951: adding at index fires with correct at', function(assert) {
assert.expect(4);
var collection = new Backbone.Collection([{val: 0}, {val: 4}]);
Expand Down

0 comments on commit be67b73

Please sign in to comment.