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 () {