UNPKG

synks

Version:
538 lines (518 loc) 19.4 kB
const SCOPE = Symbol(); const SCOPE_CHILDREN = Symbol(); const UPDATE_CONTEXT = Symbol(); class Context { constructor() { const keys = Object.getOwnPropertyNames(Object.getPrototypeOf(this)); keys.forEach((key) => { const value = this[key]; if (value instanceof Function) { this[key] = (...args) => { const output = value.apply(this, args); this[UPDATE_CONTEXT](); return output; }; } }); } [UPDATE_CONTEXT]() { } } function h(type, props, ...children) { const key = props && props.key; return { type, props, children, key: key === undefined ? null : key, }; } function arrayUnique(array) { const a = array.slice(); for (let i = 0; i < a.length; ++i) { for (let j = i + 1; j < a.length; ++j) { if (a[i] === a[j]) { a.splice(j--, 1); } } } return a; } function getChildNodes(node) { return Array.prototype.slice.call(node.childNodes); } async function build(currentNode, previousNode = {}, container) { if (currentNode.target) { return currentNode.target; } const underSameParent = container ? !!getChildNodes(container) .find((child) => child === previousNode.target) : false; // Text node if (currentNode.type === '') { const p = currentNode.props; if (p === undefined || p === null || typeof p === 'boolean') { currentNode.props = ''; } // Patch text node if (underSameParent && previousNode && previousNode.type === '' && previousNode.target) { // Patch node content if (currentNode.props !== previousNode.props) { previousNode.target.nodeValue = String(currentNode.props); } return previousNode.target; } // New text node return new Text(String(currentNode.props)); } if (!underSameParent) { previousNode.props = {}; previousNode.key = null; } // Patch node if (underSameParent && previousNode && previousNode.target && currentNode.type === previousNode.type) { return previousNode.target; } const type = String(currentNode.type); if (currentNode.type === 'svg' || container instanceof SVGElement) { return document.createElementNS('http://www.w3.org/2000/svg', type); } // New node return document.createElement(String(type)); } function isContext(value) { return !!value && Object.getPrototypeOf(value) === Context; } async function handleContext(generator, scope, context, node, contextRoot) { if (!isContext(contextRoot)) return contextRoot; const contextName = contextRoot && contextRoot.name; const currentContext = context[contextName]; if (!currentContext) { throw new Error(`${contextName} was called in <${node.type.name}> before it was defined`); } if (currentContext[1].indexOf(scope) === -1) { currentContext[1].push(scope); } return (await generator.next(currentContext[0])).value; } function isGenerator(value) { return !!value && typeof value.next === 'function' && typeof value.throw === 'function'; } async function handleHooks(generator, scope, context, node, hook) { if (!isGenerator(hook)) return hook; let value = await hook.next(); while (isContext(value.value) || value.value === SCOPE) { // Allows context in hooks if (isContext(value.value)) { const contextName = value.value && value.value.name; const currentContext = context[contextName]; if (!currentContext) { throw new Error(`${contextName} was called from hook in <${node.type.name}> before it was defined`); } if (currentContext[1].indexOf(scope) === -1) { currentContext[1].push(scope); } value = await hook.next(currentContext[0]); } // Returns current component scope in hook if (value.value === SCOPE) { value = await hook.next({ ...scope, async next() { if (value !== undefined) { Object.assign(value, await hook.next()); } return await scope.next(); }, }); } } return (await generator.next(value)).value; } async function handleCustomYields(generator, output, scope, node, context) { output = (await generator.next()).value; while (isGenerator(output) || isContext(output)) { output = await handleHooks(generator, scope, context, node, output); output = await handleContext(generator, scope, context, node, output); } return output; } var PROPS; (function (PROPS) { PROPS["innerHTML"] = "innerHTML"; PROPS["style"] = "style"; })(PROPS || (PROPS = {})); function patchStyle(currentNode, previousNode) { const target = currentNode.target; if (!currentNode.type || (target instanceof Text) || !target) return; const styleA = (currentNode.props || {}).style || {}; const styleB = (previousNode.props || {}).style || {}; Object.keys(styleA).forEach(name => { const value = styleA[name]; const oldValue = styleB[name]; if (value === oldValue) return; if (value === null || value === false || value === undefined) { return delete target[name]; } return target.style[name] = String(value); }); if (!previousNode) return; Object.keys(styleB).forEach(name => { if (styleA && (styleA[name] !== null && styleA[name] !== false && styleA[name] !== undefined)) return; return delete target.style[name]; }); } function patchProps(currentNode, previousNode) { const target = currentNode.target; if (!currentNode.type || (target instanceof Text) || !target) return; const propsA = currentNode.props || {}; const propsB = previousNode.props || {}; Object.keys(propsA).forEach(name => { if (name === PROPS.style) return; const value = propsA[name]; const oldValue = propsB[name]; if (value === oldValue) return; if (value instanceof Function) { if (value === target[name]) return; return (target[name] = value); } else { if (value === null || value === false || value === undefined) { return target.removeAttribute(name); } const newValue = String(value); if (name === PROPS.innerHTML) { return target[name] = newValue; } return target.setAttribute(name, newValue); } }); if (!previousNode) return; Object.keys(propsB).forEach(name => { if (name === PROPS.style) return; if (propsA && (propsA[name] !== null && propsA[name] !== false && propsA[name] !== undefined)) return; if (name === PROPS.innerHTML) { return delete target[name]; } return target.removeAttribute(name); }); patchStyle(currentNode, previousNode); } function checkEquality(a, b, keys) { for (const key of keys) { if (typeof a[key] === 'function' && String(a[key]) !== String(b[key])) { return false; } if (JSON.stringify(a[key]) !== JSON.stringify(b[key]) && a[key] !== b[key]) { return false; } } return true; } function quickEqual(a, b) { if (a === b) return true; if (!(a instanceof Object && a)) return false; if (!(b instanceof Object && b)) return false; const ai = Object.keys(a); const bi = Object.keys(b); if (ai.length !== bi.length) return false; return checkEquality(a, b, ai); } function removeNode(node) { if (node instanceof Array) { node.map(removeNode); return; } if (!(node && node.target && node.target.parentNode)) return; node.target.parentNode.removeChild(node.target); if (node.scope) { node.scope.destroy(); } } function removeStranglers(previous) { if (previous instanceof Array) { previous.forEach((prev) => { const instance = (prev.instance || prev); if (instance.target !== null) { removeNode(prev); } }); } } async function asyncMap(value, fn) { const output = []; for (const i in value) { output.push(await fn(value[i], parseInt(i, 10))); } return output; } async function renderChildren(currentChildren, previousChildren, container, parentIndex, context) { let offset = parentIndex; const renderedData = await asyncMap(currentChildren, async (child, index) => { const previousNode = previousChildren && previousChildren[index]; if (child instanceof Array) { const renderedData = await renderChildren(child, previousNode, container, offset + parseInt(index, 10), context); removeStranglers(previousNode); offset += Math.max([].concat(...renderedData).length - 1, 0); return renderedData; } // Handle keyed children if (child && child.key !== null && child.key !== undefined && previousNode && previousChildren instanceof Array) { if (previousNode.key === child.key) { child.target = previousNode.target; previousNode.target = null; patchProps(child, previousNode); return child; } const keyElement = previousChildren.find((p) => p.key === child.key); if (previousNode.key !== child.key && keyElement) { child.target = keyElement.target; keyElement.target = null; } } const output = await render(child, previousNode, container, parseInt(index, 10) + offset, context); if (output instanceof Array) { offset += Math.max([].concat(...output).length - 1, 0); } if (!(output instanceof Array) && output.instance instanceof Array) { offset += Math.max([].concat(...output.instance).length - 1, 0); } return output; }); removeStranglers(previousChildren); return renderedData; } function isVnode(value) { return value && value instanceof Object; } function transformNode(node) { if (node instanceof Array) { return Object.assign(node, node.map(transformNode)); } if (isVnode(node)) { if (node.children instanceof Array) { node.children = node.children.map(transformNode); } return Object.assign(node, { type: node.type, props: node.props, key: node.key === undefined ? null : node.key, children: node.children, target: null, instance: null, scope: null, }); } return { type: '', props: node, key: null, children: undefined, }; } let updateQueue = []; let cleanupQueue = []; function startCleanup() { const cleanup = cleanupQueue.slice(); cleanupQueue = []; cleanup.forEach((d) => d()); } async function render(currentNode, previousNode = currentNode && currentNode.constructor(), container, childIndex, context) { if (currentNode === undefined || currentNode === null) { return transformNode(); } if (currentNode instanceof Array) { const prevNodes = previousNode instanceof Array ? previousNode : [previousNode]; return await renderChildren(currentNode.map(transformNode), prevNodes, container, childIndex, context); } currentNode = transformNode(currentNode); if (previousNode && previousNode.type instanceof Function && currentNode && currentNode.type !== previousNode.type) { const cachedTarget = previousNode && Object.assign({}, previousNode.instance); if (previousNode.scope) { await previousNode.scope.destroy(); } if (cachedTarget) { cleanupQueue.push(() => { removeNode(cachedTarget); }); } } // Component if (currentNode.type instanceof Function) { const fn = currentNode.type; // Context if (isContext(fn)) { const currentContext = new fn(currentNode.props); const name = fn.name; const events = []; const nextFn = async (newContext) => { updateQueue = arrayUnique(events.concat(updateQueue)); // Update context state Object.assign(currentContext, newContext); const cache = updateQueue.slice().filter((d) => d.mounted && !d.rendering); updateQueue = []; // Update subscribed components for (const event of cache) { if (event.mounted && !event.rendering) { await event.next(); } } }; currentContext[UPDATE_CONTEXT] = async () => await nextFn.call(currentContext); context[name] = [currentContext, events]; // Render children return await render(currentNode.children, previousNode.children, container, childIndex, context); } // Component update if (currentNode.type === previousNode.type && !!previousNode.instance) { const props1 = Object.assign({}, currentNode.props, { children: currentNode.children, }); const props2 = Object.assign({}, previousNode.props, { children: previousNode.children, }); const key1 = currentNode.key; const key2 = previousNode.key; // Don't update component as props are the same (in shallow level) if (quickEqual(props1, props2) || (key1 !== null && key1 === key2)) { return Object.assign(currentNode, previousNode); } } // Regular component let output; const originalProps = Object.assign({}, currentNode.props, { children: currentNode.children, }); const scope = { mounted: true, rendering: false, [SCOPE_CHILDREN]: [], async onMount() { }, async onDestroy() { }, async next() { if (!scope.mounted || scope.rendering) { return; } await this.nextProps(originalProps); }, async nextProps(props) { if (!scope.mounted || scope.rendering) { return; } scope.rendering = true; // eslint-disable-next-line @typescript-eslint/no-use-before-define await renderSelf(currentNode.instance, props); scope.rendering = false; }, async destroy() { if (!scope.mounted) { return; } scope.mounted = false; await scope.onDestroy(); scope[SCOPE_CHILDREN].forEach((s) => { s.destroy(); }); scope[SCOPE_CHILDREN] = []; const parentScopes = context.scope && context.scope[SCOPE_CHILDREN]; if (parentScopes) { context.scope[SCOPE_CHILDREN] = parentScopes.filter((s) => s.mounted); } }, }; if (context.scope) { context.scope[SCOPE_CHILDREN].push(scope); } const newContext = Object.assign({}, context, { scope, }); currentNode.scope = scope; let generator; const renderSelf = async (previousTree, props) => { Object.assign(originalProps, props); if (typeof fn === 'function') { output = await fn.call(scope, props); // Generator component if (isGenerator(output)) { if (!generator) { generator = output; } output = await handleCustomYields(generator, output, scope, currentNode, context); } } const rendered = await render(output, previousTree, container, childIndex, newContext); startCleanup(); if (!(currentNode instanceof Array)) { currentNode.instance = output; } Object.assign(previousNode, currentNode); if (previousTree) { Object.assign(previousTree, rendered); } previousNode.target = null; return rendered; }; await renderSelf(previousNode, originalProps); scope.onMount(); return currentNode; } const isKeyMoved = currentNode.target && previousNode.key !== undefined && previousNode.key !== null; currentNode.target = await build(currentNode, previousNode, container); // Patch props patchProps(currentNode, previousNode); if (isKeyMoved) { container.insertBefore(currentNode.target, container.childNodes[childIndex + 1]); return currentNode; } if (currentNode.target === previousNode.target || (previousNode.instance && previousNode.target === container)) { previousNode.target = null; } // Patch children if (currentNode.children instanceof Array) { await renderChildren(currentNode.children, previousNode.children, currentNode.target, 0, context); } // Remove stranglers if (previousNode.target && previousNode.target.parentNode && previousNode.target !== currentNode.target && previousNode.target !== container) { removeNode(previousNode); } if (previousNode && previousNode.children) { removeStranglers(previousNode.children); } // Add element to dom if (!currentNode.target.parentNode) { container.insertBefore(currentNode.target, container.childNodes[childIndex]); } return currentNode; } async function mount(node, container = document.body, previousNode = node.constructor()) { if (!(container instanceof HTMLElement)) { throw new Error('[Synks] Container should be valid HTMLElement'); } if (!(node instanceof Array)) { node = [node]; } return await render(node, previousNode, container, 0, {}); } export { Context, SCOPE, h, mount };