UNPKG

atomico

Version:

Atomico is a small library for the creation of interfaces based on web-components, only using functions and hooks.

1,046 lines (929 loc) 32.1 kB
import { H as HOOK_CURRENT, a as HOOK_MOUNT, b as HOOK_UPDATED, c as HOOK_MOUNTED, d as HOOK_UNMOUNT, e as HOOK_UPDATE, I as IGNORE_CHILDREN, f as HYDRATE_PROPS, K as KEY, S as SUPPORT_STYLE_SHEET, C as CACHE_STYLE_SHEET, M as META_MAP_CHILDREN, A as ARRAY_EMPTY, g as META_KEYES, h as META_STYLE_SHEET, i as STYLE_SHEET_KEY, N as NODE_HOST, J as JOIN_CHILDREN, j as NODE_TYPE, E as ELEMENT_TRUE_VALUES, k as ELEMENT_IGNORE_ATTR, l as ELEMENT_PROPS } from './chunk/constants.js'; import { i as isFunction, a as isEqualArray, b as isArray, c as isRawNode, p as promise } from './chunk/utils.js'; function update(hook, type) { hook[0] && (hook[1] = hook[0](hook[1], type)); } function updateAll(hooks, type) { for (let i in hooks) update(hooks[i], type); } function useHook(reducer, initialState) { if (HOOK_CURRENT.ref.hook) { return HOOK_CURRENT.ref.hook.use(reducer, initialState)[1]; } } function useRender() { return HOOK_CURRENT.ref.render; } function useHost() { return useHook(0, { current: HOOK_CURRENT.ref.host }); } function createHookCollection(render, host) { let hooks = {}; let mounted; let hook = { use, load, updated, unmount }; let ref = { hook, host, render }; function load(callback, param) { HOOK_CURRENT.index = 0; HOOK_CURRENT.ref = ref; let resolve = callback(param); HOOK_CURRENT.ref = 0; return resolve; } function use(reducer, state) { let index = HOOK_CURRENT.index++; let mount; // record the hook and the initial state of this if (!hooks[index]) { hooks[index] = [null, state]; mount = 1; } // The hook always receives the last reduce. hooks[index][0] = reducer; update(hooks[index], mount ? HOOK_MOUNT : HOOK_UPDATE); return hooks[index]; } function updated() { let type = mounted ? HOOK_UPDATED : HOOK_MOUNTED; mounted = 1; updateAll(hooks, type); } function unmount() { updateAll(hooks, HOOK_UNMOUNT); } return hook; } function useState(initialState) { let render = useRender(); return useHook((state, type) => { if (HOOK_MOUNT == type) { state[0] = isFunction(initialState) ? initialState() : initialState; state[1] = nextState => { nextState = isFunction(nextState) ? nextState(state[0]) : nextState; if (nextState != state[0]) { state[0] = nextState; render(); } }; } return state; }, []); } function useEffect(callback, args) { // define whether the effect in the render cycle should be regenerated let executeEffect; useHook((state, type) => { if (executeEffect == null) { executeEffect = args && state[0] ? !isEqualArray(args, state[0]) : true; state[0] = args; } switch (type) { case HOOK_UPDATE: case HOOK_UNMOUNT: // save the current args, for comparison if ((executeEffect || type == HOOK_UNMOUNT) && state[1]) { // compare the previous snapshot with the generated state state[1](); // clean the effect collector state[1] = 0; } // delete the previous argument for a hook // run if the hook is inserted in a new node // Why? ... to perform again dom operations associated with the parent if (type == HOOK_UNMOUNT) { state[0] = null; } break; case HOOK_MOUNTED: case HOOK_UPDATED: // save the current args, for comparison, repeats due to additional type HOOK_MOUNTED if (executeEffect || type == HOOK_MOUNTED) { // save the effect collector state[1] = callback(); } // save the comparison argument break; } return state; }, []); } function useRef(current) { return useHook(0, { current }); } function useMemo(callback, args) { let state = useHook(0, []); if (!state[0] || (state[0] && (!args || !isEqualArray(state[0], args)))) { state[1] = callback(); } state[0] = args; return state[1]; } function useReducer(reducer, initialState) { let render = useRender(); let hook = useHook((state, type) => { if (HOOK_MOUNT == type) { state[0] = initialState; state[1] = action => { let nextState = state[2](state[0], action); if (nextState != state[0]) { state[0] = nextState; render(); } }; } return state; }, []); // allows the reduce to always access the scope of the component hook[2] = reducer; return hook; } /** * @todo add test use callback */ function useCallback(callback, args) { return useMemo(() => callback, args); } /** * * @param {import("./render").HTMLNode} node * @param {Object} props * @param {Object} nextProps * @param {boolean} isSvg * @param {Object} handlers **/ function diffProps(node, props, nextProps, isSvg, handlers) { props = props || {}; for (let key in props) { if (!(key in nextProps)) { setProperty(node, key, props[key], null, isSvg, handlers); } } let ignoreChildren; for (let key in nextProps) { setProperty(node, key, props[key], nextProps[key], isSvg, handlers); ignoreChildren = ignoreChildren || IGNORE_CHILDREN[key]; } return ignoreChildren; } function setProperty(node, key, prevValue, nextValue, isSvg, handlers) { key = key == "class" && !isSvg ? "className" : key; // define empty value prevValue = prevValue == null ? null : prevValue; nextValue = nextValue == null ? null : nextValue; if (key in node && HYDRATE_PROPS[key]) { prevValue = node[key]; } if (nextValue === prevValue) return; if ( key[0] == "o" && key[1] == "n" && (isFunction(nextValue) || isFunction(prevValue)) ) { setEvent(node, key, nextValue, handlers); return; } switch (key) { /** * add support {@link https://developer.mozilla.org/es/docs/Web/API/CSSStyleSheet} */ case "styleSheet": if (SUPPORT_STYLE_SHEET) node.shadowRoot.adoptedStyleSheets = [] .concat(nextValue) .map(cssText => { if (cssText instanceof CSSStyleSheet) { return cssText; } if (!CACHE_STYLE_SHEET[cssText]) { CACHE_STYLE_SHEET[cssText] = new CSSStyleSheet(); CACHE_STYLE_SHEET[cssText].replace(cssText); } return CACHE_STYLE_SHEET[cssText]; }); break; case "ref": if (nextValue) nextValue.current = node; break; case "style": setStyle(node, prevValue || "", nextValue || ""); break; case "key": node[KEY] = nextValue; break; default: if (!isSvg && key != "list" && key in node) { node[key] = nextValue == null ? "" : nextValue; } else if (nextValue == null) { node.removeAttribute(key); } else { node.setAttribute( key, typeof nextValue == "object" ? JSON.stringify(nextValue) : nextValue ); } } } /** * * @param {import("./render").HTMLNode} node * @param {string} type * @param {function} [nextHandler] * @param {object} handlers */ function setEvent(node, type, nextHandler, handlers) { // get the name of the event to use type = type.slice(type[2] == "-" ? 3 : 2); // add handleEvent to handlers if (!handlers.handleEvent) { /** * {@link https://developer.mozilla.org/es/docs/Web/API/EventTarget/addEventListener#The_value_of_this_within_the_handler} **/ handlers.handleEvent = event => handlers[event.type].call(node, event); } if (nextHandler) { // create the subscriber if it does not exist if (!handlers[type]) { node.addEventListener(type, handlers); } // update the associated event handlers[type] = nextHandler; } else { // delete the associated event if (handlers[type]) { node.removeEventListener(type, handlers); delete handlers[type]; } } } /** * define style as string inline,this generates less mutation * to the sun and cleans the previously defined properties. * @param {import("./render").HTMLNode} node * @param {(string|object)} prevValue * @param {(string|object)} nextValue */ function setStyle(node, prevValue, nextValue) { let style = node.style, prevIsObject; if (typeof prevValue == "object") { prevIsObject = true; for (let key in prevValue) { if (!(key in nextValue)) setPropertyStyle(style, key, null); } } if (typeof nextValue == "object") { for (let key in nextValue) { let value = nextValue[key]; if (prevIsObject && prevValue[key] === value) continue; setPropertyStyle(style, key, value); } } else { style.cssText = nextValue; } } function setPropertyStyle(style, key, value) { let method = "setProperty"; if (value == null) { method = "removeProperty"; value = null; } if (~key.indexOf("-")) { style[method](key, value); } else { style[key] = value; } } let vNodeEmpty = createElement(null, { children: "" }); let vNodeFill = createElement(null, { children: ARRAY_EMPTY }); /** * @param {VnodeType} nodeType * @param {VnodeProps} [props] * @param {Vnode|Vnode[]} [children] * @returns {Vnode} **/ function createElement(nodeType, props, ...children) { return { children, ...props, nodeType: nodeType || null }; } /** * toVnode, processes the object for correct use within the diff process. **/ function toVnode(value) { if (isVnodeValue(value)) { return value; } else { // this process occurs only once per vnode if (!value[META_MAP_CHILDREN]) { let { children, keyes } = mapChildren(value.children); value.children = children.length ? children : ARRAY_EMPTY; if (keyes) { value[META_KEYES] = keyes; } value[META_MAP_CHILDREN] = true; } if (value.styleSheet && !SUPPORT_STYLE_SHEET) { if (!value[META_STYLE_SHEET]) { // When patching styleSheet, define whether to keep ARRAY_EMPTY // or create a new array to fill and thus keep the reference intact value.children = value.children == ARRAY_EMPTY ? [] : value.children; // add the node to the children list value.children.unshift( toVnode( createElement( "style", value[META_KEYES] ? { key: STYLE_SHEET_KEY } : {}, value.styleSheet ) ) ); // if it is a list with keys, add the key to keyes if (value[META_KEYES]) { value[META_KEYES].unshift(STYLE_SHEET_KEY); } } value[META_STYLE_SHEET] = true; } } return value; } function mapChildren(children, scan = { children: [] }, deep = 0) { if (isArray(children)) { let length = children.length; for (let i = 0; i < length; i++) { mapChildren(children[i], scan, deep + 1); } } else { if (children == null && !deep) return scan; let vnode = toVnode(children); if (vnode != null && typeof vnode == "object") { if (isFunction(vnode.nodeType)) { let { nodeType, ...props } = vnode; return mapChildren(nodeType(props), scan, deep + 1); } if ("key" in vnode) { scan.keyes = scan.keyes || []; if (!scan.keyes.includes(vnode.key)) { scan.keyes.push(vnode.key); } } } scan.children.push(vnode); } return scan; } function isVnodeEmpty(value) { let type = typeof value; return value == null || type == "boolean" || type == "function"; } function fillVnodeValue(value) { return isVnodeEmpty(value) ? vNodeEmpty : createElement(null, { children: "" + value }); } function isVnodeValue(value) { let type = typeof value; return ( value == null || type == "string" || type == "number" || type == "function" || type == "boolean" ); } /** * @typedef {(Object<string,any>)} VnodeProps; * * @typedef {(Function|string)} VnodeType; * * @typedef {{type:VnodeType,props:VnodeProps}} Vnode **/ /** * * @param {import("./render").ConfigRender} config * @param {import("./render").HTMLNode} node * @param {import("./vnode").Vnode} nextVnode * @param {boolean} isSvg * @param {Function} currentUpdateComponent * @return {import("./render").HTMLNode} **/ function diff(id, node, nextVnode, isSvg) { let { vnode, handlers = {} } = (node && node[id]) || {}; if (vnode == nextVnode && vnode != null) return node; nextVnode = isVnodeValue(nextVnode) ? fillVnodeValue(nextVnode) : nextVnode; let { nodeType, shadowDom, children, is, ...props } = vnode || vNodeFill; let { nodeType: nextNodeType, shadowDom: nextShadowDom, children: nextChildren, is: nextIs, ...nextProps } = nextVnode; isSvg = isSvg || nextNodeType == "svg"; if ( nextNodeType != NODE_HOST && (!equalNode(node, nextNodeType) || is != nextIs) ) { let nextNode = createNode(nextNodeType, isSvg, nextIs); let parent = node && node.parentNode; if (parent) { parent.replaceChild(nextNode, node); } node = nextNode; handlers = {}; } if (JOIN_CHILDREN[nextNodeType]) { nextNodeType = null; nextChildren = nextChildren.join(""); } if (nextNodeType == null) { if (node.textContent != nextChildren) { node.textContent = nextChildren; } } else { if (shadowDom != nextShadowDom) { let { shadowRoot } = node; let mode = nextShadowDom && !shadowRoot ? "open" : !nextShadowDom && shadowRoot ? "closed" : 0; if (mode) node.attachShadow({ mode }); } let ignoreChildren = diffProps( node, props, nextProps, isSvg, handlers); if (!ignoreChildren && children != nextChildren) { diffChildren( id, nextShadowDom ? node.shadowRoot : node, nextChildren, nextProps[META_KEYES], isSvg ); } } node[id] = { vnode: nextVnode, handlers }; return node; } /** * * @param {import("./render").ConfigRender} config * @param {import("./render").HTMLNode} parent * @param {import("./vnode").Vnode[]} [nextChildren] * @param {boolean} isSvg */ function diffChildren(id, parent, children, keyes, isSvg) { let childrenLenght = children.length; let { childNodes } = parent; let childNodesKeyes = {}; let childNodesLength = childNodes.length; let index = keyes ? 0 : childNodesLength > childrenLenght ? childrenLenght : childNodesLength; for (; index < childNodesLength; index++) { let childNode = childNodes[index]; let key = index; if (keyes) { key = childNode[KEY]; if (keyes.includes(key)) { childNodesKeyes[key] = childNode; continue; } } index--; childNodesLength--; parent.removeChild(childNode); } for (let i = 0; i < childrenLenght; i++) { let child = children[i]; let indexChildNode = childNodes[i]; let key = keyes ? child.key : i; let childNode = keyes ? childNodesKeyes[key] : indexChildNode; if (keyes && childNode) { if (childNode != indexChildNode) { parent.insertBefore(childNode, indexChildNode); } } let nextChildNode = diff(id, childNode, child, isSvg); if (!childNode) { if (childNodes[i]) { parent.insertBefore(nextChildNode, childNodes[i]); } else { parent.appendChild(nextChildNode); } } } } /** * * @param {string} type * @param {boolean} isSvg * @returns {import("./render").HTMLNode} */ function createNode(type, isSvg, is) { let doc = document; let nextNode; if (type != null) { if (isRawNode(type)) { return type; } nextNode = isSvg ? doc.createElementNS("http://www.w3.org/2000/svg", type) : doc.createElement(type, is ? { is } : null); } else { nextNode = doc.createTextNode(""); } return nextNode; } /** * compare 2 nodes, to define if these are equal * @param {string|null|HTMLElement|SVGElement} nodeA * @param {string|null|HTMLElement|SVGElement} nodeB */ function equalNode(nodeA, nodeB) { let isRawA = nodeA && isRawNode(nodeA); let isRawB = nodeB && isRawNode(nodeB); if (isRawB && isRawA) { return isRawB == isRawB; } if (nodeA) { if (!nodeA[NODE_TYPE]) { nodeA[NODE_TYPE] = nodeA.nodeName.toLowerCase(); } let localName = nodeA[NODE_TYPE]; return (localName == "#text" ? null : localName) == nodeB; } } function render(vnode, node, id = "vnode") { if ( vnode != null && typeof vnode == "object" && vnode.nodeType != NODE_HOST ) { vnode = createElement(NODE_HOST, { children: vnode }); } vnode = toVnode(vnode); diff(id, node, vnode); return node; } function setAttr(node, attr, value) { if (value == null) { node.removeAttribute(attr); } else { node.setAttribute( attr, typeof value == "object" ? JSON.stringify(value) : value ); } } function formatType(value, type = String) { try { if (type == Boolean) { value = ELEMENT_TRUE_VALUES.includes(value); } else if (typeof value == "string") { value = type == Number ? Number(value) : type == Object || type == Array ? JSON.parse(value) : value; } if ({}.toString.call(value) == `[object ${type.name}]`) { return { value, error: type == Number && Number.isNaN(value) }; } } catch (e) {} return { value, error: true }; } function propToAttr(prop) { return prop.replace(/([A-Z])/g, "-$1").toLowerCase(); } function attrToProp(attr) { return attr.replace(/-(\w)/g, (all, letter) => letter.toUpperCase()); } function dispatchEvent(node, type, customEventInit) { node.dispatchEvent( new CustomEvent( type, typeof customEventInit == "object" ? customEventInit : null ) ); } function createPropError(status, message) { return Object.assign(new Error("Failed prop\n" + message), status); } let defer = Promise.resolve(); let queue = []; let running; let maxFps = 1000 / 60; const IMPORTANT = Symbol("important"); function clearQueue() { let time = performance.now(); let length = queue.length; let current = queue; queue = []; while (length--) { let callback = current[length]; // if in case one is defined as important, the execution will be forced if (callback[IMPORTANT] || performance.now() - time < maxFps) { callback(); } else { queue = queue.concat(current.slice(0, length + 1)); break; } } if (queue.length) { requestAnimationFrame(clearQueue); return; } running = false; } /** * add a task to the queue * @param {Function} callback * @returns {Promise} Generate a promise that show if the queue is complete */ function addQueue(callback) { if (!running) { running = true; defer.then(clearQueue); } // if the callback is defined as IMPORTANT, // it is assumed to be in favor of the tree // of the DOM that must be added by unshift, // assuming that the mount will be carried // out in order, the shift priority only works // after the first render if (!queue.includes(callback)) queue[callback[IMPORTANT] ? "unshift" : "push"](callback); } function load(target, componentRender, componentError) { if (target.mount) return; let id = Symbol("vnode"); let isPrevent; let isUnmount; target[ELEMENT_PROPS] = {}; let isMounted; let resolveUpdate; let rerender = () => { // disables blocking, allowing the cycle to be regenerate isPrevent = false; // After the first render it disables the important condition if (rerender[IMPORTANT]) rerender[IMPORTANT] = false; try { render( hooks.load(componentRender, { ...target[ELEMENT_PROPS] }), target, id ); resolveUpdate(); } catch (e) { (componentError || console.error)(e); } }; // mark the first render as important, target speeds up the rendering rerender[IMPORTANT] = true; target.update = () => { if (isUnmount) return; let rendered = target.rendered; if (!isPrevent) { isPrevent = true; // create a promise to observe the status of the update rendered = promise(resolve => (resolveUpdate = resolve)).then( // the UPDATED state is only propagated through // the resolution of the promise // Why? ... to improve communication between web-component parent and children hooks.updated ); // if the component is already mounted, avoid using target.mounted, // to speed up the microtask isMounted ? addQueue(rerender) : target.mounted.then(() => { isMounted = true; addQueue(rerender); }); } return (target.rendered = rendered); }; // any update from hook is added to a separate queue let hooks = createHookCollection(() => addQueue(target.update), target); // creates a collection of microtask // associated with the mounted of the component target.mounted = promise( resolve => (target.mount = () => { isMounted = false; // allows the reuse of the component when it is isUnmounted and mounted if (isUnmount == true) { isUnmount = false; target.mounted = target.update(); } resolve(); }) ); /** * creates a collection of microtask * associated with the unmounted of the component */ target.unmounted = promise( resolve => (target.unmount = () => { isUnmount = true; hooks.unmount(); resolve(); }) ); target.initialize(); target.update(); } /** * register the component, be it a class or function * @param {string} nodeType * @param {Function} component * @return {Function} returns a jsx component */ function customElement(nodeType, component, options) { if (isFunction(nodeType)) { // By defining nodeType as a function, custom ELement // allows the assignment of a constructor to be extended let BaseElement = component || HTMLElement; component = nodeType; let { props, error } = component; /**@type {Function[]}*/ let initialize = []; /**@type {string[]} */ let attrs = []; let CustomElement = class extends BaseElement { constructor() { super(); /** * identifier to store the virtual-dom state, * this is unique between instances of the * component to securely consider the host status */ load(this, component, error); } connectedCallback() { load(this, component, error); this.mount(); } disconnectedCallback() { this.unmount(); } attributeChangedCallback(attr, oldValue, value) { if (attr === this[ELEMENT_IGNORE_ATTR] || oldValue === value) return; this[attrToProp(attr)] = value; } initialize() { let length = initialize.length; while (length--) initialize[length](this); } }; let prototype = CustomElement.prototype; for (let prop in props) setProperty$1(prototype, initialize, attrs, prop, props[prop]); CustomElement.observedAttributes = attrs; CustomElement.props = props; return CustomElement; } else { let { base, ...opts } = options || {}; let define = () => customElements.define( nodeType, customElement(component, base), opts ); // it allows to wait for one or more webcomponents // to be defined before the definition of this opts.waitFor ? Promise.all( [] .concat(opts.waitFor) .map(nodeType => customElements.whenDefined(nodeType)) ).then(define) : define(); return props => opts.extends ? createElement(opts.extends, { ...props, is: nodeType }) : createElement(nodeType, props); } } function setProperty$1(prototype, initialize, attrs, prop, schema) { // avoid rewriting the prototype if (prop in prototype) return; let attr = propToAttr(prop); schema = schema.name ? { type: schema } : schema; let isTypeFunction = schema.type == Function; function set(nextValue) { let prevValue = this[ELEMENT_PROPS][prop]; // if the next value in function, with the exception of the type function, // will be executed to get the next value if (!isTypeFunction && isFunction(nextValue)) { nextValue = nextValue(prevValue); } // Evaluate the defined type, to work with the value or issue an error let { value, error } = formatType(nextValue, schema.type); // define if the definition of prop has generated a type error if (error && value != null) { throw createPropError( { target: this, schema, value }, `The value defined for prop '${prop}' must be of type '${schema.type.name}'` ); } // define if the prop definition has generated an options error if (schema.options && !schema.options.includes(value)) { throw createPropError( { target: this, schema, value }, `The value defined for prop '${prop}' It is not a valid option` ); } if (prevValue == value) return; this[ELEMENT_PROPS][prop] = value; let rendered = this.update(); if (schema.event) { // The event is only dispatched if the component has finished // the rendering cycle, this is useful to observe the changes rendered.then(() => dispatchEvent(this, schema.event.type || prop, schema.event) ); } if (schema.reflect) { // the default properties are only reflected once the web-component is mounted this.mounted.then(() => { this[ELEMENT_IGNORE_ATTR] = attr; //update is prevented setAttr( this, attr, schema.type == Boolean && !value ? null : value // ); this[ELEMENT_IGNORE_ATTR] = false; // an upcoming update is allowed }); } } function get() { return this[ELEMENT_PROPS][prop]; } Object.defineProperty(prototype, prop, { set, get }); if ("value" in schema) { initialize.push(target => { let { value } = schema; target[prop] = isFunction(value) ? value() : value; }); } attrs.push(attr); } function useProp(name) { let ref = useHost(); if (name in ref.current) { if (!ref[name]) { ref[name] = [null, nextValue => (ref.current[name] = nextValue)]; } ref[name][0] = ref.current[name]; return ref[name]; } } function useEvent(type, customEventInit) { let ref = useHost(); if (!ref[type]) { ref[type] = detail => dispatchEvent( ref.current, type, detail ? { ...customEventInit, detail } : customEventInit ); } return ref[type]; } function usePublic(name, value) { let { current } = useHost(); if (current[name] != value) { current[name] = value; } return current[name]; } export { createHookCollection, customElement, createElement as h, render, toVnode, useCallback, useEffect, useEvent, useHook, useHost, useMemo, useProp, usePublic, useReducer, useRef, useRender, useState }; //# sourceMappingURL=core.js.map