From cf806dbce10082271822276a0618b59bb4e6e71a Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 27 Oct 2025 22:31:55 +0800 Subject: [PATCH] wip: save --- packages/runtime-dom/src/apiCustomElement.ts | 8 +- .../__tests__/customElement.spec.ts | 312 +++++++++--------- .../src/apiDefineVaporCustomElement.ts | 16 +- packages/runtime-vapor/src/component.ts | 10 +- packages/runtime-vapor/src/componentProps.ts | 11 +- packages/runtime-vapor/src/renderEffect.ts | 2 +- 6 files changed, 191 insertions(+), 168 deletions(-) diff --git a/packages/runtime-dom/src/apiCustomElement.ts b/packages/runtime-dom/src/apiCustomElement.ts index 664872ad13..c1132fb660 100644 --- a/packages/runtime-dom/src/apiCustomElement.ts +++ b/packages/runtime-dom/src/apiCustomElement.ts @@ -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 diff --git a/packages/runtime-vapor/__tests__/customElement.spec.ts b/packages/runtime-vapor/__tests__/customElement.spec.ts index 5186091b31..e83bb70042 100644 --- a/packages/runtime-vapor/__tests__/customElement.spec.ts +++ b/packages/runtime-vapor/__tests__/customElement.spec.ts @@ -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('') }) - // 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('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 = `` + 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('
', 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('"
foo
"') - // }) + 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 = `` - // 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('
foo
', true)() + }, + }) + 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('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 = `` + const e = container.childNodes[0] as any + expect(e.value).toBe('hi') + }) // // #12214 // test('Boolean prop with default true', async () => { diff --git a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts index 9e70a51dba..b9ebf35db3 100644 --- a/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts +++ b/packages/runtime-vapor/src/apiDefineVaporCustomElement.ts @@ -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 } } diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts index 6f494ac6d6..df116b2f73 100644 --- a/packages/runtime-vapor/src/component.ts +++ b/packages/runtime-vapor/src/component.ts @@ -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) + } } /** diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index 6832bd9103..393e6ef225 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -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 { +export function resolveDynamicProps( + props: RawProps, + isResolved: boolean = false, +): Record { const mergedRawProps: Record = {} for (const key in props) { if (key !== '$') { - mergedRawProps[key] = props[key]() + mergedRawProps[key] = isResolved ? props[key] : props[key]() } } if (props.$) { diff --git a/packages/runtime-vapor/src/renderEffect.ts b/packages/runtime-vapor/src/renderEffect.ts index 8aece8ee92..e36ac4ba45 100644 --- a/packages/runtime-vapor/src/renderEffect.ts +++ b/packages/runtime-vapor/src/renderEffect.ts @@ -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) } -- 2.47.3