";
+ 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 = "
";
+ 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 = "
";
+ 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).extend(valueAccessor);
+ ko.applyBindingsToDescendants(childBindingContext, element);
+ return { controlsDescendantBindings: true };
+ }
+ };
+
+ testNode.innerHTML = "
";
+ 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() {
diff --git a/src/binding/bindingAttributeSyntax.js b/src/binding/bindingAttributeSyntax.js
index bc2458b00..8b89578e1 100755
--- a/src/binding/bindingAttributeSyntax.js
+++ b/src/binding/bindingAttributeSyntax.js
@@ -1,4 +1,4 @@
-(function () {
+ (function () {
ko.bindingHandlers = {};
// Use an overridable method for retrieving binding handlers so that a plugins may support dynamically created handlers
@@ -6,31 +6,78 @@
return ko.bindingHandlers[bindingKey];
};
- ko.bindingContext = function(dataItem, parentBindingContext, dataItemAlias) {
- if (parentBindingContext) {
- ko.utils.extend(this, parentBindingContext); // Inherit $root and any custom properties
- this['$parentContext'] = parentBindingContext;
- this['$parent'] = parentBindingContext['$data'];
- this['$parents'] = (parentBindingContext['$parents'] || []).slice(0);
- this['$parents'].unshift(this['$parent']);
+ ko.bindingContext = function(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback) {
+ function updateContext() {
+ var dataItem = isFunc ? dataItemOrAccessor() : dataItemOrAccessor;
+ if (parentContext) {
+ // Register a dependency on the parent context
+ if (parentContext._subscribable)
+ parentContext._subscribable();
+ // Inherit $root and any custom properties
+ ko.utils.extend(self, parentContext);
+ // Update our properties
+ if (subscribable) {
+ self['$dataFn'] = self._subscribable = subscribable;
+ }
+ } else {
+ self['$parents'] = [];
+ self['$root'] = dataItem;
+ // Export 'ko' in the binding context so it will be available in bindings and templates
+ // even if 'ko' isn't exported as a global, such as when using an AMD loader.
+ // See https://github.com/SteveSanderson/knockout/issues/490
+ self['ko'] = ko;
+ }
+ self['$data'] = dataItem;
+ if (dataItemAlias)
+ self[dataItemAlias] = dataItem;
+ if (extendCallback)
+ extendCallback(self, parentContext, dataItem);
+ return self['$data'];
+ }
+ function disposeWhen() {
+ return !ko.utils.anyDomNodeIsAttachedToDocument(nodes);
+ }
+
+ var self = this,
+ isFunc = typeof(dataItemOrAccessor) == "function",
+ nodes = [],
+ subscribable = ko.dependentObservable(updateContext, null, { disposeWhen: disposeWhen });
+
+ if (subscribable.isActive()) {
+ self['$dataFn'] = self._subscribable = subscribable;
+
+ // Add properties to *subscribable* instead of *self* because any properties added to *self* may be overwritten on updates
+ subscribable._addNode = function(node) {
+ nodes.push(node);
+ ko.utils.domNodeDisposal.addDisposeCallback(node, function(node) {
+ ko.utils.arrayRemoveItem(nodes, node);
+ if (!nodes.length) {
+ subscribable.dispose();
+ self._subscribable = subscribable = undefined;
+ }
+ });
+ // Make sure that our parent context is watching at least one node
+ if (parentContext && parentContext._subscribable && !parentContext._subscribable._nodes.length) {
+ parentContext._subscribable._addNode(node);
+ }
+ };
+ subscribable._nodes = nodes;
} else {
- this['$parents'] = [];
- this['$root'] = dataItem;
- // Export 'ko' in the binding context so it will be available in bindings and templates
- // even if 'ko' isn't exported as a global, such as when using an AMD loader.
- // See https://github.com/SteveSanderson/knockout/issues/490
- this['ko'] = ko;
+ self['$dataFn'] = function() { return self['$data']; }
}
- this['$data'] = dataItem;
- if (dataItemAlias)
- this[dataItemAlias] = dataItem;
}
- ko.bindingContext.prototype['createChildContext'] = function (dataItem, dataItemAlias) {
- return new ko.bindingContext(dataItem, this, dataItemAlias);
+ ko.bindingContext.prototype['createChildContext'] = function (dataItemOrAccessor, dataItemAlias) {
+ return new ko.bindingContext(dataItemOrAccessor, this, dataItemAlias, function(self, parentContext) {
+ self['$parentContext'] = parentContext;
+ self['$parent'] = parentContext['$data'];
+ self['$parents'] = (parentContext['$parents'] || []).slice(0);
+ self['$parents'].unshift(self['$parent']);
+ });
};
ko.bindingContext.prototype['extend'] = function(properties) {
- var clone = ko.utils.extend(new ko.bindingContext(), this);
- return ko.utils.extend(clone, properties);
+ return new ko.bindingContext(this['$dataFn'], this, null, function(self) {
+ ko.utils.extend(self, typeof(properties) == "function" ? properties() : properties);
+ });
};
// Returns the valueAccesor function for a binding value
@@ -170,18 +217,36 @@
if (!bindings) {
var provider = ko.bindingProvider['instance'],
getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;
- bindings = ko.dependencyDetection.ignore(getBindings, provider, [node, bindingContext]);
+
+ var bindingsUpdater = ko.dependentObservable(
+ function() {
+ return (bindings = getBindings.call(provider, node, bindingContext));
+ },
+ null, { disposeWhenNodeIsRemoved: node }
+ );
+
+ if (!bindings || !bindingsUpdater.isActive())
+ bindingsUpdater = null;
}
var bindingHandlerThatControlsDescendantBindings;
if (bindings) {
+ var getValueAccessor = bindingsUpdater
+ ? function(bindingKey) {
+ return function() {
+ return evaluateValueAccessor(bindingsUpdater()[bindingKey]);
+ };
+ } : function(bindingKey) {
+ return bindings[bindingKey];
+ };
+
// Use of allBindings as a function is maintained for backwards compatibility, but its use is deprecated
function allBindings() {
- return ko.utils.objectMap(bindings, evaluateValueAccessor);
+ return ko.utils.objectMap(bindingsUpdater ? bindingsUpdater() : bindings, evaluateValueAccessor);
}
// The following is the 3.x allBindings API
allBindings['get'] = function(key) {
- return bindings[key] && evaluateValueAccessor(bindings[key]);
+ return bindings[key] && evaluateValueAccessor(getValueAccessor(key));
};
allBindings['has'] = function(key) {
return key in bindings;
@@ -202,7 +267,7 @@
ko.dependencyDetection.ignore(function() {
var handlerInitFn = bindingHandler["init"];
if (typeof handlerInitFn == "function") {
- var initResult = handlerInitFn(node, bindings[bindingKey], allBindings, bindingContext['$data'], bindingContext);
+ var initResult = handlerInitFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
// If this binding handler claims to control descendant bindings, make a note of this
if (initResult && initResult['controlsDescendantBindings']) {
@@ -218,7 +283,7 @@
function() {
var handlerUpdateFn = bindingHandler["update"];
if (typeof handlerUpdateFn == "function") {
- handlerUpdateFn(node, bindings[bindingKey], allBindings, bindingContext['$data'], bindingContext);
+ handlerUpdateFn(node, getValueAccessor(bindingKey), allBindings, bindingContext['$data'], bindingContext);
}
},
null,
@@ -234,16 +299,19 @@
var storedBindingContextDomDataKey = "__ko_bindingContext__";
ko.storedBindingContextForNode = function (node, bindingContext) {
- if (arguments.length == 2)
+ if (arguments.length == 2) {
ko.utils.domData.set(node, storedBindingContextDomDataKey, bindingContext);
- else
+ if (bindingContext._subscribable)
+ bindingContext._subscribable._addNode(node);
+ } else {
return ko.utils.domData.get(node, storedBindingContextDomDataKey);
+ }
}
- function getBindingContext(viewModelOrBindingContext, options) {
+ function getBindingContext(viewModelOrBindingContext) {
return viewModelOrBindingContext && (viewModelOrBindingContext instanceof ko.bindingContext)
? viewModelOrBindingContext
- : new ko.bindingContext(ko.utils.peekObservable(viewModelOrBindingContext));
+ : new ko.bindingContext(viewModelOrBindingContext);
}
ko.applyBindingAccessorsToNode = function (node, bindings, viewModelOrBindingContext) {
diff --git a/src/binding/bindingProvider.js b/src/binding/bindingProvider.js
index faa48499a..d20a2a42d 100644
--- a/src/binding/bindingProvider.js
+++ b/src/binding/bindingProvider.js
@@ -60,7 +60,7 @@
// For each scope variable, add an extra level of "with" nesting
// Example result: with(sc1) { with(sc0) { return (expression) } }
var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options),
- functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}";
+ functionBody = "with($context){with($dataFn()||{}){return{" + rewrittenBindings + "}}}";
return new Function("$context", "$element", functionBody);
}
})();
diff --git a/src/binding/defaultBindings/event.js b/src/binding/defaultBindings/event.js
index d62ad69d5..d2fef6b56 100755
--- a/src/binding/defaultBindings/event.js
+++ b/src/binding/defaultBindings/event.js
@@ -2,19 +2,19 @@
// e.g. click:handler instead of the usual full-length event:{click:handler}
function makeEventHandlerShortcut(eventName) {
ko.bindingHandlers[eventName] = {
- 'init': function(element, valueAccessor, allBindings, viewModel) {
+ 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var newValueAccessor = function () {
var result = {};
result[eventName] = valueAccessor();
return result;
};
- return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindings, viewModel);
+ return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindings, viewModel, bindingContext);
}
}
}
ko.bindingHandlers['event'] = {
- 'init' : function (element, valueAccessor, allBindings, viewModel) {
+ 'init' : function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var eventsToHandle = valueAccessor() || {};
ko.utils.objectForEach(eventsToHandle, function(eventName) {
if (typeof eventName == "string") {
@@ -27,6 +27,7 @@ ko.bindingHandlers['event'] = {
try {
// Take all the event args, and prefix with the viewmodel
var argsForHandler = ko.utils.makeArray(arguments);
+ viewModel = bindingContext['$data'];
argsForHandler.unshift(viewModel);
handlerReturnValue = handlerFunction.apply(viewModel, argsForHandler);
} finally {
diff --git a/src/binding/defaultBindings/submit.js b/src/binding/defaultBindings/submit.js
index d18991cdf..e3415926b 100755
--- a/src/binding/defaultBindings/submit.js
+++ b/src/binding/defaultBindings/submit.js
@@ -1,11 +1,11 @@
ko.bindingHandlers['submit'] = {
- 'init': function (element, valueAccessor, allBindings, viewModel) {
+ 'init': function (element, valueAccessor, allBindings, viewModel, bindingContext) {
if (typeof valueAccessor() != "function")
throw new Error("The value for a submit binding must be a function");
ko.utils.registerEventHandler(element, "submit", function (event) {
var handlerReturnValue;
var value = valueAccessor();
- try { handlerReturnValue = value.call(viewModel, element); }
+ try { handlerReturnValue = value.call(bindingContext['$data'], element); }
finally {
if (handlerReturnValue !== true) { // Normally we want to prevent default action. Developer can override this be explicitly returning true.
if (event.preventDefault)