UNPKG

@dvo/raven

Version:

A simple store and databinding tool for vanilla applications.

328 lines (287 loc) 8.11 kB
// When raven needs to use getter and setters on outside objects, it will make use of this prefix. const prefix = '_' /* Utilidades */ const pipe = (...operations) => data => operations.reduce((acc, cur) => cur(acc), data) const isArray = Array.isArray const isObject = target => typeof target === 'object' && !isArray(target) && target !== null const isString = target => typeof target === 'string' const identity = x => x /* Data type transformation */ const fromStringToArray = path => path.split('.') const fromStringListToArrayList = paths => paths.map(fromStringToArray) const fromArrayToString = path => path.join('.') const fromArrayListToStringList = paths => paths.map(fromArrayToString) const fromArrayToObject = (path, value) => path.reverse().reduce((acc, cur, idx) => idx === 0 ? { [cur]: value } : { [cur]: acc } , 0) const fromArrayListToObject = arrayList => arrayList .map(fullPath => { const shortPath = [...fullPath] const value = shortPath.pop() return fromArrayToObject(shortPath, value) }) .reduce((acc, cur) => mergeObjects(acc, cur)) const fromStringToObject = (path, value) => fromArrayToObject(fromStringToArray(path), value) const fromObjectToArrayList = structure => { const paths = [] const getPath = (object, path) => { Object.entries(object).forEach(entry => { const [key, value] = entry if (isObject(value)) { getPath(value, [...path, key]) } else { paths.push([...path, key]) } }) } getPath(structure, []) return paths } const fromObjectToStringList = structure => fromObjectToArrayList(structure).map(fromArrayToString) const parseToObject = (dataStructure, value) => typeof dataStructure === 'string' ? fromStringToObject(dataStructure, value) : dataStructure /* Inner functions */ const reach = queryObject => path => isArray(path) ? reachArray(queryObject, path) : reachString(queryObject, path) const reachArray = (queryObject, path) => { if (path.length === 1) { return ({ parentObject: queryObject, propertyName: path[0], propertyValue: queryObject[path[0]] }) } const clonedArray = [...path] const nextProp = clonedArray.shift() const nextObject = queryObject[nextProp] return reach(nextObject)(clonedArray) } const reachString = (queryObject, path) => reachArray(queryObject, fromStringToArray(path)) const addPartialPaths = paths => isArray(paths[0]) ? addPartialArrayPaths(paths) : addPartialStringPaths(paths) const addPartialArrayPaths = paths => { const listOfPaths = [] const addPath = path => { listOfPaths.push(path) if (path.length > 1) { const clonedPath = [...path] clonedPath.pop() addPath(clonedPath) } } paths.forEach(addPath) return listOfPaths } const addPartialStringPaths = paths => pipe( fromStringListToArrayList, addPartialArrayPaths, fromArrayListToStringList )(paths) const mergeObjects = (objA, objB) => { const result = { ...objA } Object.entries(objB).forEach(entry => { const [key, valueB] = entry const valueA = result[key] if (isObject(valueA) && isObject(valueB)) { result[key] = mergeObjects(valueA, valueB) } else { result[key] = valueB } }) return result } const executeCallbacks = (request, sub = raven.funcs.subscriptions, store = raven.store) => { const paths = pipe( fromObjectToStringList, addPartialPaths)(request) const reachStore = reach(store) paths .filter(path => sub[path]) .flatMap(path => sub[path] .map(callback => ({ path, callback }) ) ) .forEach(item => item.callback(reachStore(item.path).propertyValue) ) } const setValueToObject = targetObject => value => { const pathList = fromObjectToArrayList(targetObject) .map(path => [...path, value]) return fromArrayListToObject(pathList) } const copyValueToStore = (from, to, callback) => { const stateAdd = setValueToObject(to)(callback(from.target[from.prop])) set(stateAdd) } const pushFromElement = (from, to, callback = identity) => { const events = typeof from.event === 'string' ? [from.event] : from.event || ['change', 'input'] events.forEach(event => from.target.addEventListener(event, e => { const callbackArg = e.target.value || e.target.innerHTML const callbackResult = callback(callbackArg) set(setValueToObject(to)(callbackResult)) }) ) } const pushFromObject = (from, to, callback = identity) => { from.target[`${prefix}${from.prop}`] = from.target[from.prop] copyValueToStore(from, to, callback) Object.defineProperty( from.target, from.prop, { set: value => { const result = callback(value) fromObjectToStringList(to) .forEach(path => raven.set(path, result) ) from.target[`${prefix}${from.prop}`] = value }, get: () => from.target[`${prefix}${from.prop}`] } ) } const pullToElement = (from, to, callback = identity) => { const currentValue = reach(raven.store)(from).propertyValue to.target[to.prop] = callback(currentValue) subscribe(from, value => { to.target[to.prop] = callback(value) }) } /* API */ const load = userStore => { raven.store = userStore } const set = (request, value) => { const newValue = typeof request === 'string' ? fromStringToObject(request, value) : request load(mergeObjects(raven.store, newValue)) executeCallbacks(newValue) } const subscribe = (target, callback) => { const callbackKeys = isObject(target) ? fromObjectToStringList(target) : isArray(target) ? [fromArrayToString(target)] : [target] callbackKeys.forEach(key => { raven.funcs.subscriptions[key] = [ ...(raven.funcs.subscriptions[key] || []), callback ] }) } const clear = condition => { if (!condition) { raven.funcs.subscriptions = {} return } const conditionArray = isArray(condition) ? condition : [condition] conditionArray.forEach(item => { if (isString(item)) { delete raven.funcs.subscriptions[item] } else { Object.keys(raven.store).forEach(key => { if (raven.funcs.subscriptions[key]) { raven.funcs.subscriptions[key] = raven.funcs.subscriptions[key] .filter(callback => callback !== item) } }) } }) } // Push changes from an object/DOM element to a target object const push = (from, to, callback = identity, funcEl = pushFromElement, funcObj = pushFromObject) => { const parsedTo = parseToObject(to, true) const func = from.target instanceof window.HTMLElement ? funcEl : funcObj func(from, parsedTo, callback) } // Pull changes from an object to a target object/Dom element const pull = (from, to, callback = identity, funcEl = pullToElement) => { let parsedFrom = from if (isObject(from)) { const allPaths = fromObjectToArrayList(from) if (allPaths.length > 1) { throw new Error('Raven.pull cannot be passed an object with multiple properties.') } else { parsedFrom = allPaths[0] } } funcEl(parsedFrom, to, callback) } // Push and pull shortcut that assumes a link between a DOM element and the store const sync = (path, element, callbackToPath = identity, callbackToElement = identity) => { pull(path, element, callbackToElement) push(element, path, callbackToPath) } const raven = { clear, load, pull, push, set, store: {}, subscribe, sync, funcs: { addPartialPaths, addPartialArrayPaths, addPartialStringPaths, copyValueToStore, fromArrayListToObject, fromArrayToString, fromStringToObject, fromObjectToStringList, fromObjectToArrayList, fromStringToArray, executeCallbacks, isArray, isObject, isString, mergeObjects, pullToElement, pushFromObject, pushFromElement, reach, reachArray, reachString, setValueToObject, subscriptions: {} } } export default raven