diff --git a/src/application.ts b/src/application.ts index 3ae3a49..f4af601 100644 --- a/src/application.ts +++ b/src/application.ts @@ -10,7 +10,6 @@ import { import { Simple, templateFactory, - RenderResult, ComponentDefinition, Component } from '@glimmer/runtime'; @@ -25,6 +24,8 @@ import DynamicScope from './dynamic-scope'; import Environment from './environment'; import mainTemplate from './templates/main'; +function NOOP() {} + export interface ApplicationOptions { rootName: string; resolver: Resolver; @@ -50,17 +51,20 @@ export default class Application implements Owner { private _rootsIndex: number = 0; private _registry: Registry; private _container: Container; - private _renderResult: RenderResult; - /** Whether the initial render has completed. */ - private _rendered: boolean; - /** Whether a re-render has been scheduled. */ - private _scheduled: boolean; private _initializers: Initializer[] = []; private _initialized = false; + private _rendered = false; + private _scheduled = false; + private _rerender: () => void = NOOP; + private _afterRender: () => void = NOOP; + private _renderPromise: Option>; constructor(options: ApplicationOptions) { this.rootName = options.rootName; this.resolver = options.resolver; + this._renderPromise = new Promise(resolve => { + this._afterRender = resolve; + }); } /** @hidden */ @@ -136,30 +140,58 @@ export default class Application implements Owner { this.env.commit(); + let renderResult = result.value; + + this._rerender = () => { + this.env.begin(); + renderResult.rerender(); + this.env.commit(); + this._didRender(); + }; + + this._didRender(); + } + + _didRender(): void { + let { _afterRender } = this; + + this._afterRender = NOOP; + this._renderPromise = null; this._rendered = true; - this._renderResult = result.value; + + _afterRender(); } - renderComponent(component: string | ComponentDefinition, parent: Simple.Node, nextSibling: Option): void { + renderComponent( + component: string | ComponentDefinition, + parent: Simple.Node, + nextSibling: Option = null + ): Promise { this._roots.push({ id: this._rootsIndex++, component, parent, nextSibling }); - this.scheduleRerender(); + return this.scheduleRerender(); } - /** @hidden */ - rerender(): void { - this.env.begin(); - this._renderResult.rerender(); - this.env.commit(); + scheduleRerender(): Promise { + let { _renderPromise } = this; + + if (_renderPromise === null) { + _renderPromise = this._renderPromise = new Promise(resolve => { + this._afterRender = resolve; + }); + + this._scheduleRerender(); + } + + return _renderPromise; } - /** @hidden */ - scheduleRerender(): void { - if (this._scheduled || !this._rendered) { return; } + _scheduleRerender(): void { + if (this._scheduled || !this._rendered) return; this._scheduled = true; requestAnimationFrame(() => { this._scheduled = false; - this.rerender(); + this._rerender(); }); } diff --git a/test/action-test.ts b/test/action-test.ts index 0800247..0afff6e 100644 --- a/test/action-test.ts +++ b/test/action-test.ts @@ -46,7 +46,7 @@ test('can curry arguments to actions', function(assert) { passedEvent = null; helloWorldComponent.name = "cruel world"; - app.rerender(); + app.scheduleRerender(); h1 = app.rootElement.querySelector('h1'); h1.onclick(fakeEvent); diff --git a/test/environment-test.ts b/test/environment-test.ts index c04843d..95cca82 100644 --- a/test/environment-test.ts +++ b/test/environment-test.ts @@ -64,7 +64,7 @@ test('can render a component with the component helper', function(assert) { assert.equal(app.rootElement.innerText, 'Hello Glimmer!'); - app.rerender(); + app.scheduleRerender(); assert.equal(app.rootElement.innerText, 'Hello Glimmer!'); }); @@ -95,7 +95,7 @@ test('can render a custom helper', function(assert) { assert.equal(app.rootElement.innerText, 'Hello Glimmer!'); - app.rerender(); + app.scheduleRerender(); assert.equal(app.rootElement.innerText, 'Hello Glimmer!'); }); @@ -114,7 +114,7 @@ test('can render a custom helper that takes args', function(assert) { assert.equal(app.rootElement.innerText, 'Hello Tom Dale!'); - app.rerender(); + app.scheduleRerender(); assert.equal(app.rootElement.innerText, 'Hello Tom Dale!'); }); diff --git a/test/render-component-test.ts b/test/render-component-test.ts new file mode 100644 index 0000000..c425bcb --- /dev/null +++ b/test/render-component-test.ts @@ -0,0 +1,122 @@ +import buildApp from './test-helpers/test-app'; + +const { module, test } = QUnit; + +module('renderComponent'); + +test('renders a component', function(assert) { + assert.expect(1); + + let containerElement = document.createElement('div'); + + let app = buildApp() + .template('hello-world', `

Hello Glimmer!

`) + .boot(); + + return app.renderComponent('hello-world', containerElement).then(() => { + assert.equal(containerElement.innerHTML, '

Hello Glimmer!

'); + }); +}); + +test('renders a component without affecting existing content', function(assert) { + assert.expect(2); + + let containerElement = document.createElement('div'); + let previousSibling = document.createElement('p'); + + previousSibling.appendChild(document.createTextNode('foo')); + containerElement.appendChild(previousSibling); + containerElement.appendChild(document.createTextNode('bar')); + + let app = buildApp() + .template('hello-world', `

Hello Glimmer!

`) + .boot(); + + assert.equal(containerElement.innerHTML, '

foo

bar'); + + return app.renderComponent('hello-world', containerElement).then(() => { + assert.equal(containerElement.innerHTML, '

foo

bar

Hello Glimmer!

'); + }); +}); + +test('renders a component before a given sibling', function(assert) { + assert.expect(2); + + let containerElement = document.createElement('div'); + let previousSibling = document.createElement('p'); + let nextSibling = document.createElement('aside'); + + containerElement.appendChild(previousSibling); + containerElement.appendChild(nextSibling); + + let app = buildApp() + .template('hello-world', `

Hello Glimmer!

`) + .boot(); + + assert.equal(containerElement.innerHTML, '

'); + + return app.renderComponent('hello-world', containerElement, nextSibling).then(() => { + assert.equal(containerElement.innerHTML, '

Hello Glimmer!

'); + }); +}); + +test('renders multiple components in different places', function(assert) { + assert.expect(2); + + let firstContainerElement = document.createElement('div'); + let secondContainerElement = document.createElement('div'); + + let app = buildApp() + .template('hello-world', `

Hello Glimmer!

`) + .template('hello-robbie', `

Hello Robbie!

`) + .boot(); + + return Promise.all([ + app.renderComponent('hello-world', firstContainerElement), + app.renderComponent('hello-robbie', secondContainerElement) + ]).then(() => { + assert.equal(firstContainerElement.innerHTML, '

Hello Glimmer!

'); + assert.equal(secondContainerElement.innerHTML, '

Hello Robbie!

'); + }); +}); + +test('renders multiple components in the same container', function(assert) { + assert.expect(1); + + let containerElement = document.createElement('div'); + + let app = buildApp() + .template('hello-world', `

Hello Glimmer!

`) + .template('hello-robbie', `

Hello Robbie!

`) + .boot(); + + return Promise.all([ + app.renderComponent('hello-world', containerElement), + app.renderComponent('hello-robbie', containerElement) + ]).then(() => { + assert.equal(containerElement.innerHTML, '

Hello Glimmer!

Hello Robbie!

'); + }); +}); + +test('renders multiple components in the same container in particular places', function(assert) { + assert.expect(2); + + let containerElement = document.createElement('div'); + let nextSibling = document.createElement('aside'); + + containerElement.appendChild(nextSibling); + + let app = buildApp() + .template('hello-world', `

Hello Glimmer!

`) + .template('hello-robbie', `

Hello Robbie!

`) + .boot(); + + assert.equal(containerElement.innerHTML, ''); + + return Promise.all([ + app.renderComponent('hello-world', containerElement), + app.renderComponent('hello-robbie', containerElement, nextSibling) + ]).then(() => { + assert.equal(containerElement.innerHTML, '

Hello Robbie!

Hello Glimmer!

'); + }); +}); diff --git a/test/test-helpers/test-app.ts b/test/test-helpers/test-app.ts index 0e96668..8b7512f 100644 --- a/test/test-helpers/test-app.ts +++ b/test/test-helpers/test-app.ts @@ -1,11 +1,10 @@ import Application from '../../src/application'; import Resolver, { BasicModuleRegistry } from '@glimmer/resolver'; -import { Factory } from '@glimmer/di'; import { TestComponent, TestComponentManager } from './components'; import { precompile } from './compiler'; -interface ComponentFactory { +export interface ComponentFactory { create(injections: object): TestComponent; } @@ -52,6 +51,7 @@ export class AppBuilder { constructor(name: string) { this.rootName = name; this.modules[`component-manager:/${this.rootName}/component-managers/main`] = TestComponentManager; + this.template('main', '
'); } template(name: string, template: string) {