Skip to content

Commit

Permalink
custom binding provider for two-way bindings
Browse files Browse the repository at this point in the history
  • Loading branch information
CraigCav committed Jan 18, 2014
1 parent 8f9cf23 commit f3d67e5
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 2 deletions.
68 changes: 68 additions & 0 deletions dist/knockout-es5.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,74 @@
ko.getObservable = getObservable;
ko.valueHasMutated = valueHasMutated;
ko.defineProperty = defineComputedProperty;

// 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) {
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
4 changes: 2 additions & 2 deletions dist/knockout-es5.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions spec/SpecRunner.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<link rel="shortcut icon" type="image/png" href="lib/jasmine-1.3.1/jasmine_favicon.png">
<link rel="stylesheet" type="text/css" href="lib/jasmine-1.3.1/jasmine.css">
<style type="text/css">#testNode { display: none; }</style>
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>
<script type="text/javascript" src="lib/knockout-latest.js"></script>
Expand All @@ -22,6 +23,7 @@
<script type="text/javascript" src="computed-properties.spec.js"></script>
<script type="text/javascript" src="utility-functions.spec.js"></script>
<script type="text/javascript" src="arrays.spec.js"></script>
<script type="text/javascript" src="bindings.spec.js"></script>

<script type="text/javascript">
(function() {
Expand Down
28 changes: 28 additions & 0 deletions spec/bindings.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
(function() {
var ko = this.ko || require('../src/knockout-es5.js');

beforeEach(jasmine.prepareTestNode);

describe("Bindings", function () {

it("should supply bindings with actual observable values", function() {
var observable = ko.observable(123),
obj = ko.track({ prop: observable });

ko.bindingHandlers.custombinding = {
init: function(element, valueAccessor) {
var value = valueAccessor();

element.textContent = ko.isObservable(value) ? 'true' : 'false';
}
};

testNode.innerHTML = "<div data-bind='custombinding: prop'></div>";

ko.applyBindings(obj, testNode);

expect(testNode).toContainText('true');
});

});
})();
22 changes: 22 additions & 0 deletions spec/jasmine-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,26 @@
return isSuccess;
}

jasmine.prepareTestNode = function() {
// The bindings specs make frequent use of this utility function to set up
// a clean new DOM node they can execute code against
var existingNode = document.getElementById("testNode");
if (existingNode != null)
existingNode.parentNode.removeChild(existingNode);
testNode = document.createElement("div");
testNode.id = "testNode";
document.body.appendChild(testNode);
};

jasmine.nodeText = function(node) {
return 'textContent' in node ? node.textContent : node.innerText;
}

jasmine.Matchers.prototype.toContainText = function (expectedText) {
var actualText = jasmine.nodeText(this.actual);
var cleanedActualText = actualText.replace(/\r\n/g, "\n");
this.actual = cleanedActualText; // Fix explanatory message
return cleanedActualText === expectedText;
};

})();
Loading

0 comments on commit f3d67e5

Please sign in to comment.