Skip to content

Commit

Permalink
Support afterRender for if, ifnot and with bindings, fixes #2099
Browse files Browse the repository at this point in the history
Adds support for `afterRender` callback to `if`, `ifnot` and `with`
binding.
  • Loading branch information
cervengoc committed Jul 25, 2016
1 parent 241c26c commit 5d3d60a
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 1 deletion.
79 changes: 79 additions & 0 deletions spec/defaultBindings/ifBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,83 @@ describe('Binding: If', function() {
condition2(true);
expect(testNode).toContainHtml("hello <!-- ko if: condition1 -->first is true<!-- ko if: condition2 -->both are true<!-- /ko --><!-- /ko -->");
});

it('Should call an afterRender callback function and not cause updates if an observable accessed in the callback is changed', function () {
testNode.innerHTML = "<div data-bind='if: condition, afterRender: callback'><span data-bind='text: someText'></span></div>";
var callbackObservable = ko.observable(1),
callbacks = 0;
var viewModel = { condition: ko.observable(true), someText: "hello", callback: function() { callbackObservable(); callbacks++; } };
ko.applyBindings(viewModel, testNode);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0]).toContainText('hello');

viewModel.someText = "bello";
// Update callback observable and check that the binding wasn't updated
callbackObservable(2);
expect(testNode.childNodes[0]).toContainText('hello');
});

it('Should not call an afterRender callback function when data gets cleared', function () {
testNode.innerHTML = "<div data-bind='if: condition, afterRender: callback'><span data-bind='text: someText'></span></div>";
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');

viewModel.condition(false);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0].childNodes.length).toEqual(0);

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

it('Should call an afterRender callback, passing all of the rendered nodes, accounting for node preprocessing and virtual element bindings', function () {
// Set up a binding provider that converts text nodes to expressions
var originalBindingProvider = ko.bindingProvider.instance,
preprocessingBindingProvider = function () { };
preprocessingBindingProvider.prototype = originalBindingProvider;
ko.bindingProvider.instance = new preprocessingBindingProvider();
ko.bindingProvider.instance.preprocessNode = function (node) {
if (node.nodeType === 3 && node.data.charAt(0) === "$") {
var newNodes = [
document.createComment('ko text: ' + node.data),
document.createComment('/ko')
];
for (var i = 0; i < newNodes.length; i++) {
node.parentNode.insertBefore(newNodes[i], node);
}
node.parentNode.removeChild(node);
return newNodes;
}
};

// Now perform a with binding, and see that afterRender gets the output from the preprocessor and bindings
testNode.innerHTML = "<div data-bind='if: condition, afterRender: callback'><span>[</span>$data.someText<span>]</span></div>";
var callbacks = 0;
var viewModel = {
condition: ko.observable(true),
someText: "hello",
callback: function (nodes, data) {
expect(nodes.length).toBe(5);
expect(nodes[0]).toContainText('['); // <span>[</span>
expect(nodes[1].nodeType).toBe(8); // <!-- ko text: $data.childprop -->
expect(nodes[2].nodeType).toBe(3); // text node inserted by text binding
expect(nodes[3].nodeType).toBe(8); // <!-- /ko -->
expect(nodes[4]).toContainText(']'); // <span>]</span>
expect(data).toBe(viewModel);
callbacks++;
}
};

ko.applyBindings(viewModel, testNode);

expect(testNode.childNodes[0]).toContainText('[hello]');
expect(callbacks).toBe(1);

ko.bindingProvider.instance = originalBindingProvider;
});
});
79 changes: 79 additions & 0 deletions spec/defaultBindings/ifnotBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,83 @@ describe('Binding: Ifnot', function() {
expect(testNode.childNodes[0]).toContainText("Parents: 0");
expect(ko.contextFor(testNode.childNodes[0].childNodes[1]).$parents.length).toEqual(0);
});

it('Should call an afterRender callback function and not cause updates if an observable accessed in the callback is changed', function () {
testNode.innerHTML = "<div data-bind='ifnot: condition, afterRender: callback'><span data-bind='text: someText'></span></div>";
var callbackObservable = ko.observable(1),
callbacks = 0;
var viewModel = { condition: ko.observable(false), someText: "hello", callback: function() { callbackObservable(); callbacks++; } };
ko.applyBindings(viewModel, testNode);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0]).toContainText('hello');

viewModel.someText = "bello";
// Update callback observable and check that the binding wasn't updated
callbackObservable(2);
expect(testNode.childNodes[0]).toContainText('hello');
});

it('Should not call an afterRender callback function when data gets cleared', function () {
testNode.innerHTML = "<div data-bind='ifnot: condition, afterRender: 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(1);
expect(testNode.childNodes[0]).toContainText('hello');

viewModel.condition(true);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0].childNodes.length).toEqual(0);

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

it('Should call an afterRender callback, passing all of the rendered nodes, accounting for node preprocessing and virtual element bindings', function () {
// Set up a binding provider that converts text nodes to expressions
var originalBindingProvider = ko.bindingProvider.instance,
preprocessingBindingProvider = function () { };
preprocessingBindingProvider.prototype = originalBindingProvider;
ko.bindingProvider.instance = new preprocessingBindingProvider();
ko.bindingProvider.instance.preprocessNode = function (node) {
if (node.nodeType === 3 && node.data.charAt(0) === "$") {
var newNodes = [
document.createComment('ko text: ' + node.data),
document.createComment('/ko')
];
for (var i = 0; i < newNodes.length; i++) {
node.parentNode.insertBefore(newNodes[i], node);
}
node.parentNode.removeChild(node);
return newNodes;
}
};

// Now perform a with binding, and see that afterRender gets the output from the preprocessor and bindings
testNode.innerHTML = "<div data-bind='ifnot: condition, afterRender: callback'><span>[</span>$data.someText<span>]</span></div>";
var callbacks = 0;
var viewModel = {
condition: ko.observable(false),
someText: "hello",
callback: function (nodes, data) {
expect(nodes.length).toBe(5);
expect(nodes[0]).toContainText('['); // <span>[</span>
expect(nodes[1].nodeType).toBe(8); // <!-- ko text: $data.childprop -->
expect(nodes[2].nodeType).toBe(3); // text node inserted by text binding
expect(nodes[3].nodeType).toBe(8); // <!-- /ko -->
expect(nodes[4]).toContainText(']'); // <span>]</span>
expect(data).toBe(viewModel);
callbacks++;
}
};

ko.applyBindings(viewModel, testNode);

expect(testNode.childNodes[0]).toContainText('[hello]');
expect(callbacks).toBe(1);

ko.bindingProvider.instance = originalBindingProvider;
});
});
81 changes: 81 additions & 0 deletions spec/defaultBindings/withBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,85 @@ describe('Binding: With', function() {
item('three');
expect(testNode.childNodes[0]).toHaveValues(['three']);
});

it('Should call an afterRender callback function and not cause updates if an observable accessed in the callback is changed', function () {
testNode.innerHTML = "<div data-bind='with: someItem, afterRender: callback'><span data-bind='text: childprop'></span></div>";
var callbackObservable = ko.observable(1),
someItem = ko.observable({ childprop: 'child' }),
callbacks = 0;
ko.applyBindings({ someItem: someItem, callback: function () { callbackObservable(); callbacks++; } }, testNode);
expect(callbacks).toEqual(1);

// Change the childprop which is not an observable so should not change the bound element
someItem().childprop = 'hidden child';
expect(testNode.childNodes[0]).toContainText('child');
// Update callback observable and check that the binding wasn't updated
callbackObservable(2);
expect(testNode.childNodes[0]).toContainText('child');
// Update the observableArray and verify that the binding is now updated
someItem({ childprop: 'new child' });
expect(testNode.childNodes[0]).toContainText('new child');
expect(callbacks).toEqual(2);
});

it('Should not call an afterRender callback function when data gets cleared', function () {
testNode.innerHTML = "<div data-bind='with: someItem, afterRender: callback'><span data-bind='text: childprop'></span></div>";
var someItem = ko.observable({ childprop: 'child' }),
callbacks = 0;
ko.applyBindings({ someItem: someItem, callback: function () { callbacks++; } }, testNode);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0]).toContainText('child');

someItem(null);
expect(callbacks).toEqual(1);
expect(testNode.childNodes[0].childNodes.length).toEqual(0);

someItem({ childprop: "new child" });
expect(callbacks).toEqual(2);
expect(testNode.childNodes[0]).toContainText('new child');
});

it('Should call an afterRender callback, passing all of the rendered nodes, accounting for node preprocessing and virtual element bindings', function () {
// Set up a binding provider that converts text nodes to expressions
var originalBindingProvider = ko.bindingProvider.instance,
preprocessingBindingProvider = function () { };
preprocessingBindingProvider.prototype = originalBindingProvider;
ko.bindingProvider.instance = new preprocessingBindingProvider();
ko.bindingProvider.instance.preprocessNode = function (node) {
if (node.nodeType === 3 && node.data.charAt(0) === "$") {
var newNodes = [
document.createComment('ko text: ' + node.data),
document.createComment('/ko')
];
for (var i = 0; i < newNodes.length; i++) {
node.parentNode.insertBefore(newNodes[i], node);
}
node.parentNode.removeChild(node);
return newNodes;
}
};

// Now perform a with binding, and see that afterRender gets the output from the preprocessor and bindings
testNode.innerHTML = "<div data-bind='with: someItem, afterRender: callback'><span>[</span>$data.childprop<span>]</span></div>";
var someItem = ko.observable({ childprop: 'child property' }),
callbacks = 0;
ko.applyBindings({
someItem: someItem,
callback: function (nodes, data) {
expect(nodes.length).toBe(5);
expect(nodes[0]).toContainText('['); // <span>[</span>
expect(nodes[1].nodeType).toBe(8); // <!-- ko text: $data.childprop -->
expect(nodes[2].nodeType).toBe(3); // text node inserted by text binding
expect(nodes[3].nodeType).toBe(8); // <!-- /ko -->
expect(nodes[4]).toContainText(']'); // <span>]</span>
expect(data).toBe(someItem());
callbacks++;
}
}, testNode);

expect(testNode.childNodes[0]).toContainText('[child property]');
expect(callbacks).toBe(1);

ko.bindingProvider.instance = originalBindingProvider;
});
});
7 changes: 6 additions & 1 deletion src/binding/defaultBindings/ifIfnotWith.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ function makeWithIfBinding(bindingKey, isWith, isNot, makeContextCallback) {
if (!isFirstRender) {
ko.virtualElements.setDomNodeChildren(element, ko.utils.cloneNodes(savedNodes));
}
ko.applyBindingsToDescendants(makeContextCallback ? makeContextCallback(bindingContext, rawValue) : bindingContext, element);
var newContext = makeContextCallback ? makeContextCallback(bindingContext, rawValue) : bindingContext;
ko.applyBindingsToDescendants(newContext, element);
if (element.childNodes.length) {
if (allBindings.has('afterRender'))
ko.dependencyDetection.ignore(allBindings.get('afterRender'), null, [element.childNodes, newContext['$data']]);
}
} else {
ko.virtualElements.emptyNode(element);
}
Expand Down

0 comments on commit 5d3d60a

Please sign in to comment.