Skip to content

Commit

Permalink
Support es2022 fields
Browse files Browse the repository at this point in the history
  • Loading branch information
mantou132 committed Jun 17, 2024
1 parent 1c26f33 commit cb9d173
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 40 deletions.
4 changes: 2 additions & 2 deletions packages/config/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"target": "ES2018",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"paths": {
"src/*": ["src/*"]
}
Expand Down
2 changes: 1 addition & 1 deletion packages/gem-examples/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default defineConfig({
pages: createPages(
examples.map((name) => ({
name,
entry: `/src/${name}/index.ts`,
entry: `/src/${name}/index.js`,
})),
),
rewrites: [{ from: /^\/$/, to: '/index.html' }],
Expand Down
29 changes: 14 additions & 15 deletions packages/gem/src/lib/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
/**
* 不能使用 tsc 的 `useDefineForClassFields`,
* 该参数将在实例上创建字段,导致 proto 上的 getter/setter 失效
*
* 字段装饰器运行在 `constructor` 前
*/

import { defineAttribute, defineCSSState, defineProperty, defineRef, GemElement, nativeDefineElement } from './element';
import { Store } from './store';
import { Sheet, camelToKebabCase, randomStr } from './utils';
Expand All @@ -21,10 +14,12 @@ function pushStaticField(
isSet = false,
) {
const cls = target.constructor as GemElementConstructor;
const current = cls[field];
if (!cls.hasOwnProperty(field)) {
// 继承基类
const current = new Set<unknown>(cls[field]);
current.delete(member);
Object.defineProperty(cls, field, {
value: isSet ? new Set<unknown>(current) : current ? Array.from<any>(current) : [],
value: isSet ? current : [...current],
});
}

Expand Down Expand Up @@ -60,6 +55,7 @@ export function refobject<T extends GemElement<any>, V extends HTMLElement>(
if (!target.hasOwnProperty(prop)) {
const ref = `${camelToKebabCase(prop)}-${randomStr()}`;
pushStaticField(this, 'defineRefs', ref);
pushStaticField(this, 'fields', prop);
defineRef(Object.getPrototypeOf(this), prop, ref);
}
return value;
Expand All @@ -81,6 +77,7 @@ function defineAttr(t: GemElement, prop: string, attr: string) {
const target = Object.getPrototypeOf(t);
if (!target.hasOwnProperty(prop)) {
pushStaticField(target, 'observedAttributes', attr); // 没有 observe 的效果
pushStaticField(t, 'fields', prop);
defineAttribute(target, prop, attr);
}

Expand All @@ -96,15 +93,16 @@ function defineAttr(t: GemElement, prop: string, attr: string) {
};
}
observedAttributes.set(target, attrSet);

return t.getAttribute(attr);
}
export function attribute<T extends GemElement<any>, V extends string>(
_: undefined,
context: ClassFieldDecoratorContext<T, V>,
) {
return function (this: T, value: V) {
const prop = context.name as string;
defineAttr(this, prop, camelToKebabCase(prop));
return value;
return (defineAttr(this, prop, camelToKebabCase(prop)) as V) || value;
};
}
export function boolattribute<T extends GemElement<any>>(
Expand All @@ -115,17 +113,15 @@ export function boolattribute<T extends GemElement<any>>(
const prop = context.name as string;
const attr = camelToKebabCase(prop);
pushStaticField(this, 'booleanAttributes', attr, true);
defineAttr(this, prop, attr);
return value;
return (defineAttr(this, prop, attr) as unknown as boolean) || value;
};
}
export function numattribute<T extends GemElement<any>>(_: undefined, context: ClassFieldDecoratorContext<T, number>) {
return function (this: T, value: number) {
const prop = context.name as string;
const attr = camelToKebabCase(prop);
pushStaticField(this, 'numberAttributes', attr, true);
defineAttr(this, prop, attr);
return value;
return (defineAttr(this, prop, attr) as unknown as number) || value;
};
}

Expand All @@ -145,6 +141,7 @@ export function property<T extends GemElement<any>>(_: undefined, context: Class
const target = Object.getPrototypeOf(this);
if (!target.hasOwnProperty(prop)) {
pushStaticField(this, 'observedProperties', prop);
pushStaticField(this, 'fields', prop);
defineProperty(target, prop);
}
return value;
Expand All @@ -171,6 +168,7 @@ export function state<T extends GemElement<any>>(_: undefined, context: ClassFie
if (!target.hasOwnProperty(prop)) {
const attr = camelToKebabCase(prop);
pushStaticField(this, 'defineCSSStates', attr);
pushStaticField(this, 'fields', prop);
defineCSSState(Object.getPrototypeOf(this), prop, attr);
}
return this[prop as keyof T] as boolean;
Expand Down Expand Up @@ -258,6 +256,7 @@ function defineEmitter(t: GemElement, prop: string, options?: Omit<CustomEventIn
if (!target.hasOwnProperty(prop)) {
const event = camelToKebabCase(prop);
pushStaticField(target, 'defineEvents', event);
pushStaticField(t, 'fields', prop);
defineProperty(target, prop, event, options);
}
}
Expand Down
14 changes: 13 additions & 1 deletion packages/gem/src/lib/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ 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 @@ -404,11 +406,21 @@ export abstract class GemElement<T = Record<string, unknown>> extends HTMLElemen
return;
}

const { observedStores, rootElement, fields } = 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?.();
const { observedStores, rootElement } = this.constructor as typeof GemElement;
this.#disconnectStore = observedStores?.map((store) => connect(store, this.#update));
this.#render();
this.#isMounted = true;
Expand Down
4 changes: 4 additions & 0 deletions packages/gem/src/test/gem-element/advance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ describe('gem element Memo', () => {
});
});

@customElement('i-gem')
class I extends GemElement {
@attribute appTitle = 'string';
@property appData = { a: 1 };
Expand All @@ -295,13 +296,16 @@ class InheritGem extends I {
}
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 () => {
const name = window.name;
const el: InheritGem = await fixture(html`<inherit-gem></inherit-gem>`);
expect(el.appTitle).to.equal('1');
expect(el.appTitle2).to.equal('2');
expect(el.appData.a).to.equal(1);
el.appTitle = 'b';
el.appData = { a: 2 };
Expand Down
34 changes: 13 additions & 21 deletions packages/gem/src/test/gem-element/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,28 @@ const styles = createCSSSheet(css`
`);

class GemDemo extends GemElement {
/** @attr */ attr: string | null;
/** @attr */ disabled: boolean;
/** @attr */ count: number;
/** @attr long-attr*/ longAttr: string;
static observedAttributes = ['attr', 'long-attr', 'disabled', 'count'];
static booleanAttributes = new Set(['disabled']);
static numberAttributes = new Set(['count']);

declare attr: string | null;
declare disabled: boolean;
declare count: number;
declare longAttr: string;
declare prop: { value: string };

static observedStores = [store];

prop = { value: '' };
static observedProperties = ['prop'];

static adoptedStyleSheets = [styles];
state = { value: '' };

constructor() {
super();
this.prop = { value: '' };
}

renderCount = 0;

render() {
Expand All @@ -43,9 +49,9 @@ class GemDemo extends GemElement {
customElements.define('gem-demo', GemDemo);

class DeferGemElement extends GemElement {
/** @attr */ attr: string | null;
static observedAttributes = ['attr'];
prop: { value: string };
declare attr: string | null;
declare prop: { value: string };
}

describe('基本 gem element 测试', () => {
Expand Down Expand Up @@ -78,13 +84,6 @@ describe('基本 gem element 测试', () => {
expect(el.renderCount).to.equal(2);
});
it('读取 attr', async () => {
class G extends GemElement {
static observedAttributes = ['attr'];
attr!: string;
test = expect(this.attr).to.equal('attr');
}
customElements.define('temp-field-read-attr', G);
await fixture(html`<temp-field-read-attr attr="attr"></temp-field-read-attr>`);
const el: GemDemo = await fixture(html`
<gem-demo attr="attr" ?disabled=${true} count=${1} long-attr="hi"></gem-demo>
`);
Expand Down Expand Up @@ -117,13 +116,6 @@ describe('基本 gem element 测试', () => {
});

it('读取 prop', async () => {
class G extends GemElement {
static observedPropertys = ['prop'];
prop!: any;
test = expect(this.prop).to.equal(undefined);
}
customElements.define('temp-field-read-prop', G);
await fixture(html`<temp-field-read-prop .prop=${{ value: 'prop' }}></temp-field-read-prop>`);
const el: GemDemo = await fixture(html`<gem-demo .prop=${{ value: 'prop' }}></gem-demo>`);
expect(el.prop).to.deep.equal({ value: 'prop' });
});
Expand Down

0 comments on commit cb9d173

Please sign in to comment.