Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ...attributes tests (when using tagless components). #16689

Merged
merged 1 commit into from
May 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 56 additions & 37 deletions packages/ember-glimmer/lib/component-managers/curly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,12 @@ export default class CurlyComponentManager
}

getTagName(state: ComponentStateBucket): Option<string> {
const { component } = state;
if (component.tagName === '') {
const { component, hasWrappedElement } = state;

if (!hasWrappedElement) {
return null;
}

return (component && component.tagName) || 'div';
}

Expand Down Expand Up @@ -273,8 +275,10 @@ export default class CurlyComponentManager

component.trigger('didReceiveAttrs');

let hasWrappedElement = component.tagName !== '';

// We usually do this in the `didCreateElement`, but that hook doesn't fire for tagless components
if (component.tagName === '') {
if (!hasWrappedElement) {
if (environment.isInteractive) {
component.trigger('willRender');
}
Expand All @@ -288,7 +292,13 @@ export default class CurlyComponentManager

// Track additional lifecycle metadata about this component in a state bucket.
// Essentially we're saving off all the state we'll need in the future.
let bucket = new ComponentStateBucket(environment, component, capturedArgs, finalizer);
let bucket = new ComponentStateBucket(
environment,
component,
capturedArgs,
finalizer,
hasWrappedElement
);

if (args.named.has('class')) {
bucket.classRef = args.named.get('class');
Expand All @@ -298,7 +308,7 @@ export default class CurlyComponentManager
processComponentInitializationAssertions(component, props);
}

if (environment.isInteractive && component.tagName !== '') {
if (environment.isInteractive && hasWrappedElement) {
component.trigger('willRender');
}

Expand All @@ -310,51 +320,60 @@ export default class CurlyComponentManager
}

didCreateElement(
{ component, classRef, environment }: ComponentStateBucket,
{ hasWrappedElement, component, classRef, environment }: ComponentStateBucket,
element: HTMLElement,
operations: ElementOperations
): void {
setViewElement(component, element);
// TODO: EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION update glimmer-vm to _not_
// call `didCreateElement` for each `...attributes`
if (hasWrappedElement) {
setViewElement(component, element);

let { attributeBindings, classNames, classNameBindings } = component;
let { attributeBindings, classNames, classNameBindings } = component;

operations.setAttribute('id', PrimitiveReference.create(guidFor(component)), false, null);
operations.setAttribute('id', PrimitiveReference.create(guidFor(component)), false, null);

if (attributeBindings && attributeBindings.length) {
applyAttributeBindings(element, attributeBindings, component, operations);
} else {
if (component.elementId) {
operations.setAttribute('id', PrimitiveReference.create(component.elementId), false, null);
if (attributeBindings && attributeBindings.length) {
applyAttributeBindings(element, attributeBindings, component, operations);
} else {
if (component.elementId) {
operations.setAttribute(
'id',
PrimitiveReference.create(component.elementId),
false,
null
);
}
IsVisibleBinding.install(element, component, operations);
}
IsVisibleBinding.install(element, component, operations);
}

if (classRef) {
const ref = new SimpleClassNameBindingReference(classRef, classRef['_propertyKey']);
operations.setAttribute('class', ref, false, null);
}
if (classRef) {
const ref = new SimpleClassNameBindingReference(classRef, classRef['_propertyKey']);
operations.setAttribute('class', ref, false, null);
}

if (classNames && classNames.length) {
classNames.forEach((name: string) => {
operations.setAttribute('class', PrimitiveReference.create(name), false, null);
});
}
if (classNames && classNames.length) {
classNames.forEach((name: string) => {
operations.setAttribute('class', PrimitiveReference.create(name), false, null);
});
}

if (classNameBindings && classNameBindings.length) {
classNameBindings.forEach((binding: string) => {
ClassNameBinding.install(element, component, binding, operations);
});
}
operations.setAttribute('class', PrimitiveReference.create('ember-view'), false, null);
if (classNameBindings && classNameBindings.length) {
classNameBindings.forEach((binding: string) => {
ClassNameBinding.install(element, component, binding, operations);
});
}
operations.setAttribute('class', PrimitiveReference.create('ember-view'), false, null);

if ('ariaRole' in component) {
operations.setAttribute('role', referenceForKey(component, 'ariaRole'), false, null);
}
if ('ariaRole' in component) {
operations.setAttribute('role', referenceForKey(component, 'ariaRole'), false, null);
}

component._transitionTo('hasElement');
component._transitionTo('hasElement');

if (environment.isInteractive) {
component.trigger('willInsertElement');
if (environment.isInteractive) {
component.trigger('willInsertElement');
}
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/ember-glimmer/lib/component-managers/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ class RootComponentManager extends CurlyComponentManager {

dynamicScope.view = component;

let hasWrappedElement = component.tagName !== '';

// We usually do this in the `didCreateElement`, but that hook doesn't fire for tagless components
if (component.tagName === '') {
if (!hasWrappedElement) {
if (environment.isInteractive) {
component.trigger('willRender');
}
Expand All @@ -65,7 +67,7 @@ class RootComponentManager extends CurlyComponentManager {
processComponentInitializationAssertions(component, {});
}

return new ComponentStateBucket(environment, component, null, finalizer);
return new ComponentStateBucket(environment, component, null, finalizer, hasWrappedElement);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export default class ComponentStateBucket {
public environment: Environment,
public component: Component,
public args: CapturedNamedArguments | null,
public finalizer: Finalizer
public finalizer: Finalizer,
public hasWrappedElement: boolean
) {
this.classRef = null;
this.argsRevision = args === null ? 0 : args.tag.value();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,224 @@ if (EMBER_GLIMMER_ANGLE_BRACKET_INVOCATION) {
content: 'hello',
});
}

'@test includes invocation specified attributes in `...attributes` slot in tagless component ("splattributes")'() {
this.registerComponent('foo-bar', {
ComponentClass: Component.extend({ tagName: '' }),
template: '<div ...attributes>hello</div>',
});

this.render('<FooBar data-foo={{foo}} data-bar={{bar}} />', { foo: 'foo', bar: 'bar' });

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'hello',
});

this.runTask(() => this.rerender());

this.assertElement(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.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-foo': 'FOO' },
content: 'hello',
});

this.runTask(() => {
set(this.context, 'foo', 'foo');
set(this.context, 'bar', 'bar');
});

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'hello',
});
}

'@test merges attributes with `...attributes` in tagless component ("splattributes")'() {
let instance;
this.registerComponent('foo-bar', {
ComponentClass: Component.extend({
tagName: '',
init() {
instance = this;
this._super(...arguments);
this.localProp = 'qux';
},
}),
template: '<div data-derp={{localProp}} ...attributes>hello</div>',
});

this.render('<FooBar data-foo={{foo}} data-bar={{bar}} />', { foo: 'foo', bar: 'bar' });

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-derp': 'qux', 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'hello',
});

this.runTask(() => this.rerender());

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-derp': 'qux', 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'hello',
});

this.runTask(() => {
set(this.context, 'foo', 'FOO');
set(this.context, 'bar', undefined);
set(instance, 'localProp', 'QUZ');
});

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-derp': 'QUZ', 'data-foo': 'FOO' },
content: 'hello',
});

this.runTask(() => {
set(this.context, 'foo', 'foo');
set(this.context, 'bar', 'bar');
set(instance, 'localProp', 'qux');
});

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-derp': 'qux', 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'hello',
});
}

'@test merges class attribute with `...attributes` in tagless component ("splattributes")'() {
let instance;
this.registerComponent('foo-bar', {
ComponentClass: Component.extend({
tagName: '',
init() {
instance = this;
this._super(...arguments);
this.localProp = 'qux';
},
}),
template: '<div class={{localProp}} ...attributes>hello</div>',
});

this.render('<FooBar class={{bar}} />', { bar: 'bar' });

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { class: classes('qux bar') },
content: 'hello',
});

this.runTask(() => this.rerender());

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { class: classes('qux bar') },
content: 'hello',
});

this.runTask(() => {
set(this.context, 'bar', undefined);
set(instance, 'localProp', 'QUZ');
});

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { class: classes('QUZ') },
content: 'hello',
});

this.runTask(() => {
set(this.context, 'bar', 'bar');
set(instance, 'localProp', 'qux');
});

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { class: classes('qux bar') },
content: 'hello',
});
}

'@test can include `...attributes` in multiple elements in tagless component ("splattributes")'() {
this.registerComponent('foo-bar', {
ComponentClass: Component.extend({ tagName: '' }),
template: '<div ...attributes>hello</div><p ...attributes>world</p>',
});

this.render('<FooBar data-foo={{foo}} data-bar={{bar}} />', { foo: 'foo', bar: 'bar' });

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'hello',
});
this.assertElement(this.nthChild(1), {
tagName: 'p',
attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'world',
});

this.runTask(() => this.rerender());

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'hello',
});
this.assertElement(this.nthChild(1), {
tagName: 'p',
attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'world',
});

this.runTask(() => {
set(this.context, 'foo', 'FOO');
set(this.context, 'bar', undefined);
});

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-foo': 'FOO' },
content: 'hello',
});
this.assertElement(this.nthChild(1), {
tagName: 'p',
attrs: { 'data-foo': 'FOO' },
content: 'world',
});

this.runTask(() => {
set(this.context, 'foo', 'foo');
set(this.context, 'bar', 'bar');
});

this.assertElement(this.firstChild, {
tagName: 'div',
attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'hello',
});
this.assertElement(this.nthChild(1), {
tagName: 'p',
attrs: { 'data-foo': 'foo', 'data-bar': 'bar' },
content: 'world',
});
}
}
);
}