diff --git a/.npmignore b/.npmignore index 17077e2..78f5ce2 100644 --- a/.npmignore +++ b/.npmignore @@ -1,9 +1,11 @@ .eslintrc.json jest.config.ts test/ +preview/ .parcel-cache/ Contributing.md docs/ .husky/ .github/ -.vscode/ \ No newline at end of file +.vscode/ +.vercel/ \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md index b620bc4..63b456f 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -45,7 +45,7 @@ Demo & **GitHub template**: https://web-cell.dev/scaffold/ ```shell npm init -y npm install dom-renderer mobx web-cell -npm install parcel -D +npm install parcel @parcel/config-default "@parcel/transformer-typescript-tsc" -D ``` #### `package.json` @@ -73,6 +73,17 @@ npm install parcel -D } ``` +#### `.parcelrc` + +```json +{ + "extends": "@parcel/config-default", + "transformers": { + "*.{ts,tsx}": ["@parcel/transformer-typescript-tsc"] + } +} +``` + #### `source/index.html` ```html diff --git a/preview/Clock.tsx b/preview/Clock.tsx new file mode 100644 index 0000000..904e1d3 --- /dev/null +++ b/preview/Clock.tsx @@ -0,0 +1,40 @@ +import { observable } from 'mobx'; +import { Second } from 'web-utility'; +import { component, observer } from '../source'; + +class ClockModel { + @observable + accessor time = new Date(); + + constructor() { + setInterval(() => (this.time = new Date()), Second); + } +} + +const clockStore = new ClockModel(); + +export const FunctionClock = observer(() => { + const { time } = clockStore; + + return ( + + ); +}); + +@component({ tagName: 'class-clock' }) +@observer +export class ClassClock extends HTMLElement { + state = new ClockModel(); + + render() { + const { time } = this.state; + + return ( + + ); + } +} diff --git a/preview/Home.tsx b/preview/Home.tsx index 9aa276f..08b9614 100644 --- a/preview/Home.tsx +++ b/preview/Home.tsx @@ -1,38 +1,27 @@ -import { configure, observable } from 'mobx'; -import { component, observer } from '../source'; +import { ClassClock, FunctionClock } from './Clock'; -configure({ enforceActions: 'never' }); - -@component({ tagName: 'home-page' }) -@observer -export class HomePage extends HTMLElement { - @observable - accessor time = ''; - - connectedCallback() { - setInterval(() => (this.time = new Date().toLocaleString()), 1000); - } - - render() { - const { time } = this; - - return ( - <> -

Hello Vanilla!

-
- We use the same configuration as Parcel to bundle this - sandbox, you can find more info about Parcel - - here - - . -
- - - ); - } -} +export const HomePage = () => ( + <> +

Hello Vanilla!

+
+ We use the same configuration as Parcel to bundle this sandbox, you + can find more info about Parcel + + here + + . +
+ + +); diff --git a/preview/index.tsx b/preview/index.tsx index 8a2b5a5..ce22449 100644 --- a/preview/index.tsx +++ b/preview/index.tsx @@ -1,5 +1,8 @@ import { DOMRenderer } from 'dom-renderer'; +import { configure } from 'mobx'; import { HomePage } from './Home'; +configure({ enforceActions: 'never' }); + new DOMRenderer().render(, document.querySelector('#app')!); diff --git a/source/index.ts b/source/index.ts index 4c646b4..7620e5d 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,10 +1,14 @@ -import { DOMRenderer, VNode } from 'dom-renderer'; +import { DOMRenderer, DataObject, VNode } from 'dom-renderer'; import { autorun } from 'mobx'; +import { CustomElement, isHTMLElementClass } from 'web-utility'; -interface ComponentMeta extends ElementDefinitionOptions { +export interface ComponentMeta extends ElementDefinitionOptions { tagName: string; } +/** + * Class decorator of Web components + */ export function component({ tagName, ...meta }: ComponentMeta) { return ( Class: T, @@ -35,18 +39,65 @@ export function component({ tagName, ...meta }: ComponentMeta) { }; } -export function observer( - Class: T, - _: ClassDecoratorContext -) { - class ObserverComponent extends (Class as CustomElementConstructor) { +export type FunctionComponent

= (props: P) => VNode; +export type FC

= FunctionComponent

; + +function wrapFunction

(func: FC

) { + return (props: P) => { + var tree: VNode, + renderer = new DOMRenderer(); + + const disposer = autorun(() => { + const newTree = func(props); + + tree = tree ? renderer.patch(tree, newTree) : newTree; + }); + const { unRef } = tree; + + tree.unRef = node => (disposer(), unRef?.(node)); + + return tree; + }; +} + +export type ClassComponent = CustomElementConstructor; + +function wrapClass(Class: T) { + class ObserverComponent + extends (Class as ClassComponent) + implements CustomElement + { + protected disposers = []; + constructor() { super(); const { update } = Object.getPrototypeOf(this); // @ts-ignore - this.update = () => autorun(() => update.call(this)); + this.update = () => + this.disposers.push(autorun(() => update.call(this))); + } + + disconnectedCallback() { + for (const disposer of this.disposers) disposer(); } } return ObserverComponent as unknown as T; } + +export type WebCellComponent = FunctionComponent | ClassComponent; + +/** + * Class decorator of Web components for MobX + */ +export function observer( + func: T, + _: ClassDecoratorContext +): T; +export function observer(func: T): T; +export function observer( + func: T, + _?: ClassDecoratorContext +) { + return isHTMLElementClass(func) ? wrapClass(func) : wrapFunction(func); +}