diff --git a/spec/components/componentBindingBehaviors.js b/spec/components/componentBindingBehaviors.js index 00eb094e1..91c448406 100644 --- a/spec/components/componentBindingBehaviors.js +++ b/spec/components/componentBindingBehaviors.js @@ -265,7 +265,7 @@ describe('Components: Component binding', function() { expect(renderedComponents).toEqual([ 'sub-component1', 'sub-component2', 'test-component' ]); }); - it('When components are rendered synchronously, afterRender is also synchronous even if inner components are not', function() { + it('afterRender occurs after all inner components even if outer component is rendered synchronously', function() { this.after(function() { ko.components.unregister('sub-component'); }); @@ -277,7 +277,7 @@ describe('Components: Component binding', function() { viewModel: function() { this.afterRender = function (element) { expect(element).toBe(testNode.childNodes[0]); - expect(element).toContainText('xx', /* ignoreSpaces */ true); // Ignore spaces because old-IE is inconsistent + expect(element).toContainText('x12x', /* ignoreSpaces */ true); renderedComponents.push(testComponentName); }; } @@ -292,11 +292,11 @@ describe('Components: Component binding', function() { }); ko.applyBindings(outerViewModel, testNode); - expect(renderedComponents).toEqual([ 'test-component' ]); + expect(renderedComponents).toEqual([]); expect(testNode.childNodes[0]).toContainText('xx', /* ignoreSpaces */ true); // Ignore spaces because old-IE is inconsistent jasmine.Clock.tick(1); - expect(renderedComponents).toEqual([ 'test-component', 'sub-component1', 'sub-component2' ]); + expect(renderedComponents).toEqual([ 'sub-component1', 'sub-component2', 'test-component' ]); expect(testNode.childNodes[0]).toContainText('x12x', /* ignoreSpaces */ true); // Ignore spaces because old-IE is inconsistent }); diff --git a/src/components/componentBinding.js b/src/components/componentBinding.js index c13941c0a..ce7c31119 100644 --- a/src/components/componentBinding.js +++ b/src/components/componentBinding.js @@ -1,33 +1,67 @@ (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, - currentAfterRenderWatcher, + displayedDeferred, disposeAssociatedComponentViewModel = function () { var currentViewModelDispose = currentViewModel && currentViewModel['dispose']; if (typeof currentViewModelDispose === 'function') { currentViewModelDispose.call(currentViewModel); } - if (currentAfterRenderWatcher) { - currentAfterRenderWatcher.dispose(); - } - currentAfterRenderWatcher = 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, disposeAssociatedComponentViewModel); + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + disposeAssociatedComponentViewModel(); + if (displayedDeferred) { + displayedDeferred.dispose(); + displayedDeferred = null; + } + }); ko.computed(function () { var value = ko.utils.unwrapObservable(valueAccessor()), - componentName, componentParams, - completedAsync; + componentName, componentParams; if (typeof value === 'string') { componentName = value; @@ -40,9 +74,7 @@ throw new Error('No component name specified'); } - if (ko.isObservable(bindingContext._componentActiveChildrenCount)) { - bindingContext._componentActiveChildrenCount(bindingContext._componentActiveChildrenCount.peek() + 1); - } + displayedDeferred = new ComponentDisplayDeferred(element, bindingContext._componentDisplayDeferred, displayedDeferred); var loadingOperationId = currentLoadingOperationId = ++componentLoadingOperationUniqueId; ko.components.get(componentName, function(componentDefinition) { @@ -59,37 +91,28 @@ throw new Error('Unknown component \'' + componentName + '\''); } cloneTemplateIntoElement(componentName, componentDefinition, element); - var componentViewModel = createViewModel(componentDefinition, element, originalChildNodes, componentParams), + + var componentInfo = { + 'element': element, + 'templateNodes': originalChildNodes + }; + + var componentViewModel = createViewModel(componentDefinition, componentParams, componentInfo), childBindingContext = bindingContext['createChildContext'](componentViewModel, /* dataItemAlias */ undefined, function(ctx) { ctx['$component'] = componentViewModel; ctx['$componentTemplateNodes'] = originalChildNodes; + ctx._componentDisplayDeferred = displayedDeferred; }); + + if (componentViewModel && componentViewModel['afterRender']) { + displayedDeferred.subscribable.subscribe(componentViewModel['afterRender']); + } + currentViewModel = componentViewModel; - childBindingContext._componentActiveChildrenCount = ko.observable(0); ko.applyBindingsToDescendants(childBindingContext, element); - var callAfterRenderWhenChildrenDone = function(activeChildren) { - if (!activeChildren) { - if (currentAfterRenderWatcher) { - currentAfterRenderWatcher.dispose(); - } - var currentViewModelAfterRender = currentViewModel && currentViewModel['afterRender']; - if (typeof currentViewModelAfterRender === 'function') { - currentViewModelAfterRender.call(currentViewModel, element); - } - if (ko.isObservable(bindingContext._componentActiveChildrenCount)) { - bindingContext._componentActiveChildrenCount(bindingContext._componentActiveChildrenCount.peek() - 1); - } - } - }; - if (!completedAsync || childBindingContext._componentActiveChildrenCount.peek() === 0) { - callAfterRenderWhenChildrenDone(); - } else { - currentAfterRenderWatcher = childBindingContext._componentActiveChildrenCount.subscribe(callAfterRenderWhenChildrenDone); - } + displayedDeferred.componentComplete(); }); - - completedAsync = true; }, null, { disposeWhenNodeIsRemoved: element }); return { 'controlsDescendantBindings': true }; @@ -108,10 +131,10 @@ ko.virtualElements.setDomNodeChildren(element, clonedNodesArray); } - function createViewModel(componentDefinition, element, originalChildNodes, componentParams) { + function createViewModel(componentDefinition, componentParams, componentInfo) { var componentViewModelFactory = componentDefinition['createViewModel']; return componentViewModelFactory - ? componentViewModelFactory.call(componentDefinition, componentParams, { 'element': element, 'templateNodes': originalChildNodes }) + ? componentViewModelFactory.call(componentDefinition, componentParams, componentInfo) : componentParams; // Template-only component }