Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added model.when #3135

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
92 changes: 92 additions & 0 deletions backbone.js
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,98 @@
if (!error) return true;
this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
return false;
},

// Listens for changes on the model such that:
//
// * Multiple sequential model updates cause the callback
// to execute only once
// * The callback is only executed if all dependency property
// values are defined
//
// `model.when(dependencies, callback [, thisArg])`
//
// * `dependencies` specifies the names of model properties that are
// dependencies of the callback function. `dependencies` can be
// * a string (in the case of a single dependency) or
// * an array of strings (in the case of many dependencies).
// * `callback(values...)` the callback function that is invoked after dependency
// properties change. The values of dependency properties are passed
// as arguments to the callback, in the same order specified by `dependencies`.
// * `thisArg` value to use as `this` when executing `callback`.
// * returns a `whens` object containing
// * a chainable `when` function
// * `callbacks` an object containing the callbacks for all
// `when` calls in the chain, an array of objects with:
// * `properties` an array of property names
// * `fn` the callback function added to the properties
when: function (dependencies, fn, thisArg) {

// The list of callbacks added, exposed as `whens.callbacks`
var callbacks = [],

// Grab `this` for later use.
model = this;

// Create a function inside the closure with `callbacks`.
function _when(dependencies, fn, thisArg){

// Support passing a single string as `dependencies`
if(!(dependencies instanceof Array)) {
dependencies = [dependencies];
}

// `callFn()` will invoke `fn` with values of dependency properties
// on the next tick of the JavaScript event loop.
var callFn = _.debounce(function(){

// Extract the values for each dependency property.
var args = dependencies.map(model.get, model),
allAreDefined = !args.some(function (d) {
return typeof d === 'undefined' || d === null;
});

// Only call the function if all values are defined.
if(allAreDefined) {

// Call `fn` with the dependency property values.
fn.apply(thisArg, args);
}
}, 0);

// Invoke `fn` once for initialization.
callFn();

// Invoke `fn` when dependency properties change.
dependencies.forEach(function(property){

// Listen for changes on the property.
model.on('change:' + property, callFn);

// Store the added callbacks for canceling later.
callbacks.push({
property: property,
fn: callFn
});
});

return {
when: _when,
callbacks: callbacks
};
}

return _when(dependencies, fn, thisArg);
},
// Cancels previously added `when` callback functions.
//
// `model.cancel(whens)`
//
// * `whens` the object returned from `when` or a chain of `when` calls
cancel: function(whens){
whens.callbacks.forEach(function (callback) {
this.off('change:' + callback.property, callback.fn);
}, this);
}

});
Expand Down
248 changes: 248 additions & 0 deletions test/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1127,4 +1127,252 @@
model.set({a: true});
});

asyncTest("when listens for changes to a single property", 1, function() {
var model = new Backbone.Model();
model.when("x", function (x) {
strictEqual(x, 30);
start();
});
model.set("x", 30);
});

asyncTest("when calls fn once to initialize", function() {
var model = new Backbone.Model();
model.set("x", 55);
model.when("x", function (x) {
strictEqual(x, 55);
start();
});
});

asyncTest("when calls fn with multiple dependency properties", function() {
var model = new Backbone.Model();
model.when(["x", "y", "z"], function (x, y, z) {
strictEqual(x, 5);
strictEqual(y, 6);
strictEqual(z, 7);
start();
});
model.set("x", 5);
model.set("y", 6);
model.set("z", 7);
});

asyncTest("when calls fn with dependency properties in the specified order", function() {
var model = new Backbone.Model();
model.when(["y", "z", "x"], function (y, z, x) {
strictEqual(x, 5);
strictEqual(y, 6);
strictEqual(z, 7);
start();
});
model.set("x", 5);
model.set("y", 6);
model.set("z", 7);
});

asyncTest("when calls fn only when all properties are defined", function() {
var model = new Backbone.Model();
model.when(["y", "x", "z"], function (y, x, z) {
strictEqual(x, 5);
strictEqual(y, 6);
strictEqual(z, 9);
start();
});
model.set({ x: 5, y: 6 });
setTimeout(function () {
model.set("z", 9);
}, 50);
});

asyncTest("when calls fn only once for multiple updates", function() {
var model = new Backbone.Model();
model.when("x", function (x) {
strictEqual(x, 30);
start();
});
model.set("x", 10);
model.set("x", 20);
model.set("x", 30);
});

asyncTest("when calls fn only once for multiple updates with many dependencies", function() {
var model = new Backbone.Model();
model.when(["y", "x", "z"], function (y, x, z) {
strictEqual(x, 5);
strictEqual(y, 6);
strictEqual(z, 9);
start();
});
model.set({ x: 5, y: 6 });
model.set("z", 5);
model.set("z", 6);
model.set("z", 7);
model.set("z", 8);
model.set("z", 9);
});

asyncTest("when can compute fullName from firstName and lastName", function() {
var model = new Backbone.Model();
model.when(["firstName", "lastName"], function (firstName, lastName) {
model.set("fullName", firstName + " " + lastName);
});
model.when("fullName", function (fullName) {
strictEqual(fullName, "John Doe");
start();
});
model.set("firstName", "John");
model.set("lastName", "Doe");
});

asyncTest("when should propagate changes through a data dependency graph", function() {
var model = new Backbone.Model();
model.when(["w"], function (w) {
strictEqual(w, 5);
model.set("x", w * 2);
});
model.when(["x"], function (x) {
strictEqual(x, 10);
model.set("y", x + 1);
});
model.when(["y"], function (y) {
strictEqual(y, 11);
model.set("z", y * 2);
});
model.when(["z"], function (z) {
strictEqual(z, 22);
start();
});
model.set("w", 5);
});

asyncTest("when should use thisArg", function() {
var model = new Backbone.Model(),
theThing = { foo: "bar" };
model.when("x", function (x) {
strictEqual(x, 5);
strictEqual(this, theThing);
strictEqual(this.foo, "bar");
start();
}, theThing);
model.set("x", 5);
});

asyncTest("when should propagate changes breadth first", function () {
var model = new Backbone.Model();

// Here is a data dependency graph that can test this
// (data flowing left to right):
//```
// b d
// a f
// c e
//```
//
// When "a" changes, "f" should update once only, after the changes propagated
// through the following two paths simultaneously:
//
// * a -> b -> d -> f
// * a -> c -> e -> f

// a -> (b, c)
model.when("a", function (a) {
model.set({
b: a + 1,
c: a + 2
});
});

// b -> d
model.when("b", function (b) {
model.set("d", b + 1);
});

// c -> e
model.when("c", function (c) {
model.set("e", c + 1);
});

// (d, e) -> f
model.when(["d", "e"], function (d, e) {
model.set("f", d + e);
});

model.when("f", function (f) {
if(f == 15){
model.set("a", 10);
} else {
strictEqual(f, 25);
start();
}
});
model.set("a", 5);
});

asyncTest("cancel should work for a single callback", function () {
var model = new Backbone.Model(),
xValue,
whens = model.when("x", function (x) {
xValue = x;
});
model.set("x", 5);
setTimeout(function () {
strictEqual(xValue, 5);
model.cancel(whens);
model.set("x", 6);
setTimeout(function () {
strictEqual(xValue, 5);
start();
}, 0);
}, 0);
});

asyncTest("cancel should work for a multiple chained callback", function () {
var model = new Backbone.Model(),
xValue,
yValue,
whens = model.when("x", function (x) { xValue = x; })
.when("y", function (y) { yValue = y; });
model.set("x", 5);
model.set("y", 10);
setTimeout(function () {
strictEqual(xValue, 5);
strictEqual(yValue, 10);
model.cancel(whens);
model.set("x", 6);
model.set("y", 11);
setTimeout(function () {
strictEqual(xValue, 5);
strictEqual(yValue, 10);
start();
}, 0);
}, 0);
});

asyncTest("cancel should work for independently tracked callbacks", function() {
var model = new Backbone.Model(),
xValue,
yValue,
whenX = model.when("x", function (x) { xValue = x; }),
whenY = model.when("y", function (y) { yValue = y; });
model.set("x", 5);
setTimeout(function () {
strictEqual(xValue, 5);
model.cancel(whenX);
model.set("x", 6);
model.set("y", 10);
setTimeout(function () {
strictEqual(xValue, 5);
strictEqual(yValue, 10);
model.cancel(whenY);
model.set("x", 7);
model.set("y", 11);
setTimeout(function () {
strictEqual(xValue, 5);
strictEqual(yValue, 10);
start();
}, 0);
}, 0);
}, 0);
});
})();