소스 검색

add framework

Kirk Trombley 2 년 전
부모
커밋
537b43b8ad
1개의 변경된 파일261개의 추가작업 그리고 0개의 파일을 삭제
  1. 261 0
      framework.js

+ 261 - 0
framework.js

@@ -0,0 +1,261 @@
+const U = (() => {
+  let nextObservableId = 1;
+  let reactiveContext = null;
+
+  const createObservable = (initial, { equality = (x, y) => x === y, name } = {}) => {
+    const _observableId = nextObservableId++;
+    let _value = initial;
+    let subscriberNum = 0;
+    const subscribers = new Map();
+
+    return {
+      _observableId,
+      name: name || `obs#${_observableId}`,
+      get value() {
+        reactiveContext?.add(this);
+        return _value;
+      },
+      set value(newValue) {
+        const previous = _value;
+        _value = newValue;
+        if (!equality?.(previous, _value)) {
+          const info = { previous, initial, cause: this };
+          subscribers.forEach((fn) => fn(_value, info));
+        }
+      },
+      subscribe(fn) {
+        subscriberNum++;
+        subscribers.set(subscriberNum, fn);
+        return () => subscribers.delete(subscriberNum);
+      },
+      reset() {
+        this.value = initial;
+      },
+    };
+  };
+
+  const trackDependencies = (action) => {
+    const prevContext = reactiveContext;
+    reactiveContext = new Set();
+    const result = action();
+    const dependencies = Array.from(reactiveContext.values());
+    reactiveContext = prevContext;
+    return [result, dependencies];
+  };
+
+  const getElement = (elementOrId) =>
+    typeof elementOrId === "string" ? document.getElementById(elementOrId) : elementOrId;
+
+  return {
+    // U.obs(value);
+    // U.obs(() => obs1.value + obs2.value);
+    // U.obs([obs1, obs2, obs3]);
+    obs(initializer, ...args) {
+      if (Array.isArray(initializer) && initializer[0]?._observableId) {
+        const getList = () => initializer.map(({ value }) => value);
+        const result = createObservable(getList(), ...args);
+        initializer.forEach((obs) =>
+          obs.subscribe(() => {
+            result.value = getList();
+          })
+        );
+        return result;
+      }
+
+      if (typeof initializer === "function") {
+        const [value, deps] = trackDependencies(initializer);
+        const result = createObservable(value, ...args);
+        deps.forEach((obs) =>
+          obs.subscribe(() => {
+            result.value = initializer();
+          })
+        );
+        return result;
+      }
+
+      return createObservable(initializer, ...args);
+    },
+
+    // U.fsm(states);
+    // U.fsm(s1, s2, s3);
+    fsm(config, ...rest) {
+      let initial, states;
+      if (rest.length > 0) {
+        // cycle, using [config, ...rest] as states
+        initial = config;
+        states = Object.fromEntries(
+          [config, ...rest].map((state, i, ls) => [state, ls[(i + 1) % ls.length]])
+        );
+      } else {
+        initial = config.initial;
+        states = config.states;
+      }
+
+      const state = createObservable(initial);
+
+      const transitions = Object.fromEntries(
+        Object.entries(states).map(([current, next]) => [
+          current,
+          typeof next === "string"
+            ? () => (state.value = next)
+            : (transition) => (state.value = next[transition]),
+        ])
+      );
+
+      return {
+        get current() {
+          return state.value;
+        },
+        get transition() {
+          return transitions[state.value];
+        },
+        subscribe(...args) {
+          return state.subscribe(...args);
+        },
+        reset() {
+          state.reset();
+        },
+      };
+    },
+
+    // U.reactive(() => {});
+    reactive(action) {
+      trackDependencies(action)[1].forEach((obs) => obs.subscribe(() => action()));
+    },
+
+    // U.resource(async () => {});
+    resource(sourceOrAction, actionOrInitial, ...args) {
+      let resultArgs, action, source;
+      if (typeof sourceOrAction === "function") {
+        resultArgs = [actionOrInitial, ...args];
+        action = sourceOrAction;
+        source = null;
+      } else {
+        resultArgs = args;
+        action = actionOrInitial;
+        source = sourceOrAction;
+      }
+
+      const result = createObservable(...resultArgs);
+      const loading = createObservable(true);
+      const error = createObservable(false);
+
+      const refresh = async () => {
+        loading.value = true;
+        error.value = false;
+        try {
+          result.value = await action(source?.value);
+        } catch (err) {
+          error.value = err;
+        }
+        loading.value = false;
+      };
+
+      refresh();
+      source?.subscribe(() => refresh());
+
+      return {
+        result,
+        loading,
+        error,
+        refresh,
+      };
+    },
+
+    // U.element("idOrElem", (binds, element) => {});
+    element(idOrRoot, action, keepBindPoints = false) {
+      const root = getElement(idOrRoot);
+      const binds = Object.fromEntries(
+        Array.from(root.querySelectorAll("[bind-to]")).map((element) => {
+          const name = element.getAttribute("bind-to");
+          if (!keepBindPoints) {
+            element.removeAttribute("bind-to");
+          }
+          return [name, element];
+        })
+      );
+      return action(binds, root);
+    },
+
+    // U.template("idOrElem", (binds, api) => {});
+    template(idOrTemplate, setup) {
+      const fragment = getElement(idOrTemplate).content.cloneNode(true);
+      const children = Array.from(fragment.children);
+      const api = {
+        fragment,
+        remove() {
+          children.forEach((elem) => elem.remove());
+        },
+        hide(state = true) {
+          children.forEach((elem) => (elem.hidden = state));
+        },
+      };
+
+      const target = this.element(fragment, (binds) => setup(binds, api));
+
+      if (target) {
+        const parent = getElement(target);
+        parent.appendChild(fragment);
+        return parent;
+      } else {
+        return fragment;
+      }
+    },
+
+    // U.form("idOrElem", config);
+    form(formOrId, { onSubmit, onChange = {}, onInput = {} } = {}) {
+      const formElement = getElement(formOrId);
+      formElement.setAttribute("action", "javascript:void(0);");
+
+      formElement.addEventListener("submit", () =>
+        onSubmit?.(
+          Object.fromEntries(
+            Array.from(formElement.elements)
+              .filter((elem) => elem.name)
+              .map((elem) => [elem.name, elem.value])
+          ),
+          formElement
+        )
+      );
+
+      Object.entries(onChange).forEach(([name, handler]) => {
+        formElement.elements[name].addEventListener("change", (event) =>
+          handler(event.target.value, event.target, event)
+        );
+      });
+
+      Object.entries(onInput).forEach(([name, handler]) => {
+        formElement.elements[name].addEventListener("input", (event) =>
+          handler(event.target.value, event.target, event)
+        );
+      });
+
+      return formElement;
+    },
+
+    // U.field("idOrElem", config);
+    field(fieldOrId, { allowInvalidInput = false, obs, ...config } = {}) {
+      const fieldElement = getElement(fieldOrId);
+      const fieldObs =
+        obs ||
+        createObservable(fieldElement.value, {
+          name: fieldElement.name,
+          ...config,
+        });
+
+      fieldElement.addEventListener("input", () => {
+        if (
+          !allowInvalidInput &&
+          fieldElement.willValidate &&
+          !fieldElement.validity.valid
+        ) {
+          fieldElement.value = fieldObs.value;
+        } else {
+          fieldObs.value = fieldElement.value;
+        }
+      });
+      this.reactive(() => (fieldElement.value = fieldObs.value));
+      return fieldObs;
+    },
+  };
+})();