From 5be1567e6525c740a45695f071f0b900ed7859b7 Mon Sep 17 00:00:00 2001 From: mantou132 <709922234@qq.com> Date: Sun, 23 Jun 2024 22:27:13 +0800 Subject: [PATCH] [gem] Remove `GemElement` static member Closed #166 --- packages/duoyun-ui/src/lib/element.ts | 4 +- packages/gem-devtools/src/elements/header.ts | 1 + packages/gem-devtools/src/modules/panel.ts | 2 +- packages/gem-devtools/src/scripts/get-gem.ts | 70 ++++++++----------- packages/gem-devtools/src/scripts/preload.ts | 3 +- packages/gem-devtools/src/sidebarpanel.ts | 10 +-- packages/gem-devtools/src/store.ts | 1 + packages/gem-examples/src/elements/nav.ts | 6 +- .../src/hello-world/manifest.json | 1 + packages/gem-examples/vite.config.ts | 4 +- .../001-basic/003-global-state-management.md | 2 +- .../gem/docs/en/003-api/001-gem-element.md | 11 --- .../001-basic/003-global-state-management.md | 2 +- .../gem/docs/zh/003-api/001-gem-element.md | 11 --- packages/gem/src/elements/base/dialog.ts | 24 +++---- .../gem/src/elements/base/modal-factory.ts | 26 +++---- packages/gem/src/lib/decorators.ts | 33 +++++---- packages/gem/src/lib/element.ts | 69 +++++++++--------- packages/gem/src/lib/utils.ts | 1 + .../gem/src/test/gem-element/advance.test.ts | 17 +++-- .../src/test/gem-element/decorators.test.ts | 22 +++--- 21 files changed, 147 insertions(+), 173 deletions(-) diff --git a/packages/duoyun-ui/src/lib/element.ts b/packages/duoyun-ui/src/lib/element.ts index 268e4ee5..6f189aec 100644 --- a/packages/duoyun-ui/src/lib/element.ts +++ b/packages/duoyun-ui/src/lib/element.ts @@ -1,4 +1,4 @@ -import { GemElement } from '@mantou/gem/lib/element'; +import { GemElement, gemSymbols } from '@mantou/gem/lib/element'; export function getBoundingClientRect(eleList: Element[]) { const rects = eleList.map((e) => e.getBoundingClientRect()); @@ -13,7 +13,7 @@ export function getBoundingClientRect(eleList: Element[]) { export function toggleActiveState(ele: Element | undefined | null, active: boolean) { if (!ele) return; if (ele instanceof GemElement) { - if ((ele.constructor as typeof GemElement).definedCSSStates?.includes('active')) { + if (Reflect.get(ele.constructor, gemSymbols.definedCSSStates)?.includes('active')) { (ele as any).active = active; } if (['button', 'combobox'].includes(ele.role || ele.internals.role || '')) { diff --git a/packages/gem-devtools/src/elements/header.ts b/packages/gem-devtools/src/elements/header.ts index ec7c730a..8adef247 100644 --- a/packages/gem-devtools/src/elements/header.ts +++ b/packages/gem-devtools/src/elements/header.ts @@ -17,6 +17,7 @@ const style = createCSSSheet(css` :host { display: flex; line-height: 1.5; + padding-inline: 0.5em; } .title { font-style: italic; diff --git a/packages/gem-devtools/src/modules/panel.ts b/packages/gem-devtools/src/modules/panel.ts index 6b0734e8..8cf071c0 100644 --- a/packages/gem-devtools/src/modules/panel.ts +++ b/packages/gem-devtools/src/modules/panel.ts @@ -52,7 +52,7 @@ export class Panel extends GemElement { `; } return html` - + ${panelStore.gemVersion} diff --git a/packages/gem-devtools/src/scripts/get-gem.ts b/packages/gem-devtools/src/scripts/get-gem.ts index 0c173514..4405df08 100644 --- a/packages/gem-devtools/src/scripts/get-gem.ts +++ b/packages/gem-devtools/src/scripts/get-gem.ts @@ -1,26 +1,31 @@ -import type { GemElement, SheetToken } from '@mantou/gem'; +import type { GemElement, SheetToken, Sheet, Store } from '@mantou/gem'; import type { PanelStore } from '../store'; declare let $0: any; // 不要使用作用域外的变量 -export const getSelectedGem = function (data: PanelStore, gemElementSymbols: string[]): PanelStore | string { +export const getSelectedGem = function (data: PanelStore): PanelStore | string { // https://github.com/bramus/scroll-driven-animations-debugger-extension/issues/19 if (!$0) return `Not Gem: $0 is ${$0}`; - const tagClass = $0.constructor as typeof GemElement; - const devToolsHook = window.__GEM_DEVTOOLS__HOOK__; - if (devToolsHook) { - if (!devToolsHook.GemElement || !($0 instanceof devToolsHook.GemElement)) return 'Not Gem: gem hook'; + const { __GEM_DEVTOOLS__HOOK__ } = window; + if (__GEM_DEVTOOLS__HOOK__) { + const { GemElement } = __GEM_DEVTOOLS__HOOK__; + if (!GemElement || !($0 instanceof GemElement)) return 'Not Gem: gem hook'; } else { - // 依赖 `constructor`,如果 `constructor` 被破坏,则扩展不能工作 // 没有严格检查是否是 GemElement if (!(($0 as any) instanceof HTMLElement)) return 'Not Gem: not HTMLElement'; - - const elementSymbols = new Set(Object.getOwnPropertySymbols($0).map(String)); - if (gemElementSymbols.some((symbol) => !elementSymbols.has(symbol))) return 'Not Gem: some symbol diff'; } + const tagClass = $0.constructor as typeof GemElement; + const { get } = Reflect; + // support v1 + const gemSymbols = new Proxy(get(__GEM_DEVTOOLS__HOOK__ || {}, 'gemSymbols') || {}, { + get(target, p) { + return get(target, p) || p; + }, + }); + const inspectable = (value: any) => { const type = typeof value; return (type === 'object' && value) || type === 'function'; @@ -116,7 +121,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str const buildInProperty = new Set(['internals']); const buildInAttribute = new Set(['ref']); const memberSet = getProps($0); - tagClass.observedAttributes?.forEach((attr) => { + get(tagClass, gemSymbols.observedAttributes)?.forEach((attr: string) => { const prop = kebabToCamelCase(attr); const value = $0[prop]; memberSet.delete(prop); @@ -137,7 +142,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str buildIn: buildInAttribute.has(attr) ? 1 : 0, }); }); - tagClass.observedProperties?.forEach((prop) => { + get(tagClass, gemSymbols.observedProperties)?.forEach((prop: string) => { memberSet.delete(prop); const value = $0[prop]; const type = value === null ? 'null' : typeof value; @@ -148,7 +153,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str path: inspectable(value) ? [prop] : undefined, }); }); - tagClass.definedEvents?.forEach((event) => { + get(tagClass, gemSymbols.definedEvents)?.forEach((event: string) => { const prop = kebabToCamelCase(event); memberSet.delete(prop); data.emitters.push({ @@ -158,23 +163,23 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str path: [prop], }); }); - tagClass.adoptedStyleSheets?.forEach((sheet, index) => { + get(tagClass, gemSymbols.adoptedStyleSheets)?.forEach((sheet: Sheet, index: number) => { data.adoptedStyles.push({ name: `StyleSheet${index + 1}`, value: objectToString(sheet[Object.getOwnPropertySymbols(sheet)[0] as typeof SheetToken]), type: 'object', - path: ['constructor', 'adoptedStyleSheets', String(index)], + path: ['constructor', 'gem@adoptedStyleSheets', String(index)], }); }); - tagClass.observedStores?.forEach((store, index) => { + get(tagClass, gemSymbols.observedStores)?.forEach((store: Store, index: number) => { data.observedStores.push({ name: `Store${index + 1}`, value: objectToString(store), type: 'object', - path: ['constructor', 'observedStores', String(index)], + path: ['constructor', 'gem@observedStores', String(index)], }); }); - tagClass.definedSlots?.forEach((slot) => { + get(tagClass, gemSymbols.definedSlots)?.forEach((slot: string) => { const isUnnamed = slot === 'unnamed'; const prop = kebabToCamelCase(slot); if (!$0.constructor[prop]) { @@ -194,7 +199,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str path: isNode ? ['firstChild'] : element ? ['querySelector', selector] : undefined, }); }); - tagClass.definedParts?.forEach((part) => { + get(tagClass, gemSymbols.definedParts)?.forEach((part: string) => { const prop = kebabToCamelCase(part); if (!$0.constructor[prop]) { memberSet.delete(prop); @@ -207,7 +212,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str path: [['shadowRoot', ''], 'querySelector', selector], }); }); - tagClass.definedRefs?.forEach((ref) => { + get(tagClass, gemSymbols.definedRefs)?.forEach((ref: string) => { const prop = kebabToCamelCase(ref.replace(/-\w+$/, '')); memberSet.delete(prop); data.refs.push({ @@ -217,7 +222,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str path: [['shadowRoot', ''], 'querySelector', `[ref=${$0[prop].ref}]`], }); }); - tagClass.definedCSSStates?.forEach((state) => { + get(tagClass, gemSymbols.definedCSSStates)?.forEach((state: string) => { const prop = kebabToCamelCase(state); memberSet.delete(prop); data.cssStates.push({ @@ -270,27 +275,13 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str }); }); - const buildInStaticMember = new Set([ - 'length', - 'name', - 'prototype', - 'observedAttributes', - 'observedProperties', - 'observedStores', - 'adoptedStyleSheets', - 'definedEvents', - 'definedCSSStates', - 'definedRefs', - 'definedParts', - 'definedSlots', - ]); - const buildInShowStaticMember = new Set(['rootElement']); + const buildInStaticMember = new Set(['length', 'name', 'prototype']); const getStaticMember = (cls: any, set = new Set()) => { Object.getOwnPropertyNames(cls).forEach((key) => { if ( !buildInStaticMember.has(key) && - !tagClass.definedParts?.includes(cls[key]) && - !tagClass.definedSlots?.includes(cls[key]) + !get(tagClass, gemSymbols.definedParts)?.includes(cls[key]) && + !get(tagClass, gemSymbols.definedSlots)?.includes(cls[key]) ) { set.add(key); } @@ -307,7 +298,7 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str type: typeof value, value: objectToString(value), path: inspectable(value) ? ['constructor', key] : undefined, - buildIn: buildInShowStaticMember.has(key) ? 1 : 0, + buildIn: 0, }); }); // `Class` self @@ -317,5 +308,6 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str value: objectToString(tagClass), path: ['constructor'], }); + data.gemVersion = __GEM_DEVTOOLS__HOOK__?.version ? `v${__GEM_DEVTOOLS__HOOK__.version}` : ''; return data; }; diff --git a/packages/gem-devtools/src/scripts/preload.ts b/packages/gem-devtools/src/scripts/preload.ts index 33208bb5..d4ee8cff 100644 --- a/packages/gem-devtools/src/scripts/preload.ts +++ b/packages/gem-devtools/src/scripts/preload.ts @@ -18,7 +18,8 @@ export function preload() { // [["shadowRoot", ""], "querySelector", "[ref=child-ref]"] // 只有 constructor 函数会当成对象读取 readProp(path) { - return path.reduce((p, c, index) => { + return path.reduce((p, k, index) => { + const c = typeof k === 'string' && k.startsWith('gem@') ? Symbol.for(k) : k; if (typeof p === 'function' && path[index - 1] !== 'constructor') { if (Array.isArray(c)) { return p(...c); diff --git a/packages/gem-devtools/src/sidebarpanel.ts b/packages/gem-devtools/src/sidebarpanel.ts index 307f73f2..9e04ca5c 100644 --- a/packages/gem-devtools/src/sidebarpanel.ts +++ b/packages/gem-devtools/src/sidebarpanel.ts @@ -1,5 +1,5 @@ import { devtools } from 'webextension-polyfill'; -import { customElement, GemElement, html, render } from '@mantou/gem'; +import { html, render } from '@mantou/gem'; import { logger } from '@mantou/gem/helper/logger'; import { getSelectedGem } from './scripts/get-gem'; @@ -10,15 +10,9 @@ import { execution } from './common'; import './modules/panel'; -@customElement('devtools-gem-discover') -class GemDiscover extends GemElement {} - async function updateElementProperties() { try { - const result = await execution(getSelectedGem, [ - new PanelStore(), - Object.getOwnPropertySymbols(new GemDiscover()).map(String), - ]); + const result = await execution(getSelectedGem, [new PanelStore()]); if (typeof result !== 'string') { changePanelStore(result); } else { diff --git a/packages/gem-devtools/src/store.ts b/packages/gem-devtools/src/store.ts index 2fd54842..bdc6177e 100644 --- a/packages/gem-devtools/src/store.ts +++ b/packages/gem-devtools/src/store.ts @@ -19,6 +19,7 @@ export class PanelStore { Object.assign(this, options); } isGemElement = true; + gemVersion = ''; elements = new Array(); customElements = new Array(); gemElements = new Array(); diff --git a/packages/gem-examples/src/elements/nav.ts b/packages/gem-examples/src/elements/nav.ts index b36e46ce..bb6a54a5 100644 --- a/packages/gem-examples/src/elements/nav.ts +++ b/packages/gem-examples/src/elements/nav.ts @@ -2,8 +2,6 @@ import { GemElement, html, customElement } from '@mantou/gem'; import { EXAMPLES, VERSION } from './env'; -const getGitPageUrl = (name: string) => `../${name}/`; - @customElement('gem-examples-nav') export class Nav extends GemElement { mounted = () => { @@ -78,9 +76,9 @@ export class Nav extends GemElement {
${group}
    ${(examples as typeof EXAMPLES).map( - ({ name = '' }) => html` + ({ path = '', name = '' }) => html`
  1. - +
    ${name.replace('-', ' ')}
  2. diff --git a/packages/gem-examples/src/hello-world/manifest.json b/packages/gem-examples/src/hello-world/manifest.json index a311d593..bce54dc6 100644 --- a/packages/gem-examples/src/hello-world/manifest.json +++ b/packages/gem-examples/src/hello-world/manifest.json @@ -1,4 +1,5 @@ { + "path": "", "order": -1, "name": "hello world", "group": "", diff --git a/packages/gem-examples/vite.config.ts b/packages/gem-examples/vite.config.ts index f0e5ca61..4e584dd1 100644 --- a/packages/gem-examples/vite.config.ts +++ b/packages/gem-examples/vite.config.ts @@ -34,9 +34,9 @@ export default defineConfig({ 'process.env.EXAMPLES': JSON.stringify( examples.map((example) => { try { - return { name: example, ...require(`./src/${example}/manifest.json`) }; + return { name: example, ...require(`./src/${example}/manifest.json`), path: example }; } catch { - return { name: example }; + return { path: example, name: example }; } }), ), diff --git a/packages/gem/docs/en/001-guide/001-basic/003-global-state-management.md b/packages/gem/docs/en/001-guide/001-basic/003-global-state-management.md index aa7d4b38..e09cd36a 100644 --- a/packages/gem/docs/en/001-guide/001-basic/003-global-state-management.md +++ b/packages/gem/docs/en/001-guide/001-basic/003-global-state-management.md @@ -21,7 +21,7 @@ update({ a: 2 }); disconnect(); ``` -As mentioned in the previous section, use `static observedStores`/`@connectStore` to connect to `Store`, in fact, their role is only to register the `update` method of the`GemElement` instance, therefore, when the `Store` is updated, the instance of the `GemElement` connected to the `Store` will call `update` to achieve automatic update. +As mentioned in the previous section, use `@connectStore` to connect to `Store`, in fact, their role is only to register the `update` method of the`GemElement` instance, therefore, when the `Store` is updated, the instance of the `GemElement` connected to the `Store` will call `update` to achieve automatic update. ## Planning the store diff --git a/packages/gem/docs/en/003-api/001-gem-element.md b/packages/gem/docs/en/003-api/001-gem-element.md index 938237d0..be8cd8f7 100644 --- a/packages/gem/docs/en/003-api/001-gem-element.md +++ b/packages/gem/docs/en/003-api/001-gem-element.md @@ -21,17 +21,6 @@ class GemElement extends HTMLElement { | `delegatesFocus` | When the element attempts to focus, the automatic proxy to the focus part | | `slotAssignment` | Allow manual allocation of slot | -## Static properties - -| name | description | -| -------------------- | ------------------------------------------------------------- | -| `observedStores` | Observe the specified `Store`, re-rendered by `Store` changes | -| `adoptedStyleSheets` | See [`DocumentOrShadowRoot.adoptedStyleSheets`][1] | - -[1]: https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/adoptedStyleSheets - -_Use the [decorator](./007-decorator.md) to instead_ - ## Lifecycle hook | name | description | diff --git a/packages/gem/docs/zh/001-guide/001-basic/003-global-state-management.md b/packages/gem/docs/zh/001-guide/001-basic/003-global-state-management.md index 29af8092..7006997c 100644 --- a/packages/gem/docs/zh/001-guide/001-basic/003-global-state-management.md +++ b/packages/gem/docs/zh/001-guide/001-basic/003-global-state-management.md @@ -23,7 +23,7 @@ update({ a: 2 }); disconnect(); ``` -前一节有提到,使用 `static observedStores`/`@connectStore` 来连接 `Store`, +前一节有提到,使用 `@connectStore` 来连接 `Store`, 实际上,他们的作用只是注册 `GemElement` 实例的 `update` 方法, 所以,当 `Store` 更新时,连接 `Store` 的 `GemElement` 的实例会调用 `update`,从而实现自动更新。 diff --git a/packages/gem/docs/zh/003-api/001-gem-element.md b/packages/gem/docs/zh/003-api/001-gem-element.md index c4a2db2d..25ca78b0 100644 --- a/packages/gem/docs/zh/003-api/001-gem-element.md +++ b/packages/gem/docs/zh/003-api/001-gem-element.md @@ -21,17 +21,6 @@ class GemElement extends HTMLElement { | `delegatesFocus` | 当元素尝试聚焦时自动代理到可聚焦部分 | | `slotAssignment` | 允许手动分配插槽 | -## 静态属性 - -| 名称 | 描述 | -| -------------------- | ----------------------------------------------------------- | -| `observedStores` | 监听指定的 `Store`, 当被监听的 `Store` 变化时元素将重新渲染 | -| `adoptedStyleSheets` | 同 [`DocumentOrShadowRoot.adoptedStyleSheets`][1] | - -[1]: https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/adoptedStyleSheets - -_可以使用等效的[装饰器](./007-decorator.md)替代_ - ## 生命周期钩子 | 名称 | 描述 | diff --git a/packages/gem/src/elements/base/dialog.ts b/packages/gem/src/elements/base/dialog.ts index 57e20881..dded6a33 100644 --- a/packages/gem/src/elements/base/dialog.ts +++ b/packages/gem/src/elements/base/dialog.ts @@ -1,9 +1,7 @@ -import { GemElement } from '../../lib/element'; +import { GemElement, gemSymbols } from '../../lib/element'; import { attribute, state, connectStore } from '../../lib/decorators'; import { history } from '../../lib/history'; -const final = Symbol(); - /** * 在模版中声明的 dialog,使用 `open` 方法 打开; * 模拟 top layer:https://github.com/whatwg/html/issues/4633. @@ -32,10 +30,9 @@ export abstract class GemDialogBaseElement extends GemElement { } /** - * @final * 进入关闭状态 */ - closeHandle = () => { + #closeHandle = () => { this.inert = true; this.#inertStore.forEach((e) => (e.inert = false)); if (this.#nextSibling) { @@ -45,14 +42,12 @@ export abstract class GemDialogBaseElement extends GemElement { } this.dispatchEvent(new CustomEvent('close')); this.opened = false; - return final; }; /** - * @final * 进入打开状态 */ - openHandle = () => { + #openHandle = () => { this.hidden = false; this.inert = false; this.#nextSibling = this.nextSibling; @@ -62,20 +57,19 @@ export abstract class GemDialogBaseElement extends GemElement { document.body.append(this); this.dispatchEvent(new CustomEvent('open')); this.opened = true; - return final; }; /**@final */ open = () => { if (this.opened) return; - this.openHandle(); + this.#openHandle(); history.push({ title: this.label, - open: this.openHandle, - close: this.closeHandle, + open: this.#openHandle, + close: this.#closeHandle, shouldClose: this.shouldClose, }); - return final; + return gemSymbols.final; }; shouldClose() { @@ -85,7 +79,7 @@ export abstract class GemDialogBaseElement extends GemElement { /**@final */ close = () => { history.back(); - return final; + return gemSymbols.final; }; /** @@ -97,6 +91,6 @@ export abstract class GemDialogBaseElement extends GemElement { forceClose = () => { this.opened = false; setTimeout(this.close, 100); - return final; + return gemSymbols.final; }; } diff --git a/packages/gem/src/elements/base/modal-factory.ts b/packages/gem/src/elements/base/modal-factory.ts index 1840cc89..4315f98b 100644 --- a/packages/gem/src/elements/base/modal-factory.ts +++ b/packages/gem/src/elements/base/modal-factory.ts @@ -1,5 +1,5 @@ -import { GemElement } from '../../lib/element'; -import { createStore, updateStore } from '../../lib/store'; +import { GemElement, gemSymbols } from '../../lib/element'; +import { connect, createStore, updateStore } from '../../lib/store'; import { history } from '../../lib/history'; const open = Symbol('open mark'); @@ -13,8 +13,6 @@ const open = Symbol('open mark'); * 模拟 top layer:https://github.com/whatwg/html/issues/4633. */ export function createModalClass>(options: T) { - const final = Symbol(); - return class extends GemElement { static inertStore: HTMLElement[] = []; static instance: GemElement | null = null; @@ -32,11 +30,6 @@ export function createModalClass>(options: T) return this.store[open]; } - /**@final */ - static get observedStores() { - return [history.store, this.store]; - } - /** * @final * 自带 100ms 延迟,以允许在其他 Dialog 的 `shouldClose` 中调用此方法; @@ -58,7 +51,7 @@ export function createModalClass>(options: T) shouldClose: this.shouldClose.bind(this), }); }, 100); - return final; + return gemSymbols.final; } /** @@ -67,7 +60,7 @@ export function createModalClass>(options: T) */ static close() { history.back(); - return final; + return gemSymbols.final; } /** @@ -78,7 +71,7 @@ export function createModalClass>(options: T) this.inertStore.forEach((e) => (e.inert = false)); this.instance?.remove(); updateStore(this.store, { [open]: false, ...options }); - return final; + return gemSymbols.final; } /** @@ -92,6 +85,15 @@ export function createModalClass>(options: T) super(); this.internals.role = 'alertdialog'; this.internals.ariaModal = 'true'; + + this.effect( + () => connect(history.store, this.update), + () => [], + ); + this.effect( + () => connect(new.target.store, this.update), + () => [], + ); } label = ''; diff --git a/packages/gem/src/lib/decorators.ts b/packages/gem/src/lib/decorators.ts index 355ca825..da789599 100644 --- a/packages/gem/src/lib/decorators.ts +++ b/packages/gem/src/lib/decorators.ts @@ -10,23 +10,23 @@ import * as versionExports from './version'; type GemElementPrototype = GemElement; type GemElementConstructor = typeof GemElement; -type StaticField = Exclude; type StaticFieldMember = string | Store | Sheet; +const { get, set } = Reflect; const gemElementProxyMap = new PropProxyMap(); -function pushStaticField(target: GemElement | GemElementPrototype, field: StaticField, member: StaticFieldMember) { +function pushStaticField(target: GemElement | GemElementPrototype, field: symbol, member: StaticFieldMember) { const cls = target.constructor as GemElementConstructor; if (!cls.hasOwnProperty(field)) { // 继承基类 - const current = new Set(cls[field]); + const current = new Set(get(cls, field)); current.delete(member); Object.defineProperty(cls, field, { value: [...current], }); } - cls[field]!.push(member as any); + get(cls, field)!.push(member as any); } function clearField>(instance: T, prop: string) { @@ -100,7 +100,7 @@ export function refobject, V extends HTMLElement>( const prop = context.name as string; if (!target.hasOwnProperty(prop)) { const ref = `${camelToKebabCase(prop)}-${randomStr()}`; - pushStaticField(this, 'definedRefs', ref); + pushStaticField(this, gemSymbols.definedRefs, ref); defineRef(target, prop, ref); } clearField(this, prop); @@ -191,7 +191,7 @@ function decoratorAttr>(context: ClassFieldDecoratorCo context.addInitializer(function (this: T) { const target = Object.getPrototypeOf(this); if (!target.hasOwnProperty(prop)) { - pushStaticField(target, 'observedAttributes', attr); // 没有 observe 的效果 + pushStaticField(target, gemSymbols.observedAttributes, attr); // 没有 observe 的效果 defineProperty(target, prop, { attr, attrType }); // 记录观察的 attribute const attrMap = observedTargetAttributes.get(target) || new Map(); @@ -244,7 +244,7 @@ export function property>(_: undefined, context: Class context.addInitializer(function (this: T) { const target = Object.getPrototypeOf(this); if (!target.hasOwnProperty(prop)) { - pushStaticField(this, 'observedProperties', prop); + pushStaticField(this, gemSymbols.observedProperties, prop); defineProperty(target, prop); } clearField(this, prop); @@ -294,7 +294,7 @@ export function state>(_: undefined, context: ClassFie const prop = context.name as string; if (!target.hasOwnProperty(prop)) { const attr = camelToKebabCase(prop); - pushStaticField(this, 'definedCSSStates', attr); + pushStaticField(this, gemSymbols.definedCSSStates, attr); defineCSSState(target, prop, attr); } clearField(this, prop); @@ -304,7 +304,7 @@ export function state>(_: undefined, context: ClassFie function defineStaticField( context: ClassFieldDecoratorContext, target: any, - field: StaticField, + field: symbol, value: string, ) { const prop = context.name as string; @@ -335,7 +335,7 @@ function defineStaticField( */ export function slot(_: undefined, context: ClassFieldDecoratorContext) { return function (this: any, value: string) { - return defineStaticField(context, this, 'definedSlots', value); + return defineStaticField(context, this, gemSymbols.definedSlots, value); }; } @@ -352,7 +352,7 @@ export function slot(_: undefined, context: ClassFieldDecoratorContext) { return function (this: any, value: string) { - return defineStaticField(context, this, 'definedParts', value); + return defineStaticField(context, this, gemSymbols.definedParts, value); }; } @@ -386,7 +386,7 @@ function defineEmitter(t: GemElement, prop: string, eventOptions?: Omit) { return function (cls: unknown, _: ClassDecoratorContext) { const con = cls as GemElementConstructor; - pushStaticField(con.prototype, 'adoptedStyleSheets', style); + pushStaticField(con.prototype, gemSymbols.adoptedStyleSheets, style); }; } @@ -420,7 +420,7 @@ export function adoptedStyle(style: Sheet) { export function connectStore(store: Store) { return function (cls: unknown, _: ClassDecoratorContext) { const con = cls as GemElementConstructor; - pushStaticField(con.prototype, 'observedStores', store); + pushStaticField(con.prototype, gemSymbols.observedStores, store); }; } @@ -434,9 +434,8 @@ export function connectStore(store: Store) { * ``` */ export function rootElement(rootType: string) { - return function (cls: unknown, _: ClassDecoratorContext) { - const con = cls as GemElementConstructor; - con.rootElement = rootType; + return function (cls: any, _: ClassDecoratorContext) { + set(cls, gemSymbols.rootElement, rootType); }; } diff --git a/packages/gem/src/lib/element.ts b/packages/gem/src/lib/element.ts index 405c790b..ce50812c 100644 --- a/packages/gem/src/lib/element.ts +++ b/packages/gem/src/lib/element.ts @@ -15,6 +15,8 @@ import { export { html, svg, render, directive, TemplateResult, SVGTemplateResult } from 'lit-html'; +const { get, defineProperty } = Reflect; + declare global { interface ElementInternals extends ARIAMixin { // https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet @@ -63,7 +65,24 @@ type EffectItem = { }; export const gemSymbols = { - update: Symbol('update'), + // 禁止覆盖自定义元素原生生命周期方法 + // https://github.com/microsoft/TypeScript/issues/21388#issuecomment-934345226 + final: Symbol(), + update: Symbol(), + // 指定 root 元素类型 + rootElement: Symbol(), + // 实例化时使用到,DevTools 需要读取 + observedStores: Symbol.for('gem@observedStores'), + adoptedStyleSheets: Symbol.for('gem@adoptedStyleSheets'), + sheetToken: SheetToken, + // 以下静态字段仅供外部读取,没有实际作用 + observedProperties: Symbol(), + observedAttributes: Symbol(), // 必须在定义元素前指定 + definedEvents: Symbol(), + definedCSSStates: Symbol(), + definedRefs: Symbol(), + definedParts: Symbol(), + definedSlots: Symbol(), }; export interface GemElementOptions extends Partial { @@ -73,24 +92,6 @@ export interface GemElementOptions extends Partial { } export abstract class GemElement> extends HTMLElement { - // 禁止覆盖自定义元素原生生命周期方法 - // https://github.com/microsoft/TypeScript/issues/21388#issuecomment-934345226 - static #final = Symbol(); - - // 指定 root 元素类型 - static rootElement?: string; - // 实例化时使用到 - static observedStores?: Store[]; - static adoptedStyleSheets?: Sheet[]; - // 以下静态字段仅供外部读取,没有实际作用 - static observedProperties?: string[]; - static observedAttributes?: string[]; // 必须在定义元素前指定 - static definedEvents?: string[]; - static definedCSSStates?: string[]; - static definedRefs?: string[]; - static definedParts?: string[]; - static definedSlots?: string[]; - // 定义当前元素的状态,和 attr/prop 的本质区别是不为外部输入 readonly state?: T; @@ -105,12 +106,15 @@ export abstract class GemElement> extends HTMLElemen #memoList?: EffectItem[]; #unmountCallback?: any; + [gemSymbols.update]() { + if (this.#isMounted) { + addMicrotask(this.#update); + } + } + constructor(options: GemElementOptions = {}) { super(); - // expose private Methods - Reflect.set(this, gemSymbols.update, this.#asyncUpdate); - this.#isAsync = options.isAsync; this.#renderRoot = options.isLight ? this @@ -141,10 +145,10 @@ export abstract class GemElement> extends HTMLElemen }); } }, - () => [Reflect.get(this, 'disabled')], + () => [get(this, 'disabled')], ); - const { adoptedStyleSheets } = new.target; + const adoptedStyleSheets = get(new.target, gemSymbols.adoptedStyleSheets) as Sheet[] | undefined; if (adoptedStyleSheets) { const sheets = adoptedStyleSheets.map((item) => item[SheetToken] || item); if (this.shadowRoot) { @@ -174,7 +178,7 @@ export abstract class GemElement> extends HTMLElemen this.#internals.states.add('foo'); this.#internals.states.delete('foo'); } catch { - Reflect.defineProperty(this.#internals, 'states', { + defineProperty(this.#internals, 'states', { value: { has: (v: string) => kebabToCamelCase(v) in this.dataset, add: (v: string) => (this.dataset[kebabToCamelCase(v)] = ''), @@ -341,12 +345,6 @@ export abstract class GemElement> extends HTMLElemen } }; - #asyncUpdate = () => { - if (this.#isMounted) { - addMicrotask(this.#update); - } - }; - /** * @helper * async @@ -375,7 +373,8 @@ export abstract class GemElement> extends HTMLElemen return; } - const { observedStores, rootElement } = this.constructor as typeof GemElement; + const observedStores = get(this.constructor, gemSymbols.observedStores) as Store[] | undefined; + const rootElement = get(this.constructor, gemSymbols.rootElement) as string | undefined; this.#isConnected = true; this.willMount?.(); @@ -400,7 +399,7 @@ export abstract class GemElement> extends HTMLElemen } else { this.#connectedCallback(); } - return GemElement.#final; + return gemSymbols.final; } /** @@ -408,7 +407,7 @@ export abstract class GemElement> extends HTMLElemen * @final */ adoptedCallback() { - return GemElement.#final; + return gemSymbols.final; } /** @@ -427,7 +426,7 @@ export abstract class GemElement> extends HTMLElemen this.unmounted?.(); this.#effectList = this.#clearEffect(this.#effectList); this.#memoList = this.#clearEffect(this.#memoList); - return GemElement.#final; + return gemSymbols.final; } #clearEffect = (list?: EffectItem[]) => { diff --git a/packages/gem/src/lib/utils.ts b/packages/gem/src/lib/utils.ts index 8f3be230..c6672fe1 100644 --- a/packages/gem/src/lib/utils.ts +++ b/packages/gem/src/lib/utils.ts @@ -240,6 +240,7 @@ export function css(arr: TemplateStringsArray, ...args: any[]) { } // 跨多个 gem 工作 +// TODO: move to `gemSymbols` export const SheetToken = Symbol.for('gem@sheetToken'); export type Sheet = { diff --git a/packages/gem/src/test/gem-element/advance.test.ts b/packages/gem/src/test/gem-element/advance.test.ts index fd14c9be..ce85b080 100644 --- a/packages/gem/src/test/gem-element/advance.test.ts +++ b/packages/gem/src/test/gem-element/advance.test.ts @@ -8,17 +8,26 @@ */ import { fixture, expect, nextFrame } from '@open-wc/testing'; -import { GemElement, html } from '../../lib/element'; +import { GemElement, gemSymbols, html } from '../../lib/element'; import { createStore, updateStore } from '../../lib/store'; -import { attribute, property, customElement, emitter, adoptedStyle, refobject, RefObject } from '../../lib/decorators'; +import { + attribute, + property, + customElement, + emitter, + adoptedStyle, + refobject, + RefObject, + connectStore, +} from '../../lib/decorators'; import { createCSSSheet, css } from '../../lib/utils'; const store = createStore({ a: 1, }); +@connectStore(store) class AsyncGemDemo extends GemElement { - static observedStores = [store]; constructor() { super({ isAsync: true }); } @@ -297,7 +306,7 @@ describe('gem element 继承', () => { it('静态字段继承', async () => { new I(); new InheritGem(); // 触发装饰器自定义初始化函数 - expect(InheritGem.observedAttributes).to.eql(['app-title', 'app-title2']); + expect(Reflect.get(InheritGem, gemSymbols.observedAttributes)).to.eql(['app-title', 'app-title2']); }); it('attr/prop/emitter 继承', async () => { const name = window.name; diff --git a/packages/gem/src/test/gem-element/decorators.test.ts b/packages/gem/src/test/gem-element/decorators.test.ts index 989e125b..b5091395 100644 --- a/packages/gem/src/test/gem-element/decorators.test.ts +++ b/packages/gem/src/test/gem-element/decorators.test.ts @@ -1,6 +1,6 @@ import { fixture, expect } from '@open-wc/testing'; -import { GemElement, html } from '../../lib/element'; +import { GemElement, gemSymbols, html } from '../../lib/element'; import { createStore, updateStore } from '../../lib/store'; import { createCSSSheet, css } from '../../lib/utils'; import { @@ -82,14 +82,18 @@ describe('装饰器', () => { `); updateStore(store, { a: 1 }); await Promise.resolve(); - expect(DecoratorGemElement.observedStores).to.eql([{ a: 1 }]); - expect(DecoratorGemElement.observedAttributes).to.eql(['rank-attr', 'rank-disabled', 'rank-count']); - expect(DecoratorGemElement.definedEvents).to.eql(['say-hi']); - expect(DecoratorGemElement.definedCSSStates).to.eql(['open-state']); - expect(DecoratorGemElement.definedParts).to.eql(['say-hi', 'header-part']); - expect(DecoratorGemElement.definedSlots).to.eql(['rank-attr', 'body-slot']); - expect(DecoratorGemElement.definedRefs?.[0].startsWith('input-ref-')).to.equal(true); - expect(DecoratorGemElement.observedProperties).to.eql(['propData']); + expect(Reflect.get(DecoratorGemElement, gemSymbols.observedStores)).to.eql([{ a: 1 }]); + expect(Reflect.get(DecoratorGemElement, gemSymbols.observedAttributes)).to.eql([ + 'rank-attr', + 'rank-disabled', + 'rank-count', + ]); + expect(Reflect.get(DecoratorGemElement, gemSymbols.definedEvents)).to.eql(['say-hi']); + expect(Reflect.get(DecoratorGemElement, gemSymbols.definedCSSStates)).to.eql(['open-state']); + expect(Reflect.get(DecoratorGemElement, gemSymbols.definedParts)).to.eql(['say-hi', 'header-part']); + expect(Reflect.get(DecoratorGemElement, gemSymbols.definedSlots)).to.eql(['rank-attr', 'body-slot']); + expect(Reflect.get(DecoratorGemElement, gemSymbols.definedRefs)?.[0].startsWith('input-ref-')).to.equal(true); + expect(Reflect.get(DecoratorGemElement, gemSymbols.observedProperties)).to.eql(['propData']); expect(DecoratorGemElement.sayHi).to.equal('say-hi'); expect(DecoratorGemElement.rankAttr).to.equal('rank-attr'); expect(el.rankAttr).to.equal('attr');