diff --git a/README.md b/README.md index ee402a8e..4ed9f3c4 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,18 @@ Create custom elements, bind data, route switching, and quickly develop WebApps - [Guide](https://gemjs.org/guide/) - [API](https://gemjs.org/api/) +## Project Packages + +| Package | Description | +| ---------------------------------------------- | ---------------------------------------------------------------- | +| [packages/gem](packages/gem) | Gem core | +| [packages/gem-devtools](packages/gem-devtools) | Browser debugging tool for Gem | +| [packages/gem-analyzer](packages/gem-analyzer) | Gem element analyzer, which can automatically generate documents | +| [packages/gem-book](packages/gem-book) | Documentation site builder created using Gem | +| [packages/duoyun-ui](packages/duoyun-ui) | UI library created using Gem | +| [packages/gem-port](packages/gem-port) | Export Gem elements as React/Vue/Svelte components | +| [packages/gem-examples](packages/gem-examples) | Gem and DuoyunUI examples | + ## Contribution Fork repo, submit PR diff --git a/README_zh.md b/README_zh.md index aa6ed4e4..db1c8bdd 100644 --- a/README_zh.md +++ b/README_zh.md @@ -22,13 +22,25 @@ [这里](https://rawgit.com/krausest/js-framework-benchmark/master/webdriver-ts-results/table.html)是 lit-html 和 React,Vue 的性能比较; - **异步渲染:** - 连续渲染(例如创建列表)该类元素时会避免长时间阻塞主线程,提供流畅的用户体验; + 连续渲染(例如列表项)类元素时会避免长时间阻塞主线程,提供流畅的用户体验; ## 文档 - [Guide](https://gemjs.org/guide/) - [API](https://gemjs.org/api/) +## 项目结构 + +| 目录 | 描述 | +| ---------------------------------------------- | --------------------------------------- | +| [packages/gem](packages/gem) | Gem 核心 | +| [packages/gem-devtools](packages/gem-devtools) | Gem 的浏览器调试工具 | +| [packages/gem-analyzer](packages/gem-analyzer) | Gem 元素分析器,能自动生成文档 | +| [packages/gem-book](packages/gem-book) | 使用 Gem 创建的文档站生成器 | +| [packages/duoyun-ui](packages/duoyun-ui) | 使用 Gem 创建的 UI 库 | +| [packages/gem-port](packages/gem-port) | 将 Gem 元素导出为 React/Vue/Svelte 组件 | +| [packages/gem-examples](packages/gem-examples) | 一些 Gem 和 DuoyunUI 示例 | + ## 贡献 Fork 项目,提交 PR diff --git a/packages/duoyun-ui/docs/en/01-guide/README.md b/packages/duoyun-ui/docs/en/01-guide/README.md index bc253ab6..b3625740 100644 --- a/packages/duoyun-ui/docs/en/01-guide/README.md +++ b/packages/duoyun-ui/docs/en/01-guide/README.md @@ -161,10 +161,3 @@ import('https://esm.sh/duoyun-ui/elements/input-capture').then(({ DuoyunInputCap document.body.append(new DuoyunInputCaptureElement()), ); ``` - -## Roadmap - -- Logo design -- Improve existing elements, standardize API -- Add missing elements and features -- Add more language support diff --git a/packages/duoyun-ui/docs/en/02-elements/chart-tooltip.md b/packages/duoyun-ui/docs/en/02-elements/chart-tooltip.md index debba9dd..da46e6ba 100644 --- a/packages/duoyun-ui/docs/en/02-elements/chart-tooltip.md +++ b/packages/duoyun-ui/docs/en/02-elements/chart-tooltip.md @@ -4,6 +4,25 @@ See [``](./area-chart.md) +```ts +import { ChartTooltip } from '@duoyun-fe/duoyun-ui/elements/chart-tooltip'; + +function onPointerMove({ x, y }) { + ChartTooltip.open(x, y, { + values: [ + { + label: 'label', + value: '123', + }, + ], + }); +} + +function onPointerOut() { + ChartTooltip.close(); +} +``` + ## API diff --git a/packages/duoyun-ui/docs/en/02-elements/chart-zoom.md b/packages/duoyun-ui/docs/en/02-elements/chart-zoom.md index b63ec495..09f78830 100644 --- a/packages/duoyun-ui/docs/en/02-elements/chart-zoom.md +++ b/packages/duoyun-ui/docs/en/02-elements/chart-zoom.md @@ -4,6 +4,30 @@ See [``](./area-chart.md) + + +```json +{ + "style": "width: 100%;", + "aspectRatio": 5, + "values": [ + [1, 8], + [2, 2], + [3, 6], + [4, 7], + [5, 5], + [6, 3], + [7, 4], + [8, 1], + [9, 9] + ], + "@change": "({ target, detail }) => target.value = detail", + "value": [0, 1] +} +``` + + + ## API diff --git a/packages/duoyun-ui/docs/en/02-elements/coach-mark.md b/packages/duoyun-ui/docs/en/02-elements/coach-mark.md index 3eaacde4..3f3234a4 100644 --- a/packages/duoyun-ui/docs/en/02-elements/coach-mark.md +++ b/packages/duoyun-ui/docs/en/02-elements/coach-mark.md @@ -8,12 +8,10 @@ import { render, html } from '@mantou/gem'; import { setTours } from 'duoyun-ui/elements/coach-mark'; +import 'duoyun-ui/elements/side-navigation'; + setTours( [ - { - title: 'starterTitle', - description: 'starterDesc', - }, { preview: 'https://picsum.photos/400/300', title: 'starterAnalyticsTitle', @@ -26,34 +24,28 @@ setTours( }, ], { - currentIndex: 1, + currentIndex: 0, }, ); +const items = [ + { + pattern: '/', + title: 'Nav 1', + slot: html``, + }, + { + pattern: '/test', + title: 'Nav 2', + slot: html``, + }, + { + title: 'Nav 3', + }, +]; + render( - html` - -
- - Tour1 -
-
- - Tour2 -
-
- - Tour3 -
- `, + html``, document.getElementById('root'), ); ``` diff --git a/packages/duoyun-ui/docs/en/02-elements/compartment.md b/packages/duoyun-ui/docs/en/02-elements/compartment.md index 5f6951c6..51e4c64c 100644 --- a/packages/duoyun-ui/docs/en/02-elements/compartment.md +++ b/packages/duoyun-ui/docs/en/02-elements/compartment.md @@ -3,5 +3,15 @@ Used to isolate elemental styles to avoid the contents of the user affect internal elements. ```ts -html``; +html` + + * { + color: red; + } + + `} + > +`; ``` diff --git a/packages/duoyun-ui/docs/en/02-elements/contextmenu.md b/packages/duoyun-ui/docs/en/02-elements/contextmenu.md index 4b745298..c9548f9e 100644 --- a/packages/duoyun-ui/docs/en/02-elements/contextmenu.md +++ b/packages/duoyun-ui/docs/en/02-elements/contextmenu.md @@ -2,14 +2,20 @@ ## Example - + + +```json +{ + "@click": "(e)=>{customElements.get('dy-contextmenu').open([{text:'Add',},{text:'Edit',},{text:'---',},{text:'Delete',danger:true,},],{activeElement:e.target});}", + "innerHTML": "Open ContextMenu" +} +``` + + ```ts -import { render, html } from '@mantou/gem'; import { ContextMenu } from 'duoyun-ui/elements/contextmenu'; -import 'duoyun-ui/elements/button'; - const onClick = (e: MouseEvent) => { ContextMenu.open( [ @@ -30,12 +36,8 @@ const onClick = (e: MouseEvent) => { { activeElement: e.target }, ); }; - -render(html`Open ContextMenu`, document.getElementById('root')); ``` - - ## API diff --git a/packages/duoyun-ui/docs/en/02-elements/link.md b/packages/duoyun-ui/docs/en/02-elements/link.md index 908eb6f7..508396f7 100644 --- a/packages/duoyun-ui/docs/en/02-elements/link.md +++ b/packages/duoyun-ui/docs/en/02-elements/link.md @@ -1,3 +1,24 @@ # `` See [``](https://gemjs.org/zh/api/built-in-element) + +## Example + + + +```json +[ + { + "path": "/test/test", + "pattern": "/test/*", + "innerHTML": "This link not match current route\n" + }, + { + "path": "/", + "pattern": "/*", + "innerHTML": "This link match current route" + } +] +``` + + diff --git a/packages/duoyun-ui/docs/en/02-elements/modal.md b/packages/duoyun-ui/docs/en/02-elements/modal.md index 1b5accbd..7740084d 100644 --- a/packages/duoyun-ui/docs/en/02-elements/modal.md +++ b/packages/duoyun-ui/docs/en/02-elements/modal.md @@ -6,7 +6,7 @@ name="dy-modal" props='{"header": "Title", "open": true, "@ok": "(evt) => evt.target.open = false", "@close": "(evt) => evt.target.open = false", "@maskclick": "(evt) => evt.target.open = false"}' html='
Modal
' - src="https://esm.sh/duoyun-ui/elements/modal">Current page auto open modal + src="https://esm.sh/duoyun-ui/elements/modal">Current page auto open modal,refresh ## API diff --git a/packages/duoyun-ui/docs/en/02-elements/page-loadbar.md b/packages/duoyun-ui/docs/en/02-elements/page-loadbar.md index 09ca6091..d7557ffb 100644 --- a/packages/duoyun-ui/docs/en/02-elements/page-loadbar.md +++ b/packages/duoyun-ui/docs/en/02-elements/page-loadbar.md @@ -2,11 +2,24 @@ ## Example + + +```json +{ + "@click": "()=>{const Loadbar=customElements.get('dy-page-loadbar');Loadbar.start();setTimeout(()=>Loadbar.end(),3000);}", + "innerHTML": "Show page loader" +} +``` + + + ```ts import { Loadbar } from '@duoyun-fe/duoyun-ui/elements/page-loadbar'; -Loadbar.start(); -setTimeout(() => Loadbar.end(), 3000); +function onClick() { + Loadbar.start(); + setTimeout(() => Loadbar.end(), 3000); +} ``` ## API diff --git a/packages/duoyun-ui/docs/en/02-elements/toast.md b/packages/duoyun-ui/docs/en/02-elements/toast.md index ae8ac371..62543bbf 100644 --- a/packages/duoyun-ui/docs/en/02-elements/toast.md +++ b/packages/duoyun-ui/docs/en/02-elements/toast.md @@ -2,43 +2,38 @@ ## Example - + ```json -{ - "style": "width: 100%; position: relative; top: 0; z-index: auto;", - "items": [ - { - "type": "success", - "content": "This is success" - }, - { - "type": "warning", - "content": "This is warning" - }, - { - "type": "error", - "content": "This is error" - } - ] -} +[ + { + "innerHTML": "Success", + "color": "positive", + "@click": "()=>customElements.get('dy-toast').open('success', 'This is success')" + }, + { + "innerHTML": "Warning", + "color": "notice", + "@click": "()=>customElements.get('dy-toast').open('warning', 'This is warning')" + }, + { + "innerHTML": "Error", + "color": "negative", + "@click": "()=>customElements.get('dy-toast').open('error', 'This is error')" + } +] ``` - - ```ts -import { render, html } from '@mantou/gem'; import { Toast } from 'duoyun-ui/elements/toast'; -const success = () => Toast.open('success', new Date().toLocaleString()); - -render(html``, document.getElementById('root')); +function onClick() { + Toast.open('success', 'This is success'); +} ``` - - ## API diff --git a/packages/duoyun-ui/docs/en/02-elements/wait.md b/packages/duoyun-ui/docs/en/02-elements/wait.md index 49b9f498..4f843c45 100644 --- a/packages/duoyun-ui/docs/en/02-elements/wait.md +++ b/packages/duoyun-ui/docs/en/02-elements/wait.md @@ -2,10 +2,23 @@ ## Example + + +```json +{ + "@click": "()=>customElements.get('dy-wait').wait(new Promise(res => setTimeout(res, 1500)))", + "innerHTML": "Click" +} +``` + + + ```ts import { waitLoading } from '@duoyun-fe/duoyun-ui/elements/wait'; -waitLoading(fetch('/')); +function onClick() { + waitLoading(new Promise((res) => setTimeout(res, 1500))); +} ``` ## API diff --git a/packages/duoyun-ui/docs/zh/01-guide/README.md b/packages/duoyun-ui/docs/zh/01-guide/README.md index 5feb5bf4..80b891e8 100644 --- a/packages/duoyun-ui/docs/zh/01-guide/README.md +++ b/packages/duoyun-ui/docs/zh/01-guide/README.md @@ -160,10 +160,3 @@ import('https://esm.sh/duoyun-ui/elements/input-capture').then(({ DuoyunInputCap document.body.append(new DuoyunInputCaptureElement()), ); ``` - -## 路线图 - -- Logo 设计 -- 完善现有元素,规范化 API -- 添加缺失元素和功能 -- 添加更多的语言支持 diff --git a/packages/duoyun-ui/docs/zh/02-elements/chart-tooltip.md b/packages/duoyun-ui/docs/zh/02-elements/chart-tooltip.md index debba9dd..da46e6ba 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/chart-tooltip.md +++ b/packages/duoyun-ui/docs/zh/02-elements/chart-tooltip.md @@ -4,6 +4,25 @@ See [``](./area-chart.md) +```ts +import { ChartTooltip } from '@duoyun-fe/duoyun-ui/elements/chart-tooltip'; + +function onPointerMove({ x, y }) { + ChartTooltip.open(x, y, { + values: [ + { + label: 'label', + value: '123', + }, + ], + }); +} + +function onPointerOut() { + ChartTooltip.close(); +} +``` + ## API diff --git a/packages/duoyun-ui/docs/zh/02-elements/chart-zoom.md b/packages/duoyun-ui/docs/zh/02-elements/chart-zoom.md index b63ec495..09f78830 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/chart-zoom.md +++ b/packages/duoyun-ui/docs/zh/02-elements/chart-zoom.md @@ -4,6 +4,30 @@ See [``](./area-chart.md) + + +```json +{ + "style": "width: 100%;", + "aspectRatio": 5, + "values": [ + [1, 8], + [2, 2], + [3, 6], + [4, 7], + [5, 5], + [6, 3], + [7, 4], + [8, 1], + [9, 9] + ], + "@change": "({ target, detail }) => target.value = detail", + "value": [0, 1] +} +``` + + + ## API diff --git a/packages/duoyun-ui/docs/zh/02-elements/coach-mark.md b/packages/duoyun-ui/docs/zh/02-elements/coach-mark.md index 3eaacde4..3f3234a4 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/coach-mark.md +++ b/packages/duoyun-ui/docs/zh/02-elements/coach-mark.md @@ -8,12 +8,10 @@ import { render, html } from '@mantou/gem'; import { setTours } from 'duoyun-ui/elements/coach-mark'; +import 'duoyun-ui/elements/side-navigation'; + setTours( [ - { - title: 'starterTitle', - description: 'starterDesc', - }, { preview: 'https://picsum.photos/400/300', title: 'starterAnalyticsTitle', @@ -26,34 +24,28 @@ setTours( }, ], { - currentIndex: 1, + currentIndex: 0, }, ); +const items = [ + { + pattern: '/', + title: 'Nav 1', + slot: html``, + }, + { + pattern: '/test', + title: 'Nav 2', + slot: html``, + }, + { + title: 'Nav 3', + }, +]; + render( - html` - -
- - Tour1 -
-
- - Tour2 -
-
- - Tour3 -
- `, + html``, document.getElementById('root'), ); ``` diff --git a/packages/duoyun-ui/docs/zh/02-elements/compartment.md b/packages/duoyun-ui/docs/zh/02-elements/compartment.md index 1bc5d744..33d0ea56 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/compartment.md +++ b/packages/duoyun-ui/docs/zh/02-elements/compartment.md @@ -3,5 +3,15 @@ 用于隔离元素样式,避免用户提交的内容影响内部元素。 ```ts -html``; +html` + + * { + color: red; + } + + `} + > +`; ``` diff --git a/packages/duoyun-ui/docs/zh/02-elements/contextmenu.md b/packages/duoyun-ui/docs/zh/02-elements/contextmenu.md index 93696e1a..ae87bf73 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/contextmenu.md +++ b/packages/duoyun-ui/docs/zh/02-elements/contextmenu.md @@ -2,14 +2,20 @@ ## Example - + + +```json +{ + "@click": "(e)=>{customElements.get('dy-contextmenu').open([{text:'新增',},{text:'编辑',},{text:'---',},{text:'删除',danger:true,},],{activeElement:e.target});}", + "innerHTML": "打开上下文菜单" +} +``` + + ```ts -import { render, html } from '@mantou/gem'; import { ContextMenu } from 'duoyun-ui/elements/contextmenu'; -import 'duoyun-ui/elements/button'; - const onClick = (e: MouseEvent) => { ContextMenu.open( [ @@ -30,12 +36,8 @@ const onClick = (e: MouseEvent) => { { activeElement: e.target }, ); }; - -render(html`打开上下文菜单`, document.getElementById('root')); ``` - - ## API diff --git a/packages/duoyun-ui/docs/zh/02-elements/link.md b/packages/duoyun-ui/docs/zh/02-elements/link.md index 908eb6f7..61bff8db 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/link.md +++ b/packages/duoyun-ui/docs/zh/02-elements/link.md @@ -1,3 +1,24 @@ # `` See [``](https://gemjs.org/zh/api/built-in-element) + +## Example + + + +```json +[ + { + "path": "/test/test", + "pattern": "/test/*", + "innerHTML": "这个链接没有匹配当前路由\n" + }, + { + "path": "/", + "pattern": "/*", + "innerHTML": "这个链接匹配当前路由" + } +] +``` + + diff --git a/packages/duoyun-ui/docs/zh/02-elements/modal.md b/packages/duoyun-ui/docs/zh/02-elements/modal.md index 1b5accbd..7740084d 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/modal.md +++ b/packages/duoyun-ui/docs/zh/02-elements/modal.md @@ -6,7 +6,7 @@ name="dy-modal" props='{"header": "Title", "open": true, "@ok": "(evt) => evt.target.open = false", "@close": "(evt) => evt.target.open = false", "@maskclick": "(evt) => evt.target.open = false"}' html='
Modal
' - src="https://esm.sh/duoyun-ui/elements/modal">Current page auto open modal
+ src="https://esm.sh/duoyun-ui/elements/modal">Current page auto open modal,refresh ## API diff --git a/packages/duoyun-ui/docs/zh/02-elements/page-loadbar.md b/packages/duoyun-ui/docs/zh/02-elements/page-loadbar.md index 09ca6091..f726dd9e 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/page-loadbar.md +++ b/packages/duoyun-ui/docs/zh/02-elements/page-loadbar.md @@ -2,11 +2,24 @@ ## Example + + +```json +{ + "@click": "()=>{const Loadbar=customElements.get('dy-page-loadbar');Loadbar.start();setTimeout(()=>Loadbar.end(),3000);}", + "innerHTML": "显示页面加载器" +} +``` + + + ```ts import { Loadbar } from '@duoyun-fe/duoyun-ui/elements/page-loadbar'; -Loadbar.start(); -setTimeout(() => Loadbar.end(), 3000); +function onClick() { + Loadbar.start(); + setTimeout(() => Loadbar.end(), 3000); +} ``` ## API diff --git a/packages/duoyun-ui/docs/zh/02-elements/toast.md b/packages/duoyun-ui/docs/zh/02-elements/toast.md index a04b8b93..5b0e9b48 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/toast.md +++ b/packages/duoyun-ui/docs/zh/02-elements/toast.md @@ -2,43 +2,38 @@ ## Example - + ```json -{ - "style": "width: 100%; position: relative; top: 0; z-index: auto;", - "items": [ - { - "type": "success", - "content": "This is success" - }, - { - "type": "warning", - "content": "This is warning" - }, - { - "type": "error", - "content": "This is error" - } - ] -} +[ + { + "innerHTML": "Success", + "color": "positive", + "@click": "()=>customElements.get('dy-toast').open('success', '这是一条消息')" + }, + { + "innerHTML": "Warning", + "color": "notice", + "@click": "()=>customElements.get('dy-toast').open('warning', '这是一条消息')" + }, + { + "innerHTML": "Error", + "color": "negative", + "@click": "()=>customElements.get('dy-toast').open('error', '这是一条消息')" + } +] ``` - - ```ts -import { render, html } from '@mantou/gem'; import { Toast } from 'duoyun-ui/elements/toast'; -const success = () => Toast.open('success', new Date().toLocaleString()); - -render(html``, document.getElementById('root')); +function onClick() { + Toast.open('success', '这是一条消息'); +} ``` - - ## API diff --git a/packages/duoyun-ui/docs/zh/02-elements/wait.md b/packages/duoyun-ui/docs/zh/02-elements/wait.md index 49b9f498..6a2eb176 100644 --- a/packages/duoyun-ui/docs/zh/02-elements/wait.md +++ b/packages/duoyun-ui/docs/zh/02-elements/wait.md @@ -2,10 +2,23 @@ ## Example + + +```json +{ + "@click": "()=>customElements.get('dy-wait').wait(new Promise(res => setTimeout(res, 1500)))", + "innerHTML": "点击" +} +``` + + + ```ts import { waitLoading } from '@duoyun-fe/duoyun-ui/elements/wait'; -waitLoading(fetch('/')); +function onClick() { + waitLoading(new Promise((res) => setTimeout(res, 1500))); +} ``` ## API diff --git a/packages/duoyun-ui/src/elements/area-chart.ts b/packages/duoyun-ui/src/elements/area-chart.ts index 2698192a..e8142c8e 100644 --- a/packages/duoyun-ui/src/elements/area-chart.ts +++ b/packages/duoyun-ui/src/elements/area-chart.ts @@ -293,7 +293,7 @@ export class DuoyunAreaChartElement extends DuoyunChartBaseElement { this.#paths = this.#genPath(this.#sequences); this.#areas = this.#genPath(this.#sequences, true); }, - () => [this.#sequences, this.#smooth, this.contentRect.width], + () => [this.#sequences, this.#smooth, this.contentRect.width, this.aspectRatio], ); }; diff --git a/packages/duoyun-ui/src/elements/bar-chart.ts b/packages/duoyun-ui/src/elements/bar-chart.ts index 65ccd7ca..7d0bce74 100644 --- a/packages/duoyun-ui/src/elements/bar-chart.ts +++ b/packages/duoyun-ui/src/elements/bar-chart.ts @@ -99,7 +99,7 @@ export class DuoyunBarChartElement extends DuoyunChartBaseElement { this.initYAxi(yMin, yMax); this.initViewBox(); }, - () => [this.sequences, this.contentRect.width], + () => [this.sequences, this.contentRect.width, this.aspectRatio], ); }; diff --git a/packages/duoyun-ui/src/elements/base/chart.ts b/packages/duoyun-ui/src/elements/base/chart.ts index 579e2f70..245e819e 100644 --- a/packages/duoyun-ui/src/elements/base/chart.ts +++ b/packages/duoyun-ui/src/elements/base/chart.ts @@ -56,6 +56,7 @@ const style = createCSSSheet(css` export class DuoyunChartBaseElement<_T = Record> extends DuoyunResizeBaseElement { @part static chart: string; + @property aspectRatio?: number; @property filters?: string[]; @property colors = commonColors; @property xAxi?: Axi | null; @@ -74,15 +75,18 @@ export class DuoyunChartBaseElement<_T = Record> extends Duoyun @state loading: boolean; @state noData: boolean; + get #aspectRatio() { + return this.aspectRatio || 2; + } + constructor(options?: GemElementOptions) { super(options); this.internals.role = 'img'; this.memo( - () => { - this.filtersSet = new Set(this.filters); - }, + () => (this.filtersSet = new Set(this.filters)), () => [this.filters], ); + this.memo(() => (this.stageHeight = this.stageWidth / this.#aspectRatio)); } stageWidth = 300; diff --git a/packages/duoyun-ui/src/elements/breadcrumbs.ts b/packages/duoyun-ui/src/elements/breadcrumbs.ts index fdabc6b5..25cf1a61 100644 --- a/packages/duoyun-ui/src/elements/breadcrumbs.ts +++ b/packages/duoyun-ui/src/elements/breadcrumbs.ts @@ -1,5 +1,5 @@ // https://spectrum.adobe.com/page/breadcrumbs/ -import { adoptedStyle, customElement, property, boolattribute } from '@mantou/gem/lib/decorators'; +import { adoptedStyle, customElement, property, boolattribute, part } from '@mantou/gem/lib/decorators'; import { GemElement, html } from '@mantou/gem/lib/element'; import { createCSSSheet, css, classMap } from '@mantou/gem/lib/utils'; @@ -56,6 +56,8 @@ export type BreadcrumbsItem = { @customElement('dy-breadcrumbs') @adoptedStyle(style) export class DuoyunBreadcrumbsElement extends GemElement { + @part static item: string; + @boolattribute compact: boolean; /**@deprecated */ @@ -81,6 +83,7 @@ export class DuoyunBreadcrumbsElement extends GemElement { : html` { @property values?: (number | null)[][]; - @property value = [0, 1]; + @property value?: number[]; + @property aspectRatio?: number; @emitter change: Emitter; + #defaultValue = [0, 1]; + get #value() { + return this.value || this.#defaultValue; + } + + get #aspectRatio() { + return this.aspectRatio || 25; + } + state: State = { grabbing: false, }; @@ -113,12 +123,12 @@ export class DuoyunChartZoomElement extends GemElement { }; #adjust = (detail: PanEventDetail, isStop: boolean) => { - let [start, stop] = this.value; + let [start, stop] = this.#value; if (isStop) [stop, start] = [start, stop]; const { left, width } = this.getBoundingClientRect(); const newStart = clamp(0, (detail.clientX - left) / width, 1); const newValue = [Math.min(newStart, stop), Math.max(newStart, stop)]; - if (newValue[1] - newValue[0] < 0.01) return this.#panAdjust(this.value, detail); + if (newValue[1] - newValue[0] < 0.01) return this.#panAdjust(this.#value, detail); return newValue; }; @@ -153,7 +163,7 @@ export class DuoyunChartZoomElement extends GemElement { }; #onPan = ({ detail }: CustomEvent) => { - const newValue = this.#panAdjust(this.value, detail); + const newValue = this.#panAdjust(this.#value, detail); this.change(newValue); }; @@ -163,11 +173,11 @@ export class DuoyunChartZoomElement extends GemElement { render = () => { const { grabbing, newValue } = this.state; - const [start, stop] = this.value; + const [start, stop] = this.#value; return html` { const { active } = this.state; if (active) return; - const eles = document.deepQuerySelectorAll( + const elements = document.deepQuerySelectorAll( '>>> :is([tabindex],input,textarea,button,select,area,a[href])', ) as HTMLElement[]; - if (!eles.length) { + if (!elements.length) { Toast.open('default', 'Not found focusable element'); return; } @@ -123,7 +123,7 @@ export class DuoyunKeyboardAccessElement extends GemElement { active: true, waiting: false, keydownHandles, - focusableElements: eles + focusableElements: elements .map((element) => { const { top, left, right, bottom, width, height } = element.getBoundingClientRect(); if ( @@ -142,9 +142,9 @@ export class DuoyunKeyboardAccessElement extends GemElement { // https://bugzilla.mozilla.org/show_bug.cgi?id=1750907 // https://bugs.chromium.org/p/chromium/issues/detail?id=1188919&q=elementFromPoint&can=2 const root = element.getRootNode() as ShadowRoot | (Document & { host: undefined }); - const elesFromLeftTop = root.elementsFromPoint(left + 2, top + 2); - const elesFromRightBottom = root.elementsFromPoint(left + width - 2, top + height - 2); - if (!elesFromLeftTop.includes(element) && !elesFromRightBottom.includes(element)) { + const elementsFromLeftTop = root.elementsFromPoint(left + 2, top + 2); + const elementsFromRightBottom = root.elementsFromPoint(left + width - 2, top + height - 2); + if (!elementsFromLeftTop.includes(element) && !elementsFromRightBottom.includes(element)) { return; } @@ -153,8 +153,12 @@ export class DuoyunKeyboardAccessElement extends GemElement { // `a-b` keydownHandles[[...key].join('-')] = (evt: KeyboardEvent) => { this.setState({ active: false }); - element.focus(); - element.click(); + if ('showPicker' in element) { + (element as HTMLInputElement).showPicker(); + } else { + element.focus(); + element.click(); + } this.#preventEvent(evt); }; index++; diff --git a/packages/duoyun-ui/src/elements/scatter-chart.ts b/packages/duoyun-ui/src/elements/scatter-chart.ts index b9b2feec..078bd8ad 100644 --- a/packages/duoyun-ui/src/elements/scatter-chart.ts +++ b/packages/duoyun-ui/src/elements/scatter-chart.ts @@ -73,7 +73,7 @@ export class DuoyunScatterChartElement extends DuoyunChartBaseElement { }); }); }, - () => [this.sequences, this.contentRect.width], + () => [this.sequences, this.contentRect.width, this.aspectRatio], ); }; diff --git a/packages/gem-book/src/element/elements/homepage.ts b/packages/gem-book/src/element/elements/homepage.ts index dda2a73a..07ef74c0 100644 --- a/packages/gem-book/src/element/elements/homepage.ts +++ b/packages/gem-book/src/element/elements/homepage.ts @@ -42,11 +42,13 @@ export class Homepage extends GemElement { display: flex; flex-wrap: wrap; margin: 2rem; - gap: 1rem; + gap: 1rem 2.5rem; justify-content: center; align-items: center; } gem-link { + display: flex; + gap: 1rem; color: ${theme.primaryColor}; text-decoration: none; transition: all 0.3s; @@ -75,9 +77,6 @@ export class Homepage extends GemElement { .title { font-size: 2rem; } - gem-link:first-of-type { - padding: 0.3rem 1rem; - } } @media print { .hero { diff --git a/packages/gem-book/src/element/elements/nav-logo.ts b/packages/gem-book/src/element/elements/nav-logo.ts index 3d0292b9..fa82537f 100644 --- a/packages/gem-book/src/element/elements/nav-logo.ts +++ b/packages/gem-book/src/element/elements/nav-logo.ts @@ -1,49 +1,56 @@ -import { GemElement, html, adoptedStyle, customElement, createCSSSheet, css, connectStore } from '@mantou/gem'; +import { GemElement, html, customElement, connectStore } from '@mantou/gem'; import { mediaQuery } from '@mantou/gem/helper/mediaquery'; import { bookStore } from '../store'; import { theme } from '../helper/theme'; -const style = createCSSSheet(css` - :host { - height: ${theme.headerHeight}; - font-size: 1.2rem; - font-weight: 700; - display: flex; - align-items: center; - box-sizing: border-box; - flex-shrink: 0; - } - gem-link { - display: flex; - align-items: center; - height: 100%; - text-decoration: none; - color: inherit; - } - img { - height: calc(0.8 * ${theme.headerHeight}); - min-width: calc(0.8 * ${theme.headerHeight}); - object-fit: contain; - transform: translateX(-10%); - } - @media ${mediaQuery.PHONE} { - span { - display: none; - } - } -`); /** * @customElement gem-book-nav-logo */ @customElement('gem-book-nav-logo') @connectStore(bookStore) -@adoptedStyle(style) export class GemBookNavLogoElement extends GemElement { render = () => { const { config } = bookStore; const { icon = '', title = '' } = config || {}; + if (!icon && !title) + return html` + + `; return html` + ${icon ? html`` : null} ${title} diff --git a/packages/gem-book/src/element/elements/toc.ts b/packages/gem-book/src/element/elements/toc.ts index caabb19f..94da3cae 100644 --- a/packages/gem-book/src/element/elements/toc.ts +++ b/packages/gem-book/src/element/elements/toc.ts @@ -1,14 +1,4 @@ -import { - GemElement, - html, - adoptedStyle, - customElement, - createCSSSheet, - css, - connectStore, - classMap, - useStore, -} from '@mantou/gem'; +import { GemElement, html, customElement, connectStore, classMap, useStore } from '@mantou/gem'; import { theme, themeStore } from '../helper/theme'; @@ -18,42 +8,6 @@ export const [tocStore, updateTocStore] = useStore<{ elements: HTMLHeadingElemen elements: [], }); -const style = createCSSSheet(css` - :host { - font-size: 0.875rem; - padding: 2rem 1.5rem; - box-sizing: border-box; - height: min-content; - position: sticky; - top: ${theme.headerHeight}; - } - h2 { - font-weight: bold; - font-size: 0.875em; - opacity: 0.6; - margin: 0 0 1em; - } - ul, - li { - display: contents; - } - gem-link { - display: block; - text-decoration: none; - color: inherit; - opacity: 0.8; - line-height: 1.5; - margin-block: 0.5em; - } - gem-link:hover { - opacity: 0.6; - } - gem-link.current { - opacity: 1; - color: ${theme.primaryColor}; - } -`); - type State = { current?: Element; }; @@ -62,7 +16,6 @@ type State = { * @customElement gem-book-toc */ @customElement('gem-book-toc') -@adoptedStyle(style) @connectStore(tocStore) export class GemBookTocElement extends GemElement { state: State = {}; @@ -112,6 +65,41 @@ export class GemBookTocElement extends GemElement { render = () => { if (!tocStore.elements.length) return html``; return html` +

CONTENTS

    ${tocStore.elements.map( diff --git a/packages/gem-book/src/element/index.ts b/packages/gem-book/src/element/index.ts index 6d32c69a..380c564f 100644 --- a/packages/gem-book/src/element/index.ts +++ b/packages/gem-book/src/element/index.ts @@ -15,7 +15,7 @@ import { RefObject, boolattribute, } from '@mantou/gem'; -import { GemLightRouteElement } from '@mantou/gem/elements/route'; +import { GemLightRouteElement, matchPath } from '@mantou/gem/elements/route'; import { mediaQuery } from '@mantou/gem/helper/mediaquery'; import { BookConfig } from '../common/config'; @@ -101,11 +101,15 @@ export class GemBookElement extends GemElement { if (!config) return null; const { icon = '', title = '', homeMode } = config; + const { path } = history.getParams(); const hasNavbar = icon || title || nav.length; - this.isHomePage = homePage === history.getParams().path; - const renderHomePage = homeMode && this.isHomePage; + const renderHomePage = homeMode && homePage === path; const missSidebar = homeMode ? currentSidebar?.every((e) => e.link === homePage) : !currentSidebar?.length; - const renderFullWidth = renderHomePage || missSidebar; + // 首次渲染 + const isRedirectRoute = routes.find(({ pattern }) => matchPath(pattern, path))?.redirect; + const renderFullWidth = renderHomePage || (missSidebar && !isRedirectRoute); + + this.isHomePage = !!renderHomePage; return html` diff --git a/packages/gem-devtools/src/scripts/get-gem.ts b/packages/gem-devtools/src/scripts/get-gem.ts index 504d3152..7262656b 100644 --- a/packages/gem-devtools/src/scripts/get-gem.ts +++ b/packages/gem-devtools/src/scripts/get-gem.ts @@ -55,9 +55,9 @@ export const getSelectedGem = function (data: PanelStore, gemElementSymbols: str case 'function': return funcToString(arg); case 'object': { - if (arg instanceof HTMLElement) { + if (arg instanceof Element) { try { - return (arg.cloneNode() as HTMLElement).outerHTML.replace('><', '>...<'); + return (arg.cloneNode() as Element).outerHTML.replace('><', '>...<'); } catch { // element prototype } diff --git a/packages/gem/docs/en/001-guide/001-basic/001-reactive-element.md b/packages/gem/docs/en/001-guide/001-basic/001-reactive-element.md index 4ed60548..49b4383e 100644 --- a/packages/gem/docs/en/001-guide/001-basic/001-reactive-element.md +++ b/packages/gem/docs/en/001-guide/001-basic/001-reactive-element.md @@ -9,39 +9,44 @@ Define reactive attributes, using standard static property [observedAttributes]( ```js // Omit import... +@customElement('my-element') class MyElement extends GemElement { - static observedAttributes = ['first-name']; + @attribute firstName; render() { return html`${this.firstName}`; } } -customElements.define('my-element', MyElement); ``` -After the `first-name` attribute is "Observe", he can directly access it through property, and it will automatically convert the kebab-case and camelCase format, when the `first-name` property is changed, the instance element of `MyElement` will be re-rendered. +In the above example, the field `firstName` of `MyElement` is declared as a reactive property. +When this property change, the mounted instance of `MyElement` will re-render, +additionally, this field maps to the element's `first-name` Attribute. -Similar to `observedAttributes`, GemElement also supports `observedProperties`/`observedStores` to reflect the specified property/store: +Similar to `@attribute`, GemElement also supports `@property`/`@connectStore` to reflect the specified Property/Store: ```js // Omit import... +@customElement('my-element') +@connectStore(store) class MyElement extends GemElement { - static observedProperties = ['data']; - static observedStores = [store]; + @property data; + render() { return html`${this.data.id} ${store.name}`; } } -customElements.define('my-element', MyElement); ``` -_Do not modify prop/attr within the element, they should be passed in one-way by the parent element, just like native elements_ +> [!TIP] +> Do not modify prop/attr within the element, they should be passed in one-way by the parent element, just like native elements In addition, `GemElement` provides React-like `state`/`setState` API to manage the state of the element itself. an element re-rendered is triggered whenever `setState` is called: ```js // Omit import... +@customElement('my-element') class MyElement extends GemElement { state = { id: 1 }; clicked() { @@ -51,29 +56,29 @@ class MyElement extends GemElement { return html`${this.state.id}`; } } -customElements.define('my-element', MyElement); ``` -_`GemElement` extends from [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement), don’t override the attribute/property/method/event, use [private fields](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields) to avoid overwriting the property/methods of `GemElement`/`HTMLElement`_ +> [!TIP] `GemElement` extends from [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement), don’t override the attribute/property/method/event, use [private fields](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields) to avoid overwriting the property/methods of `GemElement`/`HTMLElement` ## Example ```js index.js -import { createStore, GemElement, updateStore, render, html } from '@mantou/gem'; +import { useStore, GemElement, render, html, attribute, property, connectStore, customElement } from '@mantou/gem'; -const store = createStore({ +const [store, update] = useStore({ count: 0, }); +@customElement('my-element') +@connectStore(store) class MyElement extends GemElement { - static observedStores = [store]; - static observedAttributes = ['name']; - static observedProperties = ['data']; + @attribute name; + @property data; #onClick = () => { - updateStore(store, { count: ++store.count }); + update({ count: ++store.count }); }; render() { @@ -84,7 +89,6 @@ class MyElement extends GemElement { `; } } -customElements.define('my-element', MyElement); render(html``, document.getElementById('root')); ``` @@ -98,12 +102,12 @@ You can specify life cycle functions for GemElement. Sometimes they are useful, ```js // Omit import... +@customElement('my-element') class MyElement extends GemElement { mounted() { console.log('element mounted!'); } } -customElements.define('my-element', MyElement); ``` Complete life cycle: @@ -134,25 +138,5 @@ Complete life cycle: +---------------------------------------+ ``` -_The `constructor` and `unmounted` of the parent element are executed before the child element, but the `mounted` is executed after the child element_ - -## Use TypeScript - -When using TypeScript, you can use decorators to make reactive declarations while declaring fields: - -```ts -// Omit import... - -const store = createStore({ - count: 0, -}); - -@customElement('my-element') -@connectStore(store) -class MyElement extends GemElement { - @attribute name: string; - @boolattribute disabled: boolean; - @numattribute count: number; - @property data: Data | undefined; // property has no default value -} -``` +> [!NOTE] +> The `constructor` and `unmounted` of the parent element are executed before the child element, but the `mounted` is executed after the child element 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 5345f4fe..aa7d4b38 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 @@ -8,15 +8,17 @@ Sharing data between elements (also called "components") is a basic capability o // Omit import... // create store -const store = createStore({ a: 1 }); +const [store, update] = useStore({ a: 1 }); // connect store -connect(store, function () { +const disconnect = connect(store, function () { // Execute when store is updated }); -// pulish update -updateStore(store, { a: 2 }); +// publish update +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. @@ -28,10 +30,10 @@ You may have noticed that every time the `Store` is updated, the Gem element con ```js // Omit import... -const posts = createStore({ ... }); -const users = createStore({ ... }); -const photos = createStore({ ... }); -const profiles = createStore({ ... }); +const [posts] = useStore({ ... }); +const [users] = useStore({ ... }); +const [photos] = useStore({ ... }); +const [profiles] = useStore({ ... }); // ... ``` @@ -48,10 +50,10 @@ If this logic needs to be shared among many elements, you can use `Store` to eas const isSavingMode = () => document.visibilityState !== 'visible'; -const store = createStore({ savingMode: isSavingMode() }); +const [store, update] = useStore({ savingMode: isSavingMode() }); document.addEventListener('visibilitychange', () => { - updateStore({ savingMode: isSavingMode() }); + update({ savingMode: isSavingMode() }); }); @customElement('my-element') diff --git a/packages/gem/docs/en/001-guide/001-basic/004-route.md b/packages/gem/docs/en/001-guide/001-basic/004-route.md index a71f481b..bafca5e1 100644 --- a/packages/gem/docs/en/001-guide/001-basic/004-route.md +++ b/packages/gem/docs/en/001-guide/001-basic/004-route.md @@ -19,31 +19,31 @@ Gem built-in elements `` and `` work like this. ```js index.js -import { GemElement, html } from '@mantou/gem'; +import { GemElement, html, customElement } from '@mantou/gem'; import '@mantou/gem/elements/link'; import '@mantou/gem/elements/route'; const routes = { home: { pattern: '/', - getContent() { - return html`home page`; - }, + content: html`home page`, }, - a: { - pattern: '/a/:b', - getContent() { - return html`about page`; + page: { + pattern: '/page/:b', + async getContent(params) { + await new Promise((res) => setTimeout(res, 1000)); + return html`about page: params ${params.b}`; }, }, }; +@customElement('app-root') class App extends GemElement { render() { return html`
    @@ -51,7 +51,6 @@ class App extends GemElement { `; } } -customElements.define('app-root', App); ``` diff --git a/packages/gem/docs/en/001-guide/001-basic/006-styled-element.md b/packages/gem/docs/en/001-guide/001-basic/006-styled-element.md index 2b6ca31b..b573221c 100644 --- a/packages/gem/docs/en/001-guide/001-basic/006-styled-element.md +++ b/packages/gem/docs/en/001-guide/001-basic/006-styled-element.md @@ -8,27 +8,14 @@ Since styles cannot penetrate ShadowDOM, global style sheets cannot be used to i ```js 11 import { GemElement } from '@mantou/gem'; -import { createCSSSheet, css } from '@mantou/gem'; +import { adoptedStyle, customElement } from '@mantou/gem'; -// Create a style sheet using Constructable Stylesheet -const styles = createCSSSheet(css` +// 使用 Constructable Stylesheet 创建样式表 +const styles = createCSSSheet(` h1 { text-decoration: underline; } `); -class MyElement extends GemElement { - static adoptedStyleSheets = [styles]; -} -customElements.define('my-element', MyElement); -``` - -Just like connecting to the Store, there is a similar Typescript decorator available: `@adoptedStyle`. - -```ts 6 -import { GemElement } from '@mantou/gem'; -import { adoptedStyle, customElement } from '@mantou/gem'; - -// Omit the styles definition... @adoptedStyle(styles) @customElement('my-element') @@ -39,12 +26,11 @@ class MyElement extends GemElement {} You can reference CSS selectors in JS: -```ts 18 +```js 17 import { GemElement, html } from '@mantou/gem'; import { createCSSSheet, styled, adoptedStyle, customElement } from '@mantou/gem'; const styles = createCSSSheet({ - // This is temporarily designed as `styled.class` in order to be compatible with the syntax highlighting of `styled-component` header: styled.class` text-decoration: underline; &:hover { @@ -66,7 +52,7 @@ class MyElement extends GemElement { Use [`::part`](https://drafts.csswg.org/css-shadow-parts-1/#part) to export the internal content of the element, allowing external custom styles: -```ts 13 +```js 13 /** * The following code has the same effect as `
    `, * But Gem recommends using decorators to define parts, so that IDE integration can be done well in the future @@ -76,22 +62,22 @@ Use [`::part`](https://drafts.csswg.org/css-shadow-parts-1/#part) to export the @customElement('my-element') class MyElement extends GemElement { - @part header: string; + @part static header; render() { - return html`
    `; + return html`
    `; } } ``` Also use [`ElementInternals.states`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states) to export element internal state, external styling this element for current state: -```ts +```js // Omit import... @customElement('my-element') class MyElement extends GemElement { - @state opened: boolean; + @state opened; open() { // Can be selected by the selector `:state(opened)` @@ -101,7 +87,8 @@ class MyElement extends GemElement { } ``` -_Note the difference with `state`/`setState`._ +> [!NOTE] +> Note the difference with `state`/`setState` > [!TIP] > Can also customize element styles using hack, for example: diff --git a/packages/gem/docs/en/001-guide/002-advance/001-icon.md b/packages/gem/docs/en/001-guide/002-advance/001-icon.md index d56e8d86..72df0e66 100644 --- a/packages/gem/docs/en/001-guide/002-advance/001-icon.md +++ b/packages/gem/docs/en/001-guide/002-advance/001-icon.md @@ -36,4 +36,4 @@ render( -_`` since the content is copied for rendering, the update of `` cannot update the original `` instance_ +> [!NOTE] `` since the content is copied for rendering, the update of `` cannot update the original `` instance diff --git a/packages/gem/docs/en/001-guide/002-advance/002-gem-element-more.md b/packages/gem/docs/en/001-guide/002-advance/002-gem-element-more.md index 37c8909e..ba6ed684 100644 --- a/packages/gem/docs/en/001-guide/002-advance/002-gem-element-more.md +++ b/packages/gem/docs/en/001-guide/002-advance/002-gem-element-more.md @@ -1,17 +1,17 @@ # GemElement more -Features other than attr/prop/store/state. +Features other than Attribute/Property/Store/State. ## Reference DOM If you want to manipulate the DOM content within an element, such as reading the value of `
    ${this.src}
    `; + }; +} +``` + +## Differences from TypeScript Decorator + +TypeScript's field decorator is executed immediately after the class definition, making it easy to define accessor properties on the prototype object. +The ES decorator must use `accessor` to achieve similar effects. Even using `accessor` will cause the Gem to lose some functions. +So Gem uses a special way to implement it so that it looks no different from the TypeScript decorator, in fact, the initialization functions returned by these loaders will be run every time `MyElement` is instantiated. You can check the `tsc` compiled code: + +```js +let MyElement = (() => { + return class MyElement extends _classSuper { + src = __runInitializers(this, _src_initializers, void 0); + }; +})(); +``` + +## Pitfalls of using ES Decorators + +- For reactive Attributes, element updates cannot be triggered when modified in DevTools, because the native `observedAttributes` cannot take effect for dynamically added attributes. +- When executing the initialization function of `@attribute`, some hacking work is required, and the performance will be slightly reduced +- The element must be inserted into the document for the properties to be read correctly - only the document is inserted before there is any [chance](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields#description) of removing the fields on the element + > [!CAUTION] + > + > ```js + > const myEle = new MyElement(); + > console.assert(myEle.src, undefined); + > myEle.connectedCallback(); + > console.assert(myEle.src, ''); + > ``` diff --git a/packages/gem/docs/en/README.md b/packages/gem/docs/en/README.md index 2a103639..055fc8e7 100644 --- a/packages/gem/docs/en/README.md +++ b/packages/gem/docs/en/README.md @@ -105,6 +105,7 @@ const style = createCSSSheet(css` opacity: 0; } dy-use { + width: 1.3em; padding: 4px; } dy-use:hover { @@ -134,22 +135,22 @@ export class TodoListElement extends GemElement { ``` ```ts store.ts -import { createStore, updateStore } from '@mantou/gem'; +import { useStore } from '@mantou/gem'; type Store = { items: string[]; }; -export const todoData = createStore({ +export const [todoData, update] = useStore({ items: [], }); export const addItem = (item: string) => { - updateStore(todoData, { items: [...todoData.items, item] }); + update({ items: [...todoData.items, item] }); }; export const deleteItem = (item: string) => { - updateStore(todoData, { items: todoData.items.filter((e) => e !== item) }); + update({ items: todoData.items.filter((e) => e !== item) }); }; ``` diff --git a/packages/gem/docs/zh/001-guide/001-basic/001-reactive-element.md b/packages/gem/docs/zh/001-guide/001-basic/001-reactive-element.md index 586631b6..c9c970ba 100644 --- a/packages/gem/docs/zh/001-guide/001-basic/001-reactive-element.md +++ b/packages/gem/docs/zh/001-guide/001-basic/001-reactive-element.md @@ -10,36 +10,37 @@ ```js // 省略导入... +@customElement('my-element') class MyElement extends GemElement { - static observedAttributes = ['first-name']; + @attribute firstName; render() { return html`${this.firstName}`; } } -customElements.define('my-element', MyElement); ``` -`first-name` 属性经过“Observe”,他就能直接通过元素属性进行访问, -且会自动进行驼峰和烤串格式的转换, -当 `first-name` 属性更改时,`MyElement` 的实例元素将重新渲染。 +上述例子中 `MyElement` 的字段 `firstName` 被声明成反应性属性, +当属性更改时,`MyElement` 的已经挂载实例将重新渲染, +此外,该字段映射到元素的 `first-name` Attribute。 -类似 `observedAttributes`,GemElement 还支持 `observedProperties`/`observedStores` 用来反应指定的 property/store: +类似 `@attribute`,GemElement 还支持 `@property`/`@connectStore` 用来反应指定的 Property/Store: ```js // 省略导入... +@customElement('my-element') +@connectStore(store) class MyElement extends GemElement { - static observedProperties = ['data']; - static observedStores = [store]; + @property data; render() { return html`${this.data.id} ${store.name}`; } } -customElements.define('my-element', MyElement); ``` -_不要在元素内修改 prop/attr,他们应该由父元素单向传递进来,就像原生元素一样_ +> [!TIP] +> 不要在元素内修改 prop/attr,他们应该由父元素单向传递进来,就像原生元素一样 另外 `GemElement` 提供了类似 React 的 `state`/`setState` API 来管理元素自身的状态, 每当调用 `setState` 时触发元素重新渲染: @@ -47,6 +48,7 @@ _不要在元素内修改 prop/attr,他们应该由父元素单向传递进来 ```js // 省略导入... +@customElement('my-element') class MyElement extends GemElement { state = { id: 1 }; clicked() { @@ -56,29 +58,29 @@ class MyElement extends GemElement { return html`${this.state.id}`; } } -customElements.define('my-element', MyElement); ``` -_`GemElement` 扩展自 [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement),不要覆盖 `HTMLElement` 的 attribute/property/method/event,使用[私有字段](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields)来避免 `GemElement`/`HTMLElement` 的属性方法被覆盖_ +> [!TIP] `GemElement` 扩展自 [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement),不要覆盖 `HTMLElement` 的 attribute/property/method/event,使用[私有字段](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields)来避免 `GemElement`/`HTMLElement` 的属性方法被覆盖 ## 例子 ```js index.js -import { createStore, GemElement, updateStore, render, html } from '@mantou/gem'; +import { useStore, GemElement, render, html, attribute, property, connectStore, customElement } from '@mantou/gem'; -const store = createStore({ +const [store, update] = useStore({ count: 0, }); +@customElement('my-element') +@connectStore(store) class MyElement extends GemElement { - static observedStores = [store]; - static observedAttributes = ['name']; - static observedProperties = ['data']; + @attribute name; + @property data; #onClick = () => { - updateStore(store, { count: ++store.count }); + update({ count: ++store.count }); }; render() { @@ -89,7 +91,6 @@ class MyElement extends GemElement { `; } } -customElements.define('my-element', MyElement); render(html``, document.getElementById('root')); ``` @@ -103,12 +104,12 @@ render(html``, document. ```js // 省略导入... +@customElement('my-element') class MyElement extends GemElement { mounted() { console.log('element mounted!'); } } -customElements.define('my-element', MyElement); ``` 完整的生命周期: @@ -139,25 +140,5 @@ customElements.define('my-element', MyElement); +---------------------------------------+ ``` -_父元素的 `constructor` 和 `unmounted` 先于子元素执行,但 `mounted` 后于子元素执行_ - -## 使用 TypeScript - -当使用 TypeScript 时,可以在声明字段的同时使用装饰器进行反应性声明: - -```ts -// 省略导入... - -const store = createStore({ - count: 0, -}); - -@customElement('my-element') -@connectStore(store) -class MyElement extends GemElement { - @attribute name: string; - @boolattribute disabled: boolean; - @numattribute count: number; - @property data: Data | undefined; // property 没有默认值 -} -``` +> [!NOTE] +> 父元素的 `constructor` 和 `unmounted` 先于子元素执行,但 `mounted` 后于子元素执行 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 91f4f3a7..29af8092 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 @@ -10,15 +10,17 @@ Gem 使用发布订阅模式,让多个元素共享数据,并且数据更新 // 省略导入... // 创建 store -const store = createStore({ a: 1 }); +const [store, update] = useStore({ a: 1 }); // 连接 store -connect(store, function () { +const disconnect = connect(store, function () { // store 更新时执行 }); // 更新 store -updateStore(store, { a: 2 }); +update({ a: 2 }); + +disconnect(); ``` 前一节有提到,使用 `static observedStores`/`@connectStore` 来连接 `Store`, @@ -34,10 +36,10 @@ updateStore(store, { a: 2 }); ```js // 省略导入... -const posts = createStore({ ... }); -const users = createStore({ ... }); -const photos = createStore({ ... }); -const profiles = createStore({ ... }); +const [posts] = useStore({ ... }); +const [users] = useStore({ ... }); +const [photos] = useStore({ ... }); +const [profiles] = useStore({ ... }); // ... ``` @@ -54,10 +56,10 @@ const profiles = createStore({ ... }); const isSavingMode = () => document.visibilityState !== 'visible'; -const store = createStore({ savingMode: isSavingMode() }); +const [store, update] = useStore({ savingMode: isSavingMode() }); document.addEventListener('visibilitychange', () => { - updateStore({ savingMode: isSavingMode() }); + update({ savingMode: isSavingMode() }); }); @customElement('my-element') diff --git a/packages/gem/docs/zh/001-guide/001-basic/004-route.md b/packages/gem/docs/zh/001-guide/001-basic/004-route.md index a8740ac7..a64e4162 100644 --- a/packages/gem/docs/zh/001-guide/001-basic/004-route.md +++ b/packages/gem/docs/zh/001-guide/001-basic/004-route.md @@ -21,31 +21,31 @@ Gem 内置元素 `` 和 `` 就是这样工作。 ```js index.js -import { GemElement, html } from '@mantou/gem'; +import { GemElement, html, customElement } from '@mantou/gem'; import '@mantou/gem/elements/link'; import '@mantou/gem/elements/route'; const routes = { home: { pattern: '/', - getContent() { - return html`home page`; - }, + content: html`home page`, }, - a: { - pattern: '/a/:b', - getContent() { - return html`about page`; + page: { + pattern: '/page/:b', + async getContent(params) { + await new Promise((res) => setTimeout(res, 1000)); + return html`about page: params ${params.b}`; }, }, }; +@customElement('app-root') class App extends GemElement { render() { return html`
    @@ -53,7 +53,6 @@ class App extends GemElement { `; } } -customElements.define('app-root', App); ``` diff --git a/packages/gem/docs/zh/001-guide/001-basic/006-styled-element.md b/packages/gem/docs/zh/001-guide/001-basic/006-styled-element.md index f80a6dd1..f17e1d8b 100644 --- a/packages/gem/docs/zh/001-guide/001-basic/006-styled-element.md +++ b/packages/gem/docs/zh/001-guide/001-basic/006-styled-element.md @@ -13,27 +13,14 @@ ```js 11 import { GemElement } from '@mantou/gem'; -import { createCSSSheet, css } from '@mantou/gem'; +import { adoptedStyle, customElement } from '@mantou/gem'; // 使用 Constructable Stylesheet 创建样式表 -const styles = createCSSSheet(css` +const styles = createCSSSheet(` h1 { text-decoration: underline; } `); -class MyElement extends GemElement { - static adoptedStyleSheets = [styles]; -} -customElements.define('my-element', MyElement); -``` - -像连接 `Store` 一样,也有一个类似的 Typescript 装饰器可用:`@adoptedStyle`。 - -```ts 6 -import { GemElement } from '@mantou/gem'; -import { adoptedStyle, customElement } from '@mantou/gem'; - -// Omit the styles definition... @adoptedStyle(styles) @customElement('my-element') @@ -44,12 +31,11 @@ class MyElement extends GemElement {} 可以在 JS 中引用 CSS 选择器: -```js 18 +```js 17 import { GemElement, html } from '@mantou/gem'; import { createCSSSheet, styled, adoptedStyle, customElement } from '@mantou/gem'; const styles = createCSSSheet({ - // This is temporarily designed as `styled.class` in order to be compatible with the syntax highlighting of `styled-component` header: styled.class` text-decoration: underline; &:hover { @@ -71,7 +57,7 @@ class MyElement extends GemElement { 可以使用 [`::part`](https://drafts.csswg.org/css-shadow-parts-1/#part) 导出元素内部内容,允许外部进行自定义样式: -```ts 13 +```js 13 /** * 下面的代码跟 `
    ` 效果一样, * 但是 Gem 推荐使用装饰器来定义 part,这样在将来能很好的进行 IDE 集成 @@ -81,22 +67,22 @@ class MyElement extends GemElement { @customElement('my-element') class MyElement extends GemElement { - @part header: string; + @part static header; render() { - return html`
    `; + return html`
    `; } } ``` 还可以使用 [`ElementInternals.states`](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states) 导出元素内部状态,供外部对当前状态的元素样式化: -```ts +```js // 省略导入... @customElement('my-element') class MyElement extends GemElement { - @state opened: boolean; + @state opened; open() { // 可被选择器 `:state(opened)` 选中 @@ -119,7 +105,8 @@ class MyElement extends GemElement { > ]; > ``` -_注意跟 `state`/`setState` 的区别。_ +> [!NOTE] +> 注意跟 `state`/`setState` 的区别 ## 自定义元素外部样式 diff --git a/packages/gem/docs/zh/001-guide/002-advance/001-icon.md b/packages/gem/docs/zh/001-guide/002-advance/001-icon.md index 2b0952a8..f405dca9 100644 --- a/packages/gem/docs/zh/001-guide/002-advance/001-icon.md +++ b/packages/gem/docs/zh/001-guide/002-advance/001-icon.md @@ -35,4 +35,4 @@ render( -_`` 由于是复制内容进行渲染,所以 `` 更新不能同步更新原先的 `` 实例_ +> [!NOTE] `` 由于是复制内容进行渲染,所以 `` 更新不能同步更新原先的 `` 实例 diff --git a/packages/gem/docs/zh/001-guide/002-advance/002-gem-element-more.md b/packages/gem/docs/zh/001-guide/002-advance/002-gem-element-more.md index 2ce85892..8f6f4f0b 100644 --- a/packages/gem/docs/zh/001-guide/002-advance/002-gem-element-more.md +++ b/packages/gem/docs/zh/001-guide/002-advance/002-gem-element-more.md @@ -1,18 +1,18 @@ # GemElement 更多内容 -除了 attr/prop/store/state 外的特性。 +除了 Attribute/Property/Store/State 外的特性。 ## 引用 DOM 如果你想要在元素内操作 DOM 内容,例如读取 `
    ${this.src}
    `; + }; +} +``` + +## 和 TS 装饰器的差异 + +TS 的字段装饰器在类定义之后立即执行,能很方便的在原型对象上定义访问器属性。 +而 ES 的装饰器必须使用 `accessor` 才能达到类似的效果,就算使用 `accessor` 也将使 Gem 丧失部分功能。 +所以 Gem 使用了特殊的方式来现实,使他看起来和 TS 装饰器没有任何区别, +实际上,这些装器返回的初始化函数将在每次实例化 `MyElement` 时运行,可以检查 `tsc` 编译后的代码: + +```js +let MyElement = (() => { + return class MyElement extends _classSuper { + src = __runInitializers(this, _src_initializers, void 0); + }; +})(); +``` + +## 使用 ES 装饰器的缺陷 + +- 对于反应性的 Attribute 在 DevTools 中修改时不能触发元素更新,因为原生的 `observedAttributes` 不能为动态添加的属性生效 +- 在执行 `@attribute` 的初始化函数时,需要进行一下 hack 工作,性能将会小幅度降低 +- 元素必须插入文档才能正确读取属性——只有插入文档才有[机会](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Public_class_fields#description)删除元素上的字段 + > [!CAUTION] + > + > ```js + > const myEle = new MyElement(); + > console.assert(myEle.src, undefined); + > myEle.connectedCallback(); + > console.assert(myEle.src, ''); + > ``` diff --git a/packages/gem/docs/zh/README.md b/packages/gem/docs/zh/README.md index 0d087402..b43f22cf 100644 --- a/packages/gem/docs/zh/README.md +++ b/packages/gem/docs/zh/README.md @@ -105,6 +105,7 @@ const style = createCSSSheet(css` opacity: 0; } dy-use { + width: 1.3em; padding: 4px; } dy-use:hover { @@ -134,22 +135,22 @@ export class TodoListElement extends GemElement { ``` ```ts store.ts -import { createStore, updateStore } from '@mantou/gem'; +import { useStore } from '@mantou/gem'; type Store = { items: string[]; }; -export const todoData = createStore({ +export const [todoData, update] = useStore({ items: [], }); export const addItem = (item: string) => { - updateStore(todoData, { items: [...todoData.items, item] }); + update({ items: [...todoData.items, item] }); }; export const deleteItem = (item: string) => { - updateStore(todoData, { items: todoData.items.filter((e) => e !== item) }); + update({ items: todoData.items.filter((e) => e !== item) }); }; ``` diff --git a/packages/gem/gem-book.cli.json b/packages/gem/gem-book.cli.json index 508f0f50..7ae6bf33 100644 --- a/packages/gem/gem-book.cli.json +++ b/packages/gem/gem-book.cli.json @@ -11,6 +11,6 @@ } ], "template": "docs/template.html", - "plugin": ["raw", "docsearch", "sandpack"], + "plugin": ["raw", "docsearch", "sandpack", "code-group"], "debug": true } diff --git a/packages/gem/package.json b/packages/gem/package.json index c4f15964..50e265c7 100644 --- a/packages/gem/package.json +++ b/packages/gem/package.json @@ -1,6 +1,6 @@ { "name": "@mantou/gem", - "version": "1.7.6", + "version": "1.7.7", "description": "💎 使用自定义元素的轻量级 WebApp 开发框架", "keywords": [ "frontend", diff --git a/packages/gem/src/lib/element.ts b/packages/gem/src/lib/element.ts index c7c38810..6549c395 100644 --- a/packages/gem/src/lib/element.ts +++ b/packages/gem/src/lib/element.ts @@ -377,7 +377,11 @@ export abstract class GemElement> extends HTMLElemen this.#isMounted = true; this.#unmountCallback = this.mounted?.(); this.#initEffect(); - if (rootElement && (this.getRootNode() as ShadowRoot).host?.tagName !== rootElement.toUpperCase()) { + if ( + rootElement && + this.isConnected && + (this.getRootNode() as ShadowRoot).host?.tagName !== rootElement.toUpperCase() + ) { throw new GemError(`not allow current root type`); } }; @@ -405,10 +409,10 @@ export abstract class GemElement> extends HTMLElemen /** * @private * @final - * use `mounted` + * use `mounted`; 允许手动调用 `connectedCallback` 以清除装饰器定义的字段 */ connectedCallback() { - if (this.#isAsync) { + if (this.isConnected && 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 3ca6a9c2..3df4870e 100644 --- a/packages/gem/src/test/gem-element/advance.test.ts +++ b/packages/gem/src/test/gem-element/advance.test.ts @@ -1,3 +1,11 @@ +/** + * 测试 HTMLElement 没有的特性或者不常用的特性 + * - 异步渲染 + * - Light DOM 自定义元素 + * - 生命周期以及 Effect/Memo + * - 派生元素(类扩展) + * - 无渲染内容 Gem 元素 + */ import { fixture, expect, nextFrame } from '@open-wc/testing'; import { GemElement, html } from '../../lib/element'; @@ -120,7 +128,10 @@ describe('gem element 生命周期', () => { const el = container.firstElementChild as LifecycleGemElement; expect(el.appTitle).to.equal('title'); expect(el.renderCount).to.equal(1); - expect((el.cloneNode() as LifecycleGemElement).appTitle).to.equal('title'); + const clone = el.cloneNode() as LifecycleGemElement; + // BUG + clone.connectedCallback(); + expect(clone.appTitle).to.equal('title'); const el2 = new LifecycleGemElement('', '2'); expect(el2.appTitle).to.equal(''); diff --git a/packages/gem/src/test/gem-element/decorators.test.ts b/packages/gem/src/test/gem-element/decorators.test.ts index c850079d..f3f3e8e3 100644 --- a/packages/gem/src/test/gem-element/decorators.test.ts +++ b/packages/gem/src/test/gem-element/decorators.test.ts @@ -51,20 +51,24 @@ class DecoratorGemElement extends GemElement { } } -@connectStore(store) -@adoptedStyle(styles) -@customElement('decorator-gem-demo2') -class DecoratorGemElement2 extends GemElement { - static observedStores = []; - static adoptedStyleSheets = []; - renderCount = 0; - render() { - this.renderCount++; - return html``; - } -} - describe('装饰器', () => { + it('使用装饰器定义的未插入文档元素', async () => { + const el = new DecoratorGemElement(); + expect(el.propData).to.eql({ value: '' }); + expect(el.getAttribute('rank-attr')).to.equal(null); + // BUG + expect(el.rankAttr).to.equal(undefined); + el.connectedCallback(); + expect(el.getAttribute('rank-attr')).to.equal(null); + expect(el.rankAttr).to.equal(''); + + el.rankAttr = 'attr'; + el.propData = { value: '1' }; + const el2 = el.cloneNode() as DecoratorGemElement; + expect(el2.getAttribute('rank-attr')).to.equal('attr'); + expect(el2.rankAttr).to.equal('attr'); + expect(el2.propData).to.eql({ value: '' }); + }); it('装饰器定义的自定义元素', async () => { let a = 1; const el: DecoratorGemElement = await fixture(html` @@ -104,12 +108,4 @@ describe('装饰器', () => { expect(el.renderCount).to.equal(3); expect(a).to.equal(2); }); - - it('装饰器和静态属性共存', async () => { - const el: DecoratorGemElement2 = await fixture(html``); - updateStore(store, { a: 3 }); - await Promise.resolve(); - expect(el.renderCount).to.equal(2); - expect(el.shadowRoot?.adoptedStyleSheets.length).to.equal(1); - }); }); diff --git a/packages/gem/src/test/gem-element/multi.test.ts b/packages/gem/src/test/gem-element/multi.test.ts index 0046b503..c41c6751 100644 --- a/packages/gem/src/test/gem-element/multi.test.ts +++ b/packages/gem/src/test/gem-element/multi.test.ts @@ -1,12 +1,13 @@ import { expect, fixture } from '@open-wc/testing'; -import { customElement, property, RefObject, refobject } from '../../lib/decorators'; +import { attribute, customElement, property, RefObject, refobject } from '../../lib/decorators'; import { GemElement, html } from '../../lib/element'; @customElement('app-children') export class Children extends GemElement { @refobject inputRef: RefObject; @property value?: { value: number }; + @attribute attr: string; render() { return html``; @@ -19,13 +20,13 @@ export class App extends GemElement { @refobject childrenRef2: RefObject; render() { return html` - - + + `; } } -describe('多个 gem element', () => { - it('ref & prop', async () => { +describe('多个 gem element 一起工作', () => { + it('ref & prop & attr', async () => { const el: App = await fixture(html``); const children1 = el.childrenRef1.element!; const children2 = el.childrenRef2.element!; @@ -34,5 +35,7 @@ describe('多个 gem element', () => { expect(input1 !== input2).to.equal(true); expect(children1.value).to.eql({ value: 1 }); expect(children2.value).to.eql({ value: 2 }); + expect(children1.attr).to.eql('1'); + expect(children2.attr).to.eql('2'); }); });