Skip to content

Commit

Permalink
Merge pull request #2310 from knockout/afterRender-generic
Browse files Browse the repository at this point in the history
add generic afterRender binding
  • Loading branch information
mbest committed Nov 4, 2017
2 parents d7fc725 + ccc5653 commit 3d7cf10
Show file tree
Hide file tree
Showing 12 changed files with 283 additions and 224 deletions.
58 changes: 58 additions & 0 deletions spec/bindingAttributeBehaviors.js
Expand Up @@ -583,4 +583,62 @@ describe('Binding attribute syntax', function() {
expect(testNode).toContainHtml('<p>replaced</p><template>test</template><p>replaced</p>');
});
});

it('Should call a childrenComplete callback function after descendent elements are bound', function () {
var callbacks = 0,
callback = function (nodes, data) {
expect(nodes.length).toEqual(1);
expect(nodes[0]).toEqual(testNode.childNodes[0].childNodes[0]);
expect(data).toEqual(vm);
callbacks++;
},
vm = { callback: callback };

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

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

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

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

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

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

it('Should call childrenComplete callback registered with ko.subscribeToBindingEvent', function () {
var callbacks = 0,
vm = {};

ko.subscribeToBindingEvent(testNode, "childrenComplete", function (node) {
callbacks++;
expect(node).toEqual(testNode);
expect(ko.dataFor(node)).toEqual(vm);
});

testNode.innerHTML = "<div></div>";
ko.applyBindings(vm, testNode);
expect(callbacks).toEqual(1);
});
});
71 changes: 71 additions & 0 deletions spec/bindingDependencyBehaviors.js
Expand Up @@ -250,6 +250,77 @@ describe('Binding dependencies', function() {
expect(testNode).toContainText('new value');
});

it('Should not cause updates if an observable accessed in a childrenComplete callback is changed', function () {
ko.bindingHandlers.test = {
init: function() {
return { controlsDescendantBindings: true };
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
ko.utils.unwrapObservable(valueAccessor());
element.innerHTML = "<span data-bind='text: childprop'></span>";
ko.applyBindingsToDescendants(bindingContext, element);
}
};

var callbackObservable = ko.observable(1),
bindingObservable = ko.observable(1),
callbacks = 0,
vm = {
childprop: 'child',
bindingObservable: bindingObservable,
callback: function () { callbackObservable(); callbacks++; }
};

testNode.innerHTML = "<div data-bind='test: bindingObservable, childrenComplete: callback'></div>";
ko.applyBindings(vm, testNode);
expect(callbacks).toEqual(1);

// Change the childprop which is not an observable so should not change the bound element
vm.childprop = 'new 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 bound observable and verify that the binding is now updated
bindingObservable(2);
expect(testNode.childNodes[0]).toContainText('new child');
expect(callbacks).toEqual(2);
});

it('Should always use the latest value of a childrenComplete callback', function () {
ko.bindingHandlers.test = {
init: function() {
return { controlsDescendantBindings: true };
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var innerContext = bindingContext.createChildContext({childprop: ko.utils.unwrapObservable(valueAccessor())});
element.innerHTML = "<span data-bind='text: childprop'></span>";
ko.applyBindingsToDescendants(innerContext, element);
}
};

var callbackSpy1 = jasmine.createSpy('callbackSpy1'),
callbackSpy2 = jasmine.createSpy('callbackSpy2'),
vm = {
observable: ko.observable('value'),
callback: callbackSpy1
};

testNode.innerHTML = "<div data-bind='test: observable, childrenComplete: callback'></div>";
ko.applyBindings(vm, testNode);
expect(callbackSpy1).toHaveBeenCalled();

callbackSpy1.reset();
vm.callback = callbackSpy2;

vm.observable('new value');
expect(testNode.childNodes[0]).toContainText('new value');
expect(callbackSpy1).not.toHaveBeenCalled();
expect(callbackSpy2).toHaveBeenCalled();
});

describe('Observable view models', function() {
it('Should update bindings (including callbacks)', function() {
var vm = ko.observable(), clickedVM;
Expand Down
21 changes: 21 additions & 0 deletions spec/components/componentBindingBehaviors.js
Expand Up @@ -651,6 +651,27 @@ describe('Components: Component binding', function() {
expect(testNode).toContainText('Hello! Your param is 456 Goodbye.');
});

it('Should call a childrenComplete callback function', function () {
testNode.innerHTML = '<div data-bind="component: testComponentBindingValue, childrenComplete: callback"></div>';
ko.components.register(testComponentName, { template: '<div data-bind="text: myvalue"></div>' });
testComponentParams.myvalue = 'some parameter value';

var callbacks = 0;
outerViewModel.callback = function (nodes, data) {
expect(nodes.length).toEqual(1);
expect(nodes[0]).toEqual(testNode.childNodes[0].childNodes[0]);
expect(data).toEqual(testComponentParams);
callbacks++;
};

ko.applyBindings(outerViewModel, testNode);
expect(callbacks).toEqual(0);

jasmine.Clock.tick(1);
expect(testNode.childNodes[0]).toContainHtml('<div data-bind="text: myvalue">some parameter value</div>');
expect(callbacks).toEqual(1);
});

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
20 changes: 20 additions & 0 deletions spec/components/customElementBehaviors.js
Expand Up @@ -508,4 +508,24 @@ describe('Components: Custom elements', function() {
+ '</li>'
);
});

it('Should call an afterRender callback function', function () {
ko.components.register('test-component', { template: 'custom element'});
testNode.innerHTML = '<test-component data-bind="afterRender: callback"></test-component>';

var callbacks = 0,
viewModel = {
callback: function (nodes, data) {
expect(nodes.length).toEqual(1);
expect(nodes[0]).toEqual(testNode.childNodes[0].childNodes[0]);
expect(data).toEqual(undefined);
callbacks++;
}
};
ko.applyBindings(viewModel, testNode);
expect(callbacks).toEqual(0);

jasmine.Clock.tick(1);
expect(testNode).toContainHtml('<test-component data-bind="afterrender: callback">custom element</test-component>');
});
});
65 changes: 2 additions & 63 deletions spec/defaultBindings/ifBehaviors.js
Expand Up @@ -97,23 +97,8 @@ describe('Binding: If', function() {
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>";
it('Should call a childrenComplete callback function', function () {
testNode.innerHTML = "<div data-bind='if: condition, childrenComplete: 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++; } };
Expand All @@ -129,50 +114,4 @@ describe('Binding: If', function() {
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;
});
});
65 changes: 2 additions & 63 deletions spec/defaultBindings/ifnotBehaviors.js
Expand Up @@ -63,23 +63,8 @@ describe('Binding: Ifnot', function() {
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>";
it('Should call a childrenComplete callback function', function () {
testNode.innerHTML = "<div data-bind='ifnot: condition, childrenComplete: 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++; } };
Expand All @@ -95,50 +80,4 @@ describe('Binding: Ifnot', function() {
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;
});
});

0 comments on commit 3d7cf10

Please sign in to comment.