Skip to content

Commit c1e6915

Browse files
committed
[add] Reaction decorator
[optimize] simplify Props Delegation of Async Box [fix] some detail bugs
1 parent 654975d commit c1e6915

File tree

8 files changed

+110
-51
lines changed

8 files changed

+110
-51
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "web-cell",
3-
"version": "3.0.0-alpha.2",
3+
"version": "3.0.0-beta.0",
44
"description": "Web Components engine based on VDOM, JSX, MobX & TypeScript",
55
"keywords": [
66
"web",
@@ -28,6 +28,7 @@
2828
"dependencies": {
2929
"@swc/helpers": "^0.3.3",
3030
"mobx": ">=4.0.0 <6.0.0",
31+
"regenerator-runtime": "^0.13.9",
3132
"snabbdom": "^3.3.1",
3233
"web-utility": "^3.4.2"
3334
},

source/Async.tsx

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { observable, reaction } from 'mobx';
1+
import { observable } from 'mobx';
22

33
import { ComponentTag, WebCellProps, FunctionComponent } from './utility';
44
import { WebCellClass, WebCell } from './WebCell';
5-
import { component, observer } from './decorator';
5+
import { component, observer, reaction } from './decorator';
66
import { createCell } from './renderer';
77

88
export interface AsyncBoxProps extends WebCellProps {
99
loader: () => Promise<ComponentTag>;
10+
delegatedProps?: WebCellProps;
1011
}
1112

1213
@component({
@@ -20,40 +21,28 @@ export class AsyncBox extends WebCell<AsyncBoxProps>() {
2021
@observable
2122
component?: ComponentTag;
2223

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-
}
24+
@observable
25+
delegatedProps?: AsyncBoxProps['delegatedProps'];
26+
3027
connectedCallback() {
31-
if (this.load instanceof Function) this.load();
28+
super.connectedCallback();
3229

33-
this.disposers.push(reaction(() => this.loader, this.load));
30+
this.load();
3431
}
3532

36-
protected load = async () => {
33+
@reaction((element: AsyncBox) => element.loader)
34+
protected async load() {
3735
this.component = undefined;
3836
this.component = await this.loader();
3937

4038
this.emit('load', this.component);
41-
};
39+
}
4240

4341
render() {
44-
const {
45-
component: Tag,
46-
props: { defaultSlot, ...props },
47-
delegatedProps
48-
} = this;
42+
const { component: Tag, props, delegatedProps } = this;
43+
const { defaultSlot, ...data } = { ...props, ...delegatedProps };
4944

50-
return (
51-
Tag && (
52-
<Tag {...delegatedProps} {...props}>
53-
{defaultSlot}
54-
</Tag>
55-
)
56-
);
45+
return Tag && <Tag {...data}>{defaultSlot}</Tag>;
5746
}
5847
}
5948

@@ -67,6 +56,9 @@ export function lazy<
6756
T extends () => Promise<{ default: FunctionComponent | WebCellClass }>
6857
>(loader: T) {
6958
return (props: GetAsyncProps<T>) => (
70-
<AsyncBox {...props} loader={async () => (await loader()).default} />
59+
<AsyncBox
60+
delegatedProps={props}
61+
loader={async () => (await loader()).default}
62+
/>
7163
);
7264
}

source/WebCell.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ import {
1111
import { IReactionDisposer, observable } from 'mobx';
1212

1313
import { WebCellProps, getMobxData } from './utility';
14-
import { ComponentMeta, DOMEventDelegater } from './decorator';
15-
import { createCell, render } from './renderer';
14+
import {
15+
ComponentMeta,
16+
DOMEventDelegater,
17+
ReactionDelegater
18+
} from './decorator';
19+
import { Fragment, createCell, render } from './renderer';
1620

1721
export interface WebCellComponent<P extends WebCellProps = WebCellProps>
1822
extends CustomElement {
@@ -40,6 +44,7 @@ export interface WebCellClass<P extends WebCellProps = WebCellProps>
4044
Partial<ComponentMeta>,
4145
Constructor<WebCellComponent<P>> {
4246
eventDelegaters?: DOMEventDelegater[];
47+
reactions?: ReactionDelegater[];
4348
}
4449

4550
export function WebCell<P extends WebCellProps = WebCellProps>(
@@ -51,6 +56,7 @@ export function WebCell<P extends WebCellProps = WebCellProps>(
5156
static mode?: ComponentMeta['mode'];
5257
static delegatesFocus?: ComponentMeta['delegatesFocus'];
5358
static eventDelegaters: DOMEventDelegater[] = [];
59+
static reactions: ReactionDelegater[] = [];
5460

5561
readonly internals?: ElementInternals;
5662
readonly root: DocumentFragment | HTMLElement;
@@ -113,11 +119,7 @@ export function WebCell<P extends WebCellProps = WebCellProps>(
113119
}
114120

115121
update() {
116-
const tree = this.render();
117-
118-
if (!tree) return;
119-
120-
render(tree, this.root);
122+
render(this.render() ?? <></>, this.root);
121123

122124
this.updatedCallback?.();
123125
}
@@ -129,6 +131,8 @@ export function WebCell<P extends WebCellProps = WebCellProps>(
129131
this.root.removeEventListener(type, this[method]);
130132

131133
for (const disposer of this.disposers) disposer();
134+
135+
this.disposers.length = 0;
132136
}
133137

134138
syncPropAttr(name: string) {

source/WebField.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { observable, reaction } from 'mobx';
1+
import { observable } from 'mobx';
22
import {
33
Constructor,
44
CustomFormElement,
55
CustomFormElementClass
66
} from 'web-utility';
77

88
import { WebCell, WebCellComponent } from './WebCell';
9-
import { attribute } from './decorator';
9+
import { attribute, reaction } from './decorator';
1010
import { WebCellProps } from './utility';
1111

1212
export type WebFieldProps<T extends HTMLElement = HTMLInputElement> =
@@ -27,13 +27,9 @@ export function WebField<
2727
class WebField extends WebCell<P>() implements WebFieldComponent<P> {
2828
static formAssociated = true;
2929

30-
connectedCallback() {
31-
this.disposers.push(
32-
reaction(
33-
() => this.value,
34-
value => this.internals.setFormValue(value)
35-
)
36-
);
30+
@reaction((element: WebFieldComponent<P>) => element.value)
31+
protected setValue(value: string) {
32+
this.internals.setFormValue(value);
3733
}
3834

3935
formDisabledCallback(disabled: boolean) {

source/decorator.ts

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import {
22
toCamelCase,
3+
toHyphenCase,
34
parseJSON,
45
isHTMLElementClass,
56
DelegateEventHandler
67
} from 'web-utility';
7-
import { autorun } from 'mobx';
8+
import { IReactionPublic, reaction as watch, autorun } from 'mobx';
89

910
import { FunctionComponent } from './utility/vDOM';
10-
import { WebCellClass, ComponentClass } from './WebCell';
11+
import { WebCellComponent, WebCellClass, ComponentClass } from './WebCell';
1112
import { VNode } from 'snabbdom';
1213
import { patch } from './renderer';
1314

@@ -49,16 +50,22 @@ function wrapClass<T extends ComponentClass>(Component: T) {
4950
// @ts-ignore
5051
return class ObserverTrait extends Component {
5152
connectedCallback() {
52-
const { observedAttributes = [] } = this
53+
const { observedAttributes = [], reactions } = this
5354
.constructor as WebCellClass;
5455

55-
this.disposers = [
56+
this.disposers.push(
5657
autorun(() => this.update()),
5758

5859
...observedAttributes.map(name =>
5960
autorun(() => this.syncPropAttr(name))
61+
),
62+
...reactions.map(({ methodKey, expression }) =>
63+
watch(
64+
r => expression(this, r),
65+
(this[methodKey] as ReactionHandler).bind(this)
66+
)
6067
)
61-
];
68+
);
6269
super.connectedCallback?.();
6370
}
6471

@@ -92,7 +99,33 @@ export function attribute<T extends InstanceType<WebCellClass>>(
9299
get: () => observedAttributes
93100
});
94101
}
95-
observedAttributes.push(key);
102+
observedAttributes.push(toHyphenCase(key));
103+
}
104+
105+
type ReactionHandler<I = any, O = any> = (
106+
data?: I,
107+
reaction?: IReactionPublic
108+
) => O;
109+
110+
export interface ReactionDelegater {
111+
methodKey: string;
112+
expression: ReactionHandler;
113+
}
114+
115+
export function reaction<C extends WebCellComponent, V>(
116+
expression: ReactionHandler<C, V>
117+
) {
118+
return (
119+
{ constructor }: C,
120+
key: string,
121+
meta: TypedPropertyDescriptor<ReactionHandler<V>>
122+
) => {
123+
(constructor as WebCellClass).reactions.push({
124+
methodKey: key,
125+
expression
126+
});
127+
return meta;
128+
};
96129
}
97130

98131
export interface DOMEventDelegater {

source/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ export * from './renderer';
2020
export * from './decorator';
2121
export * from './WebCell';
2222
export * from './WebField';
23+
export * from './Async';

source/renderer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function splitProps(tagName: string, raw: VDOMData) {
8282
if (key === 'is') data.is = raw[key];
8383
else if (key === 'ref')
8484
(data.hook ||= {}).insert = ({ elm }) =>
85-
raw[key](elm as HTMLElement);
85+
raw[key]?.(elm as HTMLElement);
8686
else if (key === 'className')
8787
data.class = Object.fromEntries(
8888
raw[key]

test/MobX.spec.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import 'element-internals-polyfill';
2+
import { sleep } from 'web-utility';
23
import { observable } from 'mobx';
34

4-
import { component, observer } from '../source/decorator';
5+
import { component, observer, reaction } from '../source/decorator';
56
import { WebCell } from '../source/WebCell';
67
import { createCell, render } from '../source/renderer';
78

@@ -48,4 +49,35 @@ describe('Observer decorator', () => {
4849
'2'
4950
);
5051
});
52+
53+
it('should register a Reaction with MobX', async () => {
54+
const handler = jest.fn();
55+
56+
@component({
57+
tagName: 'reaction-cell'
58+
})
59+
@observer
60+
class ReactionCell extends WebCell() {
61+
@observable
62+
test = '';
63+
64+
@reaction((element: ReactionCell) => element.test)
65+
handleReaction(value: string) {
66+
handler(value);
67+
}
68+
}
69+
render(<ReactionCell />);
70+
71+
const tag = document.querySelector<ReactionCell>('reaction-cell');
72+
tag.test = 'a';
73+
74+
await sleep();
75+
76+
expect(handler).toBeCalledTimes(1);
77+
expect(handler).toBeCalledWith('a');
78+
79+
document.body.innerHTML = '';
80+
81+
expect(tag.disposers).toHaveLength(0);
82+
});
5183
});

0 commit comments

Comments
 (0)