diff --git a/packages/@ember/-internals/glimmer/lib/syntax/in-element.ts b/packages/@ember/-internals/glimmer/lib/syntax/in-element.ts
new file mode 100644
index 00000000000..d0d25fad539
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/lib/syntax/in-element.ts
@@ -0,0 +1,38 @@
+/**
+ @module ember
+ */
+
+/**
+ The `in-element` helper renders its block content outside of the regular flow,
+ into a DOM element given by its `destinationElement` positional argument.
+
+ Common use cases - often referred to as "portals" or "wormholes" - are rendering
+ dropdowns, modals or tooltips close to the root of the page to bypass CSS overflow
+ rules, or to render content to parts of the page that are outside of the control
+ of the Ember app itself (e.g. embedded into a static or server rendered HTML page).
+
+ ```handlebars
+ {{#in-element this.destinationElement}}
+
Some content
+ {{/in-element}}
+ ```
+
+ ### Arguments
+
+ `{{in-element}}` requires a single positional argument:
+
+ - `destinationElement` -- the DOM element to render into. It must exist at the time
+ of rendering.
+
+ It also supports an optional named argument:
+
+ - `insertBefore` -- by default the DOM element's content is replaced when used as
+ `destinationElement`. Passing `null` changes the behaviour to appended at the end
+ of any existing content. Any other value than `null` is currently not supported.
+
+ ```
+
+ @method in-element
+ @for Ember.Templates.helpers
+ @public
+ */
diff --git a/packages/@ember/-internals/glimmer/tests/integration/syntax/in-element-test.js b/packages/@ember/-internals/glimmer/tests/integration/syntax/deprecated-in-element-test.js
similarity index 86%
rename from packages/@ember/-internals/glimmer/tests/integration/syntax/in-element-test.js
rename to packages/@ember/-internals/glimmer/tests/integration/syntax/deprecated-in-element-test.js
index ee2f67be7c3..3e1b7019d4a 100644
--- a/packages/@ember/-internals/glimmer/tests/integration/syntax/in-element-test.js
+++ b/packages/@ember/-internals/glimmer/tests/integration/syntax/deprecated-in-element-test.js
@@ -1,12 +1,14 @@
import { moduleFor, RenderingTestCase, strip, equalTokens, runTask } from 'internal-test-helpers';
-
import { Component } from '@ember/-internals/glimmer';
import { set } from '@ember/-internals/metal';
+import { EMBER_GLIMMER_IN_ELEMENT } from '@ember/canary-features';
+
+const deprecationMessage = /The use of the private `{{-in-element}}` is deprecated, please refactor to the public `{{in-element}}`/;
moduleFor(
'{{-in-element}}',
class extends RenderingTestCase {
- ['@test using {{#in-element whatever}} asserts']() {
+ ['@feature(!EMBER_GLIMMER_IN_ELEMENT) using {{#in-element whatever}} asserts']() {
// the in-element keyword is not yet public API this test should be removed
// once https://github.com/emberjs/rfcs/pull/287 lands and is enabled
@@ -17,6 +19,10 @@ moduleFor(
}
['@test allows rendering into an external element']() {
+ if (EMBER_GLIMMER_IN_ELEMENT) {
+ expectDeprecation(deprecationMessage);
+ }
+
let someElement = document.createElement('div');
this.render(
@@ -47,7 +53,11 @@ moduleFor(
equalTokens(someElement, 'Whoop!');
}
- ['@test it appends to the extenal element by default']() {
+ ['@test it appends to the external element by default']() {
+ if (EMBER_GLIMMER_IN_ELEMENT) {
+ expectDeprecation(deprecationMessage);
+ }
+
let someElement = document.createElement('div');
someElement.appendChild(document.createTextNode('foo '));
@@ -80,6 +90,10 @@ moduleFor(
}
['@test allows appending to the external element with insertBefore=null']() {
+ if (EMBER_GLIMMER_IN_ELEMENT) {
+ expectDeprecation(deprecationMessage);
+ }
+
let someElement = document.createElement('div');
someElement.appendChild(document.createTextNode('foo '));
@@ -112,6 +126,10 @@ moduleFor(
}
['@test allows clearing the external element with insertBefore=undefined']() {
+ if (EMBER_GLIMMER_IN_ELEMENT) {
+ expectDeprecation(deprecationMessage);
+ }
+
let someElement = document.createElement('div');
someElement.appendChild(document.createTextNode('foo '));
@@ -144,6 +162,10 @@ moduleFor(
}
['@test does not allow insertBefore=non-null-value']() {
+ if (EMBER_GLIMMER_IN_ELEMENT) {
+ expectDeprecation(deprecationMessage);
+ }
+
let someElement = document.createElement('div');
expectAssertion(() => {
@@ -162,6 +184,10 @@ moduleFor(
}
['@test components are cleaned up properly'](assert) {
+ if (EMBER_GLIMMER_IN_ELEMENT) {
+ expectDeprecation(deprecationMessage);
+ }
+
let hooks = [];
let someElement = document.createElement('div');
@@ -229,7 +255,11 @@ moduleFor(
assert.deepEqual(hooks, ['didInsertElement', 'willDestroyElement']);
}
- ['@test appending to the root element should not cause double clearing are cleaned up properly']() {
+ ['@test appending to the root element should not cause double clearing']() {
+ if (EMBER_GLIMMER_IN_ELEMENT) {
+ expectDeprecation(deprecationMessage);
+ }
+
this.render(
strip`
Before
diff --git a/packages/@ember/-internals/glimmer/tests/integration/syntax/public-in-element-test.js b/packages/@ember/-internals/glimmer/tests/integration/syntax/public-in-element-test.js
new file mode 100644
index 00000000000..9d653c287e7
--- /dev/null
+++ b/packages/@ember/-internals/glimmer/tests/integration/syntax/public-in-element-test.js
@@ -0,0 +1,216 @@
+import { moduleFor, RenderingTestCase, strip, equalTokens, runTask } from 'internal-test-helpers';
+
+import { Component } from '@ember/-internals/glimmer';
+import { set } from '@ember/-internals/metal';
+
+moduleFor(
+ '{{in-element}}',
+ class extends RenderingTestCase {
+ ['@feature(EMBER_GLIMMER_IN_ELEMENT) allows rendering into an external element']() {
+ let someElement = document.createElement('div');
+
+ this.render(
+ strip`
+ {{#in-element someElement}}
+ {{text}}
+ {{/in-element}}
+ `,
+ {
+ someElement,
+ text: 'Whoop!',
+ }
+ );
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, 'Whoop!');
+
+ this.assertStableRerender();
+
+ runTask(() => set(this.context, 'text', 'Huzzah!!'));
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, 'Huzzah!!');
+
+ runTask(() => set(this.context, 'text', 'Whoop!'));
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, 'Whoop!');
+ }
+
+ ["@feature(EMBER_GLIMMER_IN_ELEMENT) it replaces the external element's content by default"]() {
+ let someElement = document.createElement('div');
+ someElement.appendChild(document.createTextNode('foo '));
+
+ this.render(
+ strip`
+ {{#in-element someElement insertBefore=undefined}}
+ {{text}}
+ {{/in-element}}
+ `,
+ {
+ someElement,
+ text: 'bar',
+ }
+ );
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, 'bar');
+
+ this.assertStableRerender();
+
+ runTask(() => set(this.context, 'text', 'bar!!'));
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, 'bar!!');
+
+ runTask(() => set(this.context, 'text', 'bar'));
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, 'bar');
+ }
+
+ ['@feature(EMBER_GLIMMER_IN_ELEMENT) allows appending to the external element with insertBefore=null']() {
+ let someElement = document.createElement('div');
+ someElement.appendChild(document.createTextNode('foo '));
+
+ this.render(
+ strip`
+ {{#in-element someElement insertBefore=null}}
+ {{text}}
+ {{/in-element}}
+ `,
+ {
+ someElement,
+ text: 'bar',
+ }
+ );
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, 'foo bar');
+
+ this.assertStableRerender();
+
+ runTask(() => set(this.context, 'text', 'bar!!'));
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, 'foo bar!!');
+
+ runTask(() => set(this.context, 'text', 'bar'));
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, 'foo bar');
+ }
+
+ ['@feature(EMBER_GLIMMER_IN_ELEMENT) does not allow insertBefore=non-null-value']() {
+ let someElement = document.createElement('div');
+
+ expectAssertion(() => {
+ this.render(
+ strip`
+ {{#in-element someElement insertBefore=".foo"}}
+ {{text}}
+ {{/in-element}}
+ `,
+ {
+ someElement,
+ text: 'Whoop!',
+ }
+ );
+ }, /Can only pass null to insertBefore in in-element, received:/);
+ }
+
+ ['@feature(EMBER_GLIMMER_IN_ELEMENT) components are cleaned up properly'](assert) {
+ let hooks = [];
+
+ let someElement = document.createElement('div');
+
+ this.registerComponent('modal-display', {
+ ComponentClass: Component.extend({
+ didInsertElement() {
+ hooks.push('didInsertElement');
+ },
+
+ willDestroyElement() {
+ hooks.push('willDestroyElement');
+ },
+ }),
+
+ template: `{{text}}`,
+ });
+
+ this.render(
+ strip`
+ {{#if showModal}}
+ {{#in-element someElement}}
+ {{modal-display text=text}}
+ {{/in-element}}
+ {{/if}}
+ `,
+ {
+ someElement,
+ text: 'Whoop!',
+ showModal: false,
+ }
+ );
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, '');
+
+ this.assertStableRerender();
+
+ runTask(() => set(this.context, 'showModal', true));
+
+ equalTokens(this.element, '');
+ this.assertComponentElement(someElement.firstChild, {
+ content: 'Whoop!',
+ });
+
+ runTask(() => set(this.context, 'text', 'Huzzah!'));
+
+ equalTokens(this.element, '');
+ this.assertComponentElement(someElement.firstChild, {
+ content: 'Huzzah!',
+ });
+
+ runTask(() => set(this.context, 'text', 'Whoop!'));
+
+ equalTokens(this.element, '');
+ this.assertComponentElement(someElement.firstChild, {
+ content: 'Whoop!',
+ });
+
+ runTask(() => set(this.context, 'showModal', false));
+
+ equalTokens(this.element, '');
+ equalTokens(someElement, '');
+
+ assert.deepEqual(hooks, ['didInsertElement', 'willDestroyElement']);
+ }
+
+ ['@feature(EMBER_GLIMMER_IN_ELEMENT) appending to the root element should not cause double clearing']() {
+ this.render(
+ strip`
+ Before
+ {{#in-element this.rootElement insertBefore=null}}
+ {{this.text}}
+ {{/in-element}}
+ After
+ `,
+ {
+ rootElement: this.element,
+ text: 'Whoop!',
+ }
+ );
+
+ equalTokens(this.element, 'BeforeWhoop!After');
+
+ this.assertStableRerender();
+
+ runTask(() => set(this.context, 'text', 'Huzzah!'));
+
+ equalTokens(this.element, 'BeforeHuzzah!After');
+
+ // teardown happens in afterEach and should not cause double-clearing error
+ }
+ }
+);
diff --git a/packages/@ember/canary-features/index.ts b/packages/@ember/canary-features/index.ts
index 322e1f96286..0ee835b37d9 100644
--- a/packages/@ember/canary-features/index.ts
+++ b/packages/@ember/canary-features/index.ts
@@ -20,6 +20,7 @@ export const DEFAULT_FEATURES = {
EMBER_CUSTOM_COMPONENT_ARG_PROXY: true,
EMBER_GLIMMER_SET_COMPONENT_TEMPLATE: true,
EMBER_ROUTING_MODEL_ARG: true,
+ EMBER_GLIMMER_IN_ELEMENT: null,
};
/**
@@ -79,3 +80,4 @@ export const EMBER_GLIMMER_SET_COMPONENT_TEMPLATE = featureValue(
FEATURES.EMBER_GLIMMER_SET_COMPONENT_TEMPLATE
);
export const EMBER_ROUTING_MODEL_ARG = featureValue(FEATURES.EMBER_ROUTING_MODEL_ARG);
+export const EMBER_GLIMMER_IN_ELEMENT = featureValue(FEATURES.EMBER_GLIMMER_IN_ELEMENT);
diff --git a/packages/ember-template-compiler/lib/plugins/transform-in-element.ts b/packages/ember-template-compiler/lib/plugins/transform-in-element.ts
index e90ada77221..8ccf5d30e37 100644
--- a/packages/ember-template-compiler/lib/plugins/transform-in-element.ts
+++ b/packages/ember-template-compiler/lib/plugins/transform-in-element.ts
@@ -1,5 +1,6 @@
import { StaticTemplateMeta } from '@ember/-internals/views';
-import { assert } from '@ember/debug';
+import { EMBER_GLIMMER_IN_ELEMENT } from '@ember/canary-features';
+import { assert, deprecate } from '@ember/debug';
import { AST, ASTPlugin, ASTPluginEnvironment } from '@glimmer/syntax';
import calculateLocationDisplay from '../system/calculate-location-display';
import { isPath } from './utils';
@@ -9,17 +10,10 @@ import { isPath } from './utils';
*/
/**
- glimmer-vm has made the `in-element` API public from its perspective (in
- https://github.com/glimmerjs/glimmer-vm/pull/619) so in glimmer-vm the
- correct keyword to use is `in-element`, however Ember is still working through
- its form of `in-element` (see https://github.com/emberjs/rfcs/pull/287).
+ A Glimmer2 AST transformation that handles the public `{{in-element}}` as per RFC287, and deprecates but still
+ continues support for the private `{{-in-element}}`.
- There are enough usages of the pre-existing private API (`{{-in-element`) in
- the wild that we need to transform `{{-in-element` into `{{in-element` during
- template transpilation, but since RFC#287 is not landed and enabled by default we _also_ need
- to prevent folks from starting to use `{{in-element` "for realz".
-
- Tranforms:
+ Transforms:
```handlebars
{{#-in-element someElement}}
@@ -35,16 +29,18 @@ import { isPath } from './utils';
{{/in-element}}
```
- And issues a build time assertion for:
+ And issues a deprecation message.
+
+ Issues a build time assertion for:
```handlebars
- {{#in-element someElement}}
+ {{#in-element someElement insertBefore="some-none-null-value"}}
{{modal-display text=text}}
{{/in-element}}
```
@private
- @class TransformHasBlockSyntax
+ @class TransformInElement
*/
export default function transformInElement(env: ASTPluginEnvironment): ASTPlugin {
let { moduleName } = env.meta as StaticTemplateMeta;
@@ -59,8 +55,33 @@ export default function transformInElement(env: ASTPluginEnvironment): ASTPlugin
if (!isPath(node.path)) return;
if (node.path.original === 'in-element') {
- assert(assertMessage(moduleName, node));
+ if (EMBER_GLIMMER_IN_ELEMENT) {
+ node.hash.pairs.forEach(pair => {
+ if (pair.key === 'insertBefore') {
+ assert(
+ `Can only pass null to insertBefore in in-element, received: ${JSON.stringify(
+ pair.value
+ )}`,
+ pair.value.type === 'NullLiteral' || pair.value.type === 'UndefinedLiteral'
+ );
+ }
+ });
+ } else {
+ assert(assertMessage(moduleName, node));
+ }
} else if (node.path.original === '-in-element') {
+ if (EMBER_GLIMMER_IN_ELEMENT) {
+ let sourceInformation = calculateLocationDisplay(moduleName, node.loc);
+ deprecate(
+ `The use of the private \`{{-in-element}}\` is deprecated, please refactor to the public \`{{in-element}}\`. ${sourceInformation}`,
+ false,
+ {
+ id: 'glimmer.private-in-element',
+ until: '4.0.0',
+ }
+ );
+ }
+
node.path.original = 'in-element';
node.path.parts = ['in-element'];