diff --git a/packages/@ember/canary-features/index.ts b/packages/@ember/canary-features/index.ts
index 477d3b996b6..76726056154 100644
--- a/packages/@ember/canary-features/index.ts
+++ b/packages/@ember/canary-features/index.ts
@@ -15,6 +15,7 @@ export const DEFAULT_FEATURES = {
GLIMMER_CUSTOM_COMPONENT_MANAGER: null,
EMBER_TEMPLATE_BLOCK_LET_HELPER: true,
EMBER_METAL_TRACKED_PROPERTIES: null,
+ EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION: null,
};
/**
@@ -81,3 +82,6 @@ export const GLIMMER_CUSTOM_COMPONENT_MANAGER = featureValue(
export const EMBER_TEMPLATE_BLOCK_LET_HELPER = featureValue(
FEATURES.EMBER_TEMPLATE_BLOCK_LET_HELPER
);
+export const EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION = featureValue(
+ FEATURES.EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION
+);
diff --git a/packages/ember-glimmer/tests/integration/components/angle-bracket-invocation-test.js b/packages/ember-glimmer/tests/integration/components/angle-bracket-invocation-test.js
new file mode 100644
index 00000000000..65545636714
--- /dev/null
+++ b/packages/ember-glimmer/tests/integration/components/angle-bracket-invocation-test.js
@@ -0,0 +1,565 @@
+import { EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION } from '@ember/canary-features';
+import { moduleFor, RenderingTest } from '../../utils/test-case';
+import { set } from 'ember-metal';
+import { Component } from '../../utils/helpers';
+import { strip } from '../../utils/abstract-test-case';
+import { classes } from '../../utils/test-helpers';
+
+if (EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION) {
+ moduleFor(
+ 'AngleBracket Invocation',
+ class extends RenderingTest {
+ '@test it can render a basic template only component'() {
+ this.registerComponent('foo-bar', { template: 'hello' });
+
+ this.render('');
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+ }
+
+ '@test it can render a basic component with template and javascript'() {
+ this.registerComponent('foo-bar', {
+ template: 'FIZZ BAR {{local}}',
+ ComponentClass: Component.extend({ local: 'hey' }),
+ });
+
+ this.render('');
+
+ this.assertComponentElement(this.firstChild, { content: 'FIZZ BAR hey' });
+ }
+
+ '@test it can render a single word component name'() {
+ this.registerComponent('foo', { template: 'hello' });
+
+ this.render('');
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+ }
+
+ '@test it can not render a component name without initial capital letter'(assert) {
+ this.registerComponent('div', {
+ ComponentClass: Component.extend({
+ init() {
+ assert.ok(false, 'should not have created component');
+ },
+ }),
+ });
+
+ this.render('
');
+
+ this.assertElement(this.firstChild, { tagName: 'div', content: '' });
+ }
+
+ '@test it can have a custom id and it is not bound'() {
+ this.registerComponent('foo-bar', { template: '{{id}} {{elementId}}' });
+
+ this.render('', {
+ customId: 'bizz',
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bizz bizz',
+ });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bizz bizz',
+ });
+
+ this.runTask(() => set(this.context, 'customId', 'bar'));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bar bizz',
+ });
+
+ this.runTask(() => set(this.context, 'customId', 'bizz'));
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { id: 'bizz' },
+ content: 'bizz bizz',
+ });
+ }
+
+ '@test it can have a custom tagName'() {
+ let FooBarComponent = Component.extend({
+ tagName: 'foo-bar',
+ });
+
+ this.registerComponent('foo-bar', {
+ ComponentClass: FooBarComponent,
+ template: 'hello',
+ });
+
+ this.render('');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+ }
+
+ '@test it can have a custom tagName from the invocation'() {
+ this.registerComponent('foo-bar', { template: 'hello' });
+
+ this.render('');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'foo-bar',
+ content: 'hello',
+ });
+ }
+
+ '@test it can have custom classNames'() {
+ let FooBarComponent = Component.extend({
+ classNames: ['foo', 'bar'],
+ });
+
+ this.registerComponent('foo-bar', {
+ ComponentClass: FooBarComponent,
+ template: 'hello',
+ });
+
+ this.render('');
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar') },
+ content: 'hello',
+ });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar') },
+ content: 'hello',
+ });
+ }
+
+ '@test class property on components can be dynamic'() {
+ this.registerComponent('foo-bar', { template: 'hello' });
+
+ this.render('', {
+ fooBar: true,
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view foo-bar') },
+ });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view foo-bar') },
+ });
+
+ this.runTask(() => set(this.context, 'fooBar', false));
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view') },
+ });
+
+ this.runTask(() => set(this.context, 'fooBar', true));
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello',
+ attrs: { class: classes('ember-view foo-bar') },
+ });
+ }
+
+ '@test it can set custom classNames from the invocation'() {
+ let FooBarComponent = Component.extend({
+ classNames: ['foo'],
+ });
+
+ this.registerComponent('foo-bar', {
+ ComponentClass: FooBarComponent,
+ template: 'hello',
+ });
+
+ this.render(strip`
+
+
+
+ `);
+
+ this.assertComponentElement(this.nthChild(0), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(1), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(2), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo') },
+ content: 'hello',
+ });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.nthChild(0), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(1), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo bar baz') },
+ content: 'hello',
+ });
+ this.assertComponentElement(this.nthChild(2), {
+ tagName: 'div',
+ attrs: { class: classes('ember-view foo') },
+ content: 'hello',
+ });
+ }
+
+ '@test it has an element'() {
+ let instance;
+
+ let FooBarComponent = Component.extend({
+ init() {
+ this._super();
+ instance = this;
+ },
+ });
+
+ this.registerComponent('foo-bar', {
+ ComponentClass: FooBarComponent,
+ template: 'hello',
+ });
+
+ this.render('');
+
+ let element1 = instance.element;
+
+ this.assertComponentElement(element1, { content: 'hello' });
+
+ this.runTask(() => this.rerender());
+
+ let element2 = instance.element;
+
+ this.assertComponentElement(element2, { content: 'hello' });
+
+ this.assertSameNode(element2, element1);
+ }
+
+ '@test it has the right parentView and childViews'(assert) {
+ let fooBarInstance, fooBarBazInstance;
+
+ let FooBarComponent = Component.extend({
+ init() {
+ this._super();
+ fooBarInstance = this;
+ },
+ });
+
+ let FooBarBazComponent = Component.extend({
+ init() {
+ this._super();
+ fooBarBazInstance = this;
+ },
+ });
+
+ this.registerComponent('foo-bar', {
+ ComponentClass: FooBarComponent,
+ template: 'foo-bar {{foo-bar-baz}}',
+ });
+ this.registerComponent('foo-bar-baz', {
+ ComponentClass: FooBarBazComponent,
+ template: 'foo-bar-baz',
+ });
+
+ this.render('');
+ this.assertText('foo-bar foo-bar-baz');
+
+ assert.equal(fooBarInstance.parentView, this.component);
+ assert.equal(fooBarBazInstance.parentView, fooBarInstance);
+
+ assert.deepEqual(this.component.childViews, [fooBarInstance]);
+ assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
+
+ this.runTask(() => this.rerender());
+ this.assertText('foo-bar foo-bar-baz');
+
+ assert.equal(fooBarInstance.parentView, this.component);
+ assert.equal(fooBarBazInstance.parentView, fooBarInstance);
+
+ assert.deepEqual(this.component.childViews, [fooBarInstance]);
+ assert.deepEqual(fooBarInstance.childViews, [fooBarBazInstance]);
+ }
+
+ '@test it renders passed named arguments'() {
+ this.registerComponent('foo-bar', {
+ template: '{{@foo}}',
+ });
+
+ this.render('', {
+ model: {
+ bar: 'Hola',
+ },
+ });
+
+ this.assertText('Hola');
+
+ this.runTask(() => this.rerender());
+
+ this.assertText('Hola');
+
+ this.runTask(() => this.context.set('model.bar', 'Hello'));
+
+ this.assertText('Hello');
+
+ this.runTask(() => this.context.set('model', { bar: 'Hola' }));
+
+ this.assertText('Hola');
+ }
+
+ '@test it reflects named arguments as properties'() {
+ this.registerComponent('foo-bar', {
+ template: '{{foo}}',
+ });
+
+ this.render('', {
+ model: {
+ bar: 'Hola',
+ },
+ });
+
+ this.assertText('Hola');
+
+ this.runTask(() => this.rerender());
+
+ this.assertText('Hola');
+
+ this.runTask(() => this.context.set('model.bar', 'Hello'));
+
+ this.assertText('Hello');
+
+ this.runTask(() => this.context.set('model', { bar: 'Hola' }));
+
+ this.assertText('Hola');
+ }
+
+ '@test it can render a basic component with a block'() {
+ this.registerComponent('foo-bar', {
+ template: '{{yield}} - In component',
+ });
+
+ this.render('hello');
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello - In component',
+ });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'hello - In component',
+ });
+ }
+
+ '@test it can yield internal and external properties positionally'() {
+ let instance;
+
+ let FooBarComponent = Component.extend({
+ init() {
+ this._super(...arguments);
+ instance = this;
+ },
+ greeting: 'hello',
+ });
+
+ this.registerComponent('foo-bar', {
+ ComponentClass: FooBarComponent,
+ template: '{{yield greeting greetee.firstName}}',
+ });
+
+ this.render(
+ '{{name}} {{person.lastName}}, {{greeting}}',
+ {
+ person: {
+ firstName: 'Joel',
+ lastName: 'Kang',
+ },
+ }
+ );
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'Joel Kang, hello',
+ });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'Joel Kang, hello',
+ });
+
+ this.runTask(() =>
+ set(this.context, 'person', {
+ firstName: 'Dora',
+ lastName: 'the Explorer',
+ })
+ );
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'Dora the Explorer, hello',
+ });
+
+ this.runTask(() => set(instance, 'greeting', 'hola'));
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'Dora the Explorer, hola',
+ });
+
+ this.runTask(() => {
+ set(instance, 'greeting', 'hello');
+ set(this.context, 'person', {
+ firstName: 'Joel',
+ lastName: 'Kang',
+ });
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ content: 'Joel Kang, hello',
+ });
+ }
+
+ '@test positional parameters are not allowed'() {
+ this.registerComponent('sample-component', {
+ ComponentClass: Component.extend().reopenClass({
+ positionalParams: ['name', 'age'],
+ }),
+ template: '{{name}}{{age}}',
+ });
+
+ // this is somewhat silly as the browser "corrects" for these as
+ // attribute names, but regardless the thing we care about here is that
+ // they are **not** used as positional params
+ this.render('');
+
+ this.assertText('');
+ }
+
+ '@skip can invoke curried components with capitalized block param names'() {
+ this.registerComponent('foo-bar', { template: 'hello' });
+
+ this.render(strip`
+ {{#with (component 'foo-bar') as |Other|}}
+
+ {{/with}}
+ `);
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, { content: 'hello' });
+
+ this.assertStableRerender();
+ }
+
+ '@skip has-block'() {
+ this.registerComponent('check-block', {
+ template: strip`
+ {{#if (has-block)}}
+ Yes
+ {{else}}
+ No
+ {{/if}}`,
+ });
+
+ this.render(strip`
+
+ `);
+
+ this.assertComponentElement(this.firstChild, { content: 'No' });
+ this.assertComponentElement(this.nthChild(1), { content: 'Yes' });
+
+ this.assertStableRerender();
+ }
+
+ '@skip includes invocation specified attributes in root element ("splattributes")'() {
+ this.registerComponent('foo-bar', {
+ ComponentClass: Component.extend(),
+ template: 'hello',
+ });
+
+ this.render('', { foo: 'foo', bar: 'bar' });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
+ content: 'hello',
+ });
+
+ this.runTask(() => this.rerender());
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
+ content: 'hello',
+ });
+
+ this.runTask(() => {
+ set(this.context, 'foo', 'FOO');
+ set(this.context, 'bar', undefined);
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'FOO' },
+ content: 'hello',
+ });
+
+ this.runTask(() => {
+ set(this.context, 'foo', 'foo');
+ set(this.context, 'bar', 'bar');
+ });
+
+ this.assertComponentElement(this.firstChild, {
+ tagName: 'div',
+ attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
+ content: 'hello',
+ });
+ }
+ }
+ );
+}
diff --git a/packages/ember-glimmer/tests/integration/components/contextual-components-test.js b/packages/ember-glimmer/tests/integration/components/contextual-components-test.js
index 37f9a58a4e8..cabc142f68b 100644
--- a/packages/ember-glimmer/tests/integration/components/contextual-components-test.js
+++ b/packages/ember-glimmer/tests/integration/components/contextual-components-test.js
@@ -1,3 +1,4 @@
+import { EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION } from '@ember/canary-features';
import { assign } from '@ember/polyfills';
import { Component } from '../../utils/helpers';
import { applyMixins, strip } from '../../utils/abstract-test-case';
@@ -1377,15 +1378,23 @@ moduleFor(
}
['@test GH#14632 give useful warning when calling contextual components with input as a name']() {
+ let expectedMessage = EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION
+ ? "You cannot use 'input' as a component name. Component names must contain a hyphen or start with a capital letter."
+ : "You cannot use 'input' as a component name. Component names must contain a hyphen.";
+
expectAssertion(() => {
this.render('{{component (component "input" type="text")}}');
- }, "You cannot use 'input' as a component name. Component names must contain a hyphen.");
+ }, expectedMessage);
}
['@test GH#14632 give useful warning when calling contextual components with textarea as a name']() {
+ let expectedMessage = EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION
+ ? "You cannot use 'textarea' as a component name. Component names must contain a hyphen or start with a capital letter."
+ : "You cannot use 'textarea' as a component name. Component names must contain a hyphen.";
+
expectAssertion(() => {
this.render('{{component (component "textarea" type="text")}}');
- }, "You cannot use 'textarea' as a component name. Component names must contain a hyphen.");
+ }, expectedMessage);
}
}
);
diff --git a/packages/ember-glimmer/tests/integration/components/dynamic-components-test.js b/packages/ember-glimmer/tests/integration/components/dynamic-components-test.js
index ed188318783..d46211f26e9 100644
--- a/packages/ember-glimmer/tests/integration/components/dynamic-components-test.js
+++ b/packages/ember-glimmer/tests/integration/components/dynamic-components-test.js
@@ -1,3 +1,4 @@
+import { EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION } from '@ember/canary-features';
import { set, computed } from 'ember-metal';
import { jQueryDisabled } from 'ember-views';
import { Component } from '../../utils/helpers';
@@ -579,7 +580,12 @@ moduleFor(
this.assertText('[Robert - Robert][Jacquie - Jacquie]');
}
- ['@test dashless components should not be found']() {
+ ['@test dashless components should not be found'](assert) {
+ if (EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION) {
+ assert.ok(true, 'test is not applicable');
+ return;
+ }
+
this.registerComponent('dashless2', { template: 'Do not render me!' });
expectAssertion(() => {
diff --git a/packages/ember-template-compiler/lib/plugins/assert-splattribute-expression.js b/packages/ember-template-compiler/lib/plugins/assert-splattribute-expression.js
new file mode 100644
index 00000000000..fa44b7474aa
--- /dev/null
+++ b/packages/ember-template-compiler/lib/plugins/assert-splattribute-expression.js
@@ -0,0 +1,33 @@
+import { assert } from '@ember/debug';
+import calculateLocationDisplay from '../system/calculate-location-display';
+import { EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION } from '@ember/canary-features';
+
+export default function assertSplattributeExpressions(env) {
+ let { moduleName } = env.meta;
+
+ return {
+ name: 'assert-splattribute-expressions',
+
+ visitor: {
+ AttrNode({ name, loc }) {
+ if (!EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION && name === '...attributes') {
+ assert(`${errorMessage()} ${calculateLocationDisplay(moduleName, loc)}`);
+ }
+ },
+
+ PathExpression({ original, loc }) {
+ if (original === '...attributes') {
+ assert(`${errorMessage()} ${calculateLocationDisplay(moduleName, loc)}`);
+ }
+ },
+ },
+ };
+}
+
+function errorMessage() {
+ if (EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION) {
+ return `Using "...attributes" can only be used in the element position e.g. . It cannot be used as a path.`;
+ }
+
+ return `...attributes is an invalid path`;
+}
diff --git a/packages/ember-template-compiler/lib/plugins/index.js b/packages/ember-template-compiler/lib/plugins/index.js
index 049f22cbdd0..2b9880cece9 100644
--- a/packages/ember-template-compiler/lib/plugins/index.js
+++ b/packages/ember-template-compiler/lib/plugins/index.js
@@ -16,6 +16,7 @@ import TransformDotComponentInvocation from './transform-dot-component-invocatio
import AssertInputHelperWithoutBlock from './assert-input-helper-without-block';
import TransformInElement from './transform-in-element';
import AssertIfHelperWithoutArguments from './assert-if-helper-without-arguments';
+import AssertSplattributeExpressions from './assert-splattribute-expression';
const transforms = [
TransformDotComponentInvocation,
@@ -36,6 +37,7 @@ const transforms = [
AssertInputHelperWithoutBlock,
TransformInElement,
AssertIfHelperWithoutArguments,
+ AssertSplattributeExpressions,
];
export default Object.freeze(transforms);
diff --git a/packages/ember-template-compiler/tests/plugins/assert-splattribute-expression-test.js b/packages/ember-template-compiler/tests/plugins/assert-splattribute-expression-test.js
new file mode 100644
index 00000000000..64ff178ad6c
--- /dev/null
+++ b/packages/ember-template-compiler/tests/plugins/assert-splattribute-expression-test.js
@@ -0,0 +1,57 @@
+import { EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION } from '@ember/canary-features';
+import { moduleFor, AbstractTestCase } from 'internal-test-helpers';
+import { compile } from '../../index';
+
+moduleFor(
+ 'ember-template-compiler: assert-splattribute-expression',
+ class extends AbstractTestCase {
+ expectedMessage(locInfo) {
+ return EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION
+ ? `Using "...attributes" can only be used in the element position e.g. . It cannot be used as a path. (${locInfo}) `
+ : `...attributes is an invalid path (${locInfo}) `;
+ }
+
+ '@test ...attributes is in element space'(assert) {
+ if (EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION) {
+ assert.expect(0);
+
+ compile('Foo
');
+ } else {
+ expectAssertion(() => {
+ compile('Foo
');
+ }, this.expectedMessage('L1:C5'));
+ }
+ }
+
+ '@test {{...attributes}} is not valid'() {
+ expectAssertion(() => {
+ compile('{{...attributes}}
', {
+ moduleName: 'foo-bar',
+ });
+ }, this.expectedMessage(`'foo-bar' @ L1:C7`));
+ }
+
+ '@test {{...attributes}} is not valid path expression'() {
+ expectAssertion(() => {
+ compile('{{...attributes}}
', {
+ moduleName: 'foo-bar',
+ });
+ }, this.expectedMessage(`'foo-bar' @ L1:C7`));
+ }
+ '@test {{...attributes}} is not valid modifier'() {
+ expectAssertion(() => {
+ compile('Wat
', {
+ moduleName: 'foo-bar',
+ });
+ }, this.expectedMessage(`'foo-bar' @ L1:C7`));
+ }
+
+ '@test {{...attributes}} is not valid attribute'() {
+ expectAssertion(() => {
+ compile('Wat
', {
+ moduleName: 'foo-bar',
+ });
+ }, this.expectedMessage(`'foo-bar' @ L1:C13`));
+ }
+ }
+);
diff --git a/packages/ember-views/lib/component_lookup.js b/packages/ember-views/lib/component_lookup.js
index ed6277c1c2f..d54677d1445 100644
--- a/packages/ember-views/lib/component_lookup.js
+++ b/packages/ember-views/lib/component_lookup.js
@@ -1,22 +1,38 @@
+import { EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION } from '@ember/canary-features';
import { assert } from '@ember/debug';
+import { dasherize } from '@ember/string';
import { Object as EmberObject } from 'ember-runtime';
export default EmberObject.extend({
- componentFor(name, owner, options) {
+ componentFor(_name, owner, options) {
assert(
- `You cannot use '${name}' as a component name. Component names must contain a hyphen.`,
- ~name.indexOf('-')
+ `You cannot use '${_name}' as a component name. Component names must contain a hyphen${
+ EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION ? ' or start with a capital letter' : ''
+ }.`,
+ _name.indexOf('-') > -1 ||
+ (EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION &&
+ _name.charAt(0) === _name.charAt(0).toUpperCase())
);
+ let name =
+ EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION && _name.charAt(0) === _name.charAt(0).toUpperCase()
+ ? dasherize(_name)
+ : _name;
let fullName = `component:${name}`;
return owner.factoryFor(fullName, options);
},
- layoutFor(name, owner, options) {
+ layoutFor(_name, owner, options) {
assert(
- `You cannot use '${name}' as a component name. Component names must contain a hyphen.`,
- ~name.indexOf('-')
+ `You cannot use '${_name}' as a component name. Component names must contain a hyphen.`,
+ _name.indexOf('-') > -1 ||
+ (EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION &&
+ _name.charAt(0) === _name.charAt(0).toUpperCase())
);
+ let name =
+ EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION && _name.charAt(0) === _name.charAt(0).toUpperCase()
+ ? dasherize(_name)
+ : _name;
let templateFullName = `template:components/${name}`;
return owner.lookup(templateFullName, options);
},
diff --git a/packages/internal-test-helpers/lib/module-for.js b/packages/internal-test-helpers/lib/module-for.js
index 941ba1e5dbf..e4468190b4c 100644
--- a/packages/internal-test-helpers/lib/module-for.js
+++ b/packages/internal-test-helpers/lib/module-for.js
@@ -73,7 +73,7 @@ export default function moduleFor(description, TestClass, ...mixins) {
return this.instance[name](assert);
});
} else {
- let match = /^@feature\(([a-z-!]+)\) /.exec(name);
+ let match = /^@feature\(([A-Z_a-z-!]+)\) /.exec(name);
if (match) {
let features = match[1].replace(/ /g, '').split(',');