Permalink
Browse files

Merge pull request #1811 from knockout/1811-using-binding

Add new with-type binding that is useful for tracking non-identity changes
2 parents ac30244 + 7785a44 commit 0a35a80c565e966bc75388662b7bd8f52e5c5f1c @mbest mbest committed on GitHub Dec 6, 2016
@@ -43,6 +43,7 @@ knockoutDebugCallback([
'src/binding/defaultBindings/text.js',
'src/binding/defaultBindings/textInput.js',
'src/binding/defaultBindings/uniqueName.js',
+ 'src/binding/defaultBindings/using.js',
'src/binding/defaultBindings/value.js',
'src/binding/defaultBindings/visible.js',
// click depends on event - The order matters for specs, which includes each file individually
@@ -0,0 +1,192 @@
+describe('Binding: Using', function() {
+ beforeEach(jasmine.prepareTestNode);
+
+ it('Should leave descendant nodes in the document (and bind them in the context of the supplied value) if the value is truthy', function() {
+ testNode.innerHTML = "<div data-bind='using: someItem'><span data-bind='text: existentChildProp'></span></div>";
+ expect(testNode.childNodes.length).toEqual(1);
+ ko.applyBindings({ someItem: { existentChildProp: 'Child prop value' } }, testNode);
+ expect(testNode.childNodes[0].childNodes.length).toEqual(1);
+ expect(testNode.childNodes[0].childNodes[0]).toContainText("Child prop value");
+ });
+
+ it('Should leave descendant nodes in the document (and bind them) if the value is falsy', function() {
+ testNode.innerHTML = "<div data-bind='using: someItem'><span data-bind='text: $data'></span></div>";
+ ko.applyBindings({ someItem: null }, testNode);
+ expect(testNode.childNodes[0].childNodes.length).toEqual(1);
+ expect(testNode.childNodes[0].childNodes[0]).toContainText("");
+ });
+
+ it('Should leave descendant nodes unchanged and not bind them more than once if the supplied value notifies a change', function() {
+ var countedClicks = 0;
+ var someItem = ko.observable({
+ childProp: ko.observable('Hello'),
+ handleClick: function() { countedClicks++ }
+ });
+
+ testNode.innerHTML = "<div data-bind='using: someItem'><span data-bind='text: childProp, click: handleClick'></span></div>";
+ var originalNode = testNode.childNodes[0].childNodes[0];
+
+ ko.applyBindings({ someItem: someItem }, testNode);
+ expect(testNode.childNodes[0].childNodes[0]).toEqual(originalNode);
+
+ // Initial state is one subscriber, one click handler
+ expect(testNode.childNodes[0].childNodes[0]).toContainText("Hello");
+ expect(someItem().childProp.getSubscriptionsCount()).toEqual(1);
+ ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click");
+ expect(countedClicks).toEqual(1);
+
+ // Force "update" binding handler to fire, then check we still have one subscriber...
+ someItem.valueHasMutated();
+ expect(someItem().childProp.getSubscriptionsCount()).toEqual(1);
+
+ // ... and one click handler
+ countedClicks = 0;
+ ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "click");
+ expect(countedClicks).toEqual(1);
+
+ // and the node is still the same
+ expect(testNode.childNodes[0].childNodes[0]).toEqual(originalNode);
+ });
+
+ it('Should be able to access parent binding context via $parent', function() {
+ testNode.innerHTML = "<div data-bind='using: someItem'><span data-bind='text: $parent.parentProp'></span></div>";
+ ko.applyBindings({ someItem: { }, parentProp: 'Parent prop value' }, testNode);
+ expect(testNode.childNodes[0].childNodes[0]).toContainText("Parent prop value");
+ });
+
+ it('Should be able to access all parent binding contexts via $parents, and root context via $root', function() {
+ testNode.innerHTML = "<div data-bind='using: topItem'>" +
+ "<div data-bind='using: middleItem'>" +
+ "<div data-bind='using: bottomItem'>" +
+ "<span data-bind='text: name'></span>" +
+ "<span data-bind='text: $parent.name'></span>" +
+ "<span data-bind='text: $parents[1].name'></span>" +
+ "<span data-bind='text: $parents[2].name'></span>" +
+ "<span data-bind='text: $root.name'></span>" +
+ "</div>" +
+ "</div>" +
+ "</div>";
+ ko.applyBindings({
+ name: 'outer',
+ topItem: {
+ name: 'top',
+ middleItem: {
+ name: 'middle',
+ bottomItem: {
+ name: "bottom"
+ }
+ }
+ }
+ }, testNode);
+ var finalContainer = testNode.childNodes[0].childNodes[0].childNodes[0];
+ expect(finalContainer.childNodes[0]).toContainText("bottom");
+ expect(finalContainer.childNodes[1]).toContainText("middle");
+ expect(finalContainer.childNodes[2]).toContainText("top");
+ expect(finalContainer.childNodes[3]).toContainText("outer");
+ expect(finalContainer.childNodes[4]).toContainText("outer");
+
+ // Also check that, when we later retrieve the binding contexts, we get consistent results
+ expect(ko.contextFor(testNode).$data.name).toEqual("outer");
+ expect(ko.contextFor(testNode.childNodes[0]).$data.name).toEqual("outer");
+ expect(ko.contextFor(testNode.childNodes[0].childNodes[0]).$data.name).toEqual("top");
+ expect(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0]).$data.name).toEqual("middle");
+ expect(ko.contextFor(testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0]).$data.name).toEqual("bottom");
+ var firstSpan = testNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0];
+ expect(firstSpan.tagName).toEqual("SPAN");
+ expect(ko.contextFor(firstSpan).$data.name).toEqual("bottom");
+ expect(ko.contextFor(firstSpan).$root.name).toEqual("outer");
+ expect(ko.contextFor(firstSpan).$parents[1].name).toEqual("top");
+ });
+
+ it('Should be able to define a \"using\" region using a containerless binding', function() {
+ var someitem = ko.observable({someItem: 'first value'});
+ testNode.innerHTML = "xxx <!-- ko using: someitem --><span data-bind=\"text: someItem\"></span><!-- /ko -->";
+ ko.applyBindings({ someitem: someitem }, testNode);
+
+ expect(testNode).toContainText("xxx first value");
+
+ someitem({ someItem: 'second value' });
+ expect(testNode).toContainText("xxx second value");
+ });
+
+ it('Should be able to use \"using\" within an observable top-level view model', function() {
+ var vm = ko.observable({someitem: ko.observable({someItem: 'first value'})});
+ testNode.innerHTML = "xxx <!-- ko using: someitem --><span data-bind=\"text: someItem\"></span><!-- /ko -->";
+ ko.applyBindings(vm, testNode);
+
+ expect(testNode).toContainText("xxx first value");
+
+ vm({someitem: ko.observable({ someItem: 'second value' })});
+ expect(testNode).toContainText("xxx second value");
+ });
+
+ it('Should be able to nest a template within \"using\"', function() {
+ testNode.innerHTML = "<div data-bind='using: someitem'>" +
+ "<div data-bind='foreach: childprop'><span data-bind='text: $data'></span></div></div>";
+
+ var childprop = ko.observableArray([]);
+ var someitem = ko.observable({childprop: childprop});
+ var viewModel = {someitem: someitem};
+ ko.applyBindings(viewModel, testNode);
+
+ // First it's not there (by template)
+ var container = testNode.childNodes[0];
+ expect(container).toContainHtml('<div data-bind="foreach: childprop"></div>');
+
+ // Then it's there
+ childprop.push('me')
+ expect(container).toContainHtml('<div data-bind="foreach: childprop"><span data-bind=\"text: $data\">me</span></div>');
+
+ // Then there's a second one
+ childprop.push('me2')
+ expect(container).toContainHtml('<div data-bind="foreach: childprop"><span data-bind=\"text: $data\">me</span><span data-bind=\"text: $data\">me2</span></div>');
+
+ // Then it changes
+ someitem({childprop: ['notme']});
+ expect(container).toContainHtml('<div data-bind="foreach: childprop"><span data-bind=\"text: $data\">notme</span></div>');
+ });
+
+ it('Should be able to nest a containerless template within \"using\"', function() {
+ testNode.innerHTML = "<div data-bind='using: someitem'>text" +
+ "<!-- ko foreach: childprop --><span data-bind='text: $data'></span><!-- /ko --></div>";
+
+ var childprop = ko.observableArray([]);
+ var someitem = ko.observable({childprop: childprop});
+ var viewModel = {someitem: someitem};
+ ko.applyBindings(viewModel, testNode);
+
+ // First it's not there (by template)
+ var container = testNode.childNodes[0];
+ expect(container).toContainHtml("text<!-- ko foreach: childprop --><!-- /ko -->");
+
+ // Then it's there
+ childprop.push('me')
+ expect(container).toContainHtml("text<!-- ko foreach: childprop --><span data-bind=\"text: $data\">me</span><!-- /ko -->");
+
+ // Then there's a second one
+ childprop.push('me2')
+ expect(container).toContainHtml("text<!-- ko foreach: childprop --><span data-bind=\"text: $data\">me</span><span data-bind=\"text: $data\">me2</span><!-- /ko -->");
+
+ // Then it changes
+ someitem({childprop: ['notme']});
+ container = testNode.childNodes[0];
+ expect(container).toContainHtml("text<!-- ko foreach: childprop --><span data-bind=\"text: $data\">notme</span><!-- /ko -->");
+ });
+
+ it('Should provide access to an observable viewModel through $rawData', function() {
+ testNode.innerHTML = "<div data-bind='using: item'><input data-bind='value: $rawData'/></div>";
+ var item = ko.observable('one');
+ ko.applyBindings({ item: item }, testNode);
+ expect(item.getSubscriptionsCount('change')).toEqual(2); // only subscriptions are the using and value bindings
+ expect(testNode.childNodes[0]).toHaveValues(['one']);
+
+ // Should update observable when input is changed
+ testNode.childNodes[0].childNodes[0].value = 'two';
+ ko.utils.triggerEvent(testNode.childNodes[0].childNodes[0], "change");
+ expect(item()).toEqual('two');
+
+ // Should update the input when the observable changes
+ item('three');
+ expect(testNode.childNodes[0]).toHaveValues(['three']);
+ });
+});
View
@@ -70,6 +70,7 @@
<script type="text/javascript" src="defaultBindings/textBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/textInputBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/uniqueNameBehaviors.js"></script>
+ <script type="text/javascript" src="defaultBindings/usingBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/valueBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/visibleBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/withBehaviors.js"></script>
@@ -0,0 +1,9 @@
+ko.bindingHandlers['using'] = {
+ 'init': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
+ var innerContext = bindingContext['createChildContext'](valueAccessor);
+ ko.applyBindingsToDescendants(innerContext, element);
+
+ return { 'controlsDescendantBindings': true };
+ }
+};
+ko.virtualElements.allowedBindings['using'] = true;
View
@@ -321,7 +321,7 @@ ko.utils = (function () {
if (node.nodeType === 11)
return false; // Fixes issue #1162 - can't use node.contains for document fragments on IE8
if (containedByNode.contains)
- return containedByNode.contains(node.nodeType === 3 ? node.parentNode : node);
+ return containedByNode.contains(node.nodeType !== 1 ? node.parentNode : node);
if (containedByNode.compareDocumentPosition)
return (containedByNode.compareDocumentPosition(node) & 16) == 16;
while (node && node != containedByNode) {

0 comments on commit 0a35a80

Please sign in to comment.