]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: process custom element as component
authordaiwei <daiwei521@126.com>
Mon, 27 Oct 2025 08:55:59 +0000 (16:55 +0800)
committerdaiwei <daiwei521@126.com>
Mon, 27 Oct 2025 08:55:59 +0000 (16:55 +0800)
the template helper cannot resolve them properly
they require creation via createElement

packages/compiler-vapor/src/generators/component.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transforms/transformElement.ts
packages/runtime-dom/src/apiCustomElement.ts
packages/runtime-vapor/__tests__/customElement.spec.ts
packages/runtime-vapor/src/dom/prop.ts

index 38dd701704eb7f9f165907b5125187e3278eff4c..fd770f16d35f695c8327e9c479ffd5f9d504ed49 100644 (file)
@@ -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'),
index 15cf85ae10e97fc82235b4414a786ac4825055f7..a8dcdb430cb705ba5cf52c9b717add0226113b52 100644 (file)
@@ -202,6 +202,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
   root: boolean
   once: boolean
   dynamic?: SimpleExpressionNode
+  isCustomElement: boolean
   parent?: number
   anchor?: number
   append?: boolean
index facffadff109422e2997d920e339bad747c7b720..136f29d8a4e20643555cae90b5d87972b4d99282 100644 (file)
@@ -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 = []
 }
index 892ba8d9e0ee9228d43a1da6636c91d0b118728b..b413bf74e9c234e34900cbd217a2b97d4f6c2385 100644 (file)
@@ -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]
   }
 
   /**
index dbe79a53d66b34f741fc4598361943914d4fbb3d..ff8638ac264cef609ae29509e6616616ced9487b 100644 (file)
@@ -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('<div><div id="move"></div></div>', 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 = `<div><my-element></my-element></div>`
-    //   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('<div>hello</div>')
-    // })
+    test('should not unmount on move', async () => {
+      container.innerHTML = `<div><my-element></my-element></div>`
+      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('<div>hello</div>')
+    })
 
-    // test('remove then insert again', async () => {
-    //   container.innerHTML = `<my-element></my-element>`
-    //   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('<div>hello</div>')
-    // })
+    test('remove then insert again', async () => {
+      container.innerHTML = `<my-element></my-element>`
+      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('<div>hello</div>')
+    })
   })
 
-  // 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('<div> </div>', true)() as any
+        const x0 = txt(n0) as any
+        const n1 = template('<div> </div>', 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 = `<my-el-props foo="hello" baz-qux="bye"></my-el-props>`
-  //     const e = container.childNodes[0] as VaporElement
-  //     expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div><div>bye</div>')
+      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('<div>changed</div><div>bye</div>')
+    test('props via attribute', async () => {
+      // bazQux should map to `baz-qux` attribute
+      container.innerHTML = `<my-el-props foo="hello" baz-qux="bye"></my-el-props>`
+      const e = container.childNodes[0] as VaporElement
+      expect(e.shadowRoot!.innerHTML).toBe('<div>hello</div><div>bye</div>')
 
-  //     e.setAttribute('baz-qux', 'changed')
-  //     await nextTick()
-  //     expect(e.shadowRoot!.innerHTML).toBe(
-  //       '<div>changed</div><div>changed</div>',
-  //     )
-  //   })
+      // change attr
+      e.setAttribute('foo', 'changed')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe('<div>changed</div><div>bye</div>')
 
-  //   test('props via properties', async () => {
-  //     const e = new E()
-  //     e.foo = 'one'
-  //     e.bar = { x: 'two' }
-  //     container.appendChild(e)
-  //     expect(e.shadowRoot!.innerHTML).toBe('<div>one</div><div>two</div>')
+      e.setAttribute('baz-qux', 'changed')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        '<div>changed</div><div>changed</div>',
+      )
+    })
 
-  //     // 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('<div>one</div><div>two</div>')
 
-  //     e.foo = 'three'
-  //     await nextTick()
-  //     expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>two</div>')
-  //     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('<div></div><div>two</div>')
-  //     expect(e.hasAttribute('foo')).toBe(false)
+      e.foo = 'three'
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe('<div>three</div><div>two</div>')
+      expect(e.getAttribute('foo')).toBe('three')
 
-  //     e.foo = undefined
-  //     await nextTick()
-  //     expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
-  //     expect(e.hasAttribute('foo')).toBe(false)
-  //     expect(e.foo).toBe(undefined)
+      e.foo = null
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
+      expect(e.hasAttribute('foo')).toBe(false)
 
-  //     e.bazQux = 'four'
-  //     await nextTick()
-  //     expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>four</div>')
-  //     expect(e.getAttribute('baz-qux')).toBe('four')
-  //   })
+      e.foo = undefined
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>two</div>')
+      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('<div>foo1</div><div>bar1</div>')
+      e.bazQux = 'four'
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe('<div></div><div>four</div>')
+      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('<div>foo2</div><div>bar2</div>')
-  //     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('<div>foo1</div><div>bar1</div>')
 
-  //     // change prop then attr
-  //     e.bar = { x: 'bar3' }
-  //     e.setAttribute('foo', 'foo3')
-  //     await nextTick()
-  //     expect(e.shadowRoot!.innerHTML).toBe('<div>foo3</div><div>bar3</div>')
-  //     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('<div>foo2</div><div>bar2</div>')
+      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('<my-el-comp foo-bar=""></my-el-comp>')
-  //   })
+      // change prop then attr
+      e.bar = { x: 'bar3' }
+      e.setAttribute('foo', 'foo3')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe('<div>foo3</div><div>bar3</div>')
+      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 = `<my-el-props-cast foo-bar="1" baz="12345"></my-el-props-cast>`
-  //     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('<my-el-comp foo-bar=""></my-el-comp>')
+    })
 
-  //     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 = `<my-el-props-cast foo-bar="1" baz="12345"></my-el-props-cast>`
+    //   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('"<div>foo</div>"')
-  //   })
+    // 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 = `<my-el-default-val></my-el-default-val>`
-  //     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('"<div>foo</div>"')
+    // })
 
-  //   // #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 = `<my-el-default-true></my-el-default-true>`
-  //     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 = `<my-el-setup-with-props text="hello"></my-el-setup-with-props>`
-  //     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 = `<my-el-default-val></my-el-default-val>`
+    //   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 = `<my-el-default-true></my-el-default-true>`
+    //   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(
-  //       '<div><span>1 is number</span><span>true is boolean</span></div>',
-  //     )
-  //   })
-  // })
+    // 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 = `<my-el-setup-with-props text="hello"></my-el-setup-with-props>`
+    //   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(
+    //     '<div><span>1 is number</span><span>true is boolean</span></div>',
+    //   )
+    // })
+  })
 
   // describe('attrs', () => {
   //   const E = defineVaporCustomElement({
index b104b20900d7687c46b7d654954e33273f7eedc7..7642dab9627ecc09272da5995ff64ca12c3772b0 100644 (file)
@@ -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)
   }