Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 110 additions & 32 deletions backbone-polymer-mixin.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@

var BackbonePolymerAttach = function(element, pathPrefix) {
console.log('BackbonePolymerAttach', this, element, pathPrefix);

var indexOf = this.indexOf.bind(this);

Expand All @@ -14,44 +13,123 @@ var BackbonePolymerAttach = function(element, pathPrefix) {
});
};

var splicesObject = this.models;
this.each(modelSetup.bind(this));

var addNotify = function(model) {
var ix = indexOf(model);
// Default options for `Collection#set`.
var setOptions = {add: true, remove: true, merge: true};
// Splices `insert` into `array` at index `at`.
var splice = function(array, insert, at) {
at = Math.min(Math.max(at, 0), array.length);
var args = [pathPrefix + '.models', at, 0].concat(insert);
element.splice.apply(this, args);
};
// Update a collection by `set`-ing a new list of models, adding new ones,
// removing models that are no longer present, and merging models that
// already exist in the collection, as necessary. Similar to **Model#set**,
// the core operation for updating the data contained by the collection.
this.set = function(models, options) {
if (models == null) return;

// https://www.polymer-project.org/1.0/docs/devguide/properties.html#array-observation
var change = {keySplices:[], indexSplices:[]};
change.keySplices.push({
index: ix,
removed: [],
removedItems: [],
added: [ix]
});
change.indexSplices.push({
index: ix,
addedCount: 1,
removed: [],
object: splicesObject,
type: 'splice',
addedKeys: [ix]
});
options = _.defaults({}, options, setOptions);
if (options.parse && !this._isModel(models)) models = this.parse(models, options);

element.notifyPath(pathPrefix + '.models.splices', change);
var singular = !_.isArray(models);
models = singular ? [models] : models.slice();

// remove this timeout and the rendered element goes blank
window.setTimeout(function() {
// TODO would it be possible to notify .* here?
for (var attribute in model.attributes) {
// we could reuse code with modelSetup here
var value = model.get(attribute);
element.notifyPath(pathPrefix + '.models.' + ix + '.attributes.' + attribute, value);
var at = options.at;
if (at != null) at = +at;
if (at < 0) at += this.length + 1;

var set = [];
var toAdd = [];
var toRemove = [];
var modelMap = {};

var add = options.add;
var merge = options.merge;
var remove = options.remove;

var sort = false;
var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;

// Turn bare objects into model references, and prevent invalid models
// from being added.
var model;
for (var i = 0; i < models.length; i++) {
model = models[i];

// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
var existing = this.get(model);
if (existing) {
if (merge && model !== existing) {
var attrs = this._isModel(model) ? model.attributes : model;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
if (sortable && !sort) sort = existing.hasChanged(sortAttr);
}
if (!modelMap[existing.cid]) {
modelMap[existing.cid] = true;
set.push(existing);
}
models[i] = existing;

// If this is a new, valid model, push it to the `toAdd` list.
} else if (add) {
model = models[i] = this._prepareModel(model, options);
if (model) {
toAdd.push(model);
this._addReference(model, options);
modelMap[model.cid] = true;
set.push(model);
}
}
}

// Remove stale models.
if (remove) {
for (i = 0; i < this.length; i++) {
model = this.models[i];
if (!modelMap[model.cid]) toRemove.push(model);
}
if (toRemove.length) this._removeModels(toRemove, options);
}

// See if sorting is needed, update `length` and splice in new models.
var orderChanged = false;
var replace = !sortable && add && remove;
if (set.length && replace) {
orderChanged = this.length != set.length || _.some(this.models, function(model, index) {
return model !== set[index];
});
this.models.length = 0;
splice(this.models, set, 0);
this.length = this.models.length;
} else if (toAdd.length) {
if (sortable) sort = true;
splice(this.models, toAdd, at == null ? this.length : at);
this.length = this.models.length;
}

// Silently sort the collection if appropriate.
if (sort) this.sort({silent: true});

// Unless silenced, it's time to fire all appropriate add/sort events.
if (!options.silent) {
for (i = 0; i < toAdd.length; i++) {
if (at != null) options.index = at + i;
model = toAdd[i];
model.trigger('add', model, this, options);
}
}, 1);
if (sort || orderChanged) this.trigger('sort', this, options);
if (toAdd.length || toRemove.length) this.trigger('update', this, options);
}

// Return the added (or merged) model (or models).
return singular ? models[0] : models;
};

this.each(modelSetup.bind(this));
this.on('add', addNotify.bind(this));
this.on('add', modelSetup.bind(this));
};

if (typeof module !== 'undefined') {
Expand Down
141 changes: 141 additions & 0 deletions test/backbone-emulated-events-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@

var expect = require('chai').expect;

// still undecided on how the mixin gets dependencies
_ = require('yobo')._;

var Backbone = require('yobo').Backbone;

var BackbonePolymerAttach = require('../backbone-polymer-mixin');

var PolymerElementMock = function(testArray) {

var spliced = this.spliced = [];

this.splice = function(path, index, removeCount, items) {
spliced.push({path: path, index: index, removeCount: removeCount, items: items});
if (typeof testArray !== 'undefined') {
testArray.splice.apply(testArray, [index, removeCount].concat(items));
}
};

this.notifyPath = function() {
// our code still does this for attribute changes
};

};

describe("Array modification through Polymer splices, emulate backbone events", function() {

describe("Backbone events before backbone-polymer", function() {

it("add model", function() {
var c = new Backbone.Collection();
var m = new Backbone.Model({id: 'a1', type: 'testmodel'})
c.on('add', function(model, collection, options) {
console.log('add', m === model ? '(model)' : model, c === collection ? '(collection)' : collection, JSON.stringify(options));
});
c.add(m);
});

it("add model at index", function() {
var c = new Backbone.Collection();
c.add({id: 'a2', type: 'testmodel2'});
c.add({id: 'a3', type: 'testmodel3'});
var m = new Backbone.Model({id: 'a1', type: 'testmodel'});
c.on('add', function(model, collection, options) {
console.log('add', m === model ? '(model)' : model, c === collection ? '(collection)' : collection, JSON.stringify(options));
});
m.on('add', function(model, collection, options) {
console.log('model add', m === model ? '(model)' : model, c === collection ? '(collection)' : collection, JSON.stringify(options));
});
var added = c.add(m, {at: 1});
console.log('add returned', m === added ? '(model)' : model);
// common Backbone operations
expect(c.get('a1')).to.exist.and.have.property('cid');
expect(c.at(0)).to.exist.and.have.property('cid');
});

});

describe("#add, reduced Backbone functionality", function() {

xit("Accepts only a single item", function() {
expect(function() {
var c = new Backbone.Collection();
BackbonePolymerAttach.call(c, new PolymerElementMock(), 'edit.units');
c.add([]);
}).to.throw(/single/);
});

xit("Requires item to be a real model", function() {
expect(function() {
var c = new Backbone.Collection();
BackbonePolymerAttach.call(c, new PolymerElementMock(), 'edit.units');
c.add({id: 'add1', type: 'test'});
}).to.throw(/requires model instance/);
});

xit("Bails out if the model already exists", function() {
expect(function() {
var c = new Backbone.Collection();
BackbonePolymerAttach.call(c, new PolymerElementMock(), 'edit.units');
var m = new Backbone.Model({id: 'add1', type: 'test'});
c.add(m);
c.add(m);
}).to.throw(/backbone-polymer model already exists as cid \w+/);
});

it("Is transparent to backbone add event listener", function() {
var c = new Backbone.Collection();
var e = new PolymerElementMock(c.models);

var adds = [];
c.on('add', function(m, c, o) {
adds.push({model:m, collection:c, options:o});
});

BackbonePolymerAttach.call(c, e, 'edit.units');
var m = c.add(new Backbone.Model({id: 'add1', type: 'test'}));

expect(m.get('type')).to.equal('test');
expect(adds).to.have.length(1);
expect(e.spliced).to.have.length(1);

//Expects on the options obj.
expect(adds[0].options).to.be.an('object');
expect(adds[0].options).to.have.property('add').that.equals(true);
expect(adds[0].options).to.have.property('merge').that.equals(false);
expect(adds[0].options).to.have.property('remove').that.equals(false);
expect(adds[0].options).to.not.have.property('at');
expect(adds[0].collection).to.have.property('length').and.equal(1);

var m1 = new Backbone.Model({id: 'add2', type: 'test'});
modeladd = [];
m1.on('add', function(m, c, o) {
modeladd.push({model:m, collection:c, options:o});
});
c.add(m1, {at: 1});

//Adding at an index, expects Options to have 'at'
expect(adds[1].options).to.have.property('add').that.equals(true);
expect(adds[1].options).to.have.property('at').that.equals(1);
expect(adds[1].collection).to.have.length(2);
expect(e.spliced).to.have.length(2);
expect(modeladd).to.have.length(1);
expect(modeladd[0]).to.deep.equal(adds[1]);

//Adding again at index 1, i.e. insert
var m2 = new Backbone.Model({id: 'add3', type: 'test'});
c.add(m2, {at: 1});

expect(e.spliced[2]).to.have.property('index').and.equal(1);
expect(adds[0].collection).to.have.property('length').and.equal(3);

// common Backbone operations
expect(c.get('add1')).to.exist.and.have.property('cid');
expect(c.models).to.have.length(3);
expect(c.at(1)).to.exist.and.have.property('cid');
});
});
});
2 changes: 1 addition & 1 deletion test/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@

});

it("Uses 'this' for the collection, like a regular Backbone member function", function(done) {
it("Can be used with .call instead of as mixin", function(done) {

var element1 = document.querySelector('#collection1');

Expand Down