From 2ca34e6dd005f1eb1a0dc108ea0dbf9a8d3a5e2f Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 27 Oct 2025 16:55:59 +0800 Subject: [PATCH] wip: process custom element as component the template helper cannot resolve them properly they require creation via createElement --- .../src/generators/component.ts | 10 +- packages/compiler-vapor/src/ir/index.ts | 1 + .../src/transforms/transformElement.ts | 12 +- packages/runtime-dom/src/apiCustomElement.ts | 2 +- .../__tests__/customElement.spec.ts | 745 +++++++++--------- packages/runtime-vapor/src/dom/prop.ts | 11 +- 6 files changed, 407 insertions(+), 374 deletions(-) diff --git a/packages/compiler-vapor/src/generators/component.ts b/packages/compiler-vapor/src/generators/component.ts index 38dd701704..fd770f16d3 100644 --- a/packages/compiler-vapor/src/generators/component.ts +++ b/packages/compiler-vapor/src/generators/component.ts @@ -73,9 +73,11 @@ export function genCreateComponent( ...genCall( operation.dynamic && !operation.dynamic.isStatic ? helper('createDynamicComponent') - : operation.asset + : operation.isCustomElement ? helper('createComponentWithFallback') - : helper('createComponent'), + : operation.asset + ? helper('createComponentWithFallback') + : helper('createComponent'), tag, rawProps, rawSlots, @@ -86,7 +88,9 @@ export function genCreateComponent( ] function genTag() { - if (operation.dynamic) { + if (operation.isCustomElement) { + return JSON.stringify(operation.tag) + } else if (operation.dynamic) { if (operation.dynamic.isStatic) { return genCall( helper('resolveDynamicComponent'), diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index 15cf85ae10..a8dcdb430c 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -202,6 +202,7 @@ export interface CreateComponentIRNode extends BaseIRNode { root: boolean once: boolean dynamic?: SimpleExpressionNode + isCustomElement: boolean parent?: number anchor?: number append?: boolean diff --git a/packages/compiler-vapor/src/transforms/transformElement.ts b/packages/compiler-vapor/src/transforms/transformElement.ts index facffadff1..136f29d8a4 100644 --- a/packages/compiler-vapor/src/transforms/transformElement.ts +++ b/packages/compiler-vapor/src/transforms/transformElement.ts @@ -57,7 +57,12 @@ export const transformElement: NodeTransform = (node, context) => { ) return - const isComponent = node.tagType === ElementTypes.COMPONENT + // treat custom elements as components because the template helper cannot + // resolve them properly; they require creation via createElement + const isCustomElement = !!context.options.isCustomElement(node.tag) + const isComponent = + node.tagType === ElementTypes.COMPONENT || isCustomElement + const isDynamicComponent = isComponentTag(node.tag) const propsResult = buildProps( node, @@ -88,6 +93,7 @@ export const transformElement: NodeTransform = (node, context) => { singleRoot, context, isDynamicComponent, + isCustomElement, ) } else { transformNativeElement( @@ -107,6 +113,7 @@ function transformComponentElement( singleRoot: boolean, context: TransformContext, isDynamicComponent: boolean, + isCustomElement: boolean, ) { const dynamicComponent = isDynamicComponent ? resolveDynamicComponent(node) @@ -115,7 +122,7 @@ function transformComponentElement( let { tag } = node let asset = true - if (!dynamicComponent) { + if (!dynamicComponent && !isCustomElement) { const fromSetup = resolveSetupReference(tag, context) if (fromSetup) { tag = fromSetup @@ -160,6 +167,7 @@ function transformComponentElement( slots: [...context.slots], once: context.inVOnce, dynamic: dynamicComponent, + isCustomElement, } context.slots = [] } diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 892ba8d9e0..b413bf74e9 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -533,7 +533,7 @@ export abstract class VueElementBase< * @internal */ protected _getProp(key: string): any { - return this._props[key] + return this._isVapor ? this._props[key]() : this._props[key] } /** diff --git a/packages/runtime-vapor/__tests__/customElement.spec.ts b/packages/runtime-vapor/__tests__/customElement.spec.ts index dbe79a53d6..ff8638ac26 100644 --- a/packages/runtime-vapor/__tests__/customElement.spec.ts +++ b/packages/runtime-vapor/__tests__/customElement.spec.ts @@ -28,6 +28,20 @@ describe('defineVaporCustomElement', () => { delegateEvents('input') + function render(tag: string, props: any) { + const root = document.createElement('div') + document.body.appendChild(root) + createVaporApp({ + setup() { + return createComponentWithFallback(tag, props, null, true) + }, + }).mount(root) + + return { + container: root, + } + } + beforeEach(() => { container.innerHTML = '' }) @@ -90,32 +104,12 @@ describe('defineVaporCustomElement', () => { setValue(n0, props.value) }) return n0 - // return () => - // h('input', { - // type: 'number', - // value: props.value, - // onInput: (e: InputEvent) => { - // const num = (e.target! as HTMLInputElement).valueAsNumber - // emit('update', Number.isNaN(num) ? null : num) - // }, - // }) }, }) customElements.define('my-el-input', CustomInput) const num = ref('12') const containerComp = defineVaporComponent({ setup() { - // return () => { - // return h('div', [ - // h('my-el-input', { - // value: num.value, - // onUpdate: ($event: CustomEvent) => { - // num.value = $event.detail[0] - // }, - // }), - // h('div', { id: 'move' }), - // ]) - // } const n1 = template('
', true)() as any setInsertionState(n1, 0, true) createComponentWithFallback('my-el-input', { @@ -143,385 +137,402 @@ describe('defineVaporCustomElement', () => { expect(inputEl.value).toBe('') }) - // test('should not unmount on move', async () => { - // container.innerHTML = `
` - // const e = container.childNodes[0].childNodes[0] as VaporElement - // const i = e._instance - // // moving from one parent to another - this will trigger both disconnect - // // and connected callbacks synchronously - // container.appendChild(e) - // await nextTick() - // // should be the same instance - // expect(e._instance).toBe(i) - // expect(e.shadowRoot!.innerHTML).toBe('
hello
') - // }) + test('should not unmount on move', async () => { + container.innerHTML = `
` + const e = container.childNodes[0].childNodes[0] as VaporElement + const i = e._instance + // moving from one parent to another - this will trigger both disconnect + // and connected callbacks synchronously + container.appendChild(e) + await nextTick() + // should be the same instance + expect(e._instance).toBe(i) + expect(e.shadowRoot!.innerHTML).toBe('
hello
') + }) - // test('remove then insert again', async () => { - // container.innerHTML = `` - // const e = container.childNodes[0] as VaporElement - // container.removeChild(e) - // await nextTick() - // expect(e._instance).toBe(null) - // expect(e.shadowRoot!.innerHTML).toBe('') - // container.appendChild(e) - // expect(e._instance).toBeTruthy() - // expect(e.shadowRoot!.innerHTML).toBe('
hello
') - // }) + test('remove then insert again', async () => { + container.innerHTML = `` + const e = container.childNodes[0] as VaporElement + container.removeChild(e) + await nextTick() + expect(e._instance).toBe(null) + expect(e.shadowRoot!.innerHTML).toBe('') + container.appendChild(e) + expect(e._instance).toBeTruthy() + expect(e.shadowRoot!.innerHTML).toBe('
hello
') + }) }) - // describe('props', () => { - // const E = defineVaporCustomElement({ - // props: { - // foo: [String, null], - // bar: Object, - // bazQux: null, - // value: null, - // }, - // render() { - // return [ - // h('div', null, this.foo || ''), - // h('div', null, this.bazQux || (this.bar && this.bar.x)), - // ] - // }, - // }) - // customElements.define('my-el-props', E) + describe('props', () => { + const E = defineVaporCustomElement({ + props: { + foo: [String, null], + bar: Object, + bazQux: null, + value: null, + }, + setup(props: any) { + const n0 = template('
', true)() as any + const x0 = txt(n0) as any + const n1 = template('
', true)() as any + const x1 = txt(n1) as any + + renderEffect(() => setText(x0, props.foo || '')) + renderEffect(() => + setText(x1, props.bazQux || (props.bar && props.bar.x)), + ) + return [n0, n1] + }, + }) + customElements.define('my-el-props', E) - // test('renders custom element w/ correct object prop value', () => { - // render(h('my-el-props', { value: { x: 1 } }), container) - // const el = container.children[0] - // expect((el as any).value).toEqual({ x: 1 }) - // }) + test('renders custom element w/ correct object prop value', () => { + const { container } = render('my-el-props', { + value: () => ({ + x: 1, + }), + }) - // test('props via attribute', async () => { - // // bazQux should map to `baz-qux` attribute - // container.innerHTML = `` - // const e = container.childNodes[0] as VaporElement - // expect(e.shadowRoot!.innerHTML).toBe('
hello
bye
') + const el = container.children[0] + expect((el as any).value).toEqual({ x: 1 }) + }) - // // change attr - // e.setAttribute('foo', 'changed') - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe('
changed
bye
') + test('props via attribute', async () => { + // bazQux should map to `baz-qux` attribute + container.innerHTML = `` + const e = container.childNodes[0] as VaporElement + expect(e.shadowRoot!.innerHTML).toBe('
hello
bye
') - // e.setAttribute('baz-qux', 'changed') - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe( - // '
changed
changed
', - // ) - // }) + // change attr + e.setAttribute('foo', 'changed') + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
changed
bye
') - // test('props via properties', async () => { - // const e = new E() - // e.foo = 'one' - // e.bar = { x: 'two' } - // container.appendChild(e) - // expect(e.shadowRoot!.innerHTML).toBe('
one
two
') + e.setAttribute('baz-qux', 'changed') + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe( + '
changed
changed
', + ) + }) - // // reflect - // // should reflect primitive value - // expect(e.getAttribute('foo')).toBe('one') - // // should not reflect rich data - // expect(e.hasAttribute('bar')).toBe(false) + test('props via properties', async () => { + // TODO remove this after type inference done + const e = new E() as any + e.foo = 'one' + e.bar = { x: 'two' } + container.appendChild(e) + expect(e.shadowRoot!.innerHTML).toBe('
one
two
') - // e.foo = 'three' - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe('
three
two
') - // expect(e.getAttribute('foo')).toBe('three') + // reflect + // should reflect primitive value + expect(e.getAttribute('foo')).toBe('one') + // should not reflect rich data + expect(e.hasAttribute('bar')).toBe(false) - // e.foo = null - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe('
two
') - // expect(e.hasAttribute('foo')).toBe(false) + e.foo = 'three' + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
three
two
') + expect(e.getAttribute('foo')).toBe('three') - // e.foo = undefined - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe('
two
') - // expect(e.hasAttribute('foo')).toBe(false) - // expect(e.foo).toBe(undefined) + e.foo = null + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
two
') + expect(e.hasAttribute('foo')).toBe(false) - // e.bazQux = 'four' - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe('
four
') - // expect(e.getAttribute('baz-qux')).toBe('four') - // }) + e.foo = undefined + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
two
') + expect(e.hasAttribute('foo')).toBe(false) + expect(e.foo).toBe(undefined) - // test('props via attributes and properties changed together', async () => { - // const e = new E() - // e.foo = 'foo1' - // e.bar = { x: 'bar1' } - // container.appendChild(e) - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe('
foo1
bar1
') + e.bazQux = 'four' + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
four
') + expect(e.getAttribute('baz-qux')).toBe('four') + }) - // // change attr then property - // e.setAttribute('foo', 'foo2') - // e.bar = { x: 'bar2' } - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe('
foo2
bar2
') - // expect(e.getAttribute('foo')).toBe('foo2') - // expect(e.hasAttribute('bar')).toBe(false) + test('props via attributes and properties changed together', async () => { + // TODO remove this after type inference done + const e = new E() as any + e.foo = 'foo1' + e.bar = { x: 'bar1' } + container.appendChild(e) + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
foo1
bar1
') - // // change prop then attr - // e.bar = { x: 'bar3' } - // e.setAttribute('foo', 'foo3') - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe('
foo3
bar3
') - // expect(e.getAttribute('foo')).toBe('foo3') - // expect(e.hasAttribute('bar')).toBe(false) - // }) + // change attr then property + e.setAttribute('foo', 'foo2') + e.bar = { x: 'bar2' } + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
foo2
bar2
') + expect(e.getAttribute('foo')).toBe('foo2') + expect(e.hasAttribute('bar')).toBe(false) - // test('props via hyphen property', async () => { - // const Comp = defineVaporCustomElement({ - // props: { - // fooBar: Boolean, - // }, - // render() { - // return 'Comp' - // }, - // }) - // customElements.define('my-el-comp', Comp) - // render(h('my-el-comp', { 'foo-bar': true }), container) - // const el = container.children[0] - // expect((el as any).outerHTML).toBe('') - // }) + // change prop then attr + e.bar = { x: 'bar3' } + e.setAttribute('foo', 'foo3') + await nextTick() + expect(e.shadowRoot!.innerHTML).toBe('
foo3
bar3
') + expect(e.getAttribute('foo')).toBe('foo3') + expect(e.hasAttribute('bar')).toBe(false) + }) - // test('attribute -> prop type casting', async () => { - // const E = defineVaporCustomElement({ - // props: { - // fooBar: Number, // test casting of camelCase prop names - // bar: Boolean, - // baz: String, - // }, - // render() { - // return [ - // this.fooBar, - // typeof this.fooBar, - // this.bar, - // typeof this.bar, - // this.baz, - // typeof this.baz, - // ].join(' ') - // }, - // }) - // customElements.define('my-el-props-cast', E) - // container.innerHTML = `` - // const e = container.childNodes[0] as VaporElement - // expect(e.shadowRoot!.innerHTML).toBe( - // `1 number false boolean 12345 string`, - // ) + test('props via hyphen property', async () => { + const Comp = defineVaporCustomElement({ + props: { + fooBar: Boolean, + }, + setup() { + return template('Comp')() + }, + }) + customElements.define('my-el-comp', Comp) - // e.setAttribute('bar', '') - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`) + const { container } = render('my-el-comp', { + 'foo-bar': () => true, + }) - // e.setAttribute('foo-bar', '2e1') - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe( - // `20 number true boolean 12345 string`, - // ) + const el = container.children[0] + expect((el as any).outerHTML).toBe('') + }) - // e.setAttribute('baz', '2e1') - // await nextTick() - // expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`) - // }) + // test('attribute -> prop type casting', async () => { + // const E = defineVaporCustomElement({ + // props: { + // fooBar: Number, // test casting of camelCase prop names + // bar: Boolean, + // baz: String, + // }, + // render() { + // return [ + // this.fooBar, + // typeof this.fooBar, + // this.bar, + // typeof this.bar, + // this.baz, + // typeof this.baz, + // ].join(' ') + // }, + // }) + // customElements.define('my-el-props-cast', E) + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe( + // `1 number false boolean 12345 string`, + // ) - // // #4772 - // test('attr casting w/ programmatic creation', () => { - // const E = defineVaporCustomElement({ - // props: { - // foo: Number, - // }, - // render() { - // return `foo type: ${typeof this.foo}` - // }, - // }) - // customElements.define('my-element-programmatic', E) - // const el = document.createElement('my-element-programmatic') as any - // el.setAttribute('foo', '123') - // container.appendChild(el) - // expect(el.shadowRoot.innerHTML).toBe(`foo type: number`) - // }) + // e.setAttribute('bar', '') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`) - // test('handling properties set before upgrading', () => { - // const E = defineVaporCustomElement({ - // props: { - // foo: String, - // dataAge: Number, - // }, - // setup(props) { - // expect(props.foo).toBe('hello') - // expect(props.dataAge).toBe(5) - // }, - // render() { - // return h('div', `foo: ${this.foo}`) - // }, - // }) - // const el = document.createElement('my-el-upgrade') as any - // el.foo = 'hello' - // el.dataset.age = 5 - // el.notProp = 1 - // container.appendChild(el) - // customElements.define('my-el-upgrade', E) - // expect(el.shadowRoot.firstChild.innerHTML).toBe(`foo: hello`) - // // should not reflect if not declared as a prop - // expect(el.hasAttribute('not-prop')).toBe(false) - // }) + // e.setAttribute('foo-bar', '2e1') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe( + // `20 number true boolean 12345 string`, + // ) - // test('handle properties set before connecting', () => { - // const obj = { a: 1 } - // const E = defineVaporCustomElement({ - // props: { - // foo: String, - // post: Object, - // }, - // setup(props) { - // expect(props.foo).toBe('hello') - // expect(props.post).toBe(obj) - // }, - // render() { - // return JSON.stringify(this.post) - // }, - // }) - // customElements.define('my-el-preconnect', E) - // const el = document.createElement('my-el-preconnect') as any - // el.foo = 'hello' - // el.post = obj + // e.setAttribute('baz', '2e1') + // await nextTick() + // expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`) + // }) - // container.appendChild(el) - // expect(el.shadowRoot.innerHTML).toBe(JSON.stringify(obj)) - // }) + // // #4772 + // test('attr casting w/ programmatic creation', () => { + // const E = defineVaporCustomElement({ + // props: { + // foo: Number, + // }, + // render() { + // return `foo type: ${typeof this.foo}` + // }, + // }) + // customElements.define('my-element-programmatic', E) + // const el = document.createElement('my-element-programmatic') as any + // el.setAttribute('foo', '123') + // container.appendChild(el) + // expect(el.shadowRoot.innerHTML).toBe(`foo type: number`) + // }) - // // https://github.com/vuejs/core/issues/6163 - // test('handle components with no props', async () => { - // const E = defineVaporCustomElement({ - // render() { - // return h('div', 'foo') - // }, - // }) - // customElements.define('my-element-noprops', E) - // const el = document.createElement('my-element-noprops') - // container.appendChild(el) - // await nextTick() - // expect(el.shadowRoot!.innerHTML).toMatchInlineSnapshot('"
foo
"') - // }) + // test('handling properties set before upgrading', () => { + // const E = defineVaporCustomElement({ + // props: { + // foo: String, + // dataAge: Number, + // }, + // setup(props) { + // expect(props.foo).toBe('hello') + // expect(props.dataAge).toBe(5) + // }, + // render() { + // return h('div', `foo: ${this.foo}`) + // }, + // }) + // const el = document.createElement('my-el-upgrade') as any + // el.foo = 'hello' + // el.dataset.age = 5 + // el.notProp = 1 + // container.appendChild(el) + // customElements.define('my-el-upgrade', E) + // expect(el.shadowRoot.firstChild.innerHTML).toBe(`foo: hello`) + // // should not reflect if not declared as a prop + // expect(el.hasAttribute('not-prop')).toBe(false) + // }) - // // #5793 - // test('set number value in dom property', () => { - // const E = defineVaporCustomElement({ - // props: { - // 'max-age': Number, - // }, - // render() { - // // @ts-expect-error - // return `max age: ${this.maxAge}/type: ${typeof this.maxAge}` - // }, - // }) - // customElements.define('my-element-number-property', E) - // const el = document.createElement('my-element-number-property') as any - // container.appendChild(el) - // el.maxAge = 50 - // expect(el.maxAge).toBe(50) - // expect(el.shadowRoot.innerHTML).toBe('max age: 50/type: number') - // }) + // test('handle properties set before connecting', () => { + // const obj = { a: 1 } + // const E = defineVaporCustomElement({ + // props: { + // foo: String, + // post: Object, + // }, + // setup(props) { + // expect(props.foo).toBe('hello') + // expect(props.post).toBe(obj) + // }, + // render() { + // return JSON.stringify(this.post) + // }, + // }) + // customElements.define('my-el-preconnect', E) + // const el = document.createElement('my-el-preconnect') as any + // el.foo = 'hello' + // el.post = obj + + // container.appendChild(el) + // expect(el.shadowRoot.innerHTML).toBe(JSON.stringify(obj)) + // }) - // // #9006 - // test('should reflect default value', () => { - // const E = defineVaporCustomElement({ - // props: { - // value: { - // type: String, - // default: 'hi', - // }, - // }, - // render() { - // return this.value - // }, - // }) - // customElements.define('my-el-default-val', E) - // container.innerHTML = `` - // const e = container.childNodes[0] as any - // expect(e.value).toBe('hi') - // }) + // // https://github.com/vuejs/core/issues/6163 + // test('handle components with no props', async () => { + // const E = defineVaporCustomElement({ + // render() { + // return h('div', 'foo') + // }, + // }) + // customElements.define('my-element-noprops', E) + // const el = document.createElement('my-element-noprops') + // container.appendChild(el) + // await nextTick() + // expect(el.shadowRoot!.innerHTML).toMatchInlineSnapshot('"
foo
"') + // }) - // // #12214 - // test('Boolean prop with default true', async () => { - // const E = defineVaporCustomElement({ - // props: { - // foo: { - // type: Boolean, - // default: true, - // }, - // }, - // render() { - // return String(this.foo) - // }, - // }) - // customElements.define('my-el-default-true', E) - // container.innerHTML = `` - // const e = container.childNodes[0] as HTMLElement & { foo: any }, - // shadowRoot = e.shadowRoot as ShadowRoot - // expect(shadowRoot.innerHTML).toBe('true') - // e.foo = undefined - // await nextTick() - // expect(shadowRoot.innerHTML).toBe('true') - // e.foo = false - // await nextTick() - // expect(shadowRoot.innerHTML).toBe('false') - // e.foo = null - // await nextTick() - // expect(shadowRoot.innerHTML).toBe('null') - // e.foo = '' - // await nextTick() - // expect(shadowRoot.innerHTML).toBe('true') - // }) + // // #5793 + // test('set number value in dom property', () => { + // const E = defineVaporCustomElement({ + // props: { + // 'max-age': Number, + // }, + // render() { + // // @ts-expect-error + // return `max age: ${this.maxAge}/type: ${typeof this.maxAge}` + // }, + // }) + // customElements.define('my-element-number-property', E) + // const el = document.createElement('my-element-number-property') as any + // container.appendChild(el) + // el.maxAge = 50 + // expect(el.maxAge).toBe(50) + // expect(el.shadowRoot.innerHTML).toBe('max age: 50/type: number') + // }) - // test('support direct setup function syntax with extra options', () => { - // const E = defineVaporCustomElement( - // props => { - // return () => props.text - // }, - // { - // props: { - // text: String, - // }, - // }, - // ) - // customElements.define('my-el-setup-with-props', E) - // container.innerHTML = `` - // const e = container.childNodes[0] as VaporElement - // expect(e.shadowRoot!.innerHTML).toBe('hello') - // }) + // // #9006 + // test('should reflect default value', () => { + // const E = defineVaporCustomElement({ + // props: { + // value: { + // type: String, + // default: 'hi', + // }, + // }, + // render() { + // return this.value + // }, + // }) + // customElements.define('my-el-default-val', E) + // container.innerHTML = `` + // const e = container.childNodes[0] as any + // expect(e.value).toBe('hi') + // }) - // test('prop types validation', async () => { - // const E = defineVaporCustomElement({ - // props: { - // num: { - // type: [Number, String], - // }, - // bool: { - // type: Boolean, - // }, - // }, - // render() { - // return h('div', [ - // h('span', [`${this.num} is ${typeof this.num}`]), - // h('span', [`${this.bool} is ${typeof this.bool}`]), - // ]) - // }, - // }) + // // #12214 + // test('Boolean prop with default true', async () => { + // const E = defineVaporCustomElement({ + // props: { + // foo: { + // type: Boolean, + // default: true, + // }, + // }, + // render() { + // return String(this.foo) + // }, + // }) + // customElements.define('my-el-default-true', E) + // container.innerHTML = `` + // const e = container.childNodes[0] as HTMLElement & { foo: any }, + // shadowRoot = e.shadowRoot as ShadowRoot + // expect(shadowRoot.innerHTML).toBe('true') + // e.foo = undefined + // await nextTick() + // expect(shadowRoot.innerHTML).toBe('true') + // e.foo = false + // await nextTick() + // expect(shadowRoot.innerHTML).toBe('false') + // e.foo = null + // await nextTick() + // expect(shadowRoot.innerHTML).toBe('null') + // e.foo = '' + // await nextTick() + // expect(shadowRoot.innerHTML).toBe('true') + // }) - // customElements.define('my-el-with-type-props', E) - // render(h('my-el-with-type-props', { num: 1, bool: true }), container) - // const e = container.childNodes[0] as VaporElement - // // @ts-expect-error - // expect(e.num).toBe(1) - // // @ts-expect-error - // expect(e.bool).toBe(true) - // expect(e.shadowRoot!.innerHTML).toBe( - // '
1 is numbertrue is boolean
', - // ) - // }) - // }) + // test('support direct setup function syntax with extra options', () => { + // const E = defineVaporCustomElement( + // props => { + // return () => props.text + // }, + // { + // props: { + // text: String, + // }, + // }, + // ) + // customElements.define('my-el-setup-with-props', E) + // container.innerHTML = `` + // const e = container.childNodes[0] as VaporElement + // expect(e.shadowRoot!.innerHTML).toBe('hello') + // }) + + // test('prop types validation', async () => { + // const E = defineVaporCustomElement({ + // props: { + // num: { + // type: [Number, String], + // }, + // bool: { + // type: Boolean, + // }, + // }, + // render() { + // return h('div', [ + // h('span', [`${this.num} is ${typeof this.num}`]), + // h('span', [`${this.bool} is ${typeof this.bool}`]), + // ]) + // }, + // }) + + // customElements.define('my-el-with-type-props', E) + // render(h('my-el-with-type-props', { num: 1, bool: true }), container) + // const e = container.childNodes[0] as VaporElement + // // @ts-expect-error + // expect(e.num).toBe(1) + // // @ts-expect-error + // expect(e.bool).toBe(true) + // expect(e.shadowRoot!.innerHTML).toBe( + // '
1 is numbertrue is boolean
', + // ) + // }) + }) // describe('attrs', () => { // const E = defineVaporCustomElement({ diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts index b104b20900..7642dab962 100644 --- a/packages/runtime-vapor/src/dom/prop.ts +++ b/packages/runtime-vapor/src/dom/prop.ts @@ -1,5 +1,6 @@ import { type NormalizedStyle, + camelize, canSetValueDirectly, includeBooleanAttr, isArray, @@ -37,6 +38,7 @@ import { } from '../component' import { isHydrating, logMismatchError } from './hydration' import type { Block } from '../block' +import type { VaporElement } from '../apiDefineVaporCustomElement' type TargetElement = Element & { $root?: true @@ -98,6 +100,7 @@ export function setDOMProp( key: string, value: any, forceHydrate: boolean = false, + attrName?: string, ): void { if (!isApplyingFallthroughProps && el.$root && hasFallthroughKey(key)) { return @@ -149,7 +152,7 @@ export function setDOMProp( ) } } - needRemove && el.removeAttribute(key) + needRemove && el.removeAttribute(attrName || key) } export function setClass(el: TargetElement, value: any): void { @@ -457,6 +460,12 @@ export function setDynamicProp( } else { setDOMProp(el, key, value, forceHydrate) } + } else if ( + // custom elements + (el as VaporElement)._isVueCE && + (/[A-Z]/.test(key) || !isString(value)) + ) { + setDOMProp(el, camelize(key), value, forceHydrate, key) } else { setAttr(el, key, value) } -- 2.47.3