~repos /atoms-element
git clone https://pyrossh.dev/repos/atoms-element.git
A simple web component library for defining your custom elements. It works on both client and server.
649634e3
—
Peter John 4 years ago
add effects
- example/elements/app-counter.js +15 -5
- example/server.js +13 -0
- index.d.ts +5 -5
- index.js +47 -18
example/elements/app-counter.js
CHANGED
|
@@ -1,28 +1,38 @@
|
|
|
1
|
+
import { useEffect } from '../../index.js';
|
|
1
2
|
import { createElement, html, useReducer } from '../../index.js';
|
|
2
3
|
import { totalReducer } from '../store.js';
|
|
3
4
|
|
|
4
5
|
const Counter = ({ name, meta }) => {
|
|
5
|
-
const { count, actions } = useReducer({
|
|
6
|
+
const { count, actions, effects } = useReducer({
|
|
6
7
|
initial: {
|
|
7
8
|
count: 0,
|
|
8
|
-
data:
|
|
9
|
+
data: undefined,
|
|
9
10
|
err: null,
|
|
10
11
|
},
|
|
11
12
|
reducer: {
|
|
12
13
|
increment: (state) => ({ ...state, count: state.count + 1 }),
|
|
13
14
|
decrement: (state) => ({ ...state, count: state.count - 1 }),
|
|
15
|
+
setLoading: (state, v) => ({ ...state, loading: v }),
|
|
14
16
|
setData: (state, data) => ({ ...state, data }),
|
|
15
17
|
setErr: (state, err) => ({ ...state, err }),
|
|
18
|
+
},
|
|
19
|
+
effects: {
|
|
16
|
-
|
|
20
|
+
loadData: async (actions, id) => {
|
|
17
21
|
try {
|
|
22
|
+
actions.setLoading(true);
|
|
18
23
|
const res = await fetch(`/api/posts/${id}`);
|
|
19
|
-
actions.setData(res.json());
|
|
24
|
+
actions.setData(await res.json());
|
|
20
25
|
} catch (err) {
|
|
21
|
-
actions.setErr(
|
|
26
|
+
actions.setErr(err);
|
|
27
|
+
} finally {
|
|
28
|
+
actions.setLoading(false);
|
|
22
29
|
}
|
|
23
30
|
},
|
|
24
31
|
},
|
|
25
32
|
});
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
effects.loadData(name);
|
|
35
|
+
}, []);
|
|
26
36
|
const increment = () => {
|
|
27
37
|
actions.increment();
|
|
28
38
|
totalReducer.actions.increment(count + 1);
|
example/server.js
CHANGED
|
@@ -19,6 +19,19 @@ elements.forEach((el) => {
|
|
|
19
19
|
|
|
20
20
|
http
|
|
21
21
|
.createServer((req, res) => {
|
|
22
|
+
if (req.url.includes('/api/posts')) {
|
|
23
|
+
const parts = req.url.split('/');
|
|
24
|
+
const id = parts[parts.length - 1];
|
|
25
|
+
res.setHeader('Content-type', 'application/json');
|
|
26
|
+
res.end(
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
id,
|
|
29
|
+
title: `post ${id}`,
|
|
30
|
+
description: ` description ${id}`,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
22
35
|
if (req.url === '/') {
|
|
23
36
|
res.statusCode = 200;
|
|
24
37
|
res.setHeader('Content-type', 'text/html');
|
index.d.ts
CHANGED
|
@@ -31,17 +31,17 @@ export type Location = {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
export type Reducer<P, Q> = {
|
|
34
|
+
export type Reducer<P, Q, R> = {
|
|
35
35
|
getValue: () => P;
|
|
36
36
|
subscribe: (fn: (v: P) => void) => void;
|
|
37
|
-
} & { actions: { [K in keyof Q]: (v: any) => void}}
|
|
37
|
+
} & { actions: { [K in keyof Q]: (v: any) => void}} & { effects: { [K in keyof R]: (v: any) => void}}
|
|
38
38
|
|
|
39
|
-
export type Thunk = () => void | Promise<void>;
|
|
40
|
-
export type ReducerActions<P> = {[k: string]: (state: P, v: any) => P
|
|
39
|
+
export type ReducerActions<P> = {[k: string]: (state: P, v: any) => P};
|
|
40
|
+
export type EffectActions<P, Q extends ReducerActions<P>> = {[k: string]: (actions: { [K in keyof Q]: (v: any) => void}, v: any) => void};
|
|
41
41
|
|
|
42
42
|
export function createReducer<P, Q extends ReducerActions<P>>(props: { initial: P, reducer: Q }): Reducer<P, Q>;
|
|
43
43
|
|
|
44
|
-
export function useReducer<P, Q extends ReducerActions<P>>(props: Reducer<P, Q> | { initial: P, reducer: Q }) : P & Reducer<P, Q>;
|
|
44
|
+
export function useReducer<P, Q extends ReducerActions<P>, R extends EffectActions<P, Q>>(props: Reducer<P, Q, R> | { initial: P, reducer: Q, effects: R }) : P & Reducer<P, Q, R>;
|
|
45
45
|
|
|
46
46
|
export function createElement(meta: any, renderFn: any): any
|
|
47
47
|
export type Handler = (props: any) => string;
|
index.js
CHANGED
|
@@ -931,32 +931,40 @@ export const unsafeHTML = isBrowser
|
|
|
931
931
|
: (value) => value;
|
|
932
932
|
|
|
933
933
|
const fifo = (q) => q.shift();
|
|
934
|
+
const filo = (q) => q.pop();
|
|
934
935
|
const microtask = (flush) => () => queueMicrotask(flush);
|
|
936
|
+
const task = (flush) => {
|
|
937
|
+
if (isBrowser) {
|
|
938
|
+
const ch = new window.MessageChannel();
|
|
939
|
+
ch.port1.onmessage = flush;
|
|
940
|
+
return () => ch.port2.postMessage(null);
|
|
941
|
+
} else {
|
|
942
|
+
return () => setImmediate(flush);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
const depsChanged = (prev, next) => prev == null || next.some((f, i) => !Object.is(f, prev[i]));
|
|
935
946
|
|
|
936
947
|
export const createAttrs = (attrs) => attrs;
|
|
937
948
|
|
|
938
|
-
export const createReducer = ({ initial, reducer }) => {
|
|
949
|
+
export const createReducer = ({ initial, reducer, effects }) => {
|
|
939
950
|
let value = initial;
|
|
940
951
|
const subs = new Set();
|
|
941
952
|
const actions = Object.keys(reducer).reduce((acc, key) => {
|
|
942
953
|
const reduce = reducer[key];
|
|
943
954
|
acc[key] = (v) => {
|
|
944
|
-
|
|
955
|
+
value = reduce(value, v);
|
|
945
|
-
if (typeof res === 'object') {
|
|
946
|
-
value = res;
|
|
947
|
-
}
|
|
948
|
-
if (typeof res === 'function') {
|
|
949
|
-
res();
|
|
950
|
-
}
|
|
951
956
|
subs.forEach((sub) => {
|
|
952
957
|
sub(value);
|
|
953
958
|
});
|
|
954
959
|
};
|
|
955
960
|
return acc;
|
|
956
961
|
}, {});
|
|
962
|
+
const effectActions = Object.keys(effects || {}).reduce((acc, key) => {
|
|
963
|
+
const effect = effects[key];
|
|
964
|
+
acc[key] = (v) => effect(actions, v);
|
|
965
|
+
return acc;
|
|
966
|
+
}, {});
|
|
957
967
|
return {
|
|
958
|
-
initial,
|
|
959
|
-
reducer,
|
|
960
968
|
getValue: () => value,
|
|
961
969
|
subscribe: (fn) => {
|
|
962
970
|
subs.add(fn);
|
|
@@ -965,6 +973,7 @@ export const createReducer = ({ initial, reducer }) => {
|
|
|
965
973
|
subs.remove(fn);
|
|
966
974
|
},
|
|
967
975
|
actions,
|
|
976
|
+
effects: effectActions,
|
|
968
977
|
};
|
|
969
978
|
};
|
|
970
979
|
|
|
@@ -987,16 +996,14 @@ export const useReducer = (reducer) => {
|
|
|
987
996
|
comp.hooks.data[index].subscribe(() => comp.update());
|
|
988
997
|
}
|
|
989
998
|
const state = comp.hooks.data[index].getValue();
|
|
990
|
-
return { ...state, actions: comp.hooks.data[index].actions };
|
|
999
|
+
return { ...state, actions: comp.hooks.data[index].actions, effects: comp.hooks.data[index].effects };
|
|
991
1000
|
};
|
|
992
1001
|
export const useEffect = (fn, deps) => {
|
|
993
1002
|
const comp = currentComponent.get();
|
|
994
1003
|
const index = comp.hooks.index++;
|
|
1004
|
+
if (!deps || depsChanged(comp.hooks.deps[index], deps)) {
|
|
995
|
-
|
|
1005
|
+
comp.hooks.deps[index] = deps || [];
|
|
996
|
-
comp.hooks.
|
|
1006
|
+
comp.hooks.effects[index] = fn;
|
|
997
|
-
cb: fn,
|
|
998
|
-
deps,
|
|
999
|
-
};
|
|
1000
1007
|
}
|
|
1001
1008
|
};
|
|
1002
1009
|
|
|
@@ -1019,6 +1026,9 @@ export const createElement = (meta, renderFn) => {
|
|
|
1019
1026
|
this.hooks = {
|
|
1020
1027
|
index: 0,
|
|
1021
1028
|
data: {},
|
|
1029
|
+
deps: {},
|
|
1030
|
+
effects: {},
|
|
1031
|
+
cleanups: {},
|
|
1022
1032
|
};
|
|
1023
1033
|
// this.prevClassList = [];
|
|
1024
1034
|
// this.shadow = this.attachShadow({ mode: 'open' });
|
|
@@ -1026,13 +1036,11 @@ export const createElement = (meta, renderFn) => {
|
|
|
1026
1036
|
|
|
1027
1037
|
connectedCallback() {
|
|
1028
1038
|
this._connected = true;
|
|
1029
|
-
// this.state.subscribe(() => this.update());
|
|
1030
1039
|
this.update();
|
|
1031
1040
|
}
|
|
1032
1041
|
|
|
1033
1042
|
disconnectedCallback() {
|
|
1034
1043
|
this._connected = false;
|
|
1035
|
-
// this.state.unsubscribe(() => this.update());
|
|
1036
1044
|
}
|
|
1037
1045
|
|
|
1038
1046
|
attributeChangedCallback(key, oldValue, newValue) {
|
|
@@ -1055,9 +1063,26 @@ export const createElement = (meta, renderFn) => {
|
|
|
1055
1063
|
return;
|
|
1056
1064
|
}
|
|
1057
1065
|
this.render();
|
|
1066
|
+
this.enqueueEffects();
|
|
1058
1067
|
this._dirty = false;
|
|
1059
1068
|
}
|
|
1060
1069
|
|
|
1070
|
+
_flushEffects() {
|
|
1071
|
+
const effects = this.hooks.effects;
|
|
1072
|
+
const cleanups = this.hooks.cleanups;
|
|
1073
|
+
const keys = Object.keys(effects);
|
|
1074
|
+
for (const key of keys) {
|
|
1075
|
+
if (effects[key]) {
|
|
1076
|
+
cleanups[key] && cleanups[key]();
|
|
1077
|
+
const cleanup = effects[key]();
|
|
1078
|
+
if (cleanup) {
|
|
1079
|
+
cleanups[key] = cleanup;
|
|
1080
|
+
}
|
|
1081
|
+
delete effects[key];
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1061
1086
|
batch(runner, pick, callback) {
|
|
1062
1087
|
const q = [];
|
|
1063
1088
|
const flush = () => {
|
|
@@ -1072,6 +1097,10 @@ export const createElement = (meta, renderFn) => {
|
|
|
1072
1097
|
this.batch(microtask, fifo, () => this._performUpdate());
|
|
1073
1098
|
}
|
|
1074
1099
|
|
|
1100
|
+
enqueueEffects() {
|
|
1101
|
+
this.batch(task, filo, () => this._flushEffects());
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1075
1104
|
render() {
|
|
1076
1105
|
currentComponent.set(this);
|
|
1077
1106
|
const template = renderFn({
|