Skip to content

Commit

Permalink
[add] Async Component loading API
Browse files Browse the repository at this point in the history
[add] MobX data reader utility
[optimize] several details
  • Loading branch information
TechQuery committed Feb 20, 2022
1 parent ce16370 commit 654975d
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 21 deletions.
2 changes: 1 addition & 1 deletion Migrating.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# WebCell v2 to v3 migration

## "state" concept has been totally dropped
## React-style State has been totally dropped

**WebCell v3** is heavily inspired by [the **Local Observable State** idea of **MobX**][1], and [not only React][2], Web Components can be much easier to manage the **Inner State & Logic**, without any complex things:

Expand Down
1 change: 1 addition & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ We recommend these libraries to use with WebCell:

- [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)

## More guides

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web-cell",
"version": "3.0.0-alpha.1",
"version": "3.0.0-alpha.2",
"description": "Web Components engine based on VDOM, JSX, MobX & TypeScript",
"keywords": [
"web",
Expand All @@ -27,7 +27,7 @@
"types": "dist/index.d.ts",
"dependencies": {
"@swc/helpers": "^0.3.3",
"mobx": "^5.15.7",
"mobx": ">=4.0.0 <6.0.0",
"snabbdom": "^3.3.1",
"web-utility": "^3.4.2"
},
Expand Down Expand Up @@ -58,7 +58,7 @@
"prettier": "^2.5.1",
"ts-jest": "^27.1.3",
"ts-node": "^10.5.0",
"typedoc": "^0.22.11",
"typedoc": "^0.22.12",
"typescript": "~4.3.5"
},
"scripts": {
Expand Down
72 changes: 72 additions & 0 deletions source/Async.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { observable, reaction } from 'mobx';

import { ComponentTag, WebCellProps, FunctionComponent } from './utility';
import { WebCellClass, WebCell } from './WebCell';
import { component, observer } from './decorator';
import { createCell } from './renderer';

export interface AsyncBoxProps extends WebCellProps {
loader: () => Promise<ComponentTag>;
}

@component({
tagName: 'async-box'
})
@observer
export class AsyncBox extends WebCell<AsyncBoxProps>() {
@observable
loader: AsyncBoxProps['loader'];

@observable
component?: ComponentTag;

get delegatedProps() {
return Object.fromEntries(
Object.entries(Object.getOwnPropertyDescriptors(this))
.map(([key, { value }]) => value != null && [key, value])
.filter(Boolean)
);
}
connectedCallback() {
if (this.load instanceof Function) this.load();

this.disposers.push(reaction(() => this.loader, this.load));
}

protected load = async () => {
this.component = undefined;
this.component = await this.loader();

this.emit('load', this.component);
};

render() {
const {
component: Tag,
props: { defaultSlot, ...props },
delegatedProps
} = this;

return (
Tag && (
<Tag {...delegatedProps} {...props}>
{defaultSlot}
</Tag>
)
);
}
}

type GetAsyncProps<T> = T extends () => Promise<{
default: FunctionComponent<infer P> | WebCellClass<infer P>;
}>
? P
: {};

export function lazy<
T extends () => Promise<{ default: FunctionComponent | WebCellClass }>
>(loader: T) {
return (props: GetAsyncProps<T>) => (
<AsyncBox {...props} loader={async () => (await loader()).default} />
);
}
19 changes: 13 additions & 6 deletions source/WebCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
} from 'web-utility';
import { IReactionDisposer, observable } from 'mobx';

import { WebCellProps } from './utility';
import { WebCellProps, getMobxData } from './utility';
import { ComponentMeta, DOMEventDelegater } from './decorator';
import { Fragment, createCell, render } from './renderer';
import { createCell, render } from './renderer';

export interface WebCellComponent<P extends WebCellProps = WebCellProps>
extends CustomElement {
Expand All @@ -26,7 +26,7 @@ export interface WebCellComponent<P extends WebCellProps = WebCellProps>
disposers?: IReactionDisposer[];
syncPropAttr?(name: string): void;
defaultSlot?: JSX.Element;
render?(): JSX.Element;
render?(): JSX.Element | undefined;
/**
* Called after rendering
*/
Expand Down Expand Up @@ -54,11 +54,14 @@ export function WebCell<P extends WebCellProps = WebCellProps>(

readonly internals?: ElementInternals;
readonly root: DocumentFragment | HTMLElement;
readonly props: P = {} as P;

get props() {
return getMobxData<P>(this);
}
readonly disposers: IReactionDisposer[] = [];

@observable
defaultSlot = (<></>);
defaultSlot?: JSX.Element;

[key: string]: any;

Expand Down Expand Up @@ -110,7 +113,11 @@ export function WebCell<P extends WebCellProps = WebCellProps>(
}

update() {
render(this.render(), this.root);
const tree = this.render();

if (!tree) return;

render(tree, this.root);

this.updatedCallback?.();
}
Expand Down
12 changes: 12 additions & 0 deletions source/utility/MobX.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { toJS } from 'mobx';

export function getMobxData<T extends Record<string, any>>(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)])
);
}
}
1 change: 1 addition & 0 deletions source/utility/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './vDOM';
export * from './MobX';
4 changes: 2 additions & 2 deletions source/utility/vDOM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export type WebCellProps<T extends HTMLElement = HTMLElement> = VDOMData<T> &
is?: ComponentMeta['tagName'];
};

export type FunctionComponent<P = {}, T extends HTMLElement = HTMLElement> = (
props: P & WebCellProps<T>
export type FunctionComponent<P extends WebCellProps = WebCellProps> = (
props: P
) => VNode;

export type ComponentTag =
Expand Down
28 changes: 28 additions & 0 deletions test/Async.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'element-internals-polyfill';
import { sleep } from 'web-utility';

import { WebCellProps } from '../source/utility/vDOM';
import { createCell, render } from '../source/renderer';
import { lazy } from '../source/Async';

describe('Async Box component', () => {
it('should render an Async Component', async () => {
const Async = lazy(async () => ({
default: ({
defaultSlot,
...props
}: WebCellProps<HTMLAnchorElement>) => (
<a {...props}>{defaultSlot}</a>
)
}));
render(<Async href="test">Test</Async>);

expect(document.body.innerHTML).toBe('<async-box></async-box>');

await sleep();

expect(document.body.innerHTML).toBe(
'<async-box><a href="test">Test</a></async-box>'
);
});
});
8 changes: 4 additions & 4 deletions test/renderer.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FunctionComponent } from '../source';
import { WebCellProps, FunctionComponent } from '../source';
import {
createCell,
Fragment,
Expand Down Expand Up @@ -26,9 +26,9 @@ describe('Renderer', () => {

it('should call Function while DOM rendering', () => {
const hook = jest.fn();
const Test = jest.fn(() => <i ref={hook} />) as FunctionComponent<{
prop1: number;
}>;
const Test = jest.fn(() => <i ref={hook} />) as FunctionComponent<
{ prop1: number } & WebCellProps
>;
render(<Test prop1={1}>test</Test>);

expect(hook).toBeCalledTimes(1);
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3762,7 +3762,7 @@ minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==

mobx@^5.15.7:
"mobx@>=4.0.0 <6.0.0":
version "5.15.7"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.15.7.tgz#b9a5f2b6251f5d96980d13c78e9b5d8d4ce22665"
integrity sha512-wyM3FghTkhmC+hQjyPGGFdpehrcX1KOXsDuERhfK2YbJemkUhEB+6wzEN639T21onxlfYBmriA1PFnvxTUhcKw==
Expand Down Expand Up @@ -5033,10 +5033,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"

typedoc@^0.22.11:
version "0.22.11"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.11.tgz#a3d7f4577eef9fc82dd2e8f4e2915e69f884c250"
integrity sha512-pVr3hh6dkS3lPPaZz1fNpvcrqLdtEvXmXayN55czlamSgvEjh+57GUqfhAI1Xsuu/hNHUT1KNSx8LH2wBP/7SA==
typedoc@^0.22.12:
version "0.22.12"
resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.22.12.tgz#52a8bb0e77458dcbab35fb89e24b80160ba6558d"
integrity sha512-FcyC+YuaOpr3rB9QwA1IHOi9KnU2m50sPJW5vcNRPCIdecp+3bFkh7Rq5hBU1Fyn29UR2h4h/H7twZHWDhL0sw==
dependencies:
glob "^7.2.0"
lunr "^2.3.9"
Expand Down

0 comments on commit 654975d

Please sign in to comment.