diff --git a/package.json b/package.json
index 1044aec77c7..878ef7162a7 100644
--- a/package.json
+++ b/package.json
@@ -38,7 +38,7 @@
"express": "^4.5.0",
"finalhandler": "^0.4.0",
"github": "^0.2.3",
- "glimmer-engine": "tildeio/glimmer#591a188",
+ "glimmer-engine": "tildeio/glimmer#2c22999",
"glob": "^5.0.13",
"htmlbars": "0.14.16",
"mocha": "^2.4.5",
diff --git a/packages/ember-glimmer/lib/components/curly-component.js b/packages/ember-glimmer/lib/components/curly-component.js
index 5c487fcbb32..68bf85dd6b0 100644
--- a/packages/ember-glimmer/lib/components/curly-component.js
+++ b/packages/ember-glimmer/lib/components/curly-component.js
@@ -1,5 +1,6 @@
import { StatementSyntax, ValueReference } from 'glimmer-runtime';
import { AttributeBindingReference, RootReference, applyClassNameBinding } from '../utils/references';
+import { DIRTY_TAG } from '../ember-views/component';
import EmptyObject from 'ember-metal/empty_object';
export class CurlyComponentSyntax extends StatementSyntax {
@@ -19,7 +20,7 @@ export class CurlyComponentSyntax extends StatementSyntax {
function attrsToProps(keys, attrs) {
let merged = new EmptyObject();
- merged.attrs = merged;
+ merged.attrs = attrs;
for (let i = 0, l = keys.length; i < l; i++) {
let name = keys[i];
@@ -32,9 +33,10 @@ function attrsToProps(keys, attrs) {
}
class ComponentStateBucket {
- constructor(component) {
+ constructor(component, args) {
this.component = component;
this.classRef = null;
+ this.argsRevision = args.tag.value();
}
}
@@ -46,17 +48,19 @@ class CurlyComponentManager {
let attrs = args.named.value();
let props = attrsToProps(args.named.keys, attrs);
+ props.renderer = parentView.renderer;
+
let component = klass.create(props);
dynamicScope.view = component;
parentView.appendChild(component);
- // component.trigger('didInitAttrs', { attrs });
- // component.trigger('didReceiveAttrs', { newAttrs: attrs });
- // component.trigger('willInsertElement');
- // component.trigger('willRender');
+ component.trigger('didInitAttrs', { attrs });
+ component.trigger('didReceiveAttrs', { newAttrs: attrs });
+ component.trigger('willInsertElement');
+ component.trigger('willRender');
- let bucket = new ComponentStateBucket(component);
+ let bucket = new ComponentStateBucket(component, args);
if (args.named.has('class')) {
bucket.classRef = args.named.get('class');
@@ -99,30 +103,41 @@ class CurlyComponentManager {
component._transitionTo('hasElement');
}
+ getTag({ component }) {
+ return component[DIRTY_TAG];
+ }
+
didCreate({ component }) {
- // component.trigger('didInsertElement');
- // component.trigger('didRender');
+ component.trigger('didInsertElement');
+ component.trigger('didRender');
component._transitionTo('inDOM');
}
- update({ component }, args, dynamicScope) {
- let attrs = args.named.value();
- let props = attrsToProps(args.named.keys, attrs);
+ update(bucket, args, dynamicScope) {
+ let { component, argsRevision } = bucket;
- // let oldAttrs = component.attrs;
- // let newAttrs = attrs;
+ if (!args.tag.validate(argsRevision)) {
+ bucket.argsRevision = args.tag.value();
- component.setProperties(props);
+ let attrs = args.named.value();
+ let props = attrsToProps(args.named.keys, attrs);
+
+ let oldAttrs = component.attrs;
+ let newAttrs = attrs;
+
+ component.setProperties(props);
+
+ component.trigger('didUpdateAttrs', { oldAttrs, newAttrs });
+ component.trigger('didReceiveAttrs', { oldAttrs, newAttrs });
+ }
- // component.trigger('didUpdateAttrs', { oldAttrs, newAttrs });
- // component.trigger('didReceiveAttrs', { oldAttrs, newAttrs });
- // component.trigger('willUpdate');
- // component.trigger('willRender');
+ component.trigger('willUpdate');
+ component.trigger('willRender');
}
didUpdate({ component }) {
- // component.trigger('didUpdate');
- // component.trigger('didRender');
+ component.trigger('didUpdate');
+ component.trigger('didRender');
}
getDestructor({ component }) {
diff --git a/packages/ember-glimmer/lib/components/outlet.js b/packages/ember-glimmer/lib/components/outlet.js
index e75124def09..0d22ce8905f 100644
--- a/packages/ember-glimmer/lib/components/outlet.js
+++ b/packages/ember-glimmer/lib/components/outlet.js
@@ -100,6 +100,10 @@ class AbstractOutletComponentManager {
return new RootReference(state.render.controller);
}
+ getTag(state) {
+ return null;
+ }
+
getDestructor(state) {
return null;
}
diff --git a/packages/ember-glimmer/lib/ember-metal-views/index.js b/packages/ember-glimmer/lib/ember-metal-views/index.js
index cc770c80e74..59832b7377c 100644
--- a/packages/ember-glimmer/lib/ember-metal-views/index.js
+++ b/packages/ember-glimmer/lib/ember-metal-views/index.js
@@ -61,8 +61,15 @@ class Renderer {
}
remove(view) {
+ view.trigger('willDestroyElement');
view._transitionTo('destroying');
- view['_renderResult'].destroy();
+
+ let { _renderResult } = view;
+
+ if (_renderResult) {
+ _renderResult.destroy();
+ }
+
view.destroy();
}
diff --git a/packages/ember-glimmer/lib/ember-views/component.js b/packages/ember-glimmer/lib/ember-views/component.js
index 12e78f068d1..2766d0458e4 100644
--- a/packages/ember-glimmer/lib/ember-views/component.js
+++ b/packages/ember-glimmer/lib/ember-views/component.js
@@ -6,6 +6,10 @@ import InstrumentationSupport from 'ember-views/mixins/instrumentation_support';
import AriaRoleSupport from 'ember-views/mixins/aria_role_support';
import ViewMixin from 'ember-views/mixins/view_support';
import EmberView from 'ember-views/views/view';
+import symbol from 'ember-metal/symbol';
+import { DirtyableTag } from 'glimmer-reference';
+
+export const DIRTY_TAG = symbol('DIRTY_TAG');
export default CoreView.extend(
ChildViewsSupport,
@@ -22,6 +26,12 @@ export default CoreView.extend(
init() {
this._super(...arguments);
this._viewRegistry = this._viewRegistry || EmberView.views;
+ this[DIRTY_TAG] = new DirtyableTag();
+ },
+
+ rerender() {
+ this[DIRTY_TAG].dirty();
+ this._super();
},
__defineNonEnumerable(property) {
diff --git a/packages/ember-glimmer/lib/environment.js b/packages/ember-glimmer/lib/environment.js
index cc74bea468d..57508277265 100644
--- a/packages/ember-glimmer/lib/environment.js
+++ b/packages/ember-glimmer/lib/environment.js
@@ -26,8 +26,9 @@ import { default as get } from './helpers/get';
import { default as hash } from './helpers/hash';
import { default as loc } from './helpers/loc';
import { default as log } from './helpers/log';
-import { default as classHelper } from './helpers/-class';
+import { default as readonly } from './helpers/readonly';
import { default as unbound } from './helpers/unbound';
+import { default as classHelper } from './helpers/-class';
import { OWNER } from 'container/owner';
const builtInHelpers = {
@@ -38,6 +39,7 @@ const builtInHelpers = {
hash,
loc,
log,
+ readonly,
unbound,
'-class': classHelper
};
@@ -162,4 +164,13 @@ export default class Environment extends GlimmerEnvironment {
let keyPath = args.named.get('key').value();
return createIterable(ref, keyPath);
}
+
+ didCreate(component, manager) {
+ this.createdComponents.unshift(component);
+ this.createdManagers.unshift(manager);
+ }
+
+ didDestroy(destroyable) {
+ destroyable.destroy();
+ }
}
diff --git a/packages/ember-glimmer/lib/helpers/readonly.js b/packages/ember-glimmer/lib/helpers/readonly.js
new file mode 100644
index 00000000000..dca2cdbf426
--- /dev/null
+++ b/packages/ember-glimmer/lib/helpers/readonly.js
@@ -0,0 +1,7 @@
+import { helper } from '../helper';
+
+function readonly(args) {
+ return args[0];
+}
+
+export default helper(readonly);
diff --git a/packages/ember-glimmer/tests/integration/components/life-cycle-test.js b/packages/ember-glimmer/tests/integration/components/life-cycle-test.js
new file mode 100644
index 00000000000..1e5177b8859
--- /dev/null
+++ b/packages/ember-glimmer/tests/integration/components/life-cycle-test.js
@@ -0,0 +1,543 @@
+import { set } from 'ember-metal/property_set';
+import { Component } from '../../utils/helpers';
+import { strip } from '../../utils/abstract-test-case';
+import { moduleFor, RenderingTest } from '../../utils/test-case';
+
+class LifeCycleHooksTest extends RenderingTest {
+ constructor() {
+ super();
+ this.hooks = [];
+ this.components = {};
+ }
+
+ /* abstract */
+ get ComponentClass() {
+ throw new Error('Not implemented: `ComponentClass`');
+ }
+
+ /* abstract */
+ invocationFor(name, namedArgs = {}) {
+ throw new Error('Not implemented: `invocationFor`');
+ }
+
+ /* abstract */
+ attrFor(name) {
+ throw new Error('Not implemented: `attrFor`');
+ }
+
+ get boundHelpers() {
+ return {
+ invoke: bind(this.invocationFor, this),
+ attr: bind(this.attrFor, this)
+ };
+ }
+
+ registerComponent(name, { template = null }) {
+ let pushComponent = (instance) => {
+ this.components[name] = instance;
+ };
+
+ let pushHook = (hookName, args) => {
+ this.hooks.push(hook(name, hookName, args));
+ };
+
+ let ComponentClass = this.ComponentClass.extend({
+ init() {
+ expectDeprecation(() => { this._super(...arguments); },
+ /didInitAttrs called/);
+
+ pushHook('init');
+ pushComponent(this);
+ },
+
+ didInitAttrs(options) {
+ pushHook('didInitAttrs', options);
+ },
+
+ didUpdateAttrs(options) {
+ pushHook('didUpdateAttrs', options);
+ },
+
+ willUpdate(options) {
+ pushHook('willUpdate', options);
+ },
+
+ didReceiveAttrs(options) {
+ pushHook('didReceiveAttrs', options);
+ },
+
+ willRender() {
+ pushHook('willRender');
+ },
+
+ didRender() {
+ pushHook('didRender');
+ },
+
+ didInsertElement() {
+ pushHook('didInsertElement');
+ },
+
+ didUpdate(options) {
+ pushHook('didUpdate', options);
+ }
+ });
+
+ super.registerComponent(name, { ComponentClass, template });
+ }
+
+ assertHooks(label, ...rawHooks) {
+ let hooks = rawHooks.map(raw => hook(...raw));
+ this.assert.deepEqual(json(this.hooks), json(hooks), label);
+ this.hooks = [];
+ }
+
+ ['@test lifecycle hooks are invoked in a predictable order']() {
+ let { attr, invoke } = this.boundHelpers;
+
+ this.registerComponent('the-top', { template: strip`
+
+ Twitter: {{${attr('twitter')}}}|
+ ${invoke('the-middle', { name: string('Tom Dale') })}
+
`
+ });
+
+ this.registerComponent('the-middle', { template: strip`
+
+ Name: {{${attr('name')}}}|
+ ${invoke('the-bottom', { website: string('tomdale.net') })}
+
`
+ });
+
+ this.registerComponent('the-bottom', { template: strip`
+
+ Website: {{${attr('website')}}}
+
`
+ });
+
+ this.render(invoke('the-top', { twitter: expr('twitter') }), { twitter: '@tomdale' });
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ let topAttrs = { twitter: '@tomdale' };
+ let middleAttrs = { name: 'Tom Dale' };
+ let bottomAttrs = { website: 'tomdale.net' };
+
+ this.assertHooks(
+
+ 'after initial render',
+
+ // Sync hooks
+
+ ['the-top', 'init'],
+ ['the-top', 'didInitAttrs', { attrs: topAttrs }],
+ ['the-top', 'didReceiveAttrs', { newAttrs: topAttrs }],
+ ['the-top', 'willRender'],
+
+ ['the-middle', 'init'],
+ ['the-middle', 'didInitAttrs', { attrs: middleAttrs }],
+ ['the-middle', 'didReceiveAttrs', { newAttrs: middleAttrs }],
+ ['the-middle', 'willRender'],
+
+ ['the-bottom', 'init'],
+ ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs }],
+ ['the-bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }],
+ ['the-bottom', 'willRender'],
+
+ // Async hooks
+
+ ['the-bottom', 'didInsertElement'],
+ ['the-bottom', 'didRender'],
+
+ ['the-middle', 'didInsertElement'],
+ ['the-middle', 'didRender'],
+
+ ['the-top', 'didInsertElement'],
+ ['the-top', 'didRender']
+
+ );
+
+ this.runTask(() => this.components['the-bottom'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ this.assertHooks(
+
+ 'after no-op rerender (bottom)',
+
+ // Sync hooks
+
+ ['the-bottom', 'willUpdate'],
+ ['the-bottom', 'willRender'],
+
+ // Async hooks
+
+ ['the-bottom', 'didUpdate'],
+ ['the-bottom', 'didRender']
+
+ );
+
+ this.runTask(() => this.components['the-middle'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ bottomAttrs = { oldAttrs: { website: 'tomdale.net' }, newAttrs: { website: 'tomdale.net' } };
+
+ // The original implementation of the hooks in HTMLBars does a
+ // deeper walk than necessary (using the AlwaysDirty validator),
+ // resulting in executing the experimental "new hooks" too often.
+ //
+ // In particular, hooks were executed downstream from the original
+ // call to `rerender()` even if the rerendering component did not
+ // use `this.set()` to update the arguments of downstream components.
+ //
+ // Because Glimmer uses a pull-based model instead of a blunt
+ // push-based model, we can avoid a deeper traversal than is
+ // necessary.
+
+ if (this.isHTMLBars) {
+ this.assertHooks(
+
+ 'after no-op rerender (middle)',
+
+ // Sync hooks
+
+ ['the-middle', 'willUpdate'],
+ ['the-middle', 'willRender'],
+
+ ['the-bottom', 'didUpdateAttrs', bottomAttrs],
+ ['the-bottom', 'didReceiveAttrs', bottomAttrs],
+
+ ['the-bottom', 'willUpdate'],
+ ['the-bottom', 'willRender'],
+
+ // Async hooks
+
+ ['the-bottom', 'didUpdate'],
+ ['the-bottom', 'didRender'],
+
+ ['the-middle', 'didUpdate'],
+ ['the-middle', 'didRender']
+
+ );
+ } else {
+ this.assertHooks(
+
+ 'after no-op rerender (middle)',
+
+ // Sync hooks
+
+ ['the-middle', 'willUpdate'],
+ ['the-middle', 'willRender'],
+
+ // Async hooks
+
+ ['the-middle', 'didUpdate'],
+ ['the-middle', 'didRender']
+
+ );
+ }
+
+ this.runTask(() => this.components['the-top'].rerender());
+
+ this.assertText('Twitter: @tomdale|Name: Tom Dale|Website: tomdale.net');
+
+ middleAttrs = { oldAttrs: { name: 'Tom Dale' }, newAttrs: { name: 'Tom Dale' } };
+
+
+ if (this.isHTMLBars) {
+ this.assertHooks(
+
+ 'after no-op rerender (top)',
+
+ // Sync hooks
+
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ ['the-middle', 'didUpdateAttrs', middleAttrs],
+ ['the-middle', 'didReceiveAttrs', middleAttrs],
+
+ ['the-middle', 'willUpdate'],
+ ['the-middle', 'willRender'],
+
+ ['the-bottom', 'didUpdateAttrs', bottomAttrs],
+ ['the-bottom', 'didReceiveAttrs', bottomAttrs],
+
+ ['the-bottom', 'willUpdate'],
+ ['the-bottom', 'willRender'],
+
+ // Async hooks
+
+ ['the-bottom', 'didUpdate'],
+ ['the-bottom', 'didRender'],
+
+ ['the-middle', 'didUpdate'],
+ ['the-middle', 'didRender'],
+
+ ['the-top', 'didUpdate'],
+ ['the-top', 'didRender']
+
+ );
+ } else {
+ this.assertHooks(
+
+ 'after no-op rerender (top)',
+
+ // Sync hooks
+
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ // Async hooks
+
+ ['the-top', 'didUpdate'],
+ ['the-top', 'didRender']
+
+ );
+ }
+
+ this.runTask(() => set(this.context, 'twitter', '@horsetomdale'));
+
+ this.assertText('Twitter: @horsetomdale|Name: Tom Dale|Website: tomdale.net');
+
+ // Because the `twitter` attr is only used by the topmost component,
+ // and not passed down, we do not expect to see lifecycle hooks
+ // called for child components. If the `didReceiveAttrs` hook used
+ // the new attribute to rerender itself imperatively, that would result
+ // in lifecycle hooks being invoked for the child.
+
+ topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } };
+
+ this.assertHooks(
+
+ 'after update',
+
+ // Sync hooks
+
+ ['the-top', 'didUpdateAttrs', topAttrs],
+ ['the-top', 'didReceiveAttrs', topAttrs],
+
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ // Async hooks
+
+ ['the-top', 'didUpdate'],
+ ['the-top', 'didRender']
+
+ );
+ }
+
+ ['@test passing values through attrs causes lifecycle hooks to fire if the attribute values have changed']() {
+ let { attr, invoke } = this.boundHelpers;
+
+ this.registerComponent('the-top', { template: strip`
+
+ Top: ${invoke('the-middle', { twitterTop: expr(attr('twitter')) })}
+
`
+ });
+
+ this.registerComponent('the-middle', { template: strip`
+
+ Middle: ${invoke('the-bottom', { twitterMiddle: expr(attr('twitterTop')) })}
+
`
+ });
+
+ this.registerComponent('the-bottom', { template: strip`
+
+ Bottom: {{${attr('twitterMiddle')}}}
+
`
+ });
+
+ this.render(invoke('the-top', { twitter: expr('twitter') }), { twitter: '@tomdale' });
+
+ this.assertText('Top: Middle: Bottom: @tomdale');
+
+ let topAttrs = { twitter: '@tomdale' };
+ let middleAttrs = { twitterTop: '@tomdale' };
+ let bottomAttrs = { twitterMiddle: '@tomdale' };
+
+ this.assertHooks(
+
+ 'after initial render',
+
+ // Sync hooks
+
+ ['the-top', 'init'],
+ ['the-top', 'didInitAttrs', { attrs: topAttrs }],
+ ['the-top', 'didReceiveAttrs', { newAttrs: topAttrs }],
+ ['the-top', 'willRender'],
+
+ ['the-middle', 'init'],
+ ['the-middle', 'didInitAttrs', { attrs: middleAttrs }],
+ ['the-middle', 'didReceiveAttrs', { newAttrs: middleAttrs }],
+ ['the-middle', 'willRender'],
+
+ ['the-bottom', 'init'],
+ ['the-bottom', 'didInitAttrs', { attrs: bottomAttrs }],
+ ['the-bottom', 'didReceiveAttrs', { newAttrs: bottomAttrs }],
+ ['the-bottom', 'willRender'],
+
+ // Async hooks
+
+ ['the-bottom', 'didInsertElement'],
+ ['the-bottom', 'didRender'],
+
+ ['the-middle', 'didInsertElement'],
+ ['the-middle', 'didRender'],
+
+ ['the-top', 'didInsertElement'],
+ ['the-top', 'didRender']
+
+ );
+
+ this.runTask(() => set(this.context, 'twitter', '@horsetomdale'));
+
+ this.assertText('Top: Middle: Bottom: @horsetomdale');
+
+ // Because the `twitter` attr is used by the all of the components,
+ // the lifecycle hooks are invoked for all components.
+
+ topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@horsetomdale' } };
+ middleAttrs = { oldAttrs: { twitterTop: '@tomdale' }, newAttrs: { twitterTop: '@horsetomdale' } };
+ bottomAttrs = { oldAttrs: { twitterMiddle: '@tomdale' }, newAttrs: { twitterMiddle: '@horsetomdale' } };
+
+ this.assertHooks(
+
+ 'after updating (root)',
+
+ // Sync hooks
+
+ ['the-top', 'didUpdateAttrs', topAttrs],
+ ['the-top', 'didReceiveAttrs', topAttrs],
+
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ ['the-middle', 'didUpdateAttrs', middleAttrs],
+ ['the-middle', 'didReceiveAttrs', middleAttrs],
+
+ ['the-middle', 'willUpdate'],
+ ['the-middle', 'willRender'],
+
+ ['the-bottom', 'didUpdateAttrs', bottomAttrs],
+ ['the-bottom', 'didReceiveAttrs', bottomAttrs],
+
+ ['the-bottom', 'willUpdate'],
+ ['the-bottom', 'willRender'],
+
+ // Async hooks
+
+ ['the-bottom', 'didUpdate'],
+ ['the-bottom', 'didRender'],
+
+ ['the-middle', 'didUpdate'],
+ ['the-middle', 'didRender'],
+
+ ['the-top', 'didUpdate'],
+ ['the-top', 'didRender']
+
+ );
+
+ this.runTask(() => this.rerender());
+
+ this.assertText('Top: Middle: Bottom: @horsetomdale');
+
+ // In this case, because the attrs are passed down, all child components are invoked.
+
+ topAttrs = { oldAttrs: { twitter: '@horsetomdale' }, newAttrs: { twitter: '@horsetomdale' } };
+ middleAttrs = { oldAttrs: { twitterTop: '@horsetomdale' }, newAttrs: { twitterTop: '@horsetomdale' } };
+ bottomAttrs = { oldAttrs: { twitterMiddle: '@horsetomdale' }, newAttrs: { twitterMiddle: '@horsetomdale' } };
+
+ if (this.isHTMLBars) {
+ this.assertHooks(
+
+ 'after no-op rernder (root)',
+
+ // Sync hooks
+
+ ['the-top', 'didUpdateAttrs', topAttrs],
+ ['the-top', 'didReceiveAttrs', topAttrs],
+
+ ['the-top', 'willUpdate'],
+ ['the-top', 'willRender'],
+
+ ['the-middle', 'didUpdateAttrs', middleAttrs],
+ ['the-middle', 'didReceiveAttrs', middleAttrs],
+
+ ['the-middle', 'willUpdate'],
+ ['the-middle', 'willRender'],
+
+ ['the-bottom', 'didUpdateAttrs', bottomAttrs],
+ ['the-bottom', 'didReceiveAttrs', bottomAttrs],
+
+ ['the-bottom', 'willUpdate'],
+ ['the-bottom', 'willRender'],
+
+ // Async hooks
+
+ ['the-bottom', 'didUpdate'],
+ ['the-bottom', 'didRender'],
+
+ ['the-middle', 'didUpdate'],
+ ['the-middle', 'didRender'],
+
+ ['the-top', 'didUpdate'],
+ ['the-top', 'didRender']
+
+ );
+ } else {
+ this.assertHooks('after no-op rernder (root)');
+ }
+ }
+
+}
+
+moduleFor('Components test: lifecycle hooks (curly components)', class extends LifeCycleHooksTest {
+
+ get ComponentClass() {
+ return Component;
+ }
+
+ invocationFor(name, namedArgs = {}) {
+ let attrs = Object.keys(namedArgs).map(k => `${k}=${this.val(namedArgs[k])}`).join(' ');
+ return `{{${name} ${attrs}}}`;
+ }
+
+ attrFor(name) {
+ return `${name}`;
+ }
+
+ /* private */
+ val(value) {
+ if (value.isString) {
+ return JSON.stringify(value.value);
+ } else if (value.isExpr) {
+ return `(readonly ${value.value})`;
+ } else {
+ throw new Error(`Unknown value: ${value}`);
+ }
+ }
+
+});
+
+function bind(func, thisArg) {
+ return (...args) => func.apply(thisArg, args);
+}
+
+function string(value) {
+ return { isString: true, value };
+}
+
+function expr(value) {
+ return { isExpr: true, value };
+}
+
+function hook(name, hook, args) {
+ return { name, hook, args };
+}
+
+function json(serializable) {
+ return JSON.parse(JSON.stringify(serializable));
+}
diff --git a/packages/ember-glimmer/tests/integration/components/will-destroy-element-hook-test.js b/packages/ember-glimmer/tests/integration/components/will-destroy-element-hook-test.js
new file mode 100644
index 00000000000..85268e06298
--- /dev/null
+++ b/packages/ember-glimmer/tests/integration/components/will-destroy-element-hook-test.js
@@ -0,0 +1,34 @@
+import { set } from 'ember-metal/property_set';
+import { Component } from '../../utils/helpers';
+import { moduleFor, RenderingTest } from '../../utils/test-case';
+
+moduleFor('Component willDestroyElement hook', class extends RenderingTest {
+ ['@test it calls willDestroyElement when removed by if'](assert) {
+ let didInsertElementCount = 0;
+ let willDestroyElementCount = 0;
+ let FooBarComponent = Component.extend({
+ didInsertElement() {
+ didInsertElementCount++;
+ assert.notEqual(this.element.parentNode, null, 'precond component is in DOM');
+ },
+ willDestroyElement() {
+ willDestroyElementCount++;
+ assert.notEqual(this.element.parentNode, null, 'has not been removed from DOM yet');
+ }
+ });
+
+ this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' });
+
+ this.render('{{#if switch}}{{foo-bar}}{{/if}}', { switch: true });
+
+ assert.equal(didInsertElementCount, 1, 'didInsertElement was called once');
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+
+ this.runTask(() => set(this.context, 'switch', false));
+
+ assert.equal(willDestroyElementCount, 1, 'willDestroyElement was called once');
+
+ this.assertText('');
+ }
+});
diff --git a/packages/ember-glimmer/tests/utils/abstract-test-case.js b/packages/ember-glimmer/tests/utils/abstract-test-case.js
index a5bc4ea1d1c..e2251e38812 100644
--- a/packages/ember-glimmer/tests/utils/abstract-test-case.js
+++ b/packages/ember-glimmer/tests/utils/abstract-test-case.js
@@ -98,6 +98,14 @@ export class TestCase {
this.assert = QUnit.config.current.assert;
}
+ get isHTMLBars() {
+ return packageName === 'htmlbars';
+ }
+
+ get isGlimmer() {
+ return packageName === 'glimmer';
+ }
+
teardown() {}
// The following methods require `this.element` to work
@@ -390,6 +398,10 @@ export class RenderingTest extends TestCase {
}
}
-export function strip([str]) {
+export function strip([...strings], ...values) {
+ let str = strings.map((string, index) => {
+ let interpolated = values[index];
+ return string + (interpolated !== undefined ? interpolated : '');
+ }).join('');
return str.split('\n').map(s => s.trim()).join('');
}
diff --git a/packages/ember-glimmer/tests/utils/test-case.js b/packages/ember-glimmer/tests/utils/test-case.js
index 44429eb99ef..52e27840805 100644
--- a/packages/ember-glimmer/tests/utils/test-case.js
+++ b/packages/ember-glimmer/tests/utils/test-case.js
@@ -26,6 +26,11 @@ export class RenderingTest extends AbstractRenderingTest {
owner.registerOptionsForType('component', { singleton: false });
}
+ render(...args) {
+ super.render(...args);
+ this.renderer._root = this.component;
+ }
+
runTask(callback) {
super.runTask(() => {
callback();
diff --git a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js
index c8a0734ba35..4b8c6e38df2 100644
--- a/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js
+++ b/packages/ember-htmlbars/tests/integration/component_lifecycle_test.js
@@ -316,7 +316,6 @@ styles.forEach(style => {
// Because the `twitter` attr is used by the all of the components,
// the lifecycle hooks are invoked for all components.
-
topAttrs = { oldAttrs: { twitter: '@tomdale' }, newAttrs: { twitter: '@hipstertomdale' } };
middleAttrs = { oldAttrs: { twitterTop: '@tomdale' }, newAttrs: { twitterTop: '@hipstertomdale' } };
bottomAttrs = { oldAttrs: { twitterMiddle: '@tomdale' }, newAttrs: { twitterMiddle: '@hipstertomdale' } };
diff --git a/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js b/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js
deleted file mode 100644
index 6c720a712e3..00000000000
--- a/packages/ember-htmlbars/tests/integration/will-destroy-element-hook-test.js
+++ /dev/null
@@ -1,70 +0,0 @@
-import run from 'ember-metal/run_loop';
-import Component from 'ember-views/components/component';
-import compile from 'ember-template-compiler/system/compile';
-import { runAppend, runDestroy } from 'ember-runtime/tests/utils';
-
-import { set } from 'ember-metal/property_set';
-
-import { registerKeyword, resetKeyword } from 'ember-htmlbars/tests/utils';
-import viewKeyword from 'ember-htmlbars/keywords/view';
-
-var component, originalViewKeyword;
-
-import isEnabled from 'ember-metal/features';
-if (!isEnabled('ember-glimmer')) {
- // jscs:disable
-
-QUnit.module('ember-htmlbars: destroy-element-hook tests', {
- setup() {
- originalViewKeyword = registerKeyword('view', viewKeyword);
- },
- teardown() {
- runDestroy(component);
- resetKeyword('view', originalViewKeyword);
- }
-});
-
-QUnit.test('willDestroyElement is only called once when a component leaves scope', function(assert) {
- var innerChild, innerChildDestroyed;
-
- component = Component.create({
- switch: true,
-
- layout: compile(`
- {{~#if switch~}}
- {{~#view innerChild}}Truthy{{/view~}}
- {{~/if~}}
- `),
-
- innerChild: Component.extend({
- init() {
- this._super(...arguments);
- innerChild = this;
- },
-
- willDestroyElement() {
- if (innerChildDestroyed) {
- throw new Error('willDestroyElement has already been called!!');
- } else {
- innerChildDestroyed = true;
- }
- }
- })
- });
-
- runAppend(component);
-
- assert.equal(component.$().text(), 'Truthy', 'precond - truthy template is displayed');
- assert.equal(component.get('childViews.length'), 1);
-
- run(function() {
- set(component, 'switch', false);
- });
-
- run(function() {
- assert.equal(innerChild.get('isDestroyed'), true, 'the innerChild has been destroyed');
- assert.equal(component.$().text(), '', 'truthy template is removed');
- });
-});
-
-}