diff --git a/spec/bindingAttributeBehaviors.js b/spec/bindingAttributeBehaviors.js
index 5ca525a60..9205d27c5 100644
--- a/spec/bindingAttributeBehaviors.js
+++ b/spec/bindingAttributeBehaviors.js
@@ -236,6 +236,7 @@ describe('Binding attribute syntax', function() {
var allowedProperties = ['$parents', '$root', 'ko', '$rawData', '$data', '$parentContext', '$parent'];
if (ko.utils.createSymbolOrString('') === '') {
allowedProperties.push('_subscribable');
+ allowedProperties.push('_ancestorAsyncContext');
}
ko.utils.objectForEach(ko.contextFor(testNode.childNodes[0].childNodes[0]), function (prop) {
expect(allowedProperties).toContain(prop);
@@ -584,7 +585,7 @@ describe('Binding attribute syntax', function() {
});
});
- it('Should call a childrenComplete callback function after descendent elements are bound', function () {
+ it('Should call a childrenComplete callback function after descendant elements are bound', function () {
var callbacks = 0,
callback = function (nodes, data) {
expect(nodes.length).toEqual(1);
@@ -627,11 +628,11 @@ describe('Binding attribute syntax', function() {
ko.applyBindings({}, testNode);
});
- it('Should call childrenComplete callback registered with ko.subscribeToBindingEvent', function () {
+ it('Should call childrenComplete callback registered with ko.bindingEvent.subscribe', function () {
var callbacks = 0,
vm = {};
- ko.subscribeToBindingEvent(testNode, "childrenComplete", function (node) {
+ ko.bindingEvent.subscribe(testNode, "childrenComplete", function (node) {
callbacks++;
expect(node).toEqual(testNode);
expect(ko.dataFor(node)).toEqual(vm);
diff --git a/spec/components/componentBindingBehaviors.js b/spec/components/componentBindingBehaviors.js
index d1dd4f171..91fd60dd3 100644
--- a/spec/components/componentBindingBehaviors.js
+++ b/spec/components/componentBindingBehaviors.js
@@ -322,6 +322,32 @@ describe('Components: Component binding', function() {
expect(renderedComponents).toEqual([ 'sub-component1', 'sub-component2', 'test-component' ]);
});
+ it('afterRender waits for inner component to complete even if it is several layers down', function() {
+ this.after(function() {
+ ko.components.unregister('sub-component');
+ });
+
+ var renderedComponents = [];
+ ko.components.register(testComponentName, {
+ template: '
',
+ viewModel: function() {
+ this.afterRender = function (element) { renderedComponents.push(testComponentName); };
+ }
+ });
+
+ ko.components.register('sub-component', {
+ template: '',
+ viewModel: function(params) {
+ this.myvalue = params;
+ this.afterRender = function () { renderedComponents.push('sub-component' + params); };
+ }
+ });
+
+ ko.applyBindings(outerViewModel, testNode);
+ jasmine.Clock.tick(1);
+ expect(renderedComponents).toEqual([ 'sub-component1', 'test-component' ]);
+ });
+
it('afterRender waits for inner components that are not yet loaded', function() {
this.restoreAfter(window, 'require');
this.after(function() {
diff --git a/spec/observableArrayChangeTrackingBehaviors.js b/spec/observableArrayChangeTrackingBehaviors.js
index ddcc63337..e6f48cdb2 100644
--- a/spec/observableArrayChangeTrackingBehaviors.js
+++ b/spec/observableArrayChangeTrackingBehaviors.js
@@ -373,7 +373,7 @@ describe('Observable Array change tracking', function() {
};
var list = ko.observableArray([]);
- // This adds all descendent nodes to the list when a node is added
+ // This adds all descendant nodes to the list when a node is added
list.subscribe(function (events) {
events = events.slice(0);
for (var i = 0; i < events.length; i++) {
@@ -388,7 +388,7 @@ describe('Observable Array change tracking', function() {
// Add the top-level node
list.push(toAdd);
- // See that descendent nodes are also added
+ // See that descendant nodes are also added
expect(list()).toEqual([ toAdd, toAdd.nodes[0], toAdd.nodes[1], toAdd.nodes[2], toAdd.nodes[0].nodes[0] ]);
});
diff --git a/src/binding/bindingAttributeSyntax.js b/src/binding/bindingAttributeSyntax.js
index 296e2ca5a..8629551e1 100755
--- a/src/binding/bindingAttributeSyntax.js
+++ b/src/binding/bindingAttributeSyntax.js
@@ -1,6 +1,7 @@
(function () {
// Hide or don't minify context properties, see https://github.com/knockout/knockout/issues/2294
var contextSubscribable = ko.utils.createSymbolOrString('_subscribable');
+ var contextAncestorAsyncContext = ko.utils.createSymbolOrString('_ancestorAsyncContext');
ko.bindingHandlers = {};
@@ -47,6 +48,11 @@
// Copy $root and any custom properties from the parent context
ko.utils.extend(self, parentContext);
+ // Copy Symbol properties
+ if (contextAncestorAsyncContext in parentContext) {
+ self[contextAncestorAsyncContext] = parentContext[contextAncestorAsyncContext];
+ }
+
// Because the above copy overwrites our own properties, we need to reset them.
self[contextSubscribable] = subscribable;
} else {
@@ -158,26 +164,83 @@
return this['createChildContext'](dataItemOrAccessor, dataItemAlias, null, { "exportDependencies": true });
};
- var bindingEventsDomDataKey = ko.utils.domData.nextKey();
+ var boundElementDomDataKey = ko.utils.domData.nextKey();
+
+ function AsyncCompleteContext(node, bindingInfo, parentContext) {
+ this.node = node;
+ this.bindingInfo = bindingInfo;
+ this.asyncDescendants = [];
+ this.childrenComplete = false;
- 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);
+ function dispose() {
+ if (bindingInfo.asyncContext && bindingInfo.asyncContext.parentContext) {
+ bindingInfo.asyncContext.parentContext.descendantComplete(node);
+ }
+ bindingInfo.asyncContext = undefined;
+ };
+ ko.utils.domNodeDisposal.addDisposeCallback(node, dispose);
+ this.disposalCallback = dispose; // so we can remove the disposal callback later
+
+ if (parentContext) {
+ parentContext.asyncDescendants.push(node);
+ this.parentContext = parentContext;
+ }
+ }
+ AsyncCompleteContext.prototype.descendantComplete = function (node) {
+ ko.utils.arrayRemoveItem(this.asyncDescendants, node);
+ if (!this.asyncDescendants.length && this.childrenComplete) {
+ this.completeChildren();
}
- 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);
+ AsyncCompleteContext.prototype.completeChildren = function () {
+ this.childrenComplete = true;
+ if (this.bindingInfo.asyncContext && !this.asyncDescendants.length) {
+ this.bindingInfo.asyncContext = undefined;
+ ko.utils.domNodeDisposal.removeDisposeCallback(this.node, this.disposalCallback);
+ ko.bindingEvent.notify(this.node, ko.bindingEvent.descendantsComplete);
+ if (this.parentContext) {
+ this.parentContext.descendantComplete(this.node);
+ }
}
};
+ AsyncCompleteContext.prototype.createChildContext = function (dataItemOrAccessor, dataItemAlias, extendCallback, options) {
+ var self = this;
+ return this.bindingInfo.context['createChildContext'](dataItemOrAccessor, dataItemAlias, function (ctx) {
+ extendCallback(ctx);
+ ctx[contextAncestorAsyncContext] = self;
+ }, options);
+ };
ko.bindingEvent = {
childrenComplete: "childrenComplete",
- descendentsComplete : "descendentsComplete"
+ descendantsComplete : "descendantsComplete",
+
+ subscribe: function (node, event, callback, context) {
+ var bindingInfo = ko.utils.domData.getOrSet(node, boundElementDomDataKey, {});
+ if (!bindingInfo.eventSubscribable) {
+ bindingInfo.eventSubscribable = new ko.subscribable;
+ }
+ return bindingInfo.eventSubscribable.subscribe(callback, context, event);
+ },
+
+ notify: function (node, event) {
+ var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey);
+ if (bindingInfo) {
+ if (bindingInfo.eventSubscribable) {
+ bindingInfo.eventSubscribable['notifySubscribers'](node, event);
+ }
+ if (event == ko.bindingEvent.childrenComplete && bindingInfo.asyncContext) {
+ bindingInfo.asyncContext.completeChildren();
+ }
+ }
+ },
+
+ startPossiblyAsyncContentBinding: function (node) {
+ var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey);
+ if (bindingInfo) {
+ return bindingInfo.asyncContext || (bindingInfo.asyncContext = new AsyncCompleteContext(node, bindingInfo, bindingInfo.context[contextAncestorAsyncContext]));
+ }
+ }
};
// Returns the valueAccessor function for a binding value
@@ -255,9 +318,7 @@
bindingApplied = true;
}
- if (bindingApplied) {
- ko.notifyBindingEvent(elementOrVirtualElement, ko.bindingEvent.childrenComplete);
- }
+ ko.bindingEvent.notify(elementOrVirtualElement, ko.bindingEvent.childrenComplete);
}
}
@@ -280,8 +341,6 @@
}
}
- var boundElementDomDataKey = ko.utils.domData.nextKey();
-
function topologicalSortBindings(bindings) {
// Depth-first sort
var result = [], // The list of key/handler pairs that we will return
@@ -317,15 +376,16 @@
function applyBindingsToNodeInternal(node, sourceBindings, bindingContext) {
// Prevent multiple applyBindings calls for the same node, except when a binding value is specified
- var bindingInfo = ko.utils.domData.get(node, boundElementDomDataKey);
if (!sourceBindings) {
- if (bindingInfo) {
+ var bindingInfo = ko.utils.domData.getOrSet(node, boundElementDomDataKey, {});
+ if (bindingInfo.context) {
throw Error("You cannot apply bindings multiple times to the same element.");
}
- ko.utils.domData.set(node, boundElementDomDataKey, {context: bindingContext});
- if (bindingContext[contextSubscribable])
+ bindingInfo.context = bindingContext;
+ if (bindingContext[contextSubscribable]) {
bindingContext[contextSubscribable]._addNode(node);
+ }
}
// Use bindings if given, otherwise fall back on asking the bindings provider to give us some bindings
@@ -380,7 +440,7 @@
};
if (ko.bindingEvent.childrenComplete in bindings) {
- ko.subscribeToBindingEvent(node, ko.bindingEvent.childrenComplete, function () {
+ ko.bindingEvent.subscribe(node, ko.bindingEvent.childrenComplete, function () {
var callback = evaluateValueAccessor(bindings[ko.bindingEvent.childrenComplete]);
if (callback) {
var nodes = ko.virtualElements.childNodes(node);
@@ -503,8 +563,8 @@
};
ko.exportSymbol('bindingHandlers', ko.bindingHandlers);
- ko.exportSymbol('subscribeToBindingEvent', ko.subscribeToBindingEvent);
- ko.exportSymbol('notifyBindingEvent', ko.notifyBindingEvent);
+ ko.exportSymbol('bindingEvent', ko.bindingEvent);
+ ko.exportSymbol('bindingEvent.subscribe', ko.bindingEvent.subscribe);
ko.exportSymbol('applyBindings', ko.applyBindings);
ko.exportSymbol('applyBindingsToDescendants', ko.applyBindingsToDescendants);
ko.exportSymbol('applyBindingAccessorsToNode', ko.applyBindingAccessorsToNode);
diff --git a/src/components/componentBinding.js b/src/components/componentBinding.js
index ce7c31119..a1462fa79 100644
--- a/src/components/componentBinding.js
+++ b/src/components/componentBinding.js
@@ -1,63 +1,28 @@
(function(undefined) {
var componentLoadingOperationUniqueId = 0;
- function ComponentDisplayDeferred(element, parentComponentDeferred, replacedDeferred) {
- var subscribable = new ko.subscribable();
- this.subscribable = subscribable;
-
- this._componentsToComplete = 1;
-
- this.componentComplete = function () {
- if (subscribable && !--this._componentsToComplete) {
- subscribable['notifySubscribers'](element);
- subscribable = undefined;
- if (parentComponentDeferred) {
- parentComponentDeferred.componentComplete();
- }
- }
- };
- this.dispose = function (shouldReject) {
- if (subscribable) {
- this._componentsToComplete = 0;
- subscribable = undefined;
- if (parentComponentDeferred) {
- parentComponentDeferred.componentComplete();
- }
- }
- };
-
- if (parentComponentDeferred) {
- ++parentComponentDeferred._componentsToComplete;
- }
-
- if (replacedDeferred) {
- replacedDeferred.dispose();
- }
- }
-
ko.bindingHandlers['component'] = {
'init': function(element, valueAccessor, ignored1, ignored2, bindingContext) {
var currentViewModel,
currentLoadingOperationId,
- displayedDeferred,
+ afterRenderSub,
disposeAssociatedComponentViewModel = function () {
var currentViewModelDispose = currentViewModel && currentViewModel['dispose'];
if (typeof currentViewModelDispose === 'function') {
currentViewModelDispose.call(currentViewModel);
}
+ if (afterRenderSub) {
+ afterRenderSub.dispose();
+ }
+ afterRenderSub = null;
currentViewModel = null;
// Any in-flight loading operation is no longer relevant, so make sure we ignore its completion
currentLoadingOperationId = null;
},
originalChildNodes = ko.utils.makeArray(ko.virtualElements.childNodes(element));
- ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
- disposeAssociatedComponentViewModel();
- if (displayedDeferred) {
- displayedDeferred.dispose();
- displayedDeferred = null;
- }
- });
+ ko.virtualElements.emptyNode(element);
+ ko.utils.domNodeDisposal.addDisposeCallback(element, disposeAssociatedComponentViewModel);
ko.computed(function () {
var value = ko.utils.unwrapObservable(valueAccessor()),
@@ -74,7 +39,7 @@
throw new Error('No component name specified');
}
- displayedDeferred = new ComponentDisplayDeferred(element, bindingContext._componentDisplayDeferred, displayedDeferred);
+ var asyncContext = ko.bindingEvent.startPossiblyAsyncContentBinding(element);
var loadingOperationId = currentLoadingOperationId = ++componentLoadingOperationUniqueId;
ko.components.get(componentName, function(componentDefinition) {
@@ -98,20 +63,17 @@
};
var componentViewModel = createViewModel(componentDefinition, componentParams, componentInfo),
- childBindingContext = bindingContext['createChildContext'](componentViewModel, /* dataItemAlias */ undefined, function(ctx) {
+ childBindingContext = asyncContext.createChildContext(componentViewModel, /* dataItemAlias */ undefined, function(ctx) {
ctx['$component'] = componentViewModel;
ctx['$componentTemplateNodes'] = originalChildNodes;
- ctx._componentDisplayDeferred = displayedDeferred;
});
if (componentViewModel && componentViewModel['afterRender']) {
- displayedDeferred.subscribable.subscribe(componentViewModel['afterRender']);
+ afterRenderSub = ko.bindingEvent.subscribe(element, "descendantsComplete", componentViewModel['afterRender'], componentViewModel);
}
currentViewModel = componentViewModel;
ko.applyBindingsToDescendants(childBindingContext, element);
-
- displayedDeferred.componentComplete();
});
}, null, { disposeWhenNodeIsRemoved: element });
diff --git a/src/templating/templating.js b/src/templating/templating.js
index 239ddf27e..0dbcdf09f 100644
--- a/src/templating/templating.js
+++ b/src/templating/templating.js
@@ -110,7 +110,7 @@
ko.dependencyDetection.ignore(options['afterRender'], null, [renderedNodesArray, bindingContext['$data']]);
}
if (renderMode == "replaceChildren") {
- ko.notifyBindingEvent(targetNodeOrNodeArray, ko.bindingEvent.childrenComplete);
+ ko.bindingEvent.notify(targetNodeOrNodeArray, ko.bindingEvent.childrenComplete);
}
}
@@ -209,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);
+ ko.bindingEvent.notify(targetNode, ko.bindingEvent.childrenComplete);
}, null, { disposeWhenNodeIsRemoved: targetNode });
};
diff --git a/src/utils.domData.js b/src/utils.domData.js
index 2db8ab077..ad3e0c578 100644
--- a/src/utils.domData.js
+++ b/src/utils.domData.js
@@ -58,6 +58,10 @@ ko.utils.domData = new (function () {
var dataForNode = getDataForNode(node, value !== undefined /* createIfNotFound */);
dataForNode && (dataForNode[key] = value);
},
+ getOrSet: function (node, key, value) {
+ var dataForNode = getDataForNode(node, true /* createIfNotFound */);
+ return dataForNode[key] || (dataForNode[key] = value);
+ },
clear: clear,
nextKey: function () {