~repos /atoms-element

#js

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
Files changed (4) hide show
  1. example/elements/app-counter.js +15 -5
  2. example/server.js +13 -0
  3. index.d.ts +5 -5
  4. 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: null,
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
- fetchData: (state, id) => async () => {
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(res.json());
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 | Thunk };
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
- const res = reduce(value, v);
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
- if (!comp.hooks.data[index]) {
1005
+ comp.hooks.deps[index] = deps || [];
996
- comp.hooks.data[index] = {
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({