]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: save
authordaiwei <daiwei521@126.com>
Mon, 27 Oct 2025 14:31:55 +0000 (22:31 +0800)
committerdaiwei <daiwei521@126.com>
Mon, 27 Oct 2025 14:31:55 +0000 (22:31 +0800)
packages/runtime-dom/src/apiCustomElement.ts
packages/runtime-vapor/__tests__/customElement.spec.ts
packages/runtime-vapor/src/apiDefineVaporCustomElement.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentProps.ts
packages/runtime-vapor/src/renderEffect.ts

index 664872ad13c03ea967902bdf1d94588e1624e8fa..c1132fb660ea6d8e3745adeb8eb9bae887ea44f8 100644 (file)
@@ -285,10 +285,6 @@ export abstract class VueElementBase<
     }
   }
 
-  get _isVapor(): boolean {
-    return `__vapor` in this._def
-  }
-
   connectedCallback(): void {
     // avoid resolving component if it's not connected
     if (!this.isConnected) return
@@ -533,7 +529,7 @@ export abstract class VueElementBase<
    * @internal
    */
   protected _getProp(key: string): any {
-    return this._isVapor ? this._props[key]() : this._props[key]
+    return this._props[key]
   }
 
   /**
@@ -549,7 +545,7 @@ export abstract class VueElementBase<
       if (val === REMOVAL) {
         delete this._props[key]
       } else {
-        this._props[key] = this._isVapor ? () => val : val
+        this._props[key] = val
         // support set key on ceVNode
         if (key === 'key' && this._app && this._app._ceVNode) {
           this._app._ceVNode!.key = val
index 5186091b31a800558c075b9c4367df80c5793771..e83bb700429cb8ef4136616e31b8aa125df92035 100644 (file)
@@ -71,7 +71,7 @@ describe('defineVaporCustomElement', () => {
     })
 
     test('should work w/ manual instantiation', () => {
-      const e = new E({ msg: () => 'inline' })
+      const e = new E({ msg: 'inline' })
       // should lazy init
       expect(e._instance).toBe(null)
       // should initialize on connect
@@ -162,7 +162,7 @@ describe('defineVaporCustomElement', () => {
     })
   })
 
-  describe.todo('props', () => {
+  describe('props', () => {
     const E = defineVaporCustomElement({
       props: {
         foo: [String, null],
@@ -295,163 +295,177 @@ describe('defineVaporCustomElement', () => {
       expect((el as any).outerHTML).toBe('<my-el-comp foo-bar=""></my-el-comp>')
     })
 
-    // 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('attribute -> prop type casting', async () => {
+      const E = defineVaporCustomElement({
+        props: {
+          fooBar: Number, // test casting of camelCase prop names
+          bar: Boolean,
+          baz: String,
+        },
+        setup(props: any) {
+          const n0 = template(' ')() as any
+          renderEffect(() => {
+            const texts = []
+            texts.push(
+              toDisplayString(props.fooBar),
+              toDisplayString(typeof props.fooBar),
+              toDisplayString(props.bar),
+              toDisplayString(typeof props.bar),
+              toDisplayString(props.baz),
+              toDisplayString(typeof props.baz),
+            )
+            setText(n0, texts.join(' '))
+          })
+          return n0
+        },
+      })
+      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`,
+      )
 
-    //   e.setAttribute('bar', '')
-    //   await nextTick()
-    //   expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`)
+      e.setAttribute('bar', '')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(`1 number true boolean 12345 string`)
 
-    //   e.setAttribute('foo-bar', '2e1')
-    //   await nextTick()
-    //   expect(e.shadowRoot!.innerHTML).toBe(
-    //     `20 number true boolean 12345 string`,
-    //   )
+      e.setAttribute('foo-bar', '2e1')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(
+        `20 number true boolean 12345 string`,
+      )
 
-    //   e.setAttribute('baz', '2e1')
-    //   await nextTick()
-    //   expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 string`)
-    // })
+      e.setAttribute('baz', '2e1')
+      await nextTick()
+      expect(e.shadowRoot!.innerHTML).toBe(`20 number true boolean 2e1 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`)
-    // })
+    test('attr casting w/ programmatic creation', () => {
+      const E = defineVaporCustomElement({
+        props: {
+          foo: Number,
+        },
+        setup(props: any) {
+          const n0 = template(' ')() as any
+          renderEffect(() => {
+            setText(n0, `foo type: ${typeof props.foo}`)
+          })
+          return n0
+        },
+      })
+      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`)
+    })
 
-    // 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)
-    // })
+    test('handling properties set before upgrading', () => {
+      const E = defineVaporCustomElement({
+        props: {
+          foo: String,
+          dataAge: Number,
+        },
+        setup(props: any) {
+          expect(props.foo).toBe('hello')
+          expect(props.dataAge).toBe(5)
 
-    // 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
+          const n0 = template('<div> </div>', true)() as any
+          const x0 = txt(n0) as any
+          renderEffect(() => setText(x0, `foo: ${props.foo}`))
+          return n0
+        },
+      })
+      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)
+    })
 
-    //   container.appendChild(el)
-    //   expect(el.shadowRoot.innerHTML).toBe(JSON.stringify(obj))
-    // })
+    test('handle properties set before connecting', () => {
+      const obj = { a: 1 }
+      const E = defineVaporCustomElement({
+        props: {
+          foo: String,
+          post: Object,
+        },
+        setup(props: any) {
+          expect(props.foo).toBe('hello')
+          expect(props.post).toBe(obj)
 
-    // // 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>"')
-    // })
+          const n0 = template(' ', true)() as any
+          renderEffect(() => setText(n0, JSON.stringify(props.post)))
+          return n0
+        },
+      })
+      customElements.define('my-el-preconnect', E)
+      const el = document.createElement('my-el-preconnect') as any
+      el.foo = 'hello'
+      el.post = obj
 
-    // // #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')
-    // })
+      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')
-    // })
+    test('handle components with no props', async () => {
+      const E = defineVaporCustomElement({
+        setup() {
+          return template('<div>foo</div>', true)()
+        },
+      })
+      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('set number value in dom property', () => {
+      const E = defineVaporCustomElement({
+        props: {
+          'max-age': Number,
+        },
+        setup(props: any) {
+          const n0 = template(' ')() as any
+          renderEffect(() => {
+            setText(n0, `max age: ${props.maxAge}/type: ${typeof props.maxAge}`)
+          })
+          return n0
+        },
+      })
+      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('should reflect default value', () => {
+      const E = defineVaporCustomElement({
+        props: {
+          value: {
+            type: String,
+            default: 'hi',
+          },
+        },
+        setup(props: any) {
+          const n0 = template(' ')() as any
+          renderEffect(() => setText(n0, props.value))
+          return n0
+        },
+      })
+      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')
+    })
 
     // // #12214
     // test('Boolean prop with default true', async () => {
index 9e70a51dbab5d763500cab348fd30c3948561756..b9ebf35db34903c786e819d9f8880f42fb16c26b 100644 (file)
@@ -81,7 +81,6 @@ export class VaporElement extends VueElementBase<
       def.name = 'VaporElement'
     }
 
-    def.isCE = true
     this._app = this._createApp(this._def)
     this._inheritParentContext()
     if (this._def.configureApp) {
@@ -123,14 +122,17 @@ export class VaporElement extends VueElementBase<
   }
 
   private _createComponent() {
-    this._instance = createComponent(this._def, this._props)
-    if (!this.shadowRoot) {
-      this._instance!.m = this._instance!.u = [this._renderSlots.bind(this)]
+    this._def.ce = instance => {
+      this._instance = instance
+      if (!this.shadowRoot) {
+        ;(instance.m || (instance.m = [])).push(this._renderSlots.bind(this))
+        ;(instance.u || (instance.u = [])).push(this._renderSlots.bind(this))
+      }
+      this._processInstance()
+      this._setParent()
     }
 
-    this._processInstance()
-    this._setParent()
-
+    this._instance = createComponent(this._def, this._props)
     return this._instance
   }
 }
index 6f494ac6d6af64abf6cff1c7d916563fbede390a..df116b2f73a0eadfe025c08c38883eb3d024f89d 100644 (file)
@@ -127,7 +127,10 @@ export interface ObjectVaporComponent
 
   name?: string
   vapor?: boolean
-  isCE?: boolean
+  /**
+   * @internal custom element interception hook
+   */
+  ce?: (instance: VaporComponentInstance) => void
 }
 
 interface SharedInternalOptions {
@@ -592,6 +595,11 @@ export class VaporComponentInstance implements GenericComponentInstance {
         ? new Proxy(rawSlots, dynamicSlotsProxyHandlers)
         : rawSlots
       : EMPTY_OBJ
+
+    // apply custom element special handling
+    if (comp.ce) {
+      comp.ce(this)
+    }
   }
 
   /**
index 6832bd9103c6348021933c83ff9abe079d8f7bd8..393e6ef2256aaf625364a8c81d8f7c0ae5a5c910 100644 (file)
@@ -97,7 +97,7 @@ export function getPropsProxyHandlers(
         return resolvePropValue(
           propsOptions!,
           key,
-          rawProps[rawKey](),
+          instance.type.ce ? rawProps[rawKey] : rawProps[rawKey](),
           instance,
           resolveDefault,
         )
@@ -318,7 +318,7 @@ export function setupPropsValidation(instance: VaporComponentInstance): void {
   renderEffect(() => {
     pushWarningContext(instance)
     validateProps(
-      resolveDynamicProps(rawProps),
+      resolveDynamicProps(rawProps, !!instance.type.ce),
       instance.props,
       normalizePropsOptions(instance.type)[0]!,
     )
@@ -326,11 +326,14 @@ export function setupPropsValidation(instance: VaporComponentInstance): void {
   }, true /* noLifecycle */)
 }
 
-export function resolveDynamicProps(props: RawProps): Record<string, unknown> {
+export function resolveDynamicProps(
+  props: RawProps,
+  isResolved: boolean = false,
+): Record<string, unknown> {
   const mergedRawProps: Record<string, any> = {}
   for (const key in props) {
     if (key !== '$') {
-      mergedRawProps[key] = props[key]()
+      mergedRawProps[key] = isResolved ? props[key] : props[key]()
     }
   }
   if (props.$) {
index 8aece8ee92838b15c9744353b26d9ae5ffe17cfd..e36ac4ba4586606a6858b4c6d40f8bff1e3c30d0 100644 (file)
@@ -43,7 +43,7 @@ export class RenderEffect extends ReactiveEffect {
           : void 0
       }
 
-      if (__DEV__ || instance.type.isCE) {
+      if (__DEV__ || instance.type.ce) {
         // register effect for stopping them during HMR rerender
         ;(instance.renderEffects || (instance.renderEffects = [])).push(this)
       }