import { EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION } from '@ember/canary-features';
import { set, computed } from '@ember/-internals/metal';
import { jQueryDisabled } from '@ember/-internals/views';
import { Component } from '../../utils/helpers';
import { strip } from '../../utils/abstract-test-case';
import { moduleFor, RenderingTest } from '../../utils/test-case';
moduleFor(
'Components test: dynamic components',
class extends RenderingTest {
['@test it can render a basic component with a static component name argument']() {
this.registerComponent('foo-bar', { template: 'hello {{name}}' });
this.render('{{component "foo-bar" name=name}}', { name: 'Sarah' });
this.assertComponentElement(this.firstChild, { content: 'hello Sarah' });
this.runTask(() => this.rerender());
this.assertComponentElement(this.firstChild, { content: 'hello Sarah' });
this.runTask(() => set(this.context, 'name', 'Gavin'));
this.assertComponentElement(this.firstChild, { content: 'hello Gavin' });
this.runTask(() => set(this.context, 'name', 'Sarah'));
this.assertComponentElement(this.firstChild, { content: 'hello Sarah' });
}
['@test it can render a basic component with a dynamic component name argument']() {
this.registerComponent('foo-bar', {
template: 'hello {{name}} from foo-bar',
});
this.registerComponent('foo-bar-baz', {
template: 'hello {{name}} from foo-bar-baz',
});
this.render('{{component componentName name=name}}', {
componentName: 'foo-bar',
name: 'Alex',
});
this.assertComponentElement(this.firstChild, {
content: 'hello Alex from foo-bar',
});
this.runTask(() => this.rerender());
this.assertComponentElement(this.firstChild, {
content: 'hello Alex from foo-bar',
});
this.runTask(() => set(this.context, 'name', 'Ben'));
this.assertComponentElement(this.firstChild, {
content: 'hello Ben from foo-bar',
});
this.runTask(() => set(this.context, 'componentName', 'foo-bar-baz'));
this.assertComponentElement(this.firstChild, {
content: 'hello Ben from foo-bar-baz',
});
this.runTask(() => {
set(this.context, 'componentName', 'foo-bar');
set(this.context, 'name', 'Alex');
});
this.assertComponentElement(this.firstChild, {
content: 'hello Alex from foo-bar',
});
}
['@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('{{component "foo-bar"}}');
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('{{component "foo-bar"}}');
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 can render a basic component with a block']() {
this.registerComponent('foo-bar', { template: '{{yield}}' });
this.render('{{#component "foo-bar"}}hello{{/component}}');
this.assertComponentElement(this.firstChild, { content: 'hello' });
this.runTask(() => this.rerender());
this.assertComponentElement(this.firstChild, { content: 'hello' });
}
['@test it renders the layout with the component instance as the context']() {
let instance;
let FooBarComponent = Component.extend({
init() {
this._super();
instance = this;
this.set('message', 'hello');
},
});
this.registerComponent('foo-bar', {
ComponentClass: FooBarComponent,
template: '{{message}}',
});
this.render('{{component "foo-bar"}}');
this.assertComponentElement(this.firstChild, { content: 'hello' });
this.runTask(() => this.rerender());
this.assertComponentElement(this.firstChild, { content: 'hello' });
this.runTask(() => set(instance, 'message', 'goodbye'));
this.assertComponentElement(this.firstChild, { content: 'goodbye' });
this.runTask(() => set(instance, 'message', 'hello'));
this.assertComponentElement(this.firstChild, { content: 'hello' });
}
['@test it preserves the outer context when yielding']() {
this.registerComponent('foo-bar', { template: '{{yield}}' });
this.render('{{#component "foo-bar"}}{{message}}{{/component}}', {
message: 'hello',
});
this.assertComponentElement(this.firstChild, { content: 'hello' });
this.runTask(() => this.rerender());
this.assertComponentElement(this.firstChild, { content: 'hello' });
this.runTask(() => set(this.context, 'message', 'goodbye'));
this.assertComponentElement(this.firstChild, { content: 'goodbye' });
this.runTask(() => set(this.context, 'message', 'hello'));
this.assertComponentElement(this.firstChild, { content: 'hello' });
}
['@test the component and its child components are destroyed'](assert) {
let destroyed = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0 };
this.registerComponent('foo-bar', {
template: '{{id}} {{yield}}',
ComponentClass: Component.extend({
willDestroy() {
this._super();
destroyed[this.get('id')]++;
},
}),
});
this.render(
strip`
{{#if cond1}}
{{#component "foo-bar" id=1}}
{{#if cond2}}
{{#component "foo-bar" id=2}}{{/component}}
{{#if cond3}}
{{#component "foo-bar" id=3}}
{{#if cond4}}
{{#component "foo-bar" id=4}}
{{#if cond5}}
{{#component "foo-bar" id=5}}{{/component}}
{{#component "foo-bar" id=6}}{{/component}}
{{#component "foo-bar" id=7}}{{/component}}
{{/if}}
{{#component "foo-bar" id=8}}{{/component}}
{{/component}}
{{/if}}
{{/component}}
{{/if}}
{{/if}}
{{/component}}
{{/if}}`,
{
cond1: true,
cond2: true,
cond3: true,
cond4: true,
cond5: true,
}
);
this.assertText('1 2 3 4 5 6 7 8 ');
this.runTask(() => this.rerender());
assert.deepEqual(destroyed, {
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
6: 0,
7: 0,
8: 0,
});
this.runTask(() => set(this.context, 'cond5', false));
this.assertText('1 2 3 4 8 ');
assert.deepEqual(destroyed, {
1: 0,
2: 0,
3: 0,
4: 0,
5: 1,
6: 1,
7: 1,
8: 0,
});
this.runTask(() => {
set(this.context, 'cond3', false);
set(this.context, 'cond5', true);
set(this.context, 'cond4', false);
});
assert.deepEqual(destroyed, {
1: 0,
2: 0,
3: 1,
4: 1,
5: 1,
6: 1,
7: 1,
8: 1,
});
this.runTask(() => {
set(this.context, 'cond2', false);
set(this.context, 'cond1', false);
});
assert.deepEqual(destroyed, {
1: 1,
2: 1,
3: 1,
4: 1,
5: 1,
6: 1,
7: 1,
8: 1,
});
}
['@test component helper destroys underlying component when it is swapped out'](assert) {
let destroyed = { 'foo-bar': 0, 'foo-bar-baz': 0 };
let testContext = this;
this.registerComponent('foo-bar', {
template: 'hello from foo-bar',
ComponentClass: Component.extend({
willDestroyElement() {
assert.equal(
testContext.$(`#${this.elementId}`).length,
1,
'element is still attached to the document'
);
},
willDestroy() {
this._super();
destroyed['foo-bar']++;
},
}),
});
this.registerComponent('foo-bar-baz', {
template: 'hello from foo-bar-baz',
ComponentClass: Component.extend({
willDestroy() {
this._super();
destroyed['foo-bar-baz']++;
},
}),
});
this.render('{{component componentName name=name}}', {
componentName: 'foo-bar',
});
assert.deepEqual(destroyed, { 'foo-bar': 0, 'foo-bar-baz': 0 });
this.runTask(() => this.rerender());
assert.deepEqual(destroyed, { 'foo-bar': 0, 'foo-bar-baz': 0 });
this.runTask(() => set(this.context, 'componentName', 'foo-bar-baz'));
assert.deepEqual(destroyed, { 'foo-bar': 1, 'foo-bar-baz': 0 });
this.runTask(() => set(this.context, 'componentName', 'foo-bar'));
assert.deepEqual(destroyed, { 'foo-bar': 1, 'foo-bar-baz': 1 });
}
['@test component helper with bound properties are updating correctly in init of component']() {
this.registerComponent('foo-bar', {
template: 'foo-bar {{location}} {{locationCopy}} {{yield}}',
ComponentClass: Component.extend({
init: function() {
this._super(...arguments);
this.set('locationCopy', this.get('location'));
},
}),
});
this.registerComponent('foo-bar-baz', {
template: 'foo-bar-baz {{location}} {{locationCopy}} {{yield}}',
ComponentClass: Component.extend({
init: function() {
this._super(...arguments);
this.set('locationCopy', this.get('location'));
},
}),
});
this.registerComponent('outer-component', {
template: '{{#component componentName location=location}}arepas!{{/component}}',
ComponentClass: Component.extend({
componentName: computed('location', function() {
if (this.get('location') === 'Caracas') {
return 'foo-bar';
} else {
return 'foo-bar-baz';
}
}),
}),
});
this.render('{{outer-component location=location}}', {
location: 'Caracas',
});
this.assertText('foo-bar Caracas Caracas arepas!');
this.runTask(() => this.rerender());
this.assertText('foo-bar Caracas Caracas arepas!');
this.runTask(() => set(this.context, 'location', 'Loisaida'));
this.assertText('foo-bar-baz Loisaida Loisaida arepas!');
this.runTask(() => set(this.context, 'location', 'Caracas'));
this.assertText('foo-bar Caracas Caracas arepas!');
}
['@test component helper with actions'](assert) {
this.registerComponent('inner-component', {
template: 'inner-component {{yield}}',
ComponentClass: Component.extend({
classNames: 'inner-component',
didInsertElement() {
// trigger action on click in absence of app's EventDispatcher
let sendAction = (this.eventHandler = () => {
if (this.somethingClicked) {
this.somethingClicked();
}
});
this.element.addEventListener('click', sendAction);
},
willDestroyElement() {
this.element.removeEventListener('click', this.eventHandler);
},
}),
});
let actionTriggered = 0;
this.registerComponent('outer-component', {
template:
'{{#component componentName somethingClicked=(action "mappedAction")}}arepas!{{/component}}',
ComponentClass: Component.extend({
classNames: 'outer-component',
componentName: 'inner-component',
actions: {
mappedAction() {
actionTriggered++;
},
},
}),
});
this.render('{{outer-component}}');
assert.equal(actionTriggered, 0, 'action was not triggered');
this.runTask(() => {
this.$('.inner-component').click();
});
assert.equal(actionTriggered, 1, 'action was triggered');
}
['@test nested component helpers']() {
this.registerComponent('foo-bar', {
template: 'yippie! {{attrs.location}} {{yield}}',
});
this.registerComponent('baz-qux', {
template: 'yummy {{attrs.location}} {{yield}}',
});
this.registerComponent('corge-grault', {
template: 'delicious {{attrs.location}} {{yield}}',
});
this.render(
'{{#component componentName1 location=location}}{{#component componentName2 location=location}}arepas!{{/component}}{{/component}}',
{
componentName1: 'foo-bar',
componentName2: 'baz-qux',
location: 'Caracas',
}
);
this.assertText('yippie! Caracas yummy Caracas arepas!');
this.runTask(() => this.rerender());
this.assertText('yippie! Caracas yummy Caracas arepas!');
this.runTask(() => set(this.context, 'location', 'Loisaida'));
this.assertText('yippie! Loisaida yummy Loisaida arepas!');
this.runTask(() => set(this.context, 'componentName1', 'corge-grault'));
this.assertText('delicious Loisaida yummy Loisaida arepas!');
this.runTask(() => {
set(this.context, 'componentName1', 'foo-bar');
set(this.context, 'location', 'Caracas');
});
this.assertText('yippie! Caracas yummy Caracas arepas!');
}
['@test component with dynamic name argument resolving to non-existent component']() {
expectAssertion(() => {
this.render('{{component componentName}}', {
componentName: 'does-not-exist',
});
}, /Could not find component named "does-not-exist"/);
}
['@test component with static name argument for non-existent component']() {
expectAssertion(() => {
this.render('{{component "does-not-exist"}}');
}, /Could not find component named "does-not-exist"/);
}
['@test component with dynamic component name resolving to a component, then non-existent component']() {
this.registerComponent('foo-bar', { template: 'hello {{name}}' });
this.render('{{component componentName name=name}}', {
componentName: 'foo-bar',
name: 'Alex',
});
this.assertText('hello Alex');
this.runTask(() => this.rerender());
this.assertText('hello Alex');
this.runTask(() => set(this.context, 'componentName', undefined));
this.assertText('');
this.runTask(() => set(this.context, 'componentName', 'foo-bar'));
this.assertText('hello Alex');
}
['@test component helper properly invalidates hash params inside an {{each}} invocation #11044']() {
this.registerComponent('foo-bar', {
template: '[{{internalName}} - {{name}}]',
ComponentClass: Component.extend({
willRender() {
// store internally available name to ensure that the name available in `this.attrs.name`
// matches the template lookup name
set(this, 'internalName', this.get('name'));
},
}),
});
this.render('{{#each items as |item|}}{{component "foo-bar" name=item.name}}{{/each}}', {
items: [{ name: 'Robert' }, { name: 'Jacquie' }],
});
this.assertText('[Robert - Robert][Jacquie - Jacquie]');
this.runTask(() => this.rerender());
this.assertText('[Robert - Robert][Jacquie - Jacquie]');
this.runTask(() => set(this.context, 'items', [{ name: 'Max' }, { name: 'James' }]));
this.assertText('[Max - Max][James - James]');
this.runTask(() => set(this.context, 'items', [{ name: 'Robert' }, { name: 'Jacquie' }]));
this.assertText('[Robert - Robert][Jacquie - Jacquie]');
}
['@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(() => {
this.render('{{component "dashless"}}');
}, /You cannot use 'dashless' as a component name. Component names must contain a hyphen./);
}
['@test positional parameters does not clash when rendering different components']() {
this.registerComponent('foo-bar', {
template: 'hello {{name}} ({{age}}) from foo-bar',
ComponentClass: Component.extend().reopenClass({
positionalParams: ['name', 'age'],
}),
});
this.registerComponent('foo-bar-baz', {
template: 'hello {{name}} ({{age}}) from foo-bar-baz',
ComponentClass: Component.extend().reopenClass({
positionalParams: ['name', 'age'],
}),
});
this.render('{{component componentName name age}}', {
componentName: 'foo-bar',
name: 'Alex',
age: 29,
});
this.assertComponentElement(this.firstChild, {
content: 'hello Alex (29) from foo-bar',
});
this.runTask(() => this.rerender());
this.assertComponentElement(this.firstChild, {
content: 'hello Alex (29) from foo-bar',
});
this.runTask(() => set(this.context, 'name', 'Ben'));
this.assertComponentElement(this.firstChild, {
content: 'hello Ben (29) from foo-bar',
});
this.runTask(() => set(this.context, 'age', 22));
this.assertComponentElement(this.firstChild, {
content: 'hello Ben (22) from foo-bar',
});
this.runTask(() => set(this.context, 'componentName', 'foo-bar-baz'));
this.assertComponentElement(this.firstChild, {
content: 'hello Ben (22) from foo-bar-baz',
});
this.runTask(() => {
set(this.context, 'componentName', 'foo-bar');
set(this.context, 'name', 'Alex');
set(this.context, 'age', 29);
});
this.assertComponentElement(this.firstChild, {
content: 'hello Alex (29) from foo-bar',
});
}
['@test positional parameters does not pollute the attributes when changing components']() {
this.registerComponent('normal-message', {
template: 'Normal: {{something}}!',
ComponentClass: Component.extend().reopenClass({
positionalParams: ['something'],
}),
});
this.registerComponent('alternative-message', {
template: 'Alternative: {{something}} {{somethingElse}}!',
ComponentClass: Component.extend({
something: 'Another',
}).reopenClass({
positionalParams: ['somethingElse'],
}),
});
this.render('{{component componentName message}}', {
componentName: 'normal-message',
message: 'Hello',
});
this.assertComponentElement(this.firstChild, {
content: 'Normal: Hello!',
});
this.runTask(() => this.rerender());
this.assertComponentElement(this.firstChild, {
content: 'Normal: Hello!',
});
this.runTask(() => set(this.context, 'componentName', 'alternative-message'));
this.assertComponentElement(this.firstChild, {
content: 'Alternative: Another Hello!',
});
this.runTask(() => set(this.context, 'message', 'Hi'));
this.assertComponentElement(this.firstChild, {
content: 'Alternative: Another Hi!',
});
this.runTask(() => {
set(this.context, 'componentName', 'normal-message');
set(this.context, 'message', 'Hello');
});
this.assertComponentElement(this.firstChild, {
content: 'Normal: Hello!',
});
}
['@test static arbitrary number of positional parameters']() {
this.registerComponent('sample-component', {
ComponentClass: Component.extend().reopenClass({
positionalParams: 'names',
}),
template: strip`
{{#each names as |name|}}
{{name}}
{{/each}}`,
});
this.render(`{{component "sample-component" "Foo" 4 "Bar" 5 "Baz" elementId="helper"}}`);
this.assertText('Foo4Bar5Baz');
this.runTask(() => this.rerender());
this.assertText('Foo4Bar5Baz');
}
['@test dynamic arbitrary number of positional parameters']() {
this.registerComponent('sample-component', {
ComponentClass: Component.extend().reopenClass({
positionalParams: 'n',
}),
template: strip`
{{#each n as |name|}}
{{name}}
{{/each}}`,
});
this.render(`{{component "sample-component" user1 user2}}`, {
user1: 'Foo',
user2: 4,
});
this.assertText('Foo4');
this.runTask(() => this.rerender());
this.assertText('Foo4');
this.runTask(() => this.context.set('user1', 'Bar'));
this.assertText('Bar4');
this.runTask(() => this.context.set('user2', '5'));
this.assertText('Bar5');
this.runTask(() => {
this.context.set('user1', 'Foo');
this.context.set('user2', 4);
});
this.assertText('Foo4');
}
['@test component helper emits useful backtracking re-render assertion message']() {
this.registerComponent('outer-component', {
ComponentClass: Component.extend({
init() {
this._super(...arguments);
this.set('person', { name: 'Alex' });
},
}),
template: `Hi {{person.name}}! {{component "error-component" person=person}}`,
});
this.registerComponent('error-component', {
ComponentClass: Component.extend({
init() {
this._super(...arguments);
this.set('person.name', { name: 'Ben' });
},
}),
template: '{{person.name}}',
});
let expectedBacktrackingMessage = /modified "person\.name" twice on \[object Object\] in a single render\. It was rendered in "component:outer-component" and modified in "component:error-component"/;
expectAssertion(() => {
this.render('{{component componentName}}', {
componentName: 'outer-component',
});
}, expectedBacktrackingMessage);
}
}
);
if (jQueryDisabled) {
moduleFor(
'Components test: dynamic components: jQuery disabled',
class extends RenderingTest {
['@test jQuery proxy is not available without jQuery']() {
let instance;
let FooBarComponent = Component.extend({
init() {
this._super();
instance = this;
},
});
this.registerComponent('foo-bar', {
ComponentClass: FooBarComponent,
template: 'hello',
});
this.render('{{component "foo-bar"}}');
expectAssertion(() => {
instance.$()[0];
}, 'You cannot access this.$() with `jQuery` disabled.');
}
}
);
} else {
moduleFor(
'Components test: dynamic components : jQuery enabled',
class extends RenderingTest {
['@test it has a jQuery proxy to the element']() {
let instance;
let FooBarComponent = Component.extend({
init() {
this._super();
instance = this;
},
});
this.registerComponent('foo-bar', {
ComponentClass: FooBarComponent,
template: 'hello',
});
this.render('{{component "foo-bar"}}');
let element1 = instance.$()[0];
this.assertComponentElement(element1, { content: 'hello' });
this.runTask(() => this.rerender());
let element2 = instance.$()[0];
this.assertComponentElement(element2, { content: 'hello' });
this.assertSameNode(element2, element1);
}
['@test it scopes the jQuery proxy to the component element'](assert) {
let instance;
let FooBarComponent = Component.extend({
init() {
this._super();
instance = this;
},
});
this.registerComponent('foo-bar', {
ComponentClass: FooBarComponent,
template: 'inner',
});
this.render('outer{{component "foo-bar"}}');
let $span = instance.$('span');
assert.equal($span.length, 1);
assert.equal($span.attr('class'), 'inner');
this.runTask(() => this.rerender());
$span = instance.$('span');
assert.equal($span.length, 1);
assert.equal($span.attr('class'), 'inner');
}
}
);
}