From 69148b96c3de74227829055e852106edea6219ac Mon Sep 17 00:00:00 2001 From: mantou132 <709922234@qq.com> Date: Wed, 21 Feb 2024 15:38:11 +0800 Subject: [PATCH] [gem] Fixed ES2022 field See: https://github.com/microsoft/TypeScript/issues/56606 --- packages/gem/src/lib/decorators.ts | 97 +++++++++---------- packages/gem/src/lib/element.ts | 23 +---- .../gem/src/test/gem-element/advance.test.ts | 3 - .../src/test/gem-element/decorators.test.ts | 4 - 4 files changed, 51 insertions(+), 76 deletions(-) diff --git a/packages/gem/src/lib/decorators.ts b/packages/gem/src/lib/decorators.ts index 1f2adeb3..a8348975 100644 --- a/packages/gem/src/lib/decorators.ts +++ b/packages/gem/src/lib/decorators.ts @@ -26,6 +26,12 @@ function pushStaticField( (cls[field] as any)[isSet ? 'add' : 'push'](member); } +function clearField>(instance: T, prop: string) { + const desc = Reflect.getOwnPropertyDescriptor(instance, prop)!; + Reflect.deleteProperty(instance, prop); + Reflect.set(instance, prop, desc.value); +} + export type RefObject = { ref: string; element: T | undefined; elements: T[] }; /** @@ -49,17 +55,16 @@ export function refobject, V extends HTMLElement>( _: undefined, context: ClassFieldDecoratorContext>, ) { - return function (this: T, value: RefObject) { + context.addInitializer(function (this: T) { const target = Object.getPrototypeOf(this); const prop = context.name as string; if (!target.hasOwnProperty(prop)) { const ref = `${camelToKebabCase(prop)}-${randomStr()}`; pushStaticField(this, 'defineRefs', ref); - pushStaticField(this, 'fields', prop); - defineRef(Object.getPrototypeOf(this), prop, ref); + defineRef(target, prop, ref); } - return value; - }; + clearField(this, prop); + }); } /** @@ -73,56 +78,52 @@ export function refobject, V extends HTMLElement>( * ``` */ const observedAttributes = new WeakMap>(); -function defineAttr(t: GemElement, prop: string, attr: string) { +function defineAttr(t: GemElement, prop: string, attrType?: StaticField) { const target = Object.getPrototypeOf(t); if (!target.hasOwnProperty(prop)) { + const attr = camelToKebabCase(prop); pushStaticField(target, 'observedAttributes', attr); // 没有 observe 的效果 - pushStaticField(t, 'fields', prop); + attrType && pushStaticField(target, attrType, attr, true); defineAttribute(target, prop, attr); - } - // hack `observedAttributes` - // 不在 Devtools 中工作 https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#dom_access - const attrSet = observedAttributes.get(target) || new Set(target.constructor.observedAttributes); - attrSet.add(attr); - if (!observedAttributes.has(target)) { - const setAttribute = Element.prototype.setAttribute; - target.setAttribute = function (n: string, v: string) { - setAttribute.apply(this, [n, v]); - if (attrSet.has(n)) this.attributeChangedCallback(); - }; + // hack `observedAttributes` + // 不在 Devtools 中工作 https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#dom_access + const attrSet = observedAttributes.get(target) || new Set(target.constructor.observedAttributes); + attrSet.add(attr); + if (!observedAttributes.has(target)) { + const setAttribute = Element.prototype.setAttribute; + target.setAttribute = function (n: string, v: string) { + setAttribute.apply(this, [n, v]); + if (attrSet.has(n)) this.attributeChangedCallback(); + }; + } + observedAttributes.set(target, attrSet); } - observedAttributes.set(target, attrSet); - - return t.getAttribute(attr); + clearField(t, prop); } export function attribute, V extends string>( _: undefined, context: ClassFieldDecoratorContext, ) { - return function (this: T, value: V) { + context.addInitializer(function (this: T) { const prop = context.name as string; - return (defineAttr(this, prop, camelToKebabCase(prop)) as V) || value; - }; + defineAttr(this, prop); + }); } export function boolattribute>( _: undefined, context: ClassFieldDecoratorContext, ) { - return function (this: T, value: boolean) { + context.addInitializer(function (this: T) { const prop = context.name as string; - const attr = camelToKebabCase(prop); - pushStaticField(this, 'booleanAttributes', attr, true); - return (defineAttr(this, prop, attr) as unknown as boolean) || value; - }; + defineAttr(this, prop, 'booleanAttributes'); + }); } export function numattribute>(_: undefined, context: ClassFieldDecoratorContext) { - return function (this: T, value: number) { + context.addInitializer(function (this: T) { const prop = context.name as string; - const attr = camelToKebabCase(prop); - pushStaticField(this, 'numberAttributes', attr, true); - return (defineAttr(this, prop, attr) as unknown as number) || value; - }; + defineAttr(this, prop, 'numberAttributes'); + }); } /** @@ -136,16 +137,15 @@ export function numattribute>(_: undefined, context: C * ``` */ export function property>(_: undefined, context: ClassFieldDecoratorContext) { - return function (this: T, value: any) { + context.addInitializer(function (this: T) { const prop = context.name as string; const target = Object.getPrototypeOf(this); if (!target.hasOwnProperty(prop)) { pushStaticField(this, 'observedProperties', prop); - pushStaticField(this, 'fields', prop); defineProperty(target, prop); } - return value; - }; + clearField(this, prop); + }); } /** @@ -162,17 +162,16 @@ export function property>(_: undefined, context: Class * ``` */ export function state>(_: undefined, context: ClassFieldDecoratorContext) { - return function (this: T, _value: boolean) { + context.addInitializer(function (this: T) { const target = Object.getPrototypeOf(this); const prop = context.name as string; if (!target.hasOwnProperty(prop)) { const attr = camelToKebabCase(prop); pushStaticField(this, 'defineCSSStates', attr); - pushStaticField(this, 'fields', prop); - defineCSSState(Object.getPrototypeOf(this), prop, attr); + defineCSSState(target, prop, attr); } - return this[prop as keyof T] as boolean; - }; + clearField(this, prop); + }); } /** @@ -237,28 +236,26 @@ export type Emitter = (detail?: T, options?: Omit>(_: undefined, context: ClassFieldDecoratorContext) { - return function (this: T, value: Emitter) { + context.addInitializer(function (this: T) { defineEmitter(this, context.name as string); - return value; - }; + }); } export function globalemitter>( _: undefined, context: ClassFieldDecoratorContext, ) { - return function (this: T, value: Emitter) { + context.addInitializer(function (this: T) { defineEmitter(this, context.name as string, { bubbles: true, composed: true }); - return value; - }; + }); } function defineEmitter(t: GemElement, prop: string, options?: Omit, 'detail'>) { const target = Object.getPrototypeOf(t); if (!target.hasOwnProperty(prop)) { const event = camelToKebabCase(prop); pushStaticField(target, 'defineEvents', event); - pushStaticField(t, 'fields', prop); defineProperty(target, prop, event, options); } + clearField(t, prop); } /** diff --git a/packages/gem/src/lib/element.ts b/packages/gem/src/lib/element.ts index 18df9bb7..3c8aecb5 100644 --- a/packages/gem/src/lib/element.ts +++ b/packages/gem/src/lib/element.ts @@ -92,8 +92,6 @@ export abstract class GemElement> extends HTMLElemen // https://github.com/microsoft/TypeScript/issues/21388#issuecomment-934345226 static #final = Symbol(); - // 装饰器定义的 es2022 字段 - static fields?: string[]; // 这里只是字段申明,不能赋值,否则子类会继承被共享该字段 static observedAttributes?: string[]; // 必须在定义元素前指定 static booleanAttributes?: Set; @@ -406,19 +404,10 @@ export abstract class GemElement> extends HTMLElemen return; } - const { observedStores, rootElement, fields } = this.constructor as typeof GemElement; + const { observedStores, rootElement } = this.constructor as typeof GemElement; // 似乎这是最早的判断不在 `constructor` 中的地方 Reflect.set(this, constructorSymbol, false); - // Bug: 只有插入文档才能清除元素上的字段, `new`/`cloneNode` 将不会触发 - if (fields) { - for (const prop of fields) { - const desc = Reflect.getOwnPropertyDescriptor(this, prop); - if (!desc) break; - Reflect.deleteProperty(this, prop); - Reflect.set(this, prop, desc.value); - } - } this.willMount?.(); this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update)); @@ -426,11 +415,7 @@ export abstract class GemElement> extends HTMLElemen this.#isMounted = true; this.#unmountCallback = this.mounted?.(); this.#initEffect(); - if ( - rootElement && - this.isConnected && - (this.getRootNode() as ShadowRoot).host?.tagName !== rootElement.toUpperCase() - ) { + if (rootElement && (this.getRootNode() as ShadowRoot).host?.tagName !== rootElement.toUpperCase()) { throw new GemError(`not allow current root type`); } }; @@ -453,10 +438,10 @@ export abstract class GemElement> extends HTMLElemen /** * @private * @final - * use `mounted`; 允许手动调用 `connectedCallback` 以清除装饰器定义的字段 + * use `mounted` */ connectedCallback() { - if (this.isConnected && this.#isAsync) { + if (this.#isAsync) { asyncRenderTaskList.add(this.#connectedCallback); } else { this.#connectedCallback(); diff --git a/packages/gem/src/test/gem-element/advance.test.ts b/packages/gem/src/test/gem-element/advance.test.ts index 99cc59ae..8eb87b1c 100644 --- a/packages/gem/src/test/gem-element/advance.test.ts +++ b/packages/gem/src/test/gem-element/advance.test.ts @@ -129,8 +129,6 @@ describe('gem element 生命周期', () => { expect(el.appTitle).to.equal('title'); expect(el.renderCount).to.equal(1); const clone = el.cloneNode() as LifecycleGemElement; - // v2 BUG - clone.connectedCallback(); expect(clone.appTitle).to.equal('title'); const el2 = new LifecycleGemElement('', '2'); @@ -298,7 +296,6 @@ describe('gem element 继承', () => { it('静态字段继承', async () => { new I(); new InheritGem(); // 触发装饰器自定义初始化函数 - expect(InheritGem.fields).to.eql(['appTitle', 'appData', 'sayHi', 'appTitle2']); expect(InheritGem.observedAttributes).to.eql(['app-title', 'app-title2']); }); it('attr/prop/emitter 继承', async () => { diff --git a/packages/gem/src/test/gem-element/decorators.test.ts b/packages/gem/src/test/gem-element/decorators.test.ts index 52d1558b..bf407cd0 100644 --- a/packages/gem/src/test/gem-element/decorators.test.ts +++ b/packages/gem/src/test/gem-element/decorators.test.ts @@ -56,10 +56,6 @@ describe('装饰器', () => { const el = new DecoratorGemElement(); expect(el.propData).to.eql({ value: '' }); expect(el.getAttribute('rank-attr')).to.equal(null); - // v2 BUG - expect(el.rankAttr).to.equal(''); - el.connectedCallback(); - expect(el.getAttribute('rank-attr')).to.equal(null); expect(el.rankAttr).to.equal(''); el.rankAttr = 'attr';