Skip to content

Commit

Permalink
Rename afterRender to childrenComplete. Make it work with template an…
Browse files Browse the repository at this point in the history
…d foreach bindings. Add framework for addition binding events.
  • Loading branch information
mbest committed Nov 3, 2017
1 parent 785464f commit ccc5653
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 40 deletions.
31 changes: 23 additions & 8 deletions spec/bindingAttributeBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -584,7 +584,7 @@ describe('Binding attribute syntax', function() {
});
});

it('Should call an afterRender callback function after descendent elements are bound', function () {
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);
Expand All @@ -594,12 +594,12 @@ describe('Binding attribute syntax', function() {
},
vm = { callback: callback };

testNode.innerHTML = "<div data-bind='afterRender: callback'><span data-bind='text: \"Some Text\"'></span></div>";
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 an afterRender callback function when bound to a virtual element', function () {
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);
Expand All @@ -609,21 +609,36 @@ describe('Binding attribute syntax', function() {
},
vm = { callback: callback };

testNode.innerHTML = "<!-- ko afterRender: callback --><span data-bind='text: \"Some Text\"'></span><!-- /ko -->";
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 an afterRender callback function when there are no descendant nodes', function () {
it('Should not call a childrenComplete callback function when there are no descendant nodes', function () {
var callbacks = 0;

testNode.innerHTML = "<div data-bind='afterRender: callback'></span></div>";
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 afterRender callback', function () {
testNode.innerHTML = "<div data-bind='afterRender: null'><span data-bind='text: \"Some Text\"'></span></div>";
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);
});
});
8 changes: 4 additions & 4 deletions spec/bindingDependencyBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ describe('Binding dependencies', function() {
expect(testNode).toContainText('new value');
});

it('Should not cause updates if an observable accessed in an afterRender callback is changed', function () {
it('Should not cause updates if an observable accessed in a childrenComplete callback is changed', function () {
ko.bindingHandlers.test = {
init: function() {
return { controlsDescendantBindings: true };
Expand All @@ -271,7 +271,7 @@ describe('Binding dependencies', function() {
callback: function () { callbackObservable(); callbacks++; }
};

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

Expand All @@ -289,7 +289,7 @@ describe('Binding dependencies', function() {
expect(callbacks).toEqual(2);
});

it('Should always use the latest value of an afterRender callback', function () {
it('Should always use the latest value of a childrenComplete callback', function () {
ko.bindingHandlers.test = {
init: function() {
return { controlsDescendantBindings: true };
Expand All @@ -308,7 +308,7 @@ describe('Binding dependencies', function() {
callback: callbackSpy1
};

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

Expand Down
4 changes: 2 additions & 2 deletions spec/components/componentBindingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,8 +651,8 @@ describe('Components: Component binding', function() {
expect(testNode).toContainText('Hello! Your param is 456 Goodbye.');
});

it('Should call an afterRender callback function', function () {
testNode.innerHTML = '<div data-bind="component: testComponentBindingValue, afterRender: callback"></div>';
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';

Expand Down
4 changes: 2 additions & 2 deletions spec/defaultBindings/ifBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +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', 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 Down
4 changes: 2 additions & 2 deletions spec/defaultBindings/ifnotBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +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', 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 Down
4 changes: 2 additions & 2 deletions spec/defaultBindings/withBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ describe('Binding: With', function() {
expect(testNode).toContainText("Total: 25");
});

it('Should call an afterRender callback function', function () {
testNode.innerHTML = "<div data-bind='with: someItem, afterRender: callback'><span data-bind='text: childprop'></span></div>";
it('Should call a childrenComplete callback function', function () {
testNode.innerHTML = "<div data-bind='with: someItem, childrenComplete: callback'><span data-bind='text: childprop'></span></div>";
var someItem = ko.observable({ childprop: 'child' }),
callbacks = 0;
ko.applyBindings({ someItem: someItem, callback: function () { callbacks++; } }, testNode);
Expand Down
6 changes: 3 additions & 3 deletions spec/nodePreprocessingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('Node preprocessing', function() {
expect(testNode).toContainText('BetaBetaGammaGamma');
});

it('Should call an afterRender callback, passing all of the rendered nodes, accounting for node preprocessing and virtual element bindings', function () {
it('Should call a childrenComplete 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
ko.bindingProvider.instance.preprocessNode = function (node) {
if (node.nodeType === 3 && node.data.charAt(0) === "$") {
Expand All @@ -120,7 +120,7 @@ describe('Node preprocessing', function() {
}
};

// Now perform bindings, and see that afterRender gets the output from the preprocessor and bindings
// Now perform bindings, and see that childrenComplete gets the output from the preprocessor and bindings
var callbacks = 0,
vm = {
childprop: 'child property',
Expand All @@ -136,7 +136,7 @@ describe('Node preprocessing', function() {
}
};

testNode.innerHTML = "<div data-bind='afterRender: callback'><span>[</span>$data.childprop<span>]</span></div>";
testNode.innerHTML = "<div data-bind='childrenComplete: callback'><span>[</span>$data.childprop<span>]</span></div>";
ko.applyBindings(vm, testNode);
expect(testNode.childNodes[0]).toContainText('[child property]');
expect(callbacks).toBe(1);
Expand Down
14 changes: 14 additions & 0 deletions spec/templatingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,20 @@ describe('Templating', function() {
expect(testNode.childNodes[0].innerHTML).toEqual("result = 456");
});

it('Should call a generic childrenComplete callback function', function () {
ko.setTemplateEngine(new dummyTemplateEngine({ someTemplate: "result = [js: childProp]" }));
testNode.innerHTML = "<div data-bind='template: { name: \"someTemplate\", data: someItem }, childrenComplete: callback'></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('result = child');

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

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
53 changes: 38 additions & 15 deletions src/binding/bindingAttributeSyntax.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,28 @@
return this['createChildContext'](dataItemOrAccessor, dataItemAlias, null, { "exportDependencies": true });
};

var bindingEventsDomDataKey = ko.utils.domData.nextKey();

ko.subscribeToBindingEvent = function (node, event, callback) {
var eventSubscribable = ko.utils.domData.get(node, bindingEventsDomDataKey);
if (!eventSubscribable) {
ko.utils.domData.set(node, bindingEventsDomDataKey, eventSubscribable = new ko.subscribable);
}
return eventSubscribable.subscribe(callback, null, event);
};

ko.notifyBindingEvent = function (node, event) {
var eventSubscribable = ko.utils.domData.get(node, bindingEventsDomDataKey);
if (eventSubscribable) {
eventSubscribable['notifySubscribers'](node, event);
}
};

ko.bindingEvent = {
childrenComplete: "childrenComplete",
descendentsComplete : "descendentsComplete"
};

// Returns the valueAccessor function for a binding value
function makeValueAccessor(value) {
return function() {
Expand Down Expand Up @@ -204,8 +226,6 @@
throw new Error("The binding '" + bindingName + "' cannot be used with virtual elements")
}

var afterRenderCallbackDomDataKey = ko.utils.domData.nextKey();

function applyBindingsToDescendantsInternal(bindingContext, elementOrVirtualElement) {
var nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);

Expand All @@ -227,23 +247,16 @@
nextInQueue = ko.virtualElements.firstChild(elementOrVirtualElement);
}

var bindingApplied = false;
while (currentChild = nextInQueue) {
// Keep a record of the next child *before* applying bindings, in case the binding removes the current child from its position
nextInQueue = ko.virtualElements.nextSibling(currentChild);
applyBindingsToNodeAndDescendantsInternal(bindingContext, currentChild);
bindingApplied = true;
}

var afterRender = ko.utils.domData.get(elementOrVirtualElement, afterRenderCallbackDomDataKey);
if (afterRender) {
ko.dependencyDetection.ignore(function () {
afterRender = evaluateValueAccessor(afterRender);
if (afterRender) {
var nodes = ko.virtualElements.childNodes(elementOrVirtualElement);
if (nodes.length) {
afterRender(nodes, ko.dataFor(nodes[0]));
}
}
});
if (bindingApplied) {
ko.notifyBindingEvent(elementOrVirtualElement, ko.bindingEvent.childrenComplete);
}
}
}
Expand Down Expand Up @@ -366,8 +379,16 @@
return key in bindings;
};

if ("afterRender" in bindings) {
ko.utils.domData.set(node, afterRenderCallbackDomDataKey, getValueAccessor("afterRender"));
if (ko.bindingEvent.childrenComplete in bindings) {
ko.subscribeToBindingEvent(node, ko.bindingEvent.childrenComplete, function () {
var callback = evaluateValueAccessor(bindings[ko.bindingEvent.childrenComplete]);
if (callback) {
var nodes = ko.virtualElements.childNodes(node);
if (nodes.length) {
callback(nodes, ko.dataFor(nodes[0]));
}
}
});
}

// First put the bindings into the right order
Expand Down Expand Up @@ -482,6 +503,8 @@
};

ko.exportSymbol('bindingHandlers', ko.bindingHandlers);
ko.exportSymbol('subscribeToBindingEvent', ko.subscribeToBindingEvent);
ko.exportSymbol('notifyBindingEvent', ko.notifyBindingEvent);
ko.exportSymbol('applyBindings', ko.applyBindings);
ko.exportSymbol('applyBindingsToDescendants', ko.applyBindingsToDescendants);
ko.exportSymbol('applyBindingAccessorsToNode', ko.applyBindingAccessorsToNode);
Expand Down
8 changes: 6 additions & 2 deletions src/templating/templating.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,12 @@

if (haveAddedNodesToParent) {
activateBindingsOnContinuousNodeArray(renderedNodesArray, bindingContext);
if (options['afterRender'])
if (options['afterRender']) {
ko.dependencyDetection.ignore(options['afterRender'], null, [renderedNodesArray, bindingContext['$data']]);
}
if (renderMode == "replaceChildren") {
ko.notifyBindingEvent(targetNodeOrNodeArray, ko.bindingEvent.childrenComplete);
}
}

return renderedNodesArray;
Expand Down Expand Up @@ -205,7 +209,7 @@
// Call setDomNodeChildrenFromArrayMapping, ignoring any observables unwrapped within (most likely from a callback function).
// If the array items are observables, though, they will be unwrapped in executeTemplateForArrayItem and managed within setDomNodeChildrenFromArrayMapping.
ko.dependencyDetection.ignore(ko.utils.setDomNodeChildrenFromArrayMapping, null, [targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback]);

ko.notifyBindingEvent(targetNode, ko.bindingEvent.childrenComplete);
}, null, { disposeWhenNodeIsRemoved: targetNode });
};

Expand Down

0 comments on commit ccc5653

Please sign in to comment.