Skip to content

Commit

Permalink
component afterRender: pass element, always synchronous if synchronou…
Browse files Browse the repository at this point in the history
…s:true, update specs
  • Loading branch information
mbest committed Nov 30, 2016
1 parent 14d147a commit 20f2c69
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 11 deletions.
101 changes: 93 additions & 8 deletions spec/components/componentBindingBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ describe('Components: Component binding', function() {
template: '<div data-bind="text: myvalue"></div>',
viewModel: function() {
this.myvalue = 123;
this.afterRender = function () {
this.afterRender = function (element) {
expect(element).toBe(testNode.childNodes[0]);
expect(element).toContainHtml('<div data-bind="text: myvalue">123</div>');
renderedCount++;
};
}
Expand All @@ -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: '<div data-bind="component: { name: \'sub-component\', params: 1 }"></div><div data-bind="component: { name: \'sub-component\', params: 2 }"></div>',
viewModel: function() { this.afterRender = function () { renderedComponents.push(testComponentName); }; }
template: 'x<div data-bind="component: { name: \'sub-component\', params: 1 }"></div><div data-bind="component: { name: \'sub-component\', params: 2 }"></div>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: '<span></span>',
viewModel: function(params) { this.afterRender = function () { renderedComponents.push('sub-component' + params); }; }
template: '<span data-bind="text: myvalue"></span>',
viewModel: function(params) {
this.myvalue = params;
this.afterRender = function () { renderedComponents.push('sub-component' + params); };
}
});

ko.applyBindings(outerViewModel, testNode);
Expand All @@ -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<div data-bind="component: { name: \'sub-component\', params: 1 }"></div><div data-bind="component: { name: \'sub-component\', params: 2 }"></div>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: '<span data-bind="text: myvalue"></span>',
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');
});
Expand All @@ -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: '<div data-bind="component: { name: \'sub-component\', params: 1 }"></div><div data-bind="component: { name: \'sub-component\', params: 2 }"></div>',
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('<span></span>');
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 = [];
Expand Down Expand Up @@ -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');
});
Expand Down
9 changes: 6 additions & 3 deletions src/components/componentBinding.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@

ko.computed(function () {
var value = ko.utils.unwrapObservable(valueAccessor()),
componentName, componentParams;
componentName, componentParams,
completedAsync;

if (typeof value === 'string') {
componentName = value;
Expand Down Expand Up @@ -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 };
Expand Down

0 comments on commit 20f2c69

Please sign in to comment.