|
@@ -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;
|
|
|
|
+ },
|
|
|
|
+ };
|
|
|
|
+})();
|