From 20f2c695a8f32c6230ac299c4eb868d301dd1298 Mon Sep 17 00:00:00 2001 From: Michael Best Date: Wed, 30 Nov 2016 12:47:26 -0800 Subject: [PATCH] component afterRender: pass element, always synchronous if synchronous:true, update specs --- spec/components/componentBindingBehaviors.js | 101 +++++++++++++++++-- src/components/componentBinding.js | 9 +- 2 files changed, 99 insertions(+), 11 deletions(-) diff --git a/spec/components/componentBindingBehaviors.js b/spec/components/componentBindingBehaviors.js index 2fb05a5a0..00eb094e1 100644 --- a/spec/components/componentBindingBehaviors.js +++ b/spec/components/componentBindingBehaviors.js @@ -219,7 +219,9 @@ describe('Components: Component binding', function() { template: '
', viewModel: function() { this.myvalue = 123; - this.afterRender = function () { + this.afterRender = function (element) { + expect(element).toBe(testNode.childNodes[0]); + expect(element).toContainHtml('
123
'); renderedCount++; }; } @@ -232,20 +234,28 @@ describe('Components: Component binding', function() { expect(renderedCount).toBe(1); }); - it('Calls all inner components\' afterRender functions and then an outer component\'s', function() { + it('Inner components\' afterRender occurs before the outer component\'s', function() { this.after(function() { ko.components.unregister('sub-component'); }); var renderedComponents = []; ko.components.register(testComponentName, { - template: '
', - viewModel: function() { this.afterRender = function () { renderedComponents.push(testComponentName); }; } + template: 'x
x', + viewModel: function() { + this.afterRender = function (element) { + expect(element).toContainText('x12x', /* ignoreSpaces */ true); // Ignore spaces because old-IE is inconsistent + renderedComponents.push(testComponentName); + }; + } }); ko.components.register('sub-component', { - template: '', - viewModel: function(params) { this.afterRender = function () { renderedComponents.push('sub-component' + params); }; } + template: '', + viewModel: function(params) { + this.myvalue = params; + this.afterRender = function () { renderedComponents.push('sub-component' + params); }; + } }); ko.applyBindings(outerViewModel, testNode); @@ -255,7 +265,42 @@ describe('Components: Component binding', function() { expect(renderedComponents).toEqual([ 'sub-component1', 'sub-component2', 'test-component' ]); }); - it('When components are rendered synchronously, calls all inner components\' afterRender functions and then an outer component\'s', function() { + it('When components are rendered synchronously, afterRender is also synchronous even if inner components are not', function() { + this.after(function() { + ko.components.unregister('sub-component'); + }); + + var renderedComponents = []; + ko.components.register(testComponentName, { + synchronous: true, + template: 'x
x', + 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 + 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); + expect(renderedComponents).toEqual([ 'test-component' ]); + 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(testNode.childNodes[0]).toContainText('x12x', /* ignoreSpaces */ true); // Ignore spaces because old-IE is inconsistent + }); + + it('When all components are rendered synchronously, inner components\' afterRender occurs before the outer component\'s', function() { this.after(function() { ko.components.unregister('sub-component'); }); @@ -277,6 +322,46 @@ describe('Components: Component binding', function() { expect(renderedComponents).toEqual([ 'sub-component1', 'sub-component2', 'test-component' ]); }); + it('afterRender waits for inner components that are not yet loaded', function() { + this.restoreAfter(window, 'require'); + this.after(function() { + ko.components.unregister('sub-component'); + }); + + var templateProviderCallback, + templateRequirePath = 'path/componentTemplateModule', + renderedComponents = []; + + window.require = function(modules, callback) { + expect(modules.length).toBe(1); + if (modules[0] === templateRequirePath) { + templateProviderCallback = callback; + } else { + throw new Error('Undefined module: ' + modules[0]); + } + }; + + ko.components.register(testComponentName, { + template: '
', + viewModel: function() { this.afterRender = function () { renderedComponents.push(testComponentName); }; } + }); + + ko.components.register('sub-component', { + template: { require: templateRequirePath }, + viewModel: function(params) { this.afterRender = function () { renderedComponents.push('sub-component' + params); }; } + }); + + ko.applyBindings(outerViewModel, testNode); + expect(renderedComponents).toEqual([]); + + jasmine.Clock.tick(1); + expect(renderedComponents).toEqual([ ]); + + templateProviderCallback(''); + jasmine.Clock.tick(1); + expect(renderedComponents).toEqual([ 'sub-component1', 'sub-component2', 'test-component' ]); + }); + it('Passes nonobservable params to the component', function() { // Set up a component that logs its constructor params var receivedParams = []; @@ -727,7 +812,7 @@ describe('Components: Component binding', function() { expect(testNode).toContainText('Hello! Your param is 456 Goodbye.'); }); - it('Does not call outer component\'s afterRender function if inner component is re-rendered', function() { + it('Does not call outer component\'s afterRender function if an inner component is re-rendered', function() { this.after(function() { ko.components.unregister('sub-component'); }); diff --git a/src/components/componentBinding.js b/src/components/componentBinding.js index 3837ebb43..c13941c0a 100644 --- a/src/components/componentBinding.js +++ b/src/components/componentBinding.js @@ -26,7 +26,8 @@ ko.computed(function () { var value = ko.utils.unwrapObservable(valueAccessor()), - componentName, componentParams; + componentName, componentParams, + completedAsync; if (typeof value === 'string') { componentName = value; @@ -74,19 +75,21 @@ } var currentViewModelAfterRender = currentViewModel && currentViewModel['afterRender']; if (typeof currentViewModelAfterRender === 'function') { - ko.dependencyDetection.ignore(currentViewModelAfterRender, currentViewModel); + currentViewModelAfterRender.call(currentViewModel, element); } if (ko.isObservable(bindingContext._componentActiveChildrenCount)) { bindingContext._componentActiveChildrenCount(bindingContext._componentActiveChildrenCount.peek() - 1); } } }; - if (childBindingContext._componentActiveChildrenCount.peek() === 0) { + if (!completedAsync || childBindingContext._componentActiveChildrenCount.peek() === 0) { callAfterRenderWhenChildrenDone(); } else { currentAfterRenderWatcher = childBindingContext._componentActiveChildrenCount.subscribe(callAfterRenderWhenChildrenDone); } }); + + completedAsync = true; }, null, { disposeWhenNodeIsRemoved: element }); return { 'controlsDescendantBindings': true };