From 5d487a2e87509c4a4219fd2a4097ad22617eb072 Mon Sep 17 00:00:00 2001 From: Robert Jackson Date: Fri, 25 May 2018 12:40:24 -0400 Subject: [PATCH] Add ...attributes tests (when using tagless components). * 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` --- .../lib/component-managers/curly.ts | 89 ++++--- .../lib/utils/curly-component-state-bucket.ts | 3 +- .../angle-bracket-invocation-test.js | 218 ++++++++++++++++++ 3 files changed, 273 insertions(+), 37 deletions(-) diff --git a/packages/ember-glimmer/lib/component-managers/curly.ts b/packages/ember-glimmer/lib/component-managers/curly.ts index a8f43a3b68e..632cb430a3c 100644 --- a/packages/ember-glimmer/lib/component-managers/curly.ts +++ b/packages/ember-glimmer/lib/component-managers/curly.ts @@ -145,10 +145,12 @@ export default class CurlyComponentManager } getTagName(state: ComponentStateBucket): Option { - const { component } = state; - if (component.tagName === '') { + const { component, hasWrappedElement } = state; + + if (!hasWrappedElement) { return null; } + return (component && component.tagName) || 'div'; } @@ -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'); } @@ -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'); @@ -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'); + } } } diff --git a/packages/ember-glimmer/lib/utils/curly-component-state-bucket.ts b/packages/ember-glimmer/lib/utils/curly-component-state-bucket.ts index 2aa55d42513..611fd85f9d6 100644 --- a/packages/ember-glimmer/lib/utils/curly-component-state-bucket.ts +++ b/packages/ember-glimmer/lib/utils/curly-component-state-bucket.ts @@ -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(); 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 index 65545636714..4a7145d01cb 100644 --- a/packages/ember-glimmer/tests/integration/components/angle-bracket-invocation-test.js +++ b/packages/ember-glimmer/tests/integration/components/angle-bracket-invocation-test.js @@ -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: '
hello
', + }); + + this.render('', { 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: '
hello
', + }); + + this.render('', { 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: '
hello
', + }); + + this.render('', { 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: '
hello

world

', + }); + + this.render('', { 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', + }); + } } ); }