Skip to content

Commit 654975d

Browse files
committed
[add] Async Component loading API
[add] MobX data reader utility [optimize] several details
1 parent ce16370 commit 654975d

File tree

11 files changed

+142
-21
lines changed

11 files changed

+142
-21
lines changed

Migrating.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# WebCell v2 to v3 migration
22

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

55
**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:
66

ReadMe.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ We recommend these libraries to use with WebCell:
223223

224224
- [x] [Extend **Build-in Elements** with Virtual DOM](https://github.com/snabbdom/snabbdom/pull/829)
225225
- [x] [Server-side Render](https://web.dev/declarative-shadow-dom/)
226+
- [x] [Async Component loading](https://reactjs.org/docs/react-api.html#reactlazy)
226227

227228
## More guides
228229

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "web-cell",
3-
"version": "3.0.0-alpha.1",
3+
"version": "3.0.0-alpha.2",
44
"description": "Web Components engine based on VDOM, JSX, MobX & TypeScript",
55
"keywords": [
66
"web",
@@ -27,7 +27,7 @@
2727
"types": "dist/index.d.ts",
2828
"dependencies": {
2929
"@swc/helpers": "^0.3.3",
30-
"mobx": "^5.15.7",
30+
"mobx": ">=4.0.0 <6.0.0",
3131
"snabbdom": "^3.3.1",
3232
"web-utility": "^3.4.2"
3333
},
@@ -58,7 +58,7 @@
5858
"prettier": "^2.5.1",
5959
"ts-jest": "^27.1.3",
6060
"ts-node": "^10.5.0",
61-
"typedoc": "^0.22.11",
61+
"typedoc": "^0.22.12",
6262
"typescript": "~4.3.5"
6363
},
6464
"scripts": {

source/Async.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { observable, reaction } from 'mobx';
2+
3+
import { ComponentTag, WebCellProps, FunctionComponent } from './utility';
4+
import { WebCellClass, WebCell } from './WebCell';
5+
import { component, observer } from './decorator';
6+
import { createCell } from './renderer';
7+
8+
export interface AsyncBoxProps extends WebCellProps {
9+
loader: () => Promise<ComponentTag>;
10+
}
11+
12+
@component({
13+
tagName: 'async-box'
14+
})
15+
@observer
16+
export class AsyncBox extends WebCell<AsyncBoxProps>() {
17+
@observable
18+
loader: AsyncBoxProps['loader'];
19+
20+
@observable
21+
component?: ComponentTag;
22+
23+
get delegatedProps() {
24+
return Object.fromEntries(
25+
Object.entries(Object.getOwnPropertyDescriptors(this))
26+
.map(([key, { value }]) => value != null && [key, value])
27+
.filter(Boolean)
28+
);
29+
}
30+
connectedCallback() {
31+
if (this.load instanceof Function) this.load();
32+
33+
this.disposers.push(reaction(() => this.loader, this.load));
34+
}
35+
36+
protected load = async () => {
37+
this.component = undefined;
38+
this.component = await this.loader();
39+
40+
this.emit('load', this.component);
41+
};
42+
43+
render() {
44+
const {
45+
component: Tag,
46+
props: { defaultSlot, ...props },
47+
delegatedProps
48+
} = this;
49+
50+
return (
51+
Tag && (
52+
<Tag {...delegatedProps} {...props}>
53+
{defaultSlot}
54+
</Tag>
55+
)
56+
);
57+
}
58+
}
59+
60+
type GetAsyncProps<T> = T extends () => Promise<{
61+
default: FunctionComponent<infer P> | WebCellClass<infer P>;
62+
}>
63+
? P
64+
: {};
65+
66+
export function lazy<
67+
T extends () => Promise<{ default: FunctionComponent | WebCellClass }>
68+
>(loader: T) {
69+
return (props: GetAsyncProps<T>) => (
70+
<AsyncBox {...props} loader={async () => (await loader()).default} />
71+
);
72+
}

source/WebCell.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
} from 'web-utility';
1111
import { IReactionDisposer, observable } from 'mobx';
1212

13-
import { WebCellProps } from './utility';
13+
import { WebCellProps, getMobxData } from './utility';
1414
import { ComponentMeta, DOMEventDelegater } from './decorator';
15-
import { Fragment, createCell, render } from './renderer';
15+
import { createCell, render } from './renderer';
1616

1717
export interface WebCellComponent<P extends WebCellProps = WebCellProps>
1818
extends CustomElement {
@@ -26,7 +26,7 @@ export interface WebCellComponent<P extends WebCellProps = WebCellProps>
2626
disposers?: IReactionDisposer[];
2727
syncPropAttr?(name: string): void;
2828
defaultSlot?: JSX.Element;
29-
render?(): JSX.Element;
29+
render?(): JSX.Element | undefined;
3030
/**
3131
* Called after rendering
3232
*/
@@ -54,11 +54,14 @@ export function WebCell<P extends WebCellProps = WebCellProps>(
5454

5555
readonly internals?: ElementInternals;
5656
readonly root: DocumentFragment | HTMLElement;
57-
readonly props: P = {} as P;
57+
58+
get props() {
59+
return getMobxData<P>(this);
60+
}
5861
readonly disposers: IReactionDisposer[] = [];
5962

6063
@observable
61-
defaultSlot = (<></>);
64+
defaultSlot?: JSX.Element;
6265

6366
[key: string]: any;
6467

@@ -110,7 +113,11 @@ export function WebCell<P extends WebCellProps = WebCellProps>(
110113
}
111114

112115
update() {
113-
render(this.render(), this.root);
116+
const tree = this.render();
117+
118+
if (!tree) return;
119+
120+
render(tree, this.root);
114121

115122
this.updatedCallback?.();
116123
}

source/utility/MobX.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { toJS } from 'mobx';
2+
3+
export function getMobxData<T extends Record<string, any>>(observable: any): T {
4+
for (const key of Object.getOwnPropertySymbols(observable)) {
5+
const store = observable[key]?.values;
6+
7+
if (store instanceof Map)
8+
return Object.fromEntries(
9+
Array.from(store, ([key, { value }]) => [key, toJS(value)])
10+
);
11+
}
12+
}

source/utility/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './vDOM';
2+
export * from './MobX';

source/utility/vDOM.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export type WebCellProps<T extends HTMLElement = HTMLElement> = VDOMData<T> &
2020
is?: ComponentMeta['tagName'];
2121
};
2222

23-
export type FunctionComponent<P = {}, T extends HTMLElement = HTMLElement> = (
24-
props: P & WebCellProps<T>
23+
export type FunctionComponent<P extends WebCellProps = WebCellProps> = (
24+
props: P
2525
) => VNode;
2626

2727
export type ComponentTag =

test/Async.spec.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import 'element-internals-polyfill';
2+
import { sleep } from 'web-utility';
3+
4+
import { WebCellProps } from '../source/utility/vDOM';
5+
import { createCell, render } from '../source/renderer';
6+
import { lazy } from '../source/Async';
7+
8+
describe('Async Box component', () => {
9+
it('should render an Async Component', async () => {
10+
const Async = lazy(async () => ({
11+
default: ({
12+
defaultSlot,
13+
...props
14+
}: WebCellProps<HTMLAnchorElement>) => (
15+
<a {...props}>{defaultSlot}</a>
16+
)
17+
}));
18+
render(<Async href="test">Test</Async>);
19+
20+
expect(document.body.innerHTML).toBe('<async-box></async-box>');
21+
22+
await sleep();
23+
24+
expect(document.body.innerHTML).toBe(
25+
'<async-box><a href="test">Test</a></async-box>'
26+
);
27+
});
28+
});

test/renderer.spec.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FunctionComponent } from '../source';
1+
import { WebCellProps, FunctionComponent } from '../source';
22
import {
33
createCell,
44
Fragment,
@@ -26,9 +26,9 @@ describe('Renderer', () => {
2626

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

3434
expect(hook).toBeCalledTimes(1);

0 commit comments

Comments
 (0)