diff --git a/package.json b/package.json index 9ed02857a6d..28d4ce7a1f5 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@glimmer/node": "^0.25.6", "@glimmer/reference": "^0.25.6", "@glimmer/runtime": "^0.25.6", + "@types/rsvp": "^4.0.1", "aws-sdk": "^2.46.0", "babel-plugin-check-es2015-constants": "^6.22.0", "babel-plugin-debug-macros": "^0.1.10", diff --git a/packages/ember-application/lib/system/application-instance.js b/packages/ember-application/lib/system/application-instance.js index aad828fc10d..b018d8be1bf 100644 --- a/packages/ember-application/lib/system/application-instance.js +++ b/packages/ember-application/lib/system/application-instance.js @@ -3,11 +3,11 @@ */ import { assign } from 'ember-utils'; -import { get, set, run, computed } from 'ember-metal'; -import { RSVP } from 'ember-runtime'; +import { get, set, computed } from 'ember-metal'; import { environment } from 'ember-environment'; import { jQuery } from 'ember-views'; import EngineInstance from './engine-instance'; +import { renderSettled } from 'ember-glimmer'; /** The `ApplicationInstance` encapsulates all of the stateful aspects of a @@ -243,14 +243,8 @@ const ApplicationInstance = EngineInstance.extend({ // No rendering is needed, and routing has completed, simply return. return this; } else { - return new RSVP.Promise((resolve) => { - // Resolve once rendering is completed. `router.handleURL` returns the transition (as a thennable) - // which resolves once the transition is completed, but the transition completion only queues up - // a scheduled revalidation (into the `render` queue) in the Renderer. - // - // This uses `run.schedule('afterRender', ....)` to resolve after that rendering has completed. - run.schedule('afterRender', null, resolve, this); - }); + // Ensure that the visit promise resolves when all rendering has completed + return renderSettled().then(() => this); } }; diff --git a/packages/ember-glimmer/lib/index.ts b/packages/ember-glimmer/lib/index.ts index c73a1affb95..b5cea5dd521 100644 --- a/packages/ember-glimmer/lib/index.ts +++ b/packages/ember-glimmer/lib/index.ts @@ -280,6 +280,7 @@ export { InertRenderer, InteractiveRenderer, _resetRenderers, + renderSettled, } from './renderer'; export { getTemplate, diff --git a/packages/ember-glimmer/lib/renderer.ts b/packages/ember-glimmer/lib/renderer.ts index 6e7b624585c..9f55eb10c32 100644 --- a/packages/ember-glimmer/lib/renderer.ts +++ b/packages/ember-glimmer/lib/renderer.ts @@ -27,6 +27,7 @@ import { RootReference } from './utils/references'; import OutletView, { OutletState, RootOutletStateReference } from './views/outlet'; import { ComponentDefinition, NULL_REFERENCE, RenderResult } from '@glimmer/runtime'; +import RSVP from 'rsvp'; const { backburner } = run; @@ -181,6 +182,39 @@ function loopBegin(): void { function K() { /* noop */ } +let renderSettledDeferred: RSVP.Deferred | null = null; +/* + Returns a promise which will resolve when rendering has settled. Settled in + this context is defined as when all of the tags in use are "current" (e.g. + `renderers.every(r => r._isValid())`). When this is checked at the _end_ of + the run loop, this essentially guarantees that all rendering is completed. + + @method renderSettled + @returns {Promise} a promise which fulfills when rendering has settled +*/ +export function renderSettled() { + if (renderSettledDeferred === null) { + renderSettledDeferred = RSVP.defer(); + // if there is no current runloop, the promise created above will not have + // a chance to resolve (because its resolved in backburner's "end" event) + if (!run.currentRunLoop) { + // ensure a runloop has been kicked off + backburner.schedule('actions', null, K); + } + } + + return renderSettledDeferred.promise; +} + +function resolveRenderPromise() { + if (renderSettledDeferred !== null) { + let resolve = renderSettledDeferred.resolve; + renderSettledDeferred = null; + + backburner.join(null, resolve); + } +} + let loops = 0; function loopEnd() { for (let i = 0; i < renderers.length; i++) { @@ -196,6 +230,7 @@ function loopEnd() { } } loops = 0; + resolveRenderPromise(); } backburner.on('begin', loopBegin); diff --git a/packages/ember-glimmer/tests/integration/render-settled-test.js b/packages/ember-glimmer/tests/integration/render-settled-test.js new file mode 100644 index 00000000000..0ed045cbd32 --- /dev/null +++ b/packages/ember-glimmer/tests/integration/render-settled-test.js @@ -0,0 +1,73 @@ +import { + RenderingTestCase, + moduleFor, + strip +} from 'internal-test-helpers'; +import { renderSettled } from 'ember-glimmer'; +import { all } from 'rsvp'; +import { run } from 'ember-metal'; + +moduleFor('renderSettled', class extends RenderingTestCase { + ['@test resolves when no rendering is happening'](assert) { + return renderSettled().then(() => { + assert.ok(true, 'resolved even without rendering'); + }); + } + + ['@test resolves renderers exist but no runloops are triggered'](assert) { + this.render(strip`{{foo}}`, { foo: 'bar' }); + + return renderSettled().then(() => { + assert.ok(true, 'resolved even without runloops'); + }); + } + + ['@test does not create extraneous promises'](assert) { + let first = renderSettled(); + let second = renderSettled(); + + assert.strictEqual(first, second); + + return all([first, second]); + } + + ['@test resolves when rendering has completed (after property update)']() { + this.render(strip`{{foo}}`, { foo: 'bar' }); + + this.assertText('bar'); + this.component.set('foo', 'baz'); + this.assertText('bar'); + + return renderSettled().then(() => { + this.assertText('baz'); + }); + } + + ['@test resolves in run loop when renderer has settled'](assert) { + assert.expect(3); + + this.render(strip`{{foo}}`, { foo: 'bar' }); + + this.assertText('bar'); + let promise; + + return run(() => { + run.schedule('actions', null, () => { + this.component.set('foo', 'set in actions'); + + promise = renderSettled().then(() => { + this.assertText('set in afterRender'); + }); + + run.schedule('afterRender', null, () => { + this.component.set('foo', 'set in afterRender'); + }); + }); + + // still not updated here + this.assertText('bar'); + + return promise; + }); + } +}); diff --git a/packages/ember-metal/lib/index.d.ts b/packages/ember-metal/lib/index.d.ts index 7f814ab3df7..31b93a6dccd 100644 --- a/packages/ember-metal/lib/index.d.ts +++ b/packages/ember-metal/lib/index.d.ts @@ -2,13 +2,15 @@ interface IBackburner { join(...args: any[]): void; on(...args: any[]): void; scheduleOnce(...args: any[]): void; + schedule(queueName: string, target: Object | null, method: Function | string): void; } interface IRun { (...args: any[]): any; schedule(...args: any[]): void; later(...args: any[]): void; join(...args: any[]): void; - backburner: IBackburner + backburner: IBackburner; + currentRunLoop: boolean; } export const run: IRun; diff --git a/yarn.lock b/yarn.lock index 4859ab9275c..4cee7bdd0f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -96,6 +96,10 @@ "@types/acorn" "*" "@types/source-map" "*" +"@types/rsvp@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/rsvp/-/rsvp-4.0.1.tgz#82956bc8d0a8151ec3b7e9cae64fd06808a1c714" + "@types/source-map@*": version "0.5.2" resolved "https://registry.yarnpkg.com/@types/source-map/-/source-map-0.5.2.tgz#973459e744cc7445c85b97f770275b479b138e08"