From a28ccffd0e3a1e3b5e349a6c4a98aa5deff4956e Mon Sep 17 00:00:00 2001 From: TechQuery Date: Wed, 10 Jan 2024 01:31:29 +0800 Subject: [PATCH 1/2] [add] Quick Start document of WebCell v3 [fix] 2 rendering bugs [optimize] update Upstream packages --- ReadMe.md | 364 +++++++++++++++++++++++++++++++++++++++------ package.json | 10 +- pnpm-lock.yaml | 100 ++++++------- source/Async.tsx | 3 +- source/WebCell.tsx | 3 +- 5 files changed, 373 insertions(+), 107 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index d8fc061..2139061 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -98,96 +98,362 @@ npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -D ``` -### Simple component - -`source/SubTag.tsx` +### Function component ```tsx -import { component, FC, PropsWithChildren } from 'web-cell'; +import { DOMRenderer } from 'dom-renderer'; +import { FC, PropsWithChildren } from 'web-cell'; -export const InlineTag: FC = ({ children }) => ( - {children} +const Hello: FC = ({ children = 'World' }) => ( +

Hello, {children}!

); +new DOMRenderer().renderer(WebCell); +``` + +### Class component + +#### Children slot + +```tsx +import { DOMRenderer } from 'dom-renderer'; +import { component } from 'web-cell'; + @component({ - tagName: 'sub-tag' + tagName: 'hello-world', + mode: 'open' }) -export class SubTag extends HTMLElement { +class Hello extends HTMLElement { render() { - return test; + return ( +

+ Hello, ! +

+ ); } } -``` -### Advanced component +new DOMRenderer().renderer( + <> + WebCell + {/* or */} + WebCell + +); +``` -`source/TestTag.tsx` +#### DOM Props ```tsx -import { JsxProps } from 'dom-renderer'; +import { DOMRenderer } from 'dom-renderer'; import { observable } from 'mobx'; -import { component, attribute, observer, on } from 'web-cell'; -import { stringifyCSS } from 'web-utility'; +import { component, attribute, observer } from 'web-cell'; + +interface HelloProps { + name?: string; +} + +@component({ tagName: 'hello-world' }) +@observer +class Hello extends HTMLElement { + declare props: HelloProps; -import { SubTag } from './SubTag'; + @attribute + @observable + accessor name = ''; -export interface TestTagProps extends JsxProps { - topic?: string; + render() { + return

Hello, {this.name}!

; + } +} + +new DOMRenderer().renderer(); + +// or for HTML tag names in TypeScript + +declare global { + namespace JSX { + interface IntrinsicElements { + 'hello-world': HelloProps; + } + } } +new DOMRenderer().renderer(); +``` + +### Inner state -class State { +#### Function component + +```tsx +import { DOMRenderer } from 'dom-renderer'; +import { observable } from 'mobx'; +import { FC, observer } from 'web-cell'; + +class CounterModel { @observable - accessor status = ''; + accessor times = 0; } -@component({ - tagName: 'test-tag', - mode: 'open' -}) -@observer -export class TestTag extends HTMLElement { - declare props: TestTagProps; +const couterStore = new CounterModel(); - @attribute +const Counter: FC = observer(() => ( + +)); + +new DOMRenderer().renderer(); +``` + +#### Class component + +```tsx +import { DOMRenderer } from 'dom-renderer'; +import { observable } from 'mobx'; +import { component, observer } from 'web-cell'; + +@component({ tagName: 'my-counter' }) +@observer +class Counter extends HTMLElement { @observable - accessor topic = 'Test'; + accessor times = 0; + + handleClick = () => (this.times += 1); + + render() { + return ; + } +} + +new DOMRenderer().renderer(); +``` - state = new State(); +### CSS scope +#### Inline style + +```tsx +import { component } from 'web-cell'; +import { stringifyCSS } from 'web-utility'; + +@component({ + tagName: 'my-button', + mode: 'open' +}) +export class MyButton extends HTMLElement { style = stringifyCSS({ - '.topic': { - color: 'lightblue' - }, - '.topic.active': { - color: 'lightpink' + '.btn': { + color: 'white', + background: 'lightblue' } }); - onClick = () => (this.topic = 'Example'); + render() { + return ( + <> + - @on('click', ':host h1') - onDelegate() { - this.state.status = 'active'; + + + + + ); } +} +``` - render() { - const { style, topic } = this, - { status } = this.state; +#### Link stylesheet +```tsx +import { component } from 'web-cell'; + +@component({ + tagName: 'my-button', + mode: 'open' +}) +export class MyButton extends HTMLElement { + render() { return ( <> - + + + + + + ); + } +} +``` -

- {topic} - {topic} +#### CSS module - -

- +##### `scoped.css` + +```css +.btn { + color: white; + background: lightblue; +} +``` + +##### `MyButton.tsx` + +```tsx +import { component, WebCell } from 'web-cell'; + +import styles from './scoped.css' assert { type: 'css' }; + +interface MyButton extends WebCell {} + +@component({ + tagName: 'my-button', + mode: 'open' +}) +export class MyButton extends HTMLElement { + connectedCallback() { + this.root.adoptedStyleSheets = [styles]; + } + + render() { + return ( + + + + ); + } +} +``` + +### Event delegation + +```tsx +import { component, on } from 'web-cell'; + +@component({ tagName: 'my-table' }) +export class MyTable extends HTMLElement { + @on('click', ':host td > button') + handleEdit(event: MouseEvent, { dataset: { id } }: HTMLButtonElement) { + console.log(`editing row: ${id}`); + } + + render() { + return ( + + + + + + + + + + + + + + + + +
1A + +
2B + +
3C + +
+ ); + } +} +``` + +### MobX reaction + +```tsx +import { observable } from 'mobx'; +import { component, observer, reaction } from 'web-cell'; + +@component({ tagName: 'my-counter' }) +@observer +export class Counter extends HTMLElement { + @observable + accessor times = 0; + + handleClick = () => (this.times += 1); + + @reaction(({ times }) => times) + echoTimes(newValue: number, oldValue: number) { + console.log(`newValue: ${newValue}, oldValue: ${oldValue}`); + } + + render() { + return ; + } +} +``` + +### Form association + +```tsx +import { DOMRenderer } from 'dom-renderer'; +import { HTMLFieldProps } from 'web-utility'; +import { WebField, component, formField, observer } from 'web-cell'; + +interface MyField extends WebField {} + +@component({ + tagName: 'my-field', + mode: 'open' +}) +@formField +@observer +class MyField extends HTMLElement { + declare props: HTMLFieldProps; + + render() { + const { name } = this; + + return ( + + (this.value = value) + } + /> ); } } + +new DOMRenderer().renderer( +
+ + + + +); +``` + +### Async component + +#### `AsyncTag.tsx` + +```tsx +import { FC } from 'web-cell'; + +const AsyncTag: FC = () =>
Async
; + +export default AsyncTag; +``` + +#### `index.tsx` + +```tsx +const AsyncTag = lazy(() => import('./AsyncTag')); + +new DOMRenderer().renderer(); ``` ## Basic knowledge diff --git a/package.json b/package.json index 2cecb07..7af0442 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-cell", - "version": "3.0.0-rc.4", + "version": "3.0.0-rc.5", "description": "Web Components engine based on VDOM, JSX, MobX & TypeScript", "keywords": [ "web", @@ -27,7 +27,7 @@ "types": "dist/index.d.ts", "dependencies": { "@swc/helpers": "^0.5.3", - "dom-renderer": "^2.0.4", + "dom-renderer": "^2.0.5", "mobx": ">=6.11", "regenerator-runtime": "^0.14.1", "web-utility": "^4.1.3" @@ -45,8 +45,8 @@ "@parcel/transformer-typescript-tsc": "~2.11.0", "@parcel/transformer-typescript-types": "~2.11.0", "@types/jest": "^29.5.11", - "@typescript-eslint/eslint-plugin": "^6.18.0", - "@typescript-eslint/parser": "^6.18.0", + "@typescript-eslint/eslint-plugin": "^6.18.1", + "@typescript-eslint/parser": "^6.18.1", "core-js": "^3.35.0", "element-internals-polyfill": "^1.3.10", "eslint": "^8.56.0", @@ -62,7 +62,7 @@ "rimraf": "^5.0.5", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", - "typedoc": "^0.25.6", + "typedoc": "^0.25.7", "typedoc-plugin-mdn-links": "^3.1.11", "typescript": "~5.3.3" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efe41a3..341c7af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ dependencies: specifier: ^0.5.3 version: 0.5.3 dom-renderer: - specifier: ^2.0.4 - version: 2.0.4(typescript@5.3.3) + specifier: ^2.0.5 + version: 2.0.5(typescript@5.3.3) mobx: specifier: '>=6.11' version: 6.12.0 @@ -41,11 +41,11 @@ devDependencies: specifier: ^29.5.11 version: 29.5.11 '@typescript-eslint/eslint-plugin': - specifier: ^6.18.0 - version: 6.18.0(@typescript-eslint/parser@6.18.0)(eslint@8.56.0)(typescript@5.3.3) + specifier: ^6.18.1 + version: 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3) '@typescript-eslint/parser': - specifier: ^6.18.0 - version: 6.18.0(eslint@8.56.0)(typescript@5.3.3) + specifier: ^6.18.1 + version: 6.18.1(eslint@8.56.0)(typescript@5.3.3) core-js: specifier: ^3.35.0 version: 3.35.0 @@ -92,11 +92,11 @@ devDependencies: specifier: ^10.9.2 version: 10.9.2(typescript@5.3.3) typedoc: - specifier: ^0.25.6 - version: 0.25.6(typescript@5.3.3) + specifier: ^0.25.7 + version: 0.25.7(typescript@5.3.3) typedoc-plugin-mdn-links: specifier: ^3.1.11 - version: 3.1.11(typedoc@0.25.6) + version: 3.1.11(typedoc@0.25.7) typescript: specifier: ~5.3.3 version: 5.3.3 @@ -2075,8 +2075,8 @@ packages: '@types/yargs-parser': 21.0.3 dev: true - /@typescript-eslint/eslint-plugin@6.18.0(@typescript-eslint/parser@6.18.0)(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-3lqEvQUdCozi6d1mddWqd+kf8KxmGq2Plzx36BlkjuQe3rSTm/O98cLf0A4uDO+a5N1KD2SeEEl6fW97YHY+6w==} + /@typescript-eslint/eslint-plugin@6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-nISDRYnnIpk7VCFrGcu1rnZfM1Dh9LRHnfgdkjcbi/l7g16VYRri3TjXi9Ir4lOZSw5N/gnV/3H7jIPQ8Q4daA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -2087,11 +2087,11 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.18.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/scope-manager': 6.18.0 - '@typescript-eslint/type-utils': 6.18.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/utils': 6.18.0(eslint@8.56.0)(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.18.0 + '@typescript-eslint/parser': 6.18.1(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.18.1 + '@typescript-eslint/type-utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.18.1 debug: 4.3.4 eslint: 8.56.0 graphemer: 1.4.0 @@ -2104,8 +2104,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.18.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-v6uR68SFvqhNQT41frCMCQpsP+5vySy6IdgjlzUWoo7ALCnpaWYcz/Ij2k4L8cEsL0wkvOviCMpjmtRtHNOKzA==} + /@typescript-eslint/parser@6.18.1(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-zct/MdJnVaRRNy9e84XnVtRv9Vf91/qqe+hZJtKanjojud4wAVy/7lXxJmMyX6X6J+xc6c//YEWvpeif8cAhWA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -2114,10 +2114,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.18.0 - '@typescript-eslint/types': 6.18.0 - '@typescript-eslint/typescript-estree': 6.18.0(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.18.0 + '@typescript-eslint/scope-manager': 6.18.1 + '@typescript-eslint/types': 6.18.1 + '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.18.1 debug: 4.3.4 eslint: 8.56.0 typescript: 5.3.3 @@ -2125,16 +2125,16 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager@6.18.0: - resolution: {integrity: sha512-o/UoDT2NgOJ2VfHpfr+KBY2ErWvCySNUIX/X7O9g8Zzt/tXdpfEU43qbNk8LVuWUT2E0ptzTWXh79i74PP0twA==} + /@typescript-eslint/scope-manager@6.18.1: + resolution: {integrity: sha512-BgdBwXPFmZzaZUuw6wKiHKIovms97a7eTImjkXCZE04TGHysG+0hDQPmygyvgtkoB/aOQwSM/nWv3LzrOIQOBw==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.18.0 - '@typescript-eslint/visitor-keys': 6.18.0 + '@typescript-eslint/types': 6.18.1 + '@typescript-eslint/visitor-keys': 6.18.1 dev: true - /@typescript-eslint/type-utils@6.18.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-ZeMtrXnGmTcHciJN1+u2CigWEEXgy1ufoxtWcHORt5kGvpjjIlK9MUhzHm4RM8iVy6dqSaZA/6PVkX6+r+ChjQ==} + /@typescript-eslint/type-utils@6.18.1(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-wyOSKhuzHeU/5pcRDP2G2Ndci+4g653V43gXTpt4nbyoIOAASkGDA9JIAgbQCdCkcr1MvpSYWzxTz0olCn8+/Q==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -2143,8 +2143,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.18.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.18.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3) + '@typescript-eslint/utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3) debug: 4.3.4 eslint: 8.56.0 ts-api-utils: 1.0.3(typescript@5.3.3) @@ -2153,13 +2153,13 @@ packages: - supports-color dev: true - /@typescript-eslint/types@6.18.0: - resolution: {integrity: sha512-/RFVIccwkwSdW/1zeMx3hADShWbgBxBnV/qSrex6607isYjj05t36P6LyONgqdUrNLl5TYU8NIKdHUYpFvExkA==} + /@typescript-eslint/types@6.18.1: + resolution: {integrity: sha512-4TuMAe+tc5oA7wwfqMtB0Y5OrREPF1GeJBAjqwgZh1lEMH5PJQgWgHGfYufVB51LtjD+peZylmeyxUXPfENLCw==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.18.0(typescript@5.3.3): - resolution: {integrity: sha512-klNvl+Ql4NsBNGB4W9TZ2Od03lm7aGvTbs0wYaFYsplVPhr+oeXjlPZCDI4U9jgJIDK38W1FKhacCFzCC+nbIg==} + /@typescript-eslint/typescript-estree@6.18.1(typescript@5.3.3): + resolution: {integrity: sha512-fv9B94UAhywPRhUeeV/v+3SBDvcPiLxRZJw/xZeeGgRLQZ6rLMG+8krrJUyIf6s1ecWTzlsbp0rlw7n9sjufHA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -2167,8 +2167,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.18.0 - '@typescript-eslint/visitor-keys': 6.18.0 + '@typescript-eslint/types': 6.18.1 + '@typescript-eslint/visitor-keys': 6.18.1 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -2180,8 +2180,8 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.18.0(eslint@8.56.0)(typescript@5.3.3): - resolution: {integrity: sha512-wiKKCbUeDPGaYEYQh1S580dGxJ/V9HI7K5sbGAVklyf+o5g3O+adnS4UNJajplF4e7z2q0uVBaTdT/yLb4XAVA==} + /@typescript-eslint/utils@6.18.1(eslint@8.56.0)(typescript@5.3.3): + resolution: {integrity: sha512-zZmTuVZvD1wpoceHvoQpOiewmWu3uP9FuTWo8vqpy2ffsmfCE8mklRPi+vmnIYAIk9t/4kOThri2QCDgor+OpQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -2189,9 +2189,9 @@ packages: '@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0) '@types/json-schema': 7.0.15 '@types/semver': 7.5.6 - '@typescript-eslint/scope-manager': 6.18.0 - '@typescript-eslint/types': 6.18.0 - '@typescript-eslint/typescript-estree': 6.18.0(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.18.1 + '@typescript-eslint/types': 6.18.1 + '@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3) eslint: 8.56.0 semver: 7.5.4 transitivePeerDependencies: @@ -2199,11 +2199,11 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys@6.18.0: - resolution: {integrity: sha512-1wetAlSZpewRDb2h9p/Q8kRjdGuqdTAQbkJIOUMLug2LBLG+QOjiWoSj6/3B/hA9/tVTFFdtiKvAYoYnSRW/RA==} + /@typescript-eslint/visitor-keys@6.18.1: + resolution: {integrity: sha512-/kvt0C5lRqGoCfsbmm7/CwMqoSkY3zzHLIjdhHZQW3VFrnz7ATecOHR7nb7V+xn4286MBxfnQfQhAmCI0u+bJA==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.18.0 + '@typescript-eslint/types': 6.18.1 eslint-visitor-keys: 3.4.3 dev: true @@ -2892,8 +2892,8 @@ packages: esutils: 2.0.3 dev: true - /dom-renderer@2.0.4(typescript@5.3.3): - resolution: {integrity: sha512-mV5EXc1+amGBUVN3YVbob30lmyavSzCgKv4Wcq9uisBsyCFtdL6H9PBWrg9bqQhr9nBPSuDoIH15yEvqPWKKDQ==} + /dom-renderer@2.0.5(typescript@5.3.3): + resolution: {integrity: sha512-9N5Da1qcv2y2i8UV77aJkHOHScWhmmiJIuWaiqgmIcPRn15IhQLbagJIt5Dhg4x08rf6bT9WmOccPKHMAPJDDw==} dependencies: tslib: 2.6.2 web-utility: 4.1.3(typescript@5.3.3) @@ -5631,16 +5631,16 @@ packages: engines: {node: '>=14.16'} dev: true - /typedoc-plugin-mdn-links@3.1.11(typedoc@0.25.6): + /typedoc-plugin-mdn-links@3.1.11(typedoc@0.25.7): resolution: {integrity: sha512-WmEt+FD6HKXCkcbQEmdVKtkEEJb9SLmUnGnKwKM9U5grocy9TOUQ1GmO2cXnzMBQ2i/o/wF8LK4JQf5Vch3DKg==} peerDependencies: typedoc: '>= 0.23.14 || 0.24.x || 0.25.x' dependencies: - typedoc: 0.25.6(typescript@5.3.3) + typedoc: 0.25.7(typescript@5.3.3) dev: true - /typedoc@0.25.6(typescript@5.3.3): - resolution: {integrity: sha512-1rdionQMpOkpA58qfym1J+YD+ukyA1IEIa4VZahQI2ZORez7dhOvEyUotQL/8rSoMBopdzOS+vAIsORpQO4cTA==} + /typedoc@0.25.7(typescript@5.3.3): + resolution: {integrity: sha512-m6A6JjQRg39p2ZVRIN3NKXgrN8vzlHhOS+r9ymUYtcUP/TIQPvWSq7YgE5ZjASfv5Vd5BW5xrir6Gm2XNNcOow==} engines: {node: '>= 16'} hasBin: true peerDependencies: diff --git a/source/Async.tsx b/source/Async.tsx index 1199dca..77dab7a 100644 --- a/source/Async.tsx +++ b/source/Async.tsx @@ -30,8 +30,7 @@ export class AsyncBox extends HTMLElement { @observable accessor loader: AsyncBoxProps['loader']; - @observable - accessor component: ComponentTag; + component: ComponentTag; @observable accessor delegatedProps: AsyncBoxProps['delegatedProps']; diff --git a/source/WebCell.tsx b/source/WebCell.tsx index 32ec502..566df34 100644 --- a/source/WebCell.tsx +++ b/source/WebCell.tsx @@ -3,6 +3,7 @@ import { CustomElement, DelegateEventHandler, delegate, + isEmpty, stringifyDOM } from 'web-utility'; @@ -80,7 +81,7 @@ export function component(meta: ComponentMeta) { update() { const vNode = this.render?.(); - if (vNode) this.renderer.render(vNode, this.root); + this.renderer.render(isEmpty(vNode) ? <> : vNode, this.root); } disconnectedCallback() { From 67fb43d6a868f0341b69e4470c436fd7e1b10193 Mon Sep 17 00:00:00 2001 From: TechQuery Date: Wed, 10 Jan 2024 11:51:28 +0800 Subject: [PATCH 2/2] [optimize] update MobX data to Props for MobX 6.11+ [fix] Function & Async components rendering --- ReadMe.md | 243 +++++++++++++++++++++++------------------ legacy/utility/MobX.ts | 12 -- preview/Async.tsx | 6 + preview/Home.tsx | 8 +- preview/tsconfig.json | 1 + source/Async.tsx | 15 ++- source/MobX.ts | 15 +++ source/decorator.ts | 6 + source/index.ts | 1 + 9 files changed, 186 insertions(+), 121 deletions(-) delete mode 100644 legacy/utility/MobX.ts create mode 100644 preview/Async.tsx create mode 100644 source/MobX.ts diff --git a/ReadMe.md b/ReadMe.md index 2139061..2c82cee 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -2,49 +2,53 @@ # WebCell -[Web Components][1] engine based on VDOM, [JSX][2], [MobX][12] & [TypeScript][3] +[Web Components][1] engine based on VDOM, [JSX][2], [MobX][3] & [TypeScript][4] -[![NPM Dependency](https://img.shields.io/librariesio/github/EasyWebApp/WebCell.svg)][4] -[![CI & CD](https://github.com/EasyWebApp/WebCell/actions/workflows/main.yml/badge.svg)][5] +[![NPM Dependency](https://img.shields.io/librariesio/github/EasyWebApp/WebCell.svg)][5] +[![CI & CD](https://github.com/EasyWebApp/WebCell/actions/workflows/main.yml/badge.svg)][6] -[![Anti 996 license](https://img.shields.io/badge/license-Anti%20996-blue.svg)][6] -[![jaywcjlove/sb](https://jaywcjlove.github.io/sb/ico/awesome.svg)][7] +[![Anti 996 license](https://img.shields.io/badge/license-Anti%20996-blue.svg)][7] +[![jaywcjlove/sb](https://jaywcjlove.github.io/sb/ico/awesome.svg)][8] -[![Slideshow](https://img.shields.io/badge/learn-Slideshow-blue)][8] -[![Gitter](https://badges.gitter.im/EasyWebApp/community.svg)][9] +[![Slideshow](https://img.shields.io/badge/learn-Slideshow-blue)][9] +[![Gitter](https://badges.gitter.im/EasyWebApp/community.svg)][10] -[![Edit WebCell demo](https://codesandbox.io/static/img/play-codesandbox.svg)][10] +[![Edit WebCell demo](https://codesandbox.io/static/img/play-codesandbox.svg)][11] -[![NPM](https://nodei.co/npm/web-cell.png?downloads=true&downloadRank=true&stars=true)][11] +[![NPM](https://nodei.co/npm/web-cell.png?downloads=true&downloadRank=true&stars=true)][12] ## Feature ### Engines comparison -| feature | WebCell 3 | WebCell 2 | React | Vue | -| :-----------: | :------------------: | :------------------: | :---------------------------: | :---------------------------------: | -| JS language | TypeScript 5 | TypeScript 4 | ECMAScript or TypeScript | ECMAScript or TypeScript | -| JS syntax | ES decorator stage-3 | ES decorator stage-2 | | | -| XML syntax | JSX import | JSX factory | JSX factory/import | HTML/Vue template or JSX (optional) | -| DOM API | Web components | Web components | HTML 5+ | HTML 5+ | -| view renderer | DOM renderer 2 | SnabbDOM | (built-in) | SnabbDOM (forked) | -| state API | MobX `@observable` | `this.state` | `this.state` or `useState()` | `this.$data` or `ref()` | -| props API | MobX `@observable` | `@watch` | `this.props` or `props => {}` | `this.$props` or `defineProps()` | -| state manager | MobX 6+ | MobX 4/5 | Redux | VueX | -| page router | JSX tags | JSX tags + JSON data | JSX tags | JSON data | -| asset bundler | Parcel 2 | Parcel 1 | webpack | Vite | +| feature | WebCell 3 | WebCell 2 | React | Vue | +| :-----------: | :------------------------: | :------------------: | :---------------------------: | :---------------------------------: | +| JS language | [TypeScript 5][13] | TypeScript 4 | ECMAScript or TypeScript | ECMAScript or TypeScript | +| JS syntax | [ES decorator stage-3][14] | ES decorator stage-2 | | | +| XML syntax | [JSX import][15] | JSX factory | JSX factory/import | HTML/Vue template or JSX (optional) | +| DOM API | [Web components][16] | Web components | HTML 5+ | HTML 5+ | +| view renderer | [DOM renderer 2][17] | SnabbDOM | (built-in) | SnabbDOM (forked) | +| state API | [MobX `@observable`][18] | `this.state` | `this.state` or `useState()` | `this.$data` or `ref()` | +| props API | MobX `@observable` | `@watch` | `this.props` or `props => {}` | `this.$props` or `defineProps()` | +| state manager | [MobX 6+][19] | MobX 4/5 | Redux | VueX | +| page router | [JSX][20] tags | JSX tags + JSON data | JSX tags | JSON data | +| asset bundler | [Parcel 2][21] | Parcel 1 | webpack | Vite | + +## Installation -## Usage +```shell +npm install dom-renderer mobx web-cell +``` -Demo & **GitHub template**: https://web-cell.dev/scaffold/ +## Web browser usage + +[Demo & **GitHub template**][22] ### Project bootstrap -#### Command +#### Tool chain ```shell -npm init -y -npm install dom-renderer mobx web-cell npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -D ``` @@ -65,6 +69,7 @@ npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -D { "compilerOptions": { "target": "ES6", + "module": "ES2020", "moduleResolution": "Node", "useDefineForClassFields": true, "jsx": "react-jsx", @@ -91,11 +96,9 @@ npm install parcel @parcel/config-default @parcel/transformer-typescript-tsc -D - - + - - + ``` ### Function component @@ -108,7 +111,7 @@ const Hello: FC = ({ children = 'World' }) => (

Hello, {children}!

); -new DOMRenderer().renderer(WebCell); +new DOMRenderer().render(WebCell); ``` ### Class component @@ -133,7 +136,7 @@ class Hello extends HTMLElement { } } -new DOMRenderer().renderer( +new DOMRenderer().render( <> WebCell {/* or */} @@ -167,9 +170,9 @@ class Hello extends HTMLElement { } } -new DOMRenderer().renderer(); +new DOMRenderer().render(); -// or for HTML tag names in TypeScript +// or for HTML tag props in TypeScript declare global { namespace JSX { @@ -178,7 +181,7 @@ declare global { } } } -new DOMRenderer().renderer(); +new DOMRenderer().render(); ``` ### Inner state @@ -203,7 +206,7 @@ const Counter: FC = observer(() => ( )); -new DOMRenderer().renderer(); +new DOMRenderer().render(); ``` #### Class component @@ -226,7 +229,7 @@ class Counter extends HTMLElement { } } -new DOMRenderer().renderer(); +new DOMRenderer().render(); ``` ### CSS scope @@ -427,7 +430,7 @@ class MyField extends HTMLElement { } } -new DOMRenderer().renderer( +new DOMRenderer().render(
@@ -453,99 +456,131 @@ export default AsyncTag; ```tsx const AsyncTag = lazy(() => import('./AsyncTag')); -new DOMRenderer().renderer(); +new DOMRenderer().render(); ``` -## Basic knowledge +## Node.js usage -- [Web components](https://developer.mozilla.org/en-US/docs/Web/Web_Components) -- [Custom elements](https://developers.google.cn/web/fundamentals/web-components/customelements) -- [Shadow DOM](https://developers.google.cn/web/fundamentals/web-components/shadowdom) -- [Element Internals](https://web.dev/more-capable-form-controls/) -- [CSS variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables) -- [ECMAScript 6+](http://es6-features.org/) -- [TypeScript 5+][3] +### Tool chain -## Life Cycle hooks - -1. [`connectedCallback`](https://web-cell.dev/web-utility/interfaces/DOM_type.CustomElement.html#connectedCallback) - -2. [`disconnectedCallback`](https://web-cell.dev/web-utility/interfaces/DOM_type.CustomElement.html#disconnectedCallback) - -3. [`attributeChangedCallback`](https://web-cell.dev/web-utility/interfaces/DOM_type.CustomElement.html#attributeChangedCallback) +```shell +npm install jsdom element-internals-polyfill +``` -4. [`adoptedCallback`](https://web-cell.dev/web-utility/interfaces/DOM_type.CustomElement.html#adoptedCallback) +### Polyfill -5. [`updatedCallback`](https://web-cell.dev/WebCell/interfaces/WebCell.WebCellComponent.html#updatedCallback) +```js +import 'web-cell/polyfill'; +``` -6. [`formAssociatedCallback`](https://web-cell.dev/web-utility/interfaces/DOM_type.CustomFormElement.html#formAssociatedCallback) +## Basic knowledge -7. [`formDisabledCallback`](https://web-cell.dev/web-utility/interfaces/DOM_type.CustomFormElement.html#formDisabledCallback) +- [Web components][23] +- [Custom elements][24] +- [Shadow DOM][25] +- [Element Internals][26] +- [CSS variables][27] +- [ECMAScript 6+][28] +- [TypeScript 5+][29] -8. [`formResetCallback`](https://web-cell.dev/web-utility/interfaces/DOM_type.CustomFormElement.html#formResetCallback) +## Life Cycle hooks -9. [`formStateRestoreCallback`](https://web-cell.dev/web-utility/interfaces/DOM_type.CustomFormElement.html#formStateRestoreCallback) +1. [`connectedCallback`][30] +2. [`disconnectedCallback`][31] +3. [`attributeChangedCallback`][32] +4. [`adoptedCallback`][33] +5. [`updatedCallback`][34] +6. [`formAssociatedCallback`][35] +7. [`formDisabledCallback`][36] +8. [`formResetCallback`][37] +9. [`formStateRestoreCallback`][38] ## Scaffolds -1. [Basic](https://github.com/EasyWebApp/scaffold) - -2. [DashBoard](https://github.com/EasyWebApp/DashBoard) - -3. [Static site](https://github.com/EasyWebApp/mark-wiki) +1. [Basic][39] +2. [DashBoard][40] +3. [Static site][41] ## Ecosystem We recommend these libraries to use with WebCell: -- **State management**: [MobX][12] (also powered by **TypeScript** & **Decorator**) - -- **Router**: [Cell Router][13] - +- **State management**: [MobX][42] (also powered by **TypeScript** & **Decorator**) +- **Router**: [Cell Router][43] - **UI components** - - [BootCell][14] (based on **BootStrap v4**) - - [Material Cell][15] (based on **Material Design**) - - [GitHub Web Widget][16] - -- **HTTP request**: [KoAJAX][17] (based on **Koa**-like middlewares) - -- **Utility**: [Web utility][18] (Methods & Types) - -- **Event stream**: [Iterable Observer][19] (**Observable** proposal) + - [BootCell][44] (based on **BootStrap v4**) + - [Material Cell][45] (based on **Material Design**) + - [GitHub Web Widget][46] -- **MarkDown integration**: [MarkCell][20] (**MDX** implement) +- **HTTP request**: [KoAJAX][47] (based on **Koa**\-like middlewares) +- **Utility**: [Web utility][48] (Methods & Types) +- **Event stream**: [Iterable Observer][49] (**Observable** proposal) +- **MarkDown integration**: [MarkCell][50] (**MDX** implement) ## Roadmap -- [x] [Extend **Build-in Elements** with Virtual DOM](https://github.com/snabbdom/snabbdom/pull/829) -- [x] [Server-side Render](https://web.dev/declarative-shadow-dom/) -- [x] [Async Component loading](https://reactjs.org/docs/react-api.html#reactlazy) +- [x] [Extend **Build-in Elements** with Virtual DOM][51] +- [x] [Server-side Render][52] +- [x] [Async Component loading][53] ## More guides -1. [v2 to v3 migration][21] -2. [Development contribution][22] +1. [v2 to v3 migration][54] +2. [Development contribution][55] [1]: https://www.webcomponents.org/ [2]: https://facebook.github.io/jsx/ -[3]: https://www.typescriptlang.org -[4]: https://libraries.io/npm/web-cell -[5]: https://github.com/EasyWebApp/WebCell/actions/workflows/main.yml -[6]: https://github.com/996icu/996.ICU/blob/master/LICENSE -[7]: https://github.com/jaywcjlove/awesome-uikit -[8]: https://tech-query.me/programming/web-components-practise/slide.html -[9]: https://gitter.im/EasyWebApp/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge -[10]: https://codesandbox.io/s/webcell-demo-9gyll?autoresize=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2FClock.tsx&theme=dark -[11]: https://nodei.co/npm/web-cell/ -[12]: https://github.com/mobxjs/mobx/blob/mobx4and5/docs/ -[13]: https://web-cell.dev/cell-router/ -[14]: https://bootstrap.web-cell.dev/ -[15]: https://material.web-cell.dev/ -[16]: https://tech-query.me/GitHub-Web-Widget/ -[17]: https://web-cell.dev/KoAJAX/ -[18]: https://web-cell.dev/web-utility/ -[19]: https://web-cell.dev/iterable-observer/ -[20]: https://github.com/EasyWebApp/MarkCell -[21]: https://github.com/EasyWebApp/WebCell/blob/main/Migrating.md -[22]: https://github.com/EasyWebApp/WebCell/blob/main/Contributing.md +[3]: https://mobx.js.org/ +[4]: https://www.typescriptlang.org/ +[5]: https://libraries.io/npm/web-cell +[6]: https://github.com/EasyWebApp/WebCell/actions/workflows/main.yml +[7]: https://github.com/996icu/996.ICU/blob/master/LICENSE +[8]: https://github.com/jaywcjlove/awesome-uikit +[9]: https://tech-query.me/programming/web-components-practise/slide.html +[10]: https://gitter.im/EasyWebApp/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge +[11]: https://codesandbox.io/s/webcell-demo-9gyll?autoresize=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2FClock.tsx&theme=dark +[12]: https://nodei.co/npm/web-cell/ +[13]: https://www.typescriptlang.org/ +[14]: https://github.com/tc39/proposal-decorators +[15]: https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html +[16]: https://www.webcomponents.org/ +[17]: https://github.com/EasyWebApp/DOM-Renderer +[18]: https://mobx.js.org/observable-state.html#observable +[19]: https://mobx.js.org/enabling-decorators.html +[20]: https://facebook.github.io/jsx/ +[21]: https://parceljs.org/ +[22]: https://web-cell.dev/scaffold/ +[23]: https://developer.mozilla.org/en-US/docs/Web/Web_Components +[24]: https://developers.google.cn/web/fundamentals/web-components/customelements +[25]: https://developers.google.cn/web/fundamentals/web-components/shadowdom +[26]: https://web.dev/more-capable-form-controls/ +[27]: https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_variables +[28]: http://es6-features.org/ +[29]: https://www.typescriptlang.org/ +[30]: https://web-cell.dev/web-utility/interfaces/DOM_type.CustomElement.html#connectedCallback +[31]: https://web-cell.dev/web-utility/interfaces/DOM_type.CustomElement.html#disconnectedCallback +[32]: https://web-cell.dev/web-utility/interfaces/DOM_type.CustomElement.html#attributeChangedCallback +[33]: https://web-cell.dev/web-utility/interfaces/DOM_type.CustomElement.html#adoptedCallback +[34]: https://web-cell.dev/WebCell/interfaces/WebCell.WebCellComponent.html#updatedCallback +[35]: https://web-cell.dev/web-utility/interfaces/DOM_type.CustomFormElement.html#formAssociatedCallback +[36]: https://web-cell.dev/web-utility/interfaces/DOM_type.CustomFormElement.html#formDisabledCallback +[37]: https://web-cell.dev/web-utility/interfaces/DOM_type.CustomFormElement.html#formResetCallback +[38]: https://web-cell.dev/web-utility/interfaces/DOM_type.CustomFormElement.html#formStateRestoreCallback +[39]: https://github.com/EasyWebApp/scaffold +[40]: https://github.com/EasyWebApp/DashBoard +[41]: https://github.com/EasyWebApp/mark-wiki +[42]: https://github.com/mobxjs/mobx/blob/mobx4and5/docs/ +[43]: https://web-cell.dev/cell-router/ +[44]: https://bootstrap.web-cell.dev/ +[45]: https://material.web-cell.dev/ +[46]: https://tech-query.me/GitHub-Web-Widget/ +[47]: https://web-cell.dev/KoAJAX/ +[48]: https://web-cell.dev/web-utility/ +[49]: https://web-cell.dev/iterable-observer/ +[50]: https://github.com/EasyWebApp/MarkCell +[51]: https://github.com/snabbdom/snabbdom/pull/829 +[52]: https://web.dev/declarative-shadow-dom/ +[53]: https://reactjs.org/docs/react-api.html#reactlazy +[54]: https://github.com/EasyWebApp/WebCell/blob/main/Migrating.md +[55]: https://github.com/EasyWebApp/WebCell/blob/main/Contributing.md diff --git a/legacy/utility/MobX.ts b/legacy/utility/MobX.ts deleted file mode 100644 index 68ca45c..0000000 --- a/legacy/utility/MobX.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { toJS } from 'mobx'; - -export function getMobxData>(observable: any): T { - for (const key of Object.getOwnPropertySymbols(observable)) { - const store = observable[key]?.values; - - if (store instanceof Map) - return Object.fromEntries( - Array.from(store, ([key, { value }]) => [key, toJS(value)]) - ); - } -} diff --git a/preview/Async.tsx b/preview/Async.tsx new file mode 100644 index 0000000..d2244b2 --- /dev/null +++ b/preview/Async.tsx @@ -0,0 +1,6 @@ +import { FC, PropsWithChildren } from '../source'; + +const Async: FC = ({ children }) => ( +
Async load: {children}
+); +export default Async; diff --git a/preview/Home.tsx b/preview/Home.tsx index b169320..74ae010 100644 --- a/preview/Home.tsx +++ b/preview/Home.tsx @@ -1,9 +1,11 @@ import { formToJSON } from 'web-utility'; -import { FC, PropsWithChildren } from '../source'; +import { FC, PropsWithChildren, lazy } from '../source'; import { ClassClock, FunctionClock } from './Clock'; import { TestField } from './Field'; +const Async = lazy(() => import('./Async')); + const Hello: FC = ({ children }) => (

Hello {children}!

); @@ -23,6 +25,7 @@ export const HomePage = () => ( . +
  • @@ -31,6 +34,7 @@ export const HomePage = () => (
+ @@ -41,5 +45,7 @@ export const HomePage = () => ( + + content ); diff --git a/preview/tsconfig.json b/preview/tsconfig.json index 2b48f35..db699ce 100644 --- a/preview/tsconfig.json +++ b/preview/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "target": "ES6", + "module": "ES2020", "moduleResolution": "Node", "useDefineForClassFields": true, "jsx": "react-jsx", diff --git a/source/Async.tsx b/source/Async.tsx index 77dab7a..04334d2 100644 --- a/source/Async.tsx +++ b/source/Async.tsx @@ -1,9 +1,11 @@ import { observable } from 'mobx'; -import { HTMLProps } from 'web-utility'; +import { JsxProps } from 'dom-renderer'; import { ClassComponent, WebCell, component } from './WebCell'; import { + FC, FunctionComponent, + PropsWithChildren, WebCellComponent, observer, reaction @@ -11,7 +13,7 @@ import { export type ComponentTag = string | WebCellComponent; -export type WebCellProps = HTMLProps; +export type WebCellProps = JsxProps; export interface AsyncBoxProps extends WebCellProps { loader: () => Promise; @@ -30,7 +32,8 @@ export class AsyncBox extends HTMLElement { @observable accessor loader: AsyncBoxProps['loader']; - component: ComponentTag; + @observable + accessor component: FC; @observable accessor delegatedProps: AsyncBoxProps['delegatedProps']; @@ -42,8 +45,12 @@ export class AsyncBox extends HTMLElement { @reaction((element: AsyncBox) => element.loader) protected async load() { this.component = undefined; - this.component = await this.loader(); + const Tag = await this.loader(); + + this.component = ({ children, ...props }) => ( + {children} + ); this.emit('load', this.component); } diff --git a/source/MobX.ts b/source/MobX.ts new file mode 100644 index 0000000..da8fe2e --- /dev/null +++ b/source/MobX.ts @@ -0,0 +1,15 @@ +import { DataObject } from 'dom-renderer'; +import { ObservableValue } from 'mobx/dist/internal'; + +export function getMobxData(observable: T) { + for (const key of Object.getOwnPropertySymbols(observable)) { + const store = observable[key as keyof T]?.values_ as Map< + string, + ObservableValue + >; + if (store instanceof Map) + return Object.fromEntries( + Array.from(store, ([key, { value_ }]) => [key, value_]) + ) as T; + } +} diff --git a/source/decorator.ts b/source/decorator.ts index 4a2845a..dd2bd3a 100644 --- a/source/decorator.ts +++ b/source/decorator.ts @@ -14,6 +14,7 @@ import { } from 'web-utility'; import { ClassComponent } from './WebCell'; +import { getMobxData } from './MobX'; export type PropsWithChildren

= P & { children?: JsxChildren; @@ -33,6 +34,7 @@ function wrapFunction

(func: FC

) { }); const { unRef } = tree; + tree.ref = node => (tree.node = node); tree.unRef = node => (disposer(), unRef?.(node)); return tree; @@ -52,6 +54,10 @@ function wrapClass(Component: T) { { protected disposers: IReactionDisposer[] = []; + get props() { + return getMobxData(this); + } + constructor() { super(); diff --git a/source/index.ts b/source/index.ts index 99184dd..a68df65 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,4 +1,5 @@ export * from './decorator'; +export * from './MobX'; export * from './WebCell'; export * from './WebField'; export * from './Async';