Permalink
Browse files

Include support for makeSubkeyHandler for bindings when using x.y syn…

…tax; cache handlers for x.y syntax.
  • Loading branch information...
1 parent c5a6c96 commit e6c65e4f4d04bec4812c3701e8e440c96be4cee3 @mbest committed Jul 25, 2012
@@ -674,6 +674,82 @@ describe('Binding attribute syntax', {
value_of(hasUpdatedSecondBinding).should_be(true);
},
+ 'Should be able to set and use binding handlers with x.y syntax': function() {
+ var initCalls = 0;
+ ko.bindingHandlers['a.b'] = {
+ init: function(element, valueAccessor) { if (valueAccessor()) initCalls++; }
+ };
+ testNode.innerHTML = "<div data-bind='a.b: true'></div>";
+ ko.applyBindings(null, testNode);
+ value_of(initCalls).should_be(1);
+ },
+
+ 'Should be able to use x.y binding syntax to call \'x\' handler with \'y\' as object key': function() {
+ // ensure that a.b and a.c don't exist
+ delete ko.bindingHandlers['a.b'];
+ delete ko.bindingHandlers['a.c'];
+
+ var observable = ko.observable(), lastSubKey;
+ ko.bindingHandlers['a'] = {
+ update: function(element, valueAccessor) {
+ var value = valueAccessor();
+ for (var key in value)
+ if (ko.utils.unwrapObservable(value[key]))
+ lastSubKey = key;
+ }
+ };
+ testNode.innerHTML = "<div data-bind='a.b: true, a.c: myObservable'></div>";
+ ko.applyBindings({ myObservable: observable }, testNode);
+ value_of(lastSubKey).should_be("b");
+
+ // update observable to true so a.c binding gets updated
+ observable(true);
+ value_of(lastSubKey).should_be("c");
+ },
+
+ 'Should be able to define a custom handler for x.y binding syntax': function() {
+ // ensure that a.b and a.c don't exist
+ delete ko.bindingHandlers['a.b'];
+ delete ko.bindingHandlers['a.c'];
+
+ var observable = ko.observable(), lastSubKey;
+ ko.bindingHandlers['a'] = {
+ makeSubkeyHandler: function(baseKey, subKey) {
+ return {
+ update: function(element, valueAccessor) {
+ if (ko.utils.unwrapObservable(valueAccessor()))
+ lastSubKey = subKey;
+ }
+ };
+ }
+ };
+ testNode.innerHTML = "<div data-bind='a.b: true, a.c: myObservable'></div>";
+ ko.applyBindings({ myObservable: observable }, testNode);
+ value_of(lastSubKey).should_be("b");
+
+ // update observable to true so a.c binding gets updated
+ observable(true);
+ value_of(lastSubKey).should_be("c");
+ },
+
+ 'Should be able to use x.y binding syntax in virtual elements if \'x\' binding supports it': function() {
+ delete ko.bindingHandlers['a.b']; // ensure that a.b doesn't exist
+ var lastSubKey;
+ ko.bindingHandlers['a'] = {
+ update: function(element, valueAccessor) {
+ var value = valueAccessor();
+ for (var key in value)
+ if (ko.utils.unwrapObservable(value[key]))
+ lastSubKey = key;
+ }
+ };
+ ko.virtualElements.allowedBindings.a = true;
+
+ testNode.innerHTML = "x <!-- ko a.b: true --><!--/ko-->";
+ ko.applyBindings(null, testNode);
+ value_of(lastSubKey).should_be("b");
+ },
+
'Should not subscribe to observables accessed in init function if binding are run independently': function() {
var observable = ko.observable('A');
ko.bindingHandlers.test = {
@@ -1,5 +1,12 @@
+var savedHandlers;
+function resetBindingHandlers() {
+ if (savedHandlers)
+ ko.bindingHandlers = savedHandlers;
+ savedHandlers = ko.utils.extend({}, ko.bindingHandlers);
+}
describe('Expression Rewriting', {
+ before_each: resetBindingHandlers,
'Should be able to parse simple object literals': function() {
var result = ko.expressionRewriting.parseObjectLiteral("a: 1, b: 2, \"quotedKey\": 3, 'aposQuotedKey': 4");
@@ -69,7 +76,6 @@ describe('Expression Rewriting', {
var parsedRewritten = eval("({" + rewritten + "})");
value_of(parsedRewritten.a).should_be(1);
value_of(parsedRewritten.b).should_be(true);
- delete ko.bindingHandlers.b;
},
'Should allow binding to modify value through "preprocess" method': function() {
@@ -83,7 +89,6 @@ describe('Expression Rewriting', {
var parsedRewritten = eval("({" + rewritten + "})");
value_of(parsedRewritten.a).should_be(1);
value_of(parsedRewritten.b).should_be(false);
- delete ko.bindingHandlers.b;
},
'Should allow binding to add/replace bindings through "preprocess" method\'s "addBinding" callback': function() {
@@ -97,7 +102,6 @@ describe('Expression Rewriting', {
value_of(parsedRewritten.a).should_be(1);
value_of(parsedRewritten.b).should_be(undefined);
value_of(parsedRewritten.ab).should_be(2);
- delete ko.bindingHandlers.b;
},
'Bindings added by "preprocess" should be at the root level': function() {
@@ -114,7 +118,6 @@ describe('Expression Rewriting', {
value_of(parsedRewritten.b).should_be(undefined);
value_of(parsedRewritten['b.a']).should_be(3);
value_of(parsedRewritten['ab.a']).should_be(2);
- delete ko.bindingHandlers.b;
},
'Should be able to chain "preprocess" calls when one adds a binding for another': function() {
@@ -136,9 +139,6 @@ describe('Expression Rewriting', {
value_of(parsedRewritten.a).should_be(2);
value_of(parsedRewritten.b).should_be(undefined);
value_of(parsedRewritten['a.b']).should_be(3);
-
- delete ko.bindingHandlers.a;
- delete ko.bindingHandlers.b;
},
'Should convert values to property accessors': function () {
@@ -155,7 +155,6 @@ describe('Expression Rewriting', {
parsedRewritten._ko_property_writers.b("bob2");
value_of(model.firstName).should_be("bob2");
}
- delete ko.bindingHandlers.b;
},
'Should convert a variety of values to property accessors': function () {
@@ -185,7 +184,6 @@ describe('Expression Rewriting', {
ko.expressionRewriting.writeValueToProperty(null, accessor, 'b.d', "smart");
value_of(model.obj2.prop2).should_be("sloan");
}
- delete ko.bindingHandlers.b;
},
'Should be able to eval rewritten literals that contain unquoted keywords as keys': function() {
@@ -209,6 +207,5 @@ describe('Expression Rewriting', {
parsedRewritten._ko_property_writers['a.f']("bob2");
value_of(model.firstName).should_be("bob2");
}
- delete ko.bindingHandlers.a;
}
});
@@ -37,6 +37,10 @@
ko.bindingHandlers = {};
+ ko.getBindingHandler = function(bindingKey) {
+ return ko.bindingHandlers[bindingKey] || makeKeySubkeyBinding(bindingKey);
+ };
+
// Accepts either a data value or a value accessor function; note that an observable qualifies as a value accessor function
ko.bindingContext = function(dataItemOrValueAccessor, parentContext, options, extendCallback) {
var self = this,
@@ -86,31 +90,6 @@
}
});
- function getTwoLevelBindingData(bindingKey, justHandler) {
- var dotPos = bindingKey.indexOf(".");
- if (dotPos > 0) {
- var realKey = bindingKey.substring(0, dotPos), binding = ko.bindingHandlers[realKey];
- if (binding) {
- if (!(binding['flags'] & bindingFlags_twoLevel))
- throw new Error(realKey + " does not support two-level binding");
- return justHandler
- ? binding
- : { key: realKey, subKey: bindingKey.substring(dotPos + 1), handler: binding };
- }
- }
- }
-
- function getBindingData(bindingKey) {
- var handler = ko.bindingHandlers[bindingKey];
- return handler
- ? { handler: handler, key: bindingKey }
- : getTwoLevelBindingData(bindingKey);
- }
-
- ko.getBindingHandler = function(bindingKey) {
- return ko.bindingHandlers[bindingKey] || getTwoLevelBindingData(bindingKey, true /* justHandler */);
- };
-
ko.bindingValueWrap = function(valueFunction) {
valueFunction.__ko_wraptest = ko.bindingValueWrap;
return valueFunction;
@@ -184,17 +163,11 @@
}
// These functions make values accessible to bindings.
- function makeValueAccessor(fullKey, subKey) {
- return subKey
- ? function() {
- if (bindingUpdater)
- bindingUpdater();
- var _z = {}; _z[subKey] = unwrapBindingValue(parsedBindings[fullKey]); return _z;
- }
- : function () {
+ function makeValueAccessor(bindingKey) {
+ return function () {
if (bindingUpdater)
bindingUpdater();
- return unwrapBindingValue(parsedBindings[fullKey]);
+ return unwrapBindingValue(parsedBindings[bindingKey]);
};
}
function allBindingsAccessorIndependent(key) {
@@ -281,21 +254,21 @@
if (bindingKey in bindingIndexes)
return allBindings[bindingIndexes[bindingKey]];
- var binding = getBindingData(bindingKey);
- if (binding) {
- var handler = binding.handler,
+ var handler = ko.getBindingHandler(bindingKey);
+ if (handler) {
+ var binding = { handler: handler, key: bindingKey },
dependentOrder;
binding.flags = handler['flags'];
validateThatBindingIsAllowedForVirtualElements(binding);
- binding.valueAccessor = makeValueAccessor(bindingKey, binding.subKey);
+ binding.valueAccessor = makeValueAccessor(bindingKey);
if (!independentBindings && handler['init'])
initCaller(binding)();
if (binding.flags & bindingFlags_contentBind) {
if (bindings[contentBindBinding])
- multiContentBindError(bindings[contentBindBinding].key, binding.key);
+ multiContentBindError(bindings[contentBindBinding].key, bindingKey);
bindings[binding.order = contentBindBinding] = binding;
dependentOrder = contentBindBinding - 1;
} else {
@@ -423,6 +396,7 @@
};
ko.exportSymbol('bindingHandlers', ko.bindingHandlers);
+ ko.exportSymbol('getBindingHandler', ko.getBindingHandler);
ko.exportSymbol('bindingFlags', ko.bindingFlags);
ko.exportSymbol('bindingValueWrap', ko.bindingValueWrap); // must be exported because it's used in binding parser (which uses eval)
ko.exportSymbol('applyBindings', ko.applyBindings);
@@ -1,3 +1,43 @@
+// Support a short-hand syntax of "key.subkey: value". The "key.subkey" binding
+// handler will be created as needed (through ko.getBindingHandler) but can also be
+// created initially (as event.click is).
+var keySubkeyMatch = /([^\.]+)\.(.+)/, keySubkeyBindingDivider = '.';
+function makeKeySubkeyBinding(bindingKey) {
+ var match = bindingKey.match(keySubkeyMatch);
+ if (match) {
+ var baseKey = match[1],
+ baseHandler = ko.bindingHandlers[baseKey];
+ if (baseHandler) {
+ var subKey = match[2],
+ makeSubHandler = baseHandler['makeSubkeyHandler'] || makeDefaultKeySubkeyHandler,
+ subHandler = makeSubHandler.call(baseHandler, baseKey, subKey, bindingKey);
+ ko.virtualElements.allowedBindings[bindingKey] = ko.virtualElements.allowedBindings[baseKey];
+ return (ko.bindingHandlers[bindingKey] = subHandler);
+ }
+ }
+}
+
+// Create a binding handler that translates a binding of "binding: value" to
+// "basekey: {subkey: value}". Compatible with these default bindings: event, attr, css, style.
+function makeDefaultKeySubkeyHandler(baseKey, subKey) {
+ var subHandler = ko.utils.extendInternal({}, this);
+ subHandler['flags'] ^= bindingFlags_twoLevel; // remove two-level flag if it exists
+ function setHandlerFunction(funcName) {
+ if (subHandler[funcName]) {
+ subHandler[funcName] = function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
+ function subValueAccessor() {
+ var result = {};
+ result[subKey] = valueAccessor();
+ return result;
+ }
+ return ko.bindingHandlers[baseKey][funcName](element, subValueAccessor, allBindingsAccessor, viewModel, bindingContext);
+ };
+ }
+ }
+ ko.utils.arrayForEach(['init', 'update'], setHandlerFunction);
+ return subHandler;
+}
+
function setUpTwoWayBinding(element, modelValue, elemUpdater, elemValue, modelUpdater) {
var isUpdating = false,
shouldSet = false;
@@ -21,27 +61,6 @@ function setUpTwoWayBinding(element, modelValue, elemUpdater, elemValue, modelUp
updateOnChange(modelValue, elemUpdater);
}
-// For certain common events (currently just 'click'), allow a simplified data-binding syntax
-// e.g. click:handler instead of the usual full-length event:{click:handler}
-var eventHandlersWithShortcuts = ['click'];
-ko.utils.arrayForEach(eventHandlersWithShortcuts, function(eventName) {
- ko.bindingHandlers[eventName] = {
- 'preprocess': function(val, key, addBinding) {
- addBinding('event.' + eventName, val);
- },
- 'flags': bindingFlags_eventHandler,
- 'init': function(element, valueAccessor, allBindingsAccessor, viewModel) {
- var newValueAccessor = function () {
- var result = {};
- result[eventName] = valueAccessor();
- return result;
- };
- return ko.bindingHandlers['event']['init'].call(this, element, newValueAccessor, allBindingsAccessor, viewModel);
- }
- }
-});
-
-
ko.bindingHandlers['event'] = {
'flags': bindingFlags_eventHandler | bindingFlags_twoLevel,
'init' : function (element, valueAccessor, allBindingsAccessor, viewModel) {
@@ -83,6 +102,10 @@ ko.bindingHandlers['event'] = {
}
};
+// For certain common events (currently just 'click'), allow a simplified data-binding syntax
+// e.g. click:handler instead of the usual full-length event:{click:handler}
+ko.bindingHandlers['click'] = makeKeySubkeyBinding('event' + keySubkeyBindingDivider + 'click');
+
ko.bindingHandlers['submit'] = {
'flags': bindingFlags_eventHandler,
'init': function (element, valueAccessor, allBindingsAccessor, viewModel) {
@@ -96,22 +96,22 @@ ko.expressionRewriting = (function () {
eventHandlersUseObjectForThis = eventHandlersUseObjectForThis === false ? false : true;
independentBindings = independentBindings === false ? false : true;
- function processKeyValue(key, val, parentBinding) {
+ function processKeyValue(key, val) {
var quotedKey = ensureQuoted(key),
- binding = parentBinding || ko.getBindingHandler(key),
+ binding = ko.getBindingHandler(key),
canWrap = binding || independentBindings,
flags = binding && binding['flags'];
if (val === undefined && flags & bindingFlags_noValue) {
// If the binding can used without a value set its value to 'true'
val = "true";
}
- if (!parentBinding && flags & bindingFlags_twoLevel && val.charAt(0) === "{") {
+ if (flags & bindingFlags_twoLevel && val.charAt(0) === "{") {
// Handle two-level binding specified as "binding: {key: value}" by parsing inner
// object and converting to "binding.key: value"
var subBindings = parseObjectLiteral(val);
ko.utils.arrayForEach(subBindings, function(keyValue) {
- processKeyValue(key+'.'+keyValue[0], keyValue[1], binding);
+ processKeyValue(key+'.'+keyValue[0], keyValue[1]);
});
return;
}

0 comments on commit e6c65e4

Please sign in to comment.