UNPKG

gruujs

Version:

JavaScript framework for creating dynamic content

589 lines (503 loc) 17.7 kB
const Gruu = ((function () { const exists = value => value != null && value !== false let lastId = 1 const genId = () => { lastId += 1 return lastId } const isManualAttribute = v => v.startsWith('aria') || v === 'role' const camelToKebab = s => s.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase() const last = v => v[v.length - 1] const noProxy = v => (v && v.noProxy) || v const get = (object, actions) => actions.reduce((acc, key) => acc[key], object) const findClosestNodeParent = object => (object._n ? object : findClosestNodeParent(object._parent)) const bindWithProxy = (component, fn = () => null) => ( component.noProxy ? component : fn.bind(new Proxy(component, handler(component))) ) const everyEqual = (a, b) => { for (let i = 0; i < a.length; i += 1) { if (a[i] !== b[i]) { return false } } return true } const parseTextComponent = (v) => { const vNoProxy = noProxy(v) return (!exists(vNoProxy) || typeof vNoProxy === 'object' ? vNoProxy : { _type: 'text', textContent: vNoProxy }) } const removeUndefined = (children) => { let i = children.length - 1 while (i >= 0 && !children[i]) { children.splice(i, 1) i -= 1 } } const clearListeners = (component, paramsTuRemove) => { if (component._r && component._w) { component._id = component._id || genId() Object.keys(component._w).forEach((w) => { if (paramsTuRemove) { const set = component._w[w]._l[component._id].keys set.forEach((key) => { const param = key.split('->')[1].trim() if (paramsTuRemove.includes(param)) { set.delete(key) } }) if (set.size === 0) { delete component._w[w]._l[component._id] delete component._w[w] } } else { delete component._w[w]._l[component._id] delete component._w[w] } }) } } const updateDynamicProperty = (component, key, value) => { if (key.startsWith('$')) { const pureKey = key.slice(1) processStack.push({ component, key }) const val = bindWithProxy(component, value || component[key])() processStack.pop() const oldValue = component[pureKey] const v = domModificator(val, oldValue, component, [pureKey]) component[pureKey] = v } } const overrideDynamicProperty = (component, key) => { if (key.startsWith('$')) { const pureKey = key.slice(1) processStack.push({ component, key }) const result = bindWithProxy(component, component[key])() const v = Array.isArray(result) || key !== '$children' ? result : [result] component[pureKey] = pureKey === 'children' ? v.map(parseTextComponent) : v processStack.pop() } } const handleComponentRender = (value, target, object, action, parent) => { const component = recursivelyCreateAndRenderComponent(value, parent) component._parent = parent value = component const componentNodeArray = componentToNodeArray(component) const nodeParent = findClosestNodeParent(object) if ( (nodeParent === object || (nodeParent.children.length === 1 && nodeParent.children[0] === object)) && parseInt(action, 10) > object.children.length - 1 ) { componentNodeArray.forEach(({ node }) => { if (node) { nodeParent._n.appendChild(node) } }) } else { object.children[action] = component const nodeParentArray = componentToNodeArray(nodeParent, true) object.children[action] = target const lastItem = last(componentNodeArray) if (lastItem) { const index = nodeParentArray.findIndex(({ id }) => id === lastItem.id) const item = index !== -1 ? nodeParentArray.slice(index + 1).find(v => v && v.node && v.node.parentNode) : null componentNodeArray.forEach(({ node }) => { if (node) { nodeParent._n.insertBefore(node, item && item.node) } }) } } if (target) { target._unmount() } return value } const childrenModificationHandler = (value, target, object, actions, parent) => { const action = actions[1] if (action === undefined) { if (!object.children) { object.children = [] } value = (Array.isArray(value) ? value : [value]).map(parseTextComponent) const length = target.length - value.length const val = value.concat(Array(length < 0 ? 0 : length)) for (let i = 0; i < val.length;) { const currentChild = target[i] const newChild = val[i] if (value.length >= target.length || (!currentChild || !newChild || !currentChild._key || !newChild._key || currentChild._key === newChild._key) ) { value[i] = domModificator(newChild, currentChild, object, ['children', `${i}`], { dest: CHILDREN, parent }) i += 1 } else { currentChild._unmount() target.splice(i, 1) } } removeUndefined(value) } else if (!isNaN(parseInt(action, 10))) { if ( target !== value && ( !target || !value || (!target._createdBy && !value._createdBy) || !everyEqual(target._createdBy, value._createdBy) ) ) { const targetExists = exists(target) const valueExists = exists(value) if (targetExists && !valueExists) { target._unmount() } else if (targetExists && valueExists) { clearListeners(target) if ((target._type || value._type) && (!target._type || !value._type || target._type !== value._type) && (target._type !== 'text' || typeof value === 'object')) { value = handleComponentRender(value, target, object, action, parent) } else { const component = parseTextComponent(value) component._parent = parent if (component.children && Array.isArray(component.children)) { component.children = component.children.map(noProxy) } const keys = { $: [], other: [] } Object.keys(Object.assign({}, target, component)).forEach((key) => { if (target[key] !== component[key]) { if (key.startsWith('$')) { keys.$.push(key) keys.other.push(key.slice(1)) } else if (key.startsWith('_')) { if (!['_createdBy', '_key', '_parent'].includes(key)) { component[key] = target[key] } } else { keys.other.push(key) } } }) keys.$.forEach((key) => { overrideDynamicProperty(component, key) }) keys.other.forEach((key) => { if (typeof component[key] === 'function') { component[key] = bindWithProxy(component, component[key]) } component[key] = domModificator(component[key], target[key], target, [key], { parent: component }) }) value = component } } else if (!targetExists && valueExists) { value = handleComponentRender(value, target, object, action, parent) } } else { value = target } } return value } const nodeModificationHandler = (object, actions, value, target) => { if (value == null) { const action = actions[0] object._n[action] = '' if (object._n.removeAttribute) { if (isManualAttribute(action)) { object._n.removeAttribute(camelToKebab(action)) } else { object._n.removeAttribute(action) } } } else if (value && typeof value === 'object') { const action = actions[0] const mergedValue = Object.assign({}, target, value) if (object._n) { Object.keys(mergedValue).forEach((key) => { if (!target || target[key] !== value[key]) { object._n[action][key] = value[key] || '' } }) } } else if (object._n && target !== value) { const t = get(object._n, actions.slice(0, -1)) const action = last(actions) if (isManualAttribute(action)) { t.setAttribute(camelToKebab(action), value) } else { t[action] = value } } return value } const DEFAULT = 0 const STATE = 1 const CHILDREN = 2 const NODE = 3 const findDestination = (actions = []) => { let destination = '' let isNode = true for (const action of actions) { if (destination === DEFAULT || action.startsWith('$')) { return DEFAULT } else if (destination === STATE || (action === 'state' && isNode)) { return STATE } else if (action === 'children' && isNode) { destination = CHILDREN isNode = false } else if (destination === CHILDREN && !isNaN(parseInt(action, 10))) { destination = CHILDREN isNode = true } else if (isNode) { destination = NODE isNode = true } } return destination } const domModificator = (val, oldValue, obj, actions, { dest, parent = obj } = {}) => { const destination = dest || findDestination(actions) const object = noProxy(obj) const value = noProxy(val) switch (destination) { case CHILDREN: return childrenModificationHandler(value, oldValue, object, actions, parent) case NODE: return nodeModificationHandler(object, actions, value, oldValue) default: return val } } const getHandler = (object, k, target, key) => { if (key === 'noProxy') { return target } if (typeof key === 'string' && key.startsWith('_')) { return target[key] } const { component: stackElement, key: stackKey } = last(processStack) || {} if (stackElement) { const objectNoProxy = noProxy(object) const stackElementNoProxy = noProxy(stackElement) if (!objectNoProxy._l) { objectNoProxy._l = {} } const newKey = [...k, key].join('.') stackElementNoProxy._id = stackElementNoProxy._id || genId() if (!objectNoProxy._l[stackElementNoProxy._id]) { objectNoProxy._l[stackElementNoProxy._id] = { keys: new Set() } } objectNoProxy._l[stackElementNoProxy._id].component = stackElementNoProxy objectNoProxy._l[stackElementNoProxy._id].keys.add(`${newKey} -> ${stackKey}`) if (!stackElementNoProxy._w) { stackElementNoProxy._w = {} } objectNoProxy._id = objectNoProxy._id || genId() stackElementNoProxy._w[objectNoProxy._id] = objectNoProxy } const isType = target[key] && (target[key]._type || target[key].children) const component = isType ? target[key] : object if (target[key] && typeof target[key] === 'object') { return new Proxy(noProxy(target[key]), handler(component, ...(isType ? [] : [...k, key]))) } if (typeof target[key] === 'function') { return target[key].bind(target) } return target[key] } const lastComponent = (v, actions) => { let curr = v let c = v let i = 0 let mode = NODE for (let j = 0; j < actions.length; j += 1) { const action = actions[j] curr = curr[action] if (mode === NODE && action === CHILDREN) { mode = CHILDREN } else if (mode === NODE && action === STATE) { return { c, i: j } } else if (mode === CHILDREN && !isNaN(parseInt(action, 10))) { c = curr i = j mode = NODE } } return { c, i } } const setHandler = (object, k, target, key, value) => { const actions = [...k, key] const { c, i } = lastComponent(object, actions) const oldValue = target[key] const val = noProxy(value) const v = domModificator(val, oldValue, c, actions.slice(i)) target[key] = v object._actionsToUpdate = [...(object._actionsToUpdate || []), actions.join('.')] clearTimeout(object._rerender) object._rerender = setTimeout(() => { const _actions = object._actionsToUpdate object._actionsToUpdate = [] if (object._l) { const listenersIds = Object.keys(object._l) listenersIds.forEach((id) => { if (object._l[id]) { const { component, keys } = object._l[id] const paramsToUpdate = Object.keys(component).reduce((acc, param) => [ ...acc.filter(p => p !== param), ...( param.startsWith('$') && _actions.some(_action => keys.has(`${_action} -> ${param.split('.')[0]}`)) ? [param] : [] ) ], []) clearListeners(component, paramsToUpdate) paramsToUpdate.forEach((param) => { updateDynamicProperty(component, param) }) } }) } }) return true } const handler = (object, ...k) => ({ get: (target, key) => getHandler(object, k, target, key), set: (target, key, value) => setHandler(object, k, target, key, value) }) const createElement = (component) => { const node = document.createElement(component._type) Object.keys(component).forEach((key) => { if (key.startsWith('_') || key.startsWith('$')) { return } if (isManualAttribute(key)) { node.setAttribute(camelToKebab(key), component[key]) } switch (key) { case 'style': Object.keys(component[key]).forEach((cssProp) => { node.style[cssProp] = component[key][cssProp] }) break case 'children': case 'state': return default: node[key] = component[key] } }) return node } const processStack = [] const internallyCreateComponent = (object) => { const component = noProxy(object) if (component._r) { return component } Object.keys(component).forEach((key) => { if (typeof component[key] === 'function' && !key.startsWith('$')) { component[key] = bindWithProxy(component, component[key]) } overrideDynamicProperty(component, key) }) if (component.children && Array.isArray(component.children)) { component.children = component.children.map(noProxy) } component._unmount = function () { clearListeners(this) if (this._r) { if (this._n) { const nodeParent = findClosestNodeParent(this._parent) if (this._n.parentNode) { nodeParent._n.removeChild(this._n) } } else if (this.children) { this.children.forEach((child) => { if (child) { child._unmount() } }) } } } return component } const restoreListeners = (object) => { if (object._r) { Object.keys(object).forEach((key) => { updateDynamicProperty(object, key) }) if (!object._n && object.children) { object.children.forEach(restoreListeners) } } } const recursivelyCreateAndRenderComponent = (obj, parent, nodeParent) => { const object = parseTextComponent(obj) if (!exists(object)) { return object } restoreListeners(object) const component = internallyCreateComponent(object) internallyRenderComponent(component, parent, nodeParent) if (exists(component.children) && !component._r) { if (!Array.isArray(component.children)) { component.children = [component.children] } component.children = component.children.map(child => ( recursivelyCreateAndRenderComponent(child, component, component._n ? component : nodeParent) )) } component._r = true return component } const createComponent = (obj) => { const object = parseTextComponent(obj) if (!exists(object)) { return object } return new Proxy(object, handler(object)) } const internallyRenderComponent = (component, parent, nodeParent) => { const pureComponent = noProxy(component) if (pureComponent) { if (pureComponent._type) { pureComponent._n = pureComponent._n || ( pureComponent._type === 'text' ? document.createTextNode(pureComponent.textContent) : createElement(pureComponent) ) } pureComponent._parent = noProxy(parent) if (nodeParent && pureComponent._n) { nodeParent._n.appendChild(pureComponent._n) } } } const componentToNodeArray = (component, next) => { const pureComponent = noProxy(component) if (exists(pureComponent)) { pureComponent._id = pureComponent._id || genId() if (pureComponent._n && !next) { return [{ id: pureComponent._id, node: pureComponent._n }] } if (pureComponent.children) { return pureComponent.children.reduce((acc, child) => acc.concat(child ? componentToNodeArray(child) : [{ id: pureComponent._id }]), [] ) } } return [] } const renderApp = (root, children) => { const component = noProxy(createComponent({ children })) const parent = { _n: root, children: [component] } recursivelyCreateAndRenderComponent(component, parent, parent) } return Object.assign( { createComponent, renderApp }, window.__DEV__ ? { destinations: { DEFAULT, STATE, CHILDREN, NODE }, findDestination } : {} ) })()) if (typeof module !== 'undefined') { module.exports = Gruu }