Skip to content

Commit

Permalink
Remove when binding; instead, the presence of descendantsComplete bin…
Browse files Browse the repository at this point in the history
…ding modifies how the if binding works

descendantsComplete binding can now be used on any node
Add a note that component contents might be updated while the component itself is being updated
Fix knockout#2343 - support applyBindingsToNode for components
  • Loading branch information
mbest committed Mar 21, 2018
1 parent d86f296 commit 0ac7010
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 232 deletions.
77 changes: 77 additions & 0 deletions spec/bindingAttributeBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -642,4 +642,81 @@ describe('Binding attribute syntax', function() {
ko.applyBindings(vm, testNode);
expect(callbacks).toEqual(1);
});

it('Should call a descendantsComplete callback function after descendant elements are bound', function () {
var callbacks = 0,
callback = function (node) {
expect(node).toEqual(testNode.childNodes[0]);
callbacks++;
},
vm = { callback: callback };

testNode.innerHTML = "<div data-bind='descendantsComplete: callback'><span data-bind='text: \"Some Text\"'></span></div>";
ko.applyBindings(vm, testNode);
expect(callbacks).toEqual(1);
});

it('Should call a descendantsComplete callback function when bound to a virtual element', function () {
var callbacks = 0,
callback = function (node) {
expect(node).toEqual(testNode.childNodes[1]);
callbacks++;
},
vm = { callback: callback };

testNode.innerHTML = "begin <!-- ko descendantsComplete: callback --><span data-bind='text: \"Some Text\"'></span><!-- /ko --> end";
ko.applyBindings(vm, testNode);
expect(callbacks).toEqual(1);
});

it('Should not call a descendantsComplete callback function when there are no descendant nodes', function () {
var callbacks = 0;

testNode.innerHTML = "<div data-bind='descendantsComplete: callback'></div>";
ko.applyBindings({ callback: function () { callbacks++; } }, testNode);
expect(callbacks).toEqual(0);
});

it('Should ignore (and not throw an error) for a null descendantsComplete callback', function () {
testNode.innerHTML = "<div data-bind='descendantsComplete: null'><span data-bind='text: \"Some Text\"'></span></div>";
ko.applyBindings({}, testNode);
});

it('Should call descendantsComplete callback registered with ko.bindingEvent.subscribe, if descendantsComplete is also present in the binding', function () {
var callbacks = 0;

testNode.innerHTML = "<div data-bind='descendantsComplete'><div></div></div>";
ko.bindingEvent.subscribe(testNode.childNodes[0], "descendantsComplete", function (node) {
callbacks++;
expect(node).toEqual(testNode.childNodes[0]);
});

ko.applyBindings({}, testNode);
expect(callbacks).toEqual(1);
});

it('Should throw an error if a descendantsComplete callback is registered when descendantsComplete is not present in the binding', function () {
var callbacks = 0;

testNode.innerHTML = "<div><div></div></div>";
ko.bindingEvent.subscribe(testNode.childNodes[0], "descendantsComplete", function (node) { callbacks++ });

expect(function () {
ko.applyBindings({}, testNode);
}).toThrowContaining("event not supported");
expect(callbacks).toEqual(0);
});

it('Should call a descendantsComplete callback function even if descendant element doesn\'t generate event', function () {
var callbacks = 0,
callback = function (node) {
expect(node).toEqual(testNode.childNodes[0]);
callbacks++;
},
vm = { callback: callback };

testNode.innerHTML = "<div data-bind='descendantsComplete: callback'><span data-bind='text: \"Some Text\"'></span><div data-bind='descendantsComplete'></div></div>";
ko.applyBindings(vm, testNode);
expect(callbacks).toEqual(1);
});
});
11 changes: 11 additions & 0 deletions spec/components/componentBindingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,17 @@ describe('Components: Component binding', function() {
expect(renderedComponents).toEqual([ 'sub-component1', 'test-component', 'sub-component2' ]);
});

it('Works with applyBindingsToNode', function() {
ko.components.register(testComponentName, {
template: 'Parent is outer view model: <span data-bind="text: $parent.isOuterViewModel"></span>'
});
testNode.innerHTML = "<div></div>";
ko.applyBindingsToNode(testNode.childNodes[0], {component: {name: testComponentName}}, outerViewModel);
jasmine.Clock.tick(1);

expect(testNode.childNodes[0]).toContainText('Parent is outer view model: true');
});

describe('Does not automatically subscribe to any observables you evaluate during createViewModel or a viewmodel constructor', function() {
// This clarifies that, if a developer wants to react when some observable parameter
// changes, then it's their responsibility to subscribe to it or use a computed.
Expand Down
10 changes: 7 additions & 3 deletions spec/components/customElementBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,18 +189,22 @@ describe('Components: Custom elements', function() {

it('Should update component when observable view model changes', function() {
ko.components.register('test-component', {
template: '<p>the value: <span data-bind="text: textToShow"></span></p>'
template: '<p>the component value: <span data-bind="text: textToShow"></span>, the root value: <span data-bind="text: $root.value"></span></p>'
});

testNode.innerHTML = '<test-component params="textToShow: value"></test-component>';
var vm = ko.observable({ value: 'A' });
ko.applyBindings(vm, testNode);
jasmine.Clock.tick(1);
expect(testNode).toContainText("the value: A");
expect(testNode).toContainText("the component value: A, the root value: A");

vm({ value: 'Z' });
// The view-model change updates the old component contents before the new contents get rendered.
// This is the way it has always worked, but maybe this isn't the best experience
expect(testNode).toContainText("the component value: A, the root value: Z");

jasmine.Clock.tick(1);
expect(testNode).toContainText("the value: Z");
expect(testNode).toContainText("the component value: Z, the root value: Z");
});

it('Is possible to pass observable instances', function() {
Expand Down
76 changes: 76 additions & 0 deletions spec/defaultBindings/ifBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ describe('Binding: If', function() {
var someItem = ko.observable({ childprop: 'child' }),
callbacks = 0;
var viewModel = { condition: ko.observable(true), someText: "hello", callback: function () { callbacks++; } };

ko.applyBindings(viewModel, testNode);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0]).toContainText('hello');
Expand All @@ -114,4 +115,79 @@ describe('Binding: If', function() {
expect(callbacks).toEqual(2);
expect(testNode.childNodes[0]).toContainText('hello');
});

it('Should call a descendantsComplete callback function', function () {
testNode.innerHTML = "<div data-bind='if: condition, descendantsComplete: callback'><span data-bind='text: someText'></span></div>";
var someItem = ko.observable({ childprop: 'child' }),
callbacks = 0;
var viewModel = { condition: ko.observable(false), someText: "hello", callback: function () { callbacks++; } };

ko.applyBindings(viewModel, testNode);
expect(callbacks).toEqual(0);
expect(testNode.childNodes[0]).toContainText('');

viewModel.condition(true);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0]).toContainText('hello');
});

it('Should call a descendantsComplete callback function after nested \"if\" bindings are complete', function () {
testNode.innerHTML = "<div data-bind='if: outerCondition, descendantsComplete: callback'><div data-bind='if: innerCondition, descendantsComplete'><span data-bind='text: someText'></span></div></div>";
var someItem = ko.observable({ childprop: 'child' }),
callbacks = 0;
var viewModel = { outerCondition: ko.observable(false), innerCondition: ko.observable(false), someText: "hello", callback: function () { callbacks++; } };

ko.applyBindings(viewModel, testNode);
expect(callbacks).toEqual(0);
expect(testNode.childNodes[0]).toContainText('');

// Complete the outer condition first and then the inner one
viewModel.outerCondition(true);
expect(callbacks).toEqual(0);
expect(testNode.childNodes[0]).toContainText('');

viewModel.innerCondition(true);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0]).toContainText('hello');
});

it('Should call descendantsComplete callback function when nested \"if\" bindings are complete', function () {
testNode.innerHTML = "<div data-bind='if: outerCondition, descendantsComplete: callback'><div data-bind='if: innerCondition, descendantsComplete'><span data-bind='text: someText'></span></div></div>";
var someItem = ko.observable({ childprop: 'child' }),
callbacks = 0;
var viewModel = { outerCondition: ko.observable(false), innerCondition: ko.observable(false), someText: "hello", callback: function () { callbacks++; } };

ko.applyBindings(viewModel, testNode);
expect(callbacks).toEqual(0);
expect(testNode.childNodes[0]).toContainText('');

// Complete the inner condition first and then the outer one (reverse order from previous test)
viewModel.innerCondition(true);
expect(callbacks).toEqual(0);
expect(testNode.childNodes[0]).toContainText('');

viewModel.outerCondition(true);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0]).toContainText('hello');
});

it('Should call descendantsComplete callback function if nested \"if\" bindings are disposed before completion', function () {
testNode.innerHTML = "<div data-bind='if: outerCondition, descendantsComplete: callback'><div data-bind='if: innerCondition, descendantsComplete'><span data-bind='text: someText'></span></div></div>";
var someItem = ko.observable({ childprop: 'child' }),
callbacks = 0;
var viewModel = { outerCondition: ko.observable(false), innerCondition: ko.observable(false), someText: "hello", callback: function () { callbacks++; } };

ko.applyBindings(viewModel, testNode);
expect(callbacks).toEqual(0);
expect(testNode.childNodes[0]).toContainText('');

// Complete the outer condition and then dispose the inner one
viewModel.outerCondition(true);
expect(callbacks).toEqual(0);
expect(testNode.childNodes[0]).toContainText('');

ko.cleanNode(testNode.childNodes[0].childNodes[0]);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0]).toContainText('');
});
});
155 changes: 0 additions & 155 deletions spec/defaultBindings/whenBehaviors.js

This file was deleted.

1 change: 0 additions & 1 deletion spec/runner.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@
<script type="text/javascript" src="defaultBindings/usingBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/valueBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/visibleHiddenBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/whenBehaviors.js"></script>
<script type="text/javascript" src="defaultBindings/withBehaviors.js"></script>

<script type="text/javascript" src="crossWindowBehaviors.js"></script>
Expand Down
8 changes: 8 additions & 0 deletions spec/templatingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,14 @@ describe('Templating', function() {
expect(testNode.childNodes[0]).toContainText('result = new child');
});

it('Should not call a childrenComplete callback function if template is empty', function () {
ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "" }));
testNode.innerHTML = "<div data-bind='template: { name: \"someTemplate\" }, childrenComplete: callback'></div>";
var callbacks = 0;
ko.applyBindings({ callback: function () { callbacks++; } }, testNode);
expect(callbacks).toEqual(0);
});

it('Should stop tracking inner observables immediately when the container node is removed from the document', function() {
var innerObservable = ko.observable("some value");
ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp()]" }));
Expand Down
Loading

0 comments on commit 0ac7010

Please sign in to comment.