Skip to content

Commit

Permalink
Merge pull request knockout#485 from knockout/485-observable-view-model
Browse files Browse the repository at this point in the history
support observable view models at both the root and child context level
  • Loading branch information
mbest committed Jun 19, 2013
2 parents 33b9290 + a8e8374 commit fb579af
Show file tree
Hide file tree
Showing 9 changed files with 511 additions and 243 deletions.
130 changes: 128 additions & 2 deletions spec/bindingDependencyBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,9 @@ describe('Binding dependencies', function() {
expect(lastBoundValueUpdate).toEqual("third value");

// update view model with brand-new property
/* TODO: fix observable view models
vm({ myProp: function() {return "fourth value"; }});
expect(lastBoundValueInit).toEqual("fourth value");
expect(lastBoundValueUpdate).toEqual("fourth value");*/
expect(lastBoundValueUpdate).toEqual("fourth value");
});

it('Should not update sibling bindings if a binding is updated', function() {
Expand Down Expand Up @@ -222,6 +221,133 @@ describe('Binding dependencies', function() {
expect(latestValue).toEqual(2);
});

describe('Observable view models', function() {
it('Should update bindings (including callbacks)', function() {
var vm = ko.observable(), clickedVM;
function checkVM(data) {
clickedVM = data;
}
testNode.innerHTML = "<div><input data-bind='value:someProp' /><input type='button' data-bind='click: checkVM' /></div>";
vm({ someProp: 'My prop value', checkVM: checkVM });
ko.applyBindings(vm, testNode);
expect(vm.getSubscriptionsCount()).toEqual(1);

expect(testNode.childNodes[0].childNodes[0].value).toEqual("My prop value");

// a change to the input value should be written to the model
testNode.childNodes[0].childNodes[0].value = "some user-entered value";
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "change");
expect(vm().someProp).toEqual("some user-entered value");
// a click should use correct view model
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[1], "click");
expect(clickedVM).toEqual(vm());

// set the view-model to a new object
vm({ someProp: ko.observable('My new prop value'), checkVM: checkVM });
expect(testNode.childNodes[0].childNodes[0].value).toEqual("My new prop value");

// a change to the input value should be written to the new model
testNode.childNodes[0].childNodes[0].value = "some new user-entered value";
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "change");
expect(vm().someProp()).toEqual("some new user-entered value");
// a click should use correct view model
ko.utils.triggerEvent(testNode.childNodes[0].childNodes[1], "click");
expect(clickedVM).toEqual(vm());

// clear the element and the view-model (shouldn't be any errors) and the subscription should be cleared
ko.removeNode(testNode);
vm(null);
expect(vm.getSubscriptionsCount()).toEqual(0);
});

it('Should update all child contexts (including values copied from the parent)', function() {
ko.bindingHandlers.setChildContext = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
ko.applyBindingsToDescendants(
bindingContext.createChildContext(function() { return ko.utils.unwrapObservable(valueAccessor()) }),
element);
return { controlsDescendantBindings : true };
}
};

testNode.innerHTML = "<div data-bind='setChildContext:obj1'><span data-bind='text:prop1'></span><span data-bind='text:$root.prop2'></span></div>";
var vm = ko.observable({obj1: {prop1: "First "}, prop2: "view model"});
ko.applyBindings(vm, testNode);
expect(testNode).toContainText("First view model");

// change view model to new object
vm({obj1: {prop1: "Second view "}, prop2: "model"});
expect(testNode).toContainText("Second view model");

// change it again
vm({obj1: {prop1: "Third view model"}, prop2: ""});
expect(testNode).toContainText("Third view model");

// clear the element and the view-model (shouldn't be any errors) and the subscription should be cleared
ko.removeNode(testNode);
vm(null);
expect(vm.getSubscriptionsCount()).toEqual(0);
});

it('Should update all extended contexts (including values copied from the parent)', function() {
ko.bindingHandlers.withProperties = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var innerBindingContext = bindingContext.extend(valueAccessor);
ko.applyBindingsToDescendants(innerBindingContext, element);
return { controlsDescendantBindings : true };
}
};

testNode.innerHTML = "<div data-bind='withProperties: obj1'><span data-bind='text:prop1'></span><span data-bind='text:prop2'></span></div>";
var vm = ko.observable({obj1: {prop1: "First "}, prop2: "view model"});
ko.applyBindings(vm, testNode);
expect(testNode).toContainText("First view model");

// ch ange view model to new object
vm({obj1: {prop1: "Second view "}, prop2: "model"});
expect(testNode).toContainText("Second view model");

// change it again
vm({obj1: {prop1: "Third view model"}, prop2: ""});
expect(testNode).toContainText("Third view model");

// clear the element and the view-model (shouldn't be any errors) and the subscription should be cleared
ko.removeNode(testNode);
vm(null);
expect(vm.getSubscriptionsCount()).toEqual(0);
});

it('Should update an extended child context', function() {
ko.bindingHandlers.withProperties = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var childBindingContext = bindingContext.createChildContext(null, null, function(context) {
ko.utils.extend(context, valueAccessor());
});
ko.applyBindingsToDescendants(childBindingContext, element);
return { controlsDescendantBindings: true };
}
};

testNode.innerHTML = "<div data-bind='withProperties: obj1'><span data-bind='text:prop1'></span><span data-bind='text:$parent.prop2'></span></div>";
var vm = ko.observable({obj1: {prop1: "First "}, prop2: "view model"});
ko.applyBindings(vm, testNode);
expect(testNode).toContainText("First view model");

// ch ange view model to new object
vm({obj1: {prop1: "Second view "}, prop2: "model"});
expect(testNode).toContainText("Second view model");

// change it again
vm({obj1: {prop1: "Third view model"}, prop2: ""});
expect(testNode).toContainText("Third view model");

// clear the element and the view-model (shouldn't be any errors) and the subscription should be cleared
ko.removeNode(testNode);
vm(null);
expect(vm.getSubscriptionsCount()).toEqual(0);
});
});

describe('Order', function() {
var bindingOrder;
beforeEach(function() {
Expand Down
2 changes: 1 addition & 1 deletion spec/expressionRewritingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('Expression Rewriting', function() {

it('Should return accessor functions for each value when called with the valueAccessors option', function() {
var rewritten = ko.expressionRewriting.preProcessBindings("a: 1", {valueAccessors:true});
expect(rewritten).toEqual("'a':function(){return 1}");
expect(rewritten).toEqual("'a':function(){return 1 }");
var evaluated = eval("({" + rewritten + "})");
expect(evaluated['a']()).toEqual(1);
});
Expand Down
Loading

0 comments on commit fb579af

Please sign in to comment.