diff --git a/packages/@ember/-internals/glimmer/lib/resolver.ts b/packages/@ember/-internals/glimmer/lib/resolver.ts index e1ae8935595..ed9b7e2b3c0 100644 --- a/packages/@ember/-internals/glimmer/lib/resolver.ts +++ b/packages/@ember/-internals/glimmer/lib/resolver.ts @@ -8,6 +8,7 @@ import { EMBER_GLIMMER_SET_COMPONENT_TEMPLATE, EMBER_MODULE_UNIFICATION, } from '@ember/canary-features'; +import { isTemplateOnlyComponent } from '@ember/component/template-only'; import { assert } from '@ember/debug'; import { _instrumentStart } from '@ember/instrumentation'; import { @@ -449,7 +450,14 @@ export default class RuntimeResolver implements IRuntimeResolver = null; - if (pair.component === null && ENV._TEMPLATE_ONLY_GLIMMER_COMPONENTS) { + if (pair.component === null) { + if (ENV._TEMPLATE_ONLY_GLIMMER_COMPONENTS) { + definition = new TemplateOnlyComponentDefinition(layout!); + } + } else if ( + EMBER_GLIMMER_SET_COMPONENT_TEMPLATE && + isTemplateOnlyComponent(pair.component.class) + ) { definition = new TemplateOnlyComponentDefinition(layout!); } diff --git a/packages/@ember/-internals/glimmer/tests/integration/components/template-only-components-test.js b/packages/@ember/-internals/glimmer/tests/integration/components/template-only-components-test.js index f35044b16d2..ba1fb154a47 100644 --- a/packages/@ember/-internals/glimmer/tests/integration/components/template-only-components-test.js +++ b/packages/@ember/-internals/glimmer/tests/integration/components/template-only-components-test.js @@ -1,6 +1,10 @@ import { moduleFor, RenderingTestCase, classes, runTask } from 'internal-test-helpers'; +import { EMBER_GLIMMER_SET_COMPONENT_TEMPLATE } from '@ember/canary-features'; import { ENV } from '@ember/-internals/environment'; +import { setComponentTemplate } from '@ember/-internals/glimmer'; +import templateOnly from '@ember/component/template-only'; +import { compile } from 'ember-template-compiler'; class TemplateOnlyComponentsTest extends RenderingTestCase { registerComponent(name, template) { @@ -247,3 +251,57 @@ moduleFor( } } ); + +if (EMBER_GLIMMER_SET_COMPONENT_TEMPLATE) { + moduleFor( + 'Components test: template-only components (using `templateOnlyComponent()`)', + class extends RenderingTestCase { + ['@test it can render a component']() { + this.registerComponent('foo-bar', { ComponentClass: templateOnly(), template: 'hello' }); + + this.render('{{foo-bar}}'); + + this.assertInnerHTML('hello'); + + this.assertStableRerender(); + } + + ['@test it can render a component when template was not registered']() { + let ComponentClass = templateOnly(); + setComponentTemplate(compile('hello'), ComponentClass); + + this.registerComponent('foo-bar', { ComponentClass }); + + this.render('{{foo-bar}}'); + + this.assertInnerHTML('hello'); + + this.assertStableRerender(); + } + + ['@test setComponentTemplate takes precedence over registered layout']() { + let ComponentClass = templateOnly(); + setComponentTemplate(compile('hello'), ComponentClass); + + this.registerComponent('foo-bar', { + ComponentClass, + template: 'this should not be rendered', + }); + + this.render('{{foo-bar}}'); + + this.assertInnerHTML('hello'); + + this.assertStableRerender(); + } + + ['@test templateOnly accepts a moduleName to be used for debugging / toString purposes']( + assert + ) { + let ComponentClass = templateOnly('my-app/components/foo'); + + assert.equal(`${ComponentClass}`, 'my-app/components/foo'); + } + } + ); +} diff --git a/packages/@ember/component/index.ts b/packages/@ember/component/index.ts new file mode 100644 index 00000000000..4a85c81bf0c --- /dev/null +++ b/packages/@ember/component/index.ts @@ -0,0 +1 @@ +export { Component } from '@ember/-internals/glimmer'; diff --git a/packages/@ember/component/template-only.ts b/packages/@ember/component/template-only.ts new file mode 100644 index 00000000000..a74f1b5e195 --- /dev/null +++ b/packages/@ember/component/template-only.ts @@ -0,0 +1,45 @@ +// This is only exported for types, don't use this class directly +export class TemplateOnlyComponent { + constructor(public moduleName = '@ember/component/template-only') {} + + toString(): string { + return this.moduleName; + } +} + +/** + @module @ember/component/template-only + @public +*/ + +/** + This utility function is used to declare a given component has no backing class. When the rendering engine detects this it + is able to perform a number of optimizations. Templates that are associated with `templateOnly()` will be rendered _as is_ + without adding a wrapping `
` (or any of the other element customization behaviors of [@ember/component](/ember/release/classes/Component)). + Specifically, this means that the template will be rendered as "outer HTML". + + In general, this method will be used by build time tooling and would not be directly written in an application. However, + at times it may be useful to use directly to leverage the "outer HTML" semantics mentioned above. For example, if an addon would like + to use these semantics for its templates but cannot be certain it will only be consumed by applications that have enabled the + `template-only-glimmer-components` optional feature. + + @example + + ```js + import templateOnly from '@ember/component/template-only'; + + export default templateOnly(); + ``` + + @public + @method templateOnly + @param {String} moduleName the module name that the template only component represents, this will be used for debugging purposes + @category EMBER_GLIMMER_SET_COMPONENT_TEMPLATE +*/ +export default function templateOnlyComponent(moduleName: string): TemplateOnlyComponent { + return new TemplateOnlyComponent(moduleName); +} + +export function isTemplateOnlyComponent(component: unknown): component is TemplateOnlyComponent { + return component instanceof TemplateOnlyComponent; +} diff --git a/packages/ember/index.js b/packages/ember/index.js index c42f51f7c2e..e92318ed02e 100644 --- a/packages/ember/index.js +++ b/packages/ember/index.js @@ -137,7 +137,7 @@ import Engine from '@ember/engine'; import EngineInstance from '@ember/engine/instance'; import { assign, merge } from '@ember/polyfills'; import { LOGGER, EMBER_EXTEND_PROTOTYPES, JQUERY_INTEGRATION } from '@ember/deprecated-features'; - +import templateOnlyComponent from '@ember/component/template-only'; // ****@ember/-internals/environment**** const Ember = (typeof context.imports.Ember === 'object' && context.imports.Ember) || {}; @@ -540,6 +540,7 @@ Ember._modifierManagerCapabilties = modifierCapabilties; if (EMBER_GLIMMER_SET_COMPONENT_TEMPLATE) { Ember._getComponentTemplate = getComponentTemplate; Ember._setComponentTemplate = setComponentTemplate; + Ember._templateOnlyComponent = templateOnlyComponent; } Ember.Handlebars = { template, diff --git a/packages/ember/tests/reexports_test.js b/packages/ember/tests/reexports_test.js index 54b5dac153e..d2873650453 100644 --- a/packages/ember/tests/reexports_test.js +++ b/packages/ember/tests/reexports_test.js @@ -232,6 +232,9 @@ let allExports = [ EMBER_GLIMMER_SET_COMPONENT_TEMPLATE ? ['_getComponentTemplate', '@ember/-internals/glimmer', 'getComponentTemplate'] : null, + EMBER_GLIMMER_SET_COMPONENT_TEMPLATE + ? ['_templateOnlyComponent', '@ember/component/template-only', 'default'] + : null, // @ember/-internals/runtime ['A', '@ember/-internals/runtime'], diff --git a/tests/docs/expected.js b/tests/docs/expected.js index 2575a1b2dd7..3ed1967bb02 100644 --- a/tests/docs/expected.js +++ b/tests/docs/expected.js @@ -532,6 +532,7 @@ module.exports = { 'target', 'teardownViews', 'templateName', + 'templateOnly', 'testCheckboxClick', 'testHelpers', 'testing',