diff --git a/.gitignore b/.gitignore index 51bf0d7..ba12610 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .DS_Store .idea/ +/nbproject/private/ \ No newline at end of file diff --git a/dist/knockout-es5.js b/dist/knockout-es5.js index 3add4b9..ec7b1d8 100644 --- a/dist/knockout-es5.js +++ b/dist/knockout-es5.js @@ -377,6 +377,80 @@ void function(global, undefined_, undefined){ get: observable, set: ko.isWriteableObservable(observable) ? observable : undefined }; + + // Custom Binding Provider + // ------------------- + // + // To ensure that when using this plugin any custom bindings are provided with the observable + // rather than only the value of the property, a 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 + // that, especially if they use an options object. + + function CustomBindingProvider(providerToWrap) { + this.bindingCache = {}; + this._providerToWrap = providerToWrap; + this._nativeBindingProvider = new ko.bindingProvider(); + } + + CustomBindingProvider.prototype.nodeHasBindings = function() { + return this._providerToWrap.nodeHasBindings.apply(this._providerToWrap, arguments); + }; + + CustomBindingProvider.prototype.getBindingAccessors = function(node, bindingContext) { + var bindingsString = this._nativeBindingProvider.getBindingsString(node, bindingContext); + return bindingsString ? this.parseBindingsString(bindingsString, bindingContext, node, {'valueAccessors':true}) : null; + }; + + CustomBindingProvider.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) { + // Handle arrays if value starts with bracket + if(val.match(/^\[/)){ + // This is required or will throw errors + resultStrings.push(key + ':ko.observableArray(' + val + ')'); + }else{ + resultStrings.push(key + ':ko.getObservable($data,"' + val + '")||' + val); + } + + } + + var resultStrings = [], + keyValueArray = typeof bindingsStringOrKeyValueArray === 'string' ? + ko.expressionRewriting.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); + } + + ko.es5BindingProvider = CustomBindingProvider; + + ko.bindingProvider.instance = new CustomBindingProvider(ko.bindingProvider.instance); } function createLazyPropertyDescriptor(originalValue, prop, map) { diff --git a/dist/knockout-es5.min.js b/dist/knockout-es5.min.js index 2806142..8bd28f7 100644 --- a/dist/knockout-es5.min.js +++ b/dist/knockout-es5.min.js @@ -191,9 +191,23 @@ function t(a,b){var c=r(a,b);c&&c.valueHasMutated()} // (currently that's just the WeakMap shim), and then finally attaches itself to whichever // instance of Knockout.js it can find. // Extends a Knockout instance with Knockout-ES5 functionality -function u(a){a.track=c,a.untrack=k,a.getObservable=r,a.valueHasMutated=t,a.defineProperty=l, +function u(a){ +// Custom Binding Provider +// ------------------- +// +// To ensure that when using this plugin any custom bindings are provided with the observable +// rather than only the value of the property, a 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 +// that, especially if they use an options object. +function b(b){this.bindingCache={},this._providerToWrap=b,this._nativeBindingProvider=new a.bindingProvider}function d(b,c){function d(a,b){ +// Handle arrays if value starts with bracket +b.match(/^\[/)? +// This is required or will throw errors +e.push(a+":ko.observableArray("+b+")"):e.push(a+':ko.getObservable($data,"'+b+'")||'+b)}c=c||{};var e=[],f="string"==typeof b?a.expressionRewriting.parseObjectLiteral(b):b;return f.forEach(function(a){d(a.key||a.unknown,a.value)}),a.expressionRewriting.preProcessBindings(e.join(","),c)}function e(a,b,c){var d=a+(c&&c.valueAccessors||"");return b[d]||(b[d]=f(a,c))}function f(a,b){var c=d(a,b),e="with($context){with($data||{}){return{"+c+"}}}";/* jshint -W054 */ +return new Function("$context","$element",e)}a.track=c,a.untrack=k,a.getObservable=r,a.valueHasMutated=t,a.defineProperty=l, // todo: test it, maybe added it to ko. directly -a.es5={getAllObservablesForObject:j,notifyWhenPresentOrFutureArrayValuesMutate:m,isTracked:s}} +a.es5={getAllObservablesForObject:j,notifyWhenPresentOrFutureArrayValuesMutate:m,isTracked:s},b.prototype.nodeHasBindings=function(){return this._providerToWrap.nodeHasBindings.apply(this._providerToWrap,arguments)},b.prototype.getBindingAccessors=function(a,b){var c=this._nativeBindingProvider.getBindingsString(a,b);return c?this.parseBindingsString(c,b,a,{valueAccessors:!0}):null},b.prototype.parseBindingsString=function(a,b,c,d){try{var f=e(a,this.bindingCache,d);return f(b,c)}catch(g){throw g.message="Unable to parse bindings.\nBindings value: "+a+"\nMessage: "+g.message,g}},a.es5BindingProvider=b,a.bindingProvider.instance=new b(a.bindingProvider.instance)} // Determines which module loading scenario we're in, grabs dependencies, and attaches to KO function v(){if("object"==typeof exports&&"object"==typeof module){ // Node.js case - load KO and WeakMap modules synchronously diff --git a/src/knockout-es5.js b/src/knockout-es5.js index 7a7c2f8..f529330 100644 --- a/src/knockout-es5.js +++ b/src/knockout-es5.js @@ -442,6 +442,85 @@ notifyWhenPresentOrFutureArrayValuesMutate: notifyWhenPresentOrFutureArrayValuesMutate, isTracked: isTracked }; + + // Custom Binding Provider + // ------------------- + // + // To ensure that when using this plugin any custom bindings are provided with the observable + // rather than only the value of the property, a 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 + // that, especially if they use an options object. + + function CustomBindingProvider(providerToWrap) { + this.bindingCache = {}; + this._providerToWrap = providerToWrap; + this._nativeBindingProvider = new ko.bindingProvider(); + } + + CustomBindingProvider.prototype.nodeHasBindings = function() { + return this._providerToWrap.nodeHasBindings.apply(this._providerToWrap, arguments); + }; + + CustomBindingProvider.prototype.getBindingAccessors = function(node, bindingContext) { + var bindingsString = this._nativeBindingProvider.getBindingsString(node, bindingContext); + return bindingsString ? this.parseBindingsString(bindingsString, bindingContext, node, {'valueAccessors':true}) : null; + }; + + CustomBindingProvider.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) { + // Handle arrays if value starts with bracket + if(val.match(/^\[/)){ + // This is required or will throw errors + resultStrings.push(key + ':ko.observableArray(' + val + ')'); + }else{ + // Trim out whitespace after opening brackets (might be in some cases), + // or it would will cause parse error in createBindingsStringEvaluator + if(val.match(/^\{\s*/)){ + val = val.replace(/^\{\s*/, '{'); + } + resultStrings.push(key + ':ko.getObservable($data,"' + val + '")||' + val); + } + + } + + var resultStrings = [], + keyValueArray = typeof bindingsStringOrKeyValueArray === 'string' ? + ko.expressionRewriting.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); + } + + ko.es5BindingProvider = CustomBindingProvider; + + ko.bindingProvider.instance = new CustomBindingProvider(ko.bindingProvider.instance); } // Determines which module loading scenario we're in, grabs dependencies, and attaches to KO