Skip to content

Commit

Permalink
[gem] Fixed ES2022 field
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed May 13, 2024
1 parent 4cefedd commit 69148b9
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 76 deletions.
97 changes: 47 additions & 50 deletions packages/gem/src/lib/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ function pushStaticField(
(cls[field] as any)[isSet ? 'add' : 'push'](member);
}

function clearField<T extends GemElement<any>>(instance: T, prop: string) {
const desc = Reflect.getOwnPropertyDescriptor(instance, prop)!;
Reflect.deleteProperty(instance, prop);
Reflect.set(instance, prop, desc.value);
}

export type RefObject<T = HTMLElement> = { ref: string; element: T | undefined; elements: T[] };

/**
Expand All @@ -49,17 +55,16 @@ export function refobject<T extends GemElement<any>, V extends HTMLElement>(
_: undefined,
context: ClassFieldDecoratorContext<T, RefObject<V>>,
) {
return function (this: T, value: RefObject<V>) {
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);
});
}

/**
Expand All @@ -73,56 +78,52 @@ export function refobject<T extends GemElement<any>, V extends HTMLElement>(
* ```
*/
const observedAttributes = new WeakMap<GemElementPrototype, Set<string>>();
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<T extends GemElement<any>, V extends string>(
_: undefined,
context: ClassFieldDecoratorContext<T, V>,
) {
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<T extends GemElement<any>>(
_: undefined,
context: ClassFieldDecoratorContext<T, boolean>,
) {
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<T extends GemElement<any>>(_: undefined, context: ClassFieldDecoratorContext<T, number>) {
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');
});
}

/**
Expand All @@ -136,16 +137,15 @@ export function numattribute<T extends GemElement<any>>(_: undefined, context: C
* ```
*/
export function property<T extends GemElement<any>>(_: undefined, context: ClassFieldDecoratorContext<T>) {
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);
});
}

/**
Expand All @@ -162,17 +162,16 @@ export function property<T extends GemElement<any>>(_: undefined, context: Class
* ```
*/
export function state<T extends GemElement<any>>(_: undefined, context: ClassFieldDecoratorContext<T, boolean>) {
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);
});
}

/**
Expand Down Expand Up @@ -237,28 +236,26 @@ export type Emitter<T = any> = (detail?: T, options?: Omit<CustomEventInit<unkno
* ```
*/
export function emitter<T extends GemElement<any>>(_: undefined, context: ClassFieldDecoratorContext<T, Emitter>) {
return function (this: T, value: Emitter) {
context.addInitializer(function (this: T) {
defineEmitter(this, context.name as string);
return value;
};
});
}
export function globalemitter<T extends GemElement<any>>(
_: undefined,
context: ClassFieldDecoratorContext<T, Emitter>,
) {
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<CustomEventInit<unknown>, '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);
}

/**
Expand Down
23 changes: 4 additions & 19 deletions packages/gem/src/lib/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,6 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
// https://github.com/microsoft/TypeScript/issues/21388#issuecomment-934345226
static #final = Symbol();

// 装饰器定义的 es2022 字段
static fields?: string[];
// 这里只是字段申明,不能赋值,否则子类会继承被共享该字段
static observedAttributes?: string[]; // 必须在定义元素前指定
static booleanAttributes?: Set<string>;
Expand Down Expand Up @@ -406,31 +404,18 @@ export abstract class GemElement<T = Record<string, unknown>> 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));
this.#render();
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`);
}
};
Expand All @@ -453,10 +438,10 @@ export abstract class GemElement<T = Record<string, unknown>> 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();
Expand Down
3 changes: 0 additions & 3 deletions packages/gem/src/test/gem-element/advance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 () => {
Expand Down
4 changes: 0 additions & 4 deletions packages/gem/src/test/gem-element/decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 69148b9

Please sign in to comment.