Skip to content

Commit

Permalink
Merge pull request #1811 from knockout/1811-using-binding
Browse files Browse the repository at this point in the history
Add new with-type binding that is useful for tracking non-identity changes
  • Loading branch information
mbest committed Dec 6, 2016
2 parents ac30244 + 7785a44 commit 0a35a80
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 1 deletion.
1 change: 1 addition & 0 deletions build/fragments/source-references.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
192 changes: 192 additions & 0 deletions spec/defaultBindings/usingBehaviors.js
Original file line number Diff line number Diff line change
@@ -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']);
});
});
1 change: 1 addition & 0 deletions spec/runner.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
9 changes: 9 additions & 0 deletions src/binding/defaultBindings/using.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 0a35a80

Please sign in to comment.