Skip to content

Commit

Permalink
Add ...attributes tests (when using tagless components).
Browse files Browse the repository at this point in the history
* Wrap `didCreateElement` logic of curly component manager in a guard
(so that we only do things like add classes and whatnot when the
component we created uses a wrapped element.
* Added various tests for tagless components using `...attributes`
  • Loading branch information
rwjblue committed May 25, 2018
1 parent 1c0fe00 commit 5d487a2
Show file tree
Hide file tree
Showing 3 changed files with 273 additions and 37 deletions.
89 changes: 53 additions & 36 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 @@ -310,51 +320,58 @@ export default class CurlyComponentManager
}

didCreateElement(
{ component, classRef, environment }: ComponentStateBucket,
{ hasWrappedElement, component, classRef, environment }: ComponentStateBucket,
element: HTMLElement,
operations: ElementOperations
): void {
setViewElement(component, element);
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
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',
});
}
}
);
}

0 comments on commit 5d487a2

Please sign in to comment.