framework.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. const U = (() => {
  2. let nextObservableId = 1;
  3. let reactiveContext = null;
  4. const createObservable = (initial, { equality = (x, y) => x === y, name } = {}) => {
  5. const _observableId = nextObservableId++;
  6. let _value = initial;
  7. let subscriberNum = 0;
  8. const subscribers = new Map();
  9. return {
  10. _observableId,
  11. name: name || `obs#${_observableId}`,
  12. get value() {
  13. reactiveContext?.add(this);
  14. return _value;
  15. },
  16. set value(newValue) {
  17. const previous = _value;
  18. _value = newValue;
  19. if (!equality?.(previous, _value)) {
  20. const info = { previous, initial, cause: this };
  21. subscribers.forEach((fn) => fn(_value, info));
  22. }
  23. },
  24. subscribe(fn) {
  25. subscriberNum++;
  26. subscribers.set(subscriberNum, fn);
  27. return () => subscribers.delete(subscriberNum);
  28. },
  29. reset() {
  30. this.value = initial;
  31. },
  32. };
  33. };
  34. const trackDependencies = (action) => {
  35. const prevContext = reactiveContext;
  36. reactiveContext = new Set();
  37. const result = action();
  38. const dependencies = Array.from(reactiveContext.values());
  39. reactiveContext = prevContext;
  40. return [result, dependencies];
  41. };
  42. const getElement = (elementOrId) =>
  43. typeof elementOrId === "string" ? document.getElementById(elementOrId) : elementOrId;
  44. return {
  45. // U.obs(value);
  46. // U.obs(() => obs1.value + obs2.value);
  47. // U.obs([obs1, obs2, obs3]);
  48. obs(initializer, ...args) {
  49. if (Array.isArray(initializer) && initializer[0]?._observableId) {
  50. const getList = () => initializer.map(({ value }) => value);
  51. const result = createObservable(getList(), ...args);
  52. initializer.forEach((obs) =>
  53. obs.subscribe(() => {
  54. result.value = getList();
  55. })
  56. );
  57. return result;
  58. }
  59. if (typeof initializer === "function") {
  60. const [value, deps] = trackDependencies(initializer);
  61. const result = createObservable(value, ...args);
  62. deps.forEach((obs) =>
  63. obs.subscribe(() => {
  64. result.value = initializer();
  65. })
  66. );
  67. return result;
  68. }
  69. return createObservable(initializer, ...args);
  70. },
  71. // U.fsm(states);
  72. // U.fsm(s1, s2, s3);
  73. fsm(config, ...rest) {
  74. let initial, states;
  75. if (rest.length > 0) {
  76. // cycle, using [config, ...rest] as states
  77. initial = config;
  78. states = Object.fromEntries(
  79. [config, ...rest].map((state, i, ls) => [state, ls[(i + 1) % ls.length]])
  80. );
  81. } else {
  82. initial = config.initial;
  83. states = config.states;
  84. }
  85. const state = createObservable(initial);
  86. const transitions = Object.fromEntries(
  87. Object.entries(states).map(([current, next]) => [
  88. current,
  89. typeof next === "string"
  90. ? () => (state.value = next)
  91. : (transition) => (state.value = next[transition]),
  92. ])
  93. );
  94. return {
  95. get current() {
  96. return state.value;
  97. },
  98. get transition() {
  99. return transitions[state.value];
  100. },
  101. subscribe(...args) {
  102. return state.subscribe(...args);
  103. },
  104. reset() {
  105. state.reset();
  106. },
  107. };
  108. },
  109. // U.reactive(() => {});
  110. reactive(action) {
  111. trackDependencies(action)[1].forEach((obs) => obs.subscribe(() => action()));
  112. },
  113. // U.resource(async () => {});
  114. resource(sourceOrAction, actionOrInitial, ...args) {
  115. let resultArgs, action, source;
  116. if (typeof sourceOrAction === "function") {
  117. resultArgs = [actionOrInitial, ...args];
  118. action = sourceOrAction;
  119. source = null;
  120. } else {
  121. resultArgs = args;
  122. action = actionOrInitial;
  123. source = sourceOrAction;
  124. }
  125. const result = createObservable(...resultArgs);
  126. const loading = createObservable(true);
  127. const error = createObservable(false);
  128. const refresh = async () => {
  129. loading.value = true;
  130. error.value = false;
  131. try {
  132. result.value = await action(source?.value);
  133. } catch (err) {
  134. error.value = err;
  135. }
  136. loading.value = false;
  137. };
  138. refresh();
  139. source?.subscribe(() => refresh());
  140. return {
  141. result,
  142. loading,
  143. error,
  144. refresh,
  145. };
  146. },
  147. // U.element("idOrElem", (binds, element) => {});
  148. element(idOrRoot, action, keepBindPoints = false) {
  149. const root = getElement(idOrRoot);
  150. const binds = Object.fromEntries(
  151. Array.from(root.querySelectorAll("[bind-to]")).map((element) => {
  152. const name = element.getAttribute("bind-to");
  153. if (!keepBindPoints) {
  154. element.removeAttribute("bind-to");
  155. }
  156. return [name, element];
  157. })
  158. );
  159. return action(binds, root);
  160. },
  161. // U.template("idOrElem", (binds, api) => {});
  162. template(idOrTemplate, setup) {
  163. const fragment = getElement(idOrTemplate).content.cloneNode(true);
  164. const children = Array.from(fragment.children);
  165. const api = {
  166. fragment,
  167. remove() {
  168. children.forEach((elem) => elem.remove());
  169. },
  170. hide(state = true) {
  171. children.forEach((elem) => (elem.hidden = state));
  172. },
  173. };
  174. const target = this.element(fragment, (binds) => setup(binds, api));
  175. if (target) {
  176. const parent = getElement(target);
  177. parent.appendChild(fragment);
  178. return parent;
  179. } else {
  180. return fragment;
  181. }
  182. },
  183. // U.form("idOrElem", config);
  184. form(formOrId, { onSubmit, onChange = {}, onInput = {} } = {}) {
  185. const formElement = getElement(formOrId);
  186. formElement.setAttribute("action", "javascript:void(0);");
  187. formElement.addEventListener("submit", () =>
  188. onSubmit?.(
  189. Object.fromEntries(
  190. Array.from(formElement.elements)
  191. .filter((elem) => elem.name)
  192. .map((elem) => [elem.name, elem.value])
  193. ),
  194. formElement
  195. )
  196. );
  197. formElement.addEventListener("change", (event) => {
  198. onChange?.[event.target.name]?.(event.target.value, event.target, event);
  199. });
  200. formElement.addEventListener("input", (event) => {
  201. onInput?.[event.target.name]?.(event.target.value, event.target, event);
  202. });
  203. return formElement;
  204. },
  205. // U.field("idOrElem", config);
  206. field(fieldOrId, { allowInvalidInput = false, obs, ...config } = {}) {
  207. const fieldElement = getElement(fieldOrId);
  208. const fieldObs =
  209. obs ||
  210. createObservable(fieldElement.value, {
  211. name: fieldElement.name,
  212. ...config,
  213. });
  214. fieldElement.addEventListener("input", () => {
  215. if (
  216. !allowInvalidInput &&
  217. fieldElement.willValidate &&
  218. !fieldElement.validity.valid
  219. ) {
  220. fieldElement.value = fieldObs.value;
  221. } else {
  222. fieldObs.value = fieldElement.value;
  223. }
  224. });
  225. this.reactive(() => (fieldElement.value = fieldObs.value));
  226. return fieldObs;
  227. },
  228. };
  229. })();