From 2feb3c649e89448bace065a56b9450872724747c Mon Sep 17 00:00:00 2001 From: Craig Cavalier Date: Mon, 26 Jan 2015 10:18:22 -0500 Subject: [PATCH] custom binding provider for two-way bindings --- dist/knockout-es5.js | 69 ++++++++++++++++++++++++++++++++++++++++ dist/knockout-es5.min.js | 2 +- src/knockout-es5.js | 69 ++++++++++++++++++++++++++++++++++++++++ test/bindings.spec.js | 35 ++++++++++++++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 test/bindings.spec.js diff --git a/dist/knockout-es5.js b/dist/knockout-es5.js index 0d0ba84..3fc6ceb 100644 --- a/dist/knockout-es5.js +++ b/dist/knockout-es5.js @@ -304,6 +304,73 @@ } } + // Custom two-way Binding Provider + // ------------------- + // + // Custom bindings are often need to be provided with the observable rather than only the value + // of the property. This custom binding provider supplies bindings with actual observable values. + // The built in bindings use Knockout's internal `_ko_property_writers` feature to be able to + // write back to the property, but custom bindings may not be able to use `_ko_property_writers` + // unless it is exposed. + + function TwoWayBindingProvider(providerToWrap) { + this.bindingCache = {}; + this._providerToWrap = providerToWrap; + this._nativeBindingProvider = new ko.bindingProvider(); + } + + TwoWayBindingProvider.prototype.nodeHasBindings = function() { + return this._providerToWrap.nodeHasBindings.apply(this._providerToWrap, arguments); + }; + + TwoWayBindingProvider.prototype.getBindingAccessors = function(node, bindingContext) { + var s = this._nativeBindingProvider.getBindingsString(node, bindingContext); + return s ? this.parseBindingsString(s, bindingContext, node, {'valueAccessors':true}) : null; + }; + + TwoWayBindingProvider.prototype.parseBindingsString = function(bindingsString, bindingContext, node, options) { + try { + 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; + throw ex; + } + }; + + function preProcessBindings(bindingsStringOrKeyValueArray, bindingOptions) { + bindingOptions = bindingOptions || {}; + + function processKeyValue(key, val) { + resultStrings.push(key + ':ko.getObservable($data,"' + val + '")||' + val); + } + + var resultStrings = [], + isString = typeof bindingsStringOrKeyValueArray === 'string', + parseObjectLiteral = ko.expressionRewriting.parseObjectLiteral, + keyValueArray = isString + ? parseObjectLiteral(bindingsStringOrKeyValueArray) + : bindingsStringOrKeyValueArray; + + keyValueArray.forEach(function(keyValue) { + processKeyValue(keyValue.key || keyValue.unknown, keyValue.value); + }); + + return ko.expressionRewriting.preProcessBindings(resultStrings.join(','), bindingOptions); + } + + function createBindingsStringEvaluatorViaCache(bindingsString, cache, options) { + var cacheKey = bindingsString + (options && options.valueAccessors || ''); + return cache[cacheKey] || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, options)); + } + + function createBindingsStringEvaluator(bindingsString, options) { + var rewrittenBindings = preProcessBindings(bindingsString, options), + functionBody = 'with($context){with($data||{}){return{' + rewrittenBindings + '}}}'; + /* jshint -W054 */ + return new Function('$context', '$element', functionBody); + } + // Module initialisation // --------------------- // @@ -323,6 +390,8 @@ ko.getObservable = getObservable; ko.valueHasMutated = valueHasMutated; ko.defineProperty = defineComputedProperty; + ko.es5BindingProvider = TwoWayBindingProvider; + ko.bindingProvider.instance = new TwoWayBindingProvider(ko.bindingProvider.instance); } // Determines which module loading scenario we're in, grabs dependencies, and attaches to KO diff --git a/dist/knockout-es5.min.js b/dist/knockout-es5.min.js index ce1bc23..db802d3 100644 --- a/dist/knockout-es5.min.js +++ b/dist/knockout-es5.min.js @@ -3,7 +3,7 @@ * Copyright (c) Steve Sanderson * MIT license */ -!function(a,b){"use strict";function c(a,c){if(!a||"object"!=typeof a)throw new Error("When calling ko.track, you must pass an object as the first parameter.");var e=d(a,!0);return c=c||Object.getOwnPropertyNames(a),c.forEach(function(c){if(!(c in e)&&!1!==Object.getOwnPropertyDescriptor(a,c).configurable){var d=a[c],f=Array.isArray(d),h=p.isObservable(d)?d:f?p.observableArray(d):p.observable(d);Object.defineProperty(a,c,{configurable:!0,enumerable:!0,get:h,set:p.isWriteableObservable(h)?h:b}),e[c]=h,f&&g(p,h)}}),a}function d(a,b){q||(q=s());var c=q.get(a);return!c&&b&&(c={},q.set(a,c)),c}function e(a,b){if(q)if(1===arguments.length)q["delete"](a);else{var c=d(a,!1);c&&b.forEach(function(a){delete c[a]})}}function f(a,b,d){var e=this,f={owner:a,deferEvaluation:!0};if("function"==typeof d)f.read=d;else{if("value"in d)throw new Error('For ko.defineProperty, you must not specify a "value" for the property. You must provide a "get" function.');if("function"!=typeof d.get)throw new Error('For ko.defineProperty, the third parameter must be either an evaluator function, or an options object containing a function called "get".');f.read=d.get,f.write=d.set}return a[b]=e.computed(f),c.call(e,a,[b]),a}function g(a,b){var c=null;a.computed(function(){c&&(c.dispose(),c=null);var d=b();d instanceof Array&&(c=h(a,b,d))})}function h(a,b,c){var d=i(a,c);return d.subscribe(b)}function i(a,b){r||(r=s());var c=r.get(b);if(!c){c=new a.subscribable,r.set(b,c);var d={};j(b,c,d),k(a,b,c,d)}return c}function j(a,b,c){["pop","push","reverse","shift","sort","splice","unshift"].forEach(function(d){var e=a[d];a[d]=function(){var a=e.apply(this,arguments);return c.pause!==!0&&b.notifySubscribers(this),a}})}function k(a,b,c,d){["remove","removeAll","destroy","destroyAll","replace"].forEach(function(e){Object.defineProperty(b,e,{enumerable:!1,value:function(){var f;d.pause=!0;try{f=a.observableArray.fn[e].apply(a.observableArray(b),arguments)}finally{d.pause=!1}return c.notifySubscribers(b),f}})})}function l(a,b){if(!a||"object"!=typeof a)return null;var c=d(a,!1);return c&&c[b]||null}function m(a,b){var c=l(a,b);c&&c.valueHasMutated()}function n(a){a.track=c,a.untrack=e,a.getObservable=l,a.valueHasMutated=m,a.defineProperty=f}function o(){if("undefined"!=typeof module){p=require("knockout");var b=require("weakmap");n(p),s=function(){return new b},module.exports=p}else"function"==typeof define&&define.amd?define(["knockout"],function(b){return p=b,n(b),s=function(){return new a.WeakMap},b}):"ko"in a&&(p=a.ko,n(a.ko),s=function(){return new a.WeakMap})}var p,q,r,s;o()}(this),/*! WeakMap shim +!function(a,b){"use strict";function c(a,c){if(!a||"object"!=typeof a)throw new Error("When calling ko.track, you must pass an object as the first parameter.");var e=d(a,!0);return c=c||Object.getOwnPropertyNames(a),c.forEach(function(c){if(!(c in e)&&!1!==Object.getOwnPropertyDescriptor(a,c).configurable){var d=a[c],f=Array.isArray(d),h=t.isObservable(d)?d:f?t.observableArray(d):t.observable(d);Object.defineProperty(a,c,{configurable:!0,enumerable:!0,get:h,set:t.isWriteableObservable(h)?h:b}),e[c]=h,f&&g(t,h)}}),a}function d(a,b){u||(u=w());var c=u.get(a);return!c&&b&&(c={},u.set(a,c)),c}function e(a,b){if(u)if(1===arguments.length)u["delete"](a);else{var c=d(a,!1);c&&b.forEach(function(a){delete c[a]})}}function f(a,b,d){var e=this,f={owner:a,deferEvaluation:!0};if("function"==typeof d)f.read=d;else{if("value"in d)throw new Error('For ko.defineProperty, you must not specify a "value" for the property. You must provide a "get" function.');if("function"!=typeof d.get)throw new Error('For ko.defineProperty, the third parameter must be either an evaluator function, or an options object containing a function called "get".');f.read=d.get,f.write=d.set}return a[b]=e.computed(f),c.call(e,a,[b]),a}function g(a,b){var c=null;a.computed(function(){c&&(c.dispose(),c=null);var d=b();d instanceof Array&&(c=h(a,b,d))})}function h(a,b,c){var d=i(a,c);return d.subscribe(b)}function i(a,b){v||(v=w());var c=v.get(b);if(!c){c=new a.subscribable,v.set(b,c);var d={};j(b,c,d),k(a,b,c,d)}return c}function j(a,b,c){["pop","push","reverse","shift","sort","splice","unshift"].forEach(function(d){var e=a[d];a[d]=function(){var a=e.apply(this,arguments);return c.pause!==!0&&b.notifySubscribers(this),a}})}function k(a,b,c,d){["remove","removeAll","destroy","destroyAll","replace"].forEach(function(e){Object.defineProperty(b,e,{enumerable:!1,value:function(){var f;d.pause=!0;try{f=a.observableArray.fn[e].apply(a.observableArray(b),arguments)}finally{d.pause=!1}return c.notifySubscribers(b),f}})})}function l(a,b){if(!a||"object"!=typeof a)return null;var c=d(a,!1);return c&&c[b]||null}function m(a,b){var c=l(a,b);c&&c.valueHasMutated()}function n(a){this.bindingCache={},this._providerToWrap=a,this._nativeBindingProvider=new t.bindingProvider}function o(a,b){function c(a,b){d.push(a+':ko.getObservable($data,"'+b+'")||'+b)}b=b||{};var d=[],e="string"==typeof a,f=t.expressionRewriting.parseObjectLiteral,g=e?f(a):a;return g.forEach(function(a){c(a.key||a.unknown,a.value)}),t.expressionRewriting.preProcessBindings(d.join(","),b)}function p(a,b,c){var d=a+(c&&c.valueAccessors||"");return b[d]||(b[d]=q(a,c))}function q(a,b){var c=o(a,b),d="with($context){with($data||{}){return{"+c+"}}}";return new Function("$context","$element",d)}function r(a){a.track=c,a.untrack=e,a.getObservable=l,a.valueHasMutated=m,a.defineProperty=f,a.es5BindingProvider=n,a.bindingProvider.instance=new n(a.bindingProvider.instance)}function s(){if("undefined"!=typeof module){t=require("knockout");var b=require("weakmap");r(t),w=function(){return new b},module.exports=t}else"function"==typeof define&&define.amd?define(["knockout"],function(b){return t=b,r(b),w=function(){return new a.WeakMap},b}):"ko"in a&&(t=a.ko,r(a.ko),w=function(){return new a.WeakMap})}var t,u,v;n.prototype.nodeHasBindings=function(){return this._providerToWrap.nodeHasBindings.apply(this._providerToWrap,arguments)},n.prototype.getBindingAccessors=function(a,b){var c=this._nativeBindingProvider.getBindingsString(a,b);return c?this.parseBindingsString(c,b,a,{valueAccessors:!0}):null},n.prototype.parseBindingsString=function(a,b,c,d){try{var e=p(a,this.bindingCache,d);return e(b,c)}catch(f){throw f.message="Unable to parse bindings.\nBindings value: "+a+"\nMessage: "+f.message,f}};var w;s()}(this),/*! WeakMap shim * (The MIT License) * * Copyright (c) 2012 Brandon Benvie diff --git a/src/knockout-es5.js b/src/knockout-es5.js index fb471dc..28835ec 100644 --- a/src/knockout-es5.js +++ b/src/knockout-es5.js @@ -304,6 +304,73 @@ } } + // Custom Two-Way Binding Provider + // ------------------- + // + // Custom bindings often need to be provided with the observable rather than the value of the + // property. This custom binding provider supplies bindings with actual observable values. + // NOTE: Knockout's built in bindings use its's internal `_ko_property_writers` feature to be able to + // write back to the property, but custom bindings may not be able to use `_ko_property_writers` + // unless/until it is exposed. + + function TwoWayBindingProvider(providerToWrap) { + this.bindingCache = {}; + this._providerToWrap = providerToWrap; + this._nativeBindingProvider = new ko.bindingProvider(); + } + + TwoWayBindingProvider.prototype.nodeHasBindings = function() { + return this._providerToWrap.nodeHasBindings.apply(this._providerToWrap, arguments); + }; + + TwoWayBindingProvider.prototype.getBindingAccessors = function(node, bindingContext) { + var s = this._nativeBindingProvider.getBindingsString(node, bindingContext); + return s ? this.parseBindingsString(s, bindingContext, node, {'valueAccessors':true}) : null; + }; + + TwoWayBindingProvider.prototype.parseBindingsString = function(bindingsString, bindingContext, node, options) { + try { + 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; + throw ex; + } + }; + + function preProcessBindings(bindingsStringOrKeyValueArray, bindingOptions) { + bindingOptions = bindingOptions || {}; + + function processKeyValue(key, val) { + resultStrings.push(key + ':ko.getObservable($data,"' + val + '")||' + val); + } + + var resultStrings = [], + isString = typeof bindingsStringOrKeyValueArray === 'string', + parseObjectLiteral = ko.expressionRewriting.parseObjectLiteral, + keyValueArray = isString + ? parseObjectLiteral(bindingsStringOrKeyValueArray) + : bindingsStringOrKeyValueArray; + + keyValueArray.forEach(function(keyValue) { + processKeyValue(keyValue.key || keyValue.unknown, keyValue.value); + }); + + return ko.expressionRewriting.preProcessBindings(resultStrings.join(','), bindingOptions); + } + + function createBindingsStringEvaluatorViaCache(bindingsString, cache, options) { + var cacheKey = bindingsString + (options && options.valueAccessors || ''); + return cache[cacheKey] || (cache[cacheKey] = createBindingsStringEvaluator(bindingsString, options)); + } + + function createBindingsStringEvaluator(bindingsString, options) { + var rewrittenBindings = preProcessBindings(bindingsString, options), + functionBody = 'with($context){with($data||{}){return{' + rewrittenBindings + '}}}'; + /* jshint -W054 */ + return new Function('$context', '$element', functionBody); + } + // Module initialisation // --------------------- // @@ -323,6 +390,8 @@ ko.getObservable = getObservable; ko.valueHasMutated = valueHasMutated; ko.defineProperty = defineComputedProperty; + ko.es5BindingProvider = TwoWayBindingProvider; + ko.bindingProvider.instance = new TwoWayBindingProvider(ko.bindingProvider.instance); } // Determines which module loading scenario we're in, grabs dependencies, and attaches to KO diff --git a/test/bindings.spec.js b/test/bindings.spec.js new file mode 100644 index 0000000..9a29037 --- /dev/null +++ b/test/bindings.spec.js @@ -0,0 +1,35 @@ +describe("Bindings", function () { + var testNode; + + beforeEach(prepareTestNode); + + it("should supply bindings with actual observable values", function() { + var obj = { prop: 123 }; + + ko.track(obj); + + // create a custom binding that expects an observable (rather than a raw value) + ko.bindingHandlers.custombinding = { + init: function(element, valueAccessor) { + var value = valueAccessor(); + element.textContent = ko.isObservable(value) ? 'true' : 'false'; + } + }; + + testNode.innerHTML = "
"; + + ko.applyBindings(obj, testNode); + + assert.equal(testNode.innerText, 'true'); + }); + + function prepareTestNode() { + var existingNode = document.getElementById("testNode"); + if (existingNode != null) + existingNode.parentNode.removeChild(existingNode); + testNode = document.createElement("div"); + testNode.id = "testNode"; + document.body.appendChild(testNode); + } + +}); \ No newline at end of file