123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- 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) {
- const thisSubscriber = subscriberNum++;
- subscribers.set(thisSubscriber, fn);
- return () => subscribers.delete(thisSubscriber);
- },
- 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) {
- const unsubs = trackDependencies(action)[1].map((obs) =>
- obs.subscribe(() => action())
- );
- return () => unsubs.forEach((fn) => fn());
- },
- // 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 unsubs = [];
- const api = {
- fragment,
- remove() {
- children.forEach((elem) => elem.remove());
- },
- hide(state = true) {
- children.forEach((elem) => (elem.hidden = state));
- },
- reactive: (...args) => {
- unsubs.push(this.reactive(...args));
- },
- };
- const result = {
- fragment,
- parent: null,
- destroy: () => {
- unsubs.forEach((fn) => fn());
- api.remove();
- },
- };
- const target = this.element(fragment, (binds) => setup(binds, api));
- if (target) {
- const parent = getElement(target);
- parent.appendChild(fragment);
- result.parent = parent;
- }
- return result;
- },
- // U.list(idOrTemplate, (binds, api, arg) => {})([arg1, arg2, ...]);
- list(idOrTemplate, setup) {
- const old = [];
- const component = (arg) =>
- this.template(idOrTemplate, (binds, api) => setup(binds, api, arg));
- return (list) => {
- old.forEach((el) => el.destroy());
- old.splice(0);
- old.push(...list.map(component));
- };
- },
- // 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
- )
- );
- formElement.addEventListener("change", (event) => {
- onChange?.[event.target.name]?.(event.target.value, event.target, event);
- });
- formElement.addEventListener("input", (event) => {
- onInput?.[event.target.name]?.(event.target.value, event.target, event);
- });
- return formElement;
- },
- // U.field("idOrElem", config);
- field(fieldOrId, { allowInvalidInput = false, obs, ...config } = {}) {
- const fieldElement = getElement(fieldOrId);
- const valueKey = fieldElement.type === "checkbox" ? "checked" : "value";
- const fieldObs =
- obs ||
- createObservable(fieldElement[valueKey], {
- name: fieldElement.name,
- ...config,
- });
- fieldElement.addEventListener("input", () => {
- if (
- !allowInvalidInput &&
- fieldElement.willValidate &&
- !fieldElement.validity.valid
- ) {
- fieldElement[valueKey] = fieldObs.value;
- } else {
- fieldObs.value = fieldElement[valueKey];
- }
- });
- this.reactive(() => (fieldElement[valueKey] = fieldObs.value));
- return fieldObs;
- },
- };
- })();
|