Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for custom two-way bindings #48

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
.DS_Store

.idea/
/nbproject/private/
74 changes: 74 additions & 0 deletions dist/knockout-es5.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
18 changes: 16 additions & 2 deletions dist/knockout-es5.min.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions src/knockout-es5.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down