Skip to content

Commit

Permalink
Use valueAccessors option in bindingProvider and templateRewriting. I…
Browse files Browse the repository at this point in the history
…n bindingAttributeSyntax, use new binding format internally and provide conversions from old format for backwards compatibility.
  • Loading branch information
mbest committed Apr 17, 2013
1 parent e8175ab commit 32fe210
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 46 deletions.
73 changes: 73 additions & 0 deletions spec/bindingDependencyBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,77 @@ describe('Binding dependencies', function() {
ko.applyBindings({ myObservable: observable }, testNode);
expect(hasUpdatedSecondBinding).toEqual(true);
});

it('Should be able to get all updates to observables in both init and update', function() {
var lastBoundValueInit, lastBoundValueUpdate;
ko.bindingHandlers.testInit = {
init: function(element, valueAccessor) {
ko.dependentObservable(function() {
lastBoundValueInit = ko.utils.unwrapObservable(valueAccessor());
});
}
};
ko.bindingHandlers.testUpdate = {
update: function(element, valueAccessor) {
lastBoundValueUpdate = ko.utils.unwrapObservable(valueAccessor());
}
};
testNode.innerHTML = "<div data-bind='testInit: myProp()'></div><div data-bind='testUpdate: myProp()'></div>";
var vm = ko.observable({ myProp: ko.observable("initial value") });
ko.applyBindings(vm, testNode);
expect(lastBoundValueInit).toEqual("initial value");
expect(lastBoundValueUpdate).toEqual("initial value");

// update value of observable
vm().myProp("second value");
expect(lastBoundValueInit).toEqual("second value");
expect(lastBoundValueUpdate).toEqual("second value");

// update value of observable to another observable
vm().myProp(ko.observable("third value"));
expect(lastBoundValueInit).toEqual("third value");
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");*/
});

// This is a temporary spec that should fail once the independent bindings fix is included
it('Should update all bindings if a binding unwraps an observable in dependent mode', function() {
var countUpdates = 0, observable = ko.observable(1);
ko.bindingHandlers.countingHandler = {
update: function() { countUpdates++; }
}
ko.bindingHandlers.unwrappingHandler = {
update: function(element, valueAccessor) { valueAccessor(); }
}
testNode.innerHTML = "<div data-bind='countingHandler: true, unwrappingHandler: myObservable()'></div>";

ko.applyBindings({ myObservable: observable }, testNode);
expect(countUpdates).toEqual(1);
observable(3);
expect(countUpdates).toEqual(2);
});

it('Should access latest value from extra binding when normal binding is updated', function() {
delete ko.bindingHandlers.nonexistentHandler;
var observable = ko.observable(), updateValue;
var vm = {myObservable: observable, myNonObservable: "first value"};
ko.bindingHandlers.existentHandler = {
update: function(element, valueAccessor, allBindingsAccessor) {
valueAccessor()(); // create dependency
updateValue = allBindingsAccessor().nonexistentHandler;
}
}
testNode.innerHTML = "<div data-bind='existentHandler: myObservable, nonexistentHandler: myNonObservable'></div>";

ko.applyBindings(vm, testNode);
expect(updateValue).toEqual("first value");
vm.myNonObservable = "second value";
observable.notifySubscribers();
expect(updateValue).toEqual("second value");
});
});
45 changes: 42 additions & 3 deletions spec/templatingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ describe('Templating', function() {
expect(viewModel.didCallMyFunction).toEqual(true);
});

it('Data binding syntax should permit nested templates, and only bind inner templates once', function() {
it('Data binding syntax should permit nested templates, and only bind inner templates once when using getBindings', function() {
// Will verify that bindings are applied only once for both inline (rewritten) bindings,
// and external (non-rewritten) ones
var originalBindingProvider = ko.bindingProvider.instance;
Expand All @@ -375,8 +375,13 @@ describe('Templating', function() {
return (node.tagName == 'EM') || originalBindingProvider.nodeHasBindings(node, bindingContext);
},
getBindings: function(node, bindingContext) {
if (node.tagName == 'EM')
return { text: ++model.numBindings };
if (node.tagName == 'EM') {
return {
text: ko.bindingValueIsAccessor(function() { // the value must be wrapped in an accessor function
return ++model.numBindings;
})
};
}
return originalBindingProvider.getBindings(node, bindingContext);
}
};
Expand All @@ -395,6 +400,40 @@ describe('Templating', function() {
ko.bindingProvider.instance = originalBindingProvider;
});

it('Data binding syntax should permit nested templates, and only bind inner templates once when using getBindingAccessors', function() {
// Will verify that bindings are applied only once for both inline (rewritten) bindings,
// and external (non-rewritten) ones
var originalBindingProvider = ko.bindingProvider.instance;
ko.bindingProvider.instance = {
nodeHasBindings: function(node, bindingContext) {
return (node.tagName == 'EM') || originalBindingProvider.nodeHasBindings(node, bindingContext);
},
getBindingAccessors: function(node, bindingContext) {
if (node.tagName == 'EM') {
return {
text: function() {
return ++model.numBindings;
}
};
}
return originalBindingProvider.getBindingAccessors(node, bindingContext);
}
};

ko.setTemplateEngine(new dummyTemplateEngine({
outerTemplate: "Outer <div data-bind='template: { name: \"innerTemplate\", bypassDomNodeWrap: true }'></div>",
innerTemplate: "Inner via inline binding: <span data-bind='text: ++numBindings'></span>"
+ "Inner via external binding: <em></em>"
}));
var model = { numBindings: 0 };
testNode.innerHTML = "<div data-bind='template: { name: \"outerTemplate\", bypassDomNodeWrap: true }'></div>";
ko.applyBindings(model, testNode);
expect(model.numBindings).toEqual(2);
expect(testNode.childNodes[0]).toContainHtml("outer <div>inner via inline binding: <span>2</span>inner via external binding: <em>1</em></div>");

ko.bindingProvider.instance = originalBindingProvider;
});

it('Data binding syntax should support \'foreach\' option, whereby it renders for each item in an array but doesn\'t rerender everything if you push or splice', function () {
var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "<div>The item is [js: personName]</div>" }));
Expand Down
120 changes: 86 additions & 34 deletions src/binding/bindingAttributeSyntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,58 @@
return ko.utils.extend(clone, properties);
};

// ko.bindingValueWrap is used to mark that a particular value of a binding
// is actually a value-accessor function.
ko.bindingValueIsAccessor = function(valueFunction) {
valueFunction['__ko_marked'] = ko.bindingValueIsAccessor;
return valueFunction;
};

// Check if a binding value is actually a marked value-accessor function.
ko.isBindingValueAccessor = function(value) {
return (value && value['__ko_marked'] === ko.bindingValueIsAccessor);
}

// Returns the valueAccesor function for a binding value
function wrapValue(value) {
return ko.isBindingValueAccessor(value) ? value : function() {
return value;
};
}

// Returns the value of a valueAccessor function
function unwrapValue(valueAccessor) {
return valueAccessor();
}

// Given a function that returns bindings, create and return a new object that contains
// binding value-accessors functions. Each accessor function calls the original function
// so that it always gets the latest value and all dependencies are captured. This is used
// by ko.applyBindingsToNode and getBindingsAndMakeAccessors.
function makeAccessorsFromFunction(callback) {
return ko.utils.objectMap(ko.dependencyDetection.ignore(callback), function(value, key) {
return (ko.isBindingValueAccessor(value) && value) || function() {
return callback()[key];
};
});
}

// Given a bindings function or object, create and return a new object that contains
// binding value-accessors functions. This is used by ko.applyBindingsToNode.
function makeBindingAccessors(bindings, context, node) {
if (typeof bindings === 'function') {
return makeAccessorsFromFunction(bindings.bind(null, context, node));
} else {
return ko.utils.objectMap(bindings, wrapValue);
}
}

// This function is used if the binding provider doesn't include a getBindingAccessors function.
// It must be called with 'this' set to the provider instance.
function getBindingsAndMakeAccessors(node, context) {
return makeAccessorsFromFunction(this['getBindings'].bind(this, node, context));
}

function validateThatBindingIsAllowedForVirtualElements(bindingName) {
var validator = ko.virtualElements.allowedBindings[bindingName];
if (!validator)
Expand Down Expand Up @@ -80,22 +132,6 @@
// Need to be sure that inits are only run once, and updates never run until all the inits have been run
var initPhase = 0; // 0 = before all inits, 1 = during inits, 2 = after all inits

// Each time the dependentObservable is evaluated (after data changes),
// the binding attribute is reparsed so that it can pick out the correct
// model properties in the context of the changed data.
// DOM event callbacks need to be able to access this changed data,
// so we need a single parsedBindings variable (shared by all callbacks
// associated with this node's bindings) that all the closures can access.
var parsedBindings;
function makeValueAccessor(bindingKey) {
return function () { return parsedBindings[bindingKey] }
}
function parsedBindingsAccessor() {
return parsedBindings;
}

var bindingHandlerThatControlsDescendantBindings;

// Prevent multiple applyBindings calls for the same node, except when a binding value is specified
var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
if (!bindings) {
Expand All @@ -111,27 +147,35 @@
if (!alreadyBound && bindingContextMayDifferFromDomParentElement)
ko.storedBindingContextForNode(node, bindingContext);

ko.dependentObservable(
function () {
// Ensure we have a nonnull binding context to work with
var viewModel = bindingContext['$data'];
// Use bindings if given, otherwise fall back on asking the bindings provider to give us some bindings
if (!bindings) {
var provider = ko.bindingProvider['instance'],
getBindings = provider['getBindingAccessors'] || getBindingsAndMakeAccessors;
bindings = ko.dependencyDetection.ignore(getBindings, provider, [node, bindingContext]);
}

var bindingHandlerThatControlsDescendantBindings;
if (bindings) {
function allBindingAccessors() {
return ko.utils.objectMap(bindings, unwrapValue);
}
ko.utils.extend(allBindingAccessors, bindings);

// Use evaluatedBindings if given, otherwise fall back on asking the bindings provider to give us some bindings
var evaluatedBindings = (typeof bindings == "function") ? bindings(bindingContext, node) : bindings;
parsedBindings = evaluatedBindings || ko.bindingProvider['instance']['getBindings'](node, bindingContext);
ko.dependentObservable(
function () {
var viewModel = bindingContext['$data'];

if (parsedBindings) {
// First run all the inits, so bindings can register for notification on changes
if (initPhase === 0) {
initPhase = 1;
ko.utils.objectForEach(parsedBindings, function(bindingKey) {
ko.utils.objectForEach(bindings, function(bindingKey) {
var binding = ko['getBindingHandler'](bindingKey);
if (binding && node.nodeType === 8)
validateThatBindingIsAllowedForVirtualElements(bindingKey);

if (binding && typeof binding["init"] == "function") {
var handlerInitFn = binding["init"];
var initResult = handlerInitFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContext);
var initResult = handlerInitFn(node, bindings[bindingKey], allBindingAccessors, viewModel, bindingContext);

// If this binding handler claims to control descendant bindings, make a note of this
if (initResult && initResult['controlsDescendantBindings']) {
Expand All @@ -146,19 +190,19 @@

// ... then run all the updates, which might trigger changes even on the first evaluation
if (initPhase === 2) {
ko.utils.objectForEach(parsedBindings, function(bindingKey) {
ko.utils.objectForEach(bindings, function(bindingKey) {
var binding = ko['getBindingHandler'](bindingKey);
if (binding && typeof binding["update"] == "function") {
var handlerUpdateFn = binding["update"];
handlerUpdateFn(node, makeValueAccessor(bindingKey), parsedBindingsAccessor, viewModel, bindingContext);
handlerUpdateFn(node, bindings[bindingKey], allBindingAccessors, viewModel, bindingContext);
}
});
}
}
},
null,
{ disposeWhenNodeIsRemoved : node }
);
},
null,
{ disposeWhenNodeIsRemoved : node }
);
}

return {
'shouldBindDescendants': bindingHandlerThatControlsDescendantBindings === undefined
Expand All @@ -179,12 +223,17 @@
: new ko.bindingContext(ko.utils.peekObservable(viewModelOrBindingContext));
}

ko.applyBindingsToNode = function (node, bindings, viewModelOrBindingContext) {
ko.applyBindingAccessorsToNode = function (node, bindings, viewModelOrBindingContext) {
if (node.nodeType === 1) // If it's an element, workaround IE <= 8 HTML parsing weirdness
ko.virtualElements.normaliseVirtualElementDomStructure(node);
return applyBindingsToNodeInternal(node, bindings, getBindingContext(viewModelOrBindingContext), true);
};

ko.applyBindingsToNode = function (node, bindings, viewModelOrBindingContext) {
var context = getBindingContext(viewModelOrBindingContext);
return ko.applyBindingAccessorsToNode(node, makeBindingAccessors(bindings, context, node), context);
};

ko.applyBindingsToDescendants = function(viewModelOrBindingContext, rootNode) {
if (rootNode.nodeType === 1 || rootNode.nodeType === 8)
applyBindingsToDescendantsInternal(getBindingContext(viewModelOrBindingContext), rootNode, true);
Expand Down Expand Up @@ -217,8 +266,11 @@
};

ko.exportSymbol('bindingHandlers', ko.bindingHandlers);
ko.exportSymbol('bindingValueIsAccessor', ko.bindingValueIsAccessor);
ko.exportSymbol('isBindingValueAccessor', ko.isBindingValueAccessor);
ko.exportSymbol('applyBindings', ko.applyBindings);
ko.exportSymbol('applyBindingsToDescendants', ko.applyBindingsToDescendants);
ko.exportSymbol('applyBindingAccessorsToNode', ko.applyBindingAccessorsToNode);
ko.exportSymbol('applyBindingsToNode', ko.applyBindingsToNode);
ko.exportSymbol('contextFor', ko.contextFor);
ko.exportSymbol('dataFor', ko.dataFor);
Expand Down
19 changes: 12 additions & 7 deletions src/binding/bindingProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node) : null;
},

'getBindingAccessors': function(node, bindingContext) {
var bindingsString = this['getBindingsString'](node, bindingContext);
return bindingsString ? this['parseBindingsString'](bindingsString, bindingContext, node, {'valueAccessors':true}) : null;
},

// The following function is only used internally by this default provider.
// It's not part of the interface definition for a general binding provider.
'getBindingsString': function(node, bindingContext) {
Expand All @@ -31,9 +36,9 @@

// The following function is only used internally by this default provider.
// It's not part of the interface definition for a general binding provider.
'parseBindingsString': function(bindingsString, bindingContext, node) {
'parseBindingsString': function(bindingsString, bindingContext, node, options) {
try {
var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache);
var bindingFunction = createBindingsStringEvaluatorViaCache(bindingsString, this.bindingCache, options);
return bindingFunction(bindingContext, node);
} catch (ex) {
ex.message = "Unable to parse bindings.\nBindings value: " + bindingsString + "\nMessage: " + ex.message;
Expand All @@ -44,17 +49,17 @@

ko.bindingProvider['instance'] = new ko.bindingProvider();

function createBindingsStringEvaluatorViaCache(bindingsString, cache) {
var cacheKey = bindingsString;
function createBindingsStringEvaluatorViaCache(bindingsString, cache, options) {
var cacheKey = bindingsString + (options && options['valueAccessors'] || '');
return cache[cacheKey]
|| (cache[cacheKey] = createBindingsStringEvaluator(bindingsString));
|| (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, options));
}

function createBindingsStringEvaluator(bindingsString) {
function createBindingsStringEvaluator(bindingsString, options) {
// Build the source for a function that evaluates "expression"
// 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),
var rewrittenBindings = ko.expressionRewriting.preProcessBindings(bindingsString, options),
functionBody = "with($context){with($data||{}){return{" + rewrittenBindings + "}}}";
return new Function("$context", "$element", functionBody);
}
Expand Down
Loading

0 comments on commit 32fe210

Please sign in to comment.