UNPKG

dom-reactive

Version:

A library for working with DOM with syntax similar to Vue.

521 lines (452 loc) 16.7 kB
/** * @source https://github.com/artemsites/dom * @source https://gitverse.ru/artemsites/dom * * @api: * createScope - creating an object in the DOM for working with the library * ref - reactive state * data-class is an attribute of an HTML element for dynamic class management */ import mitt from "mitt"; export const emitter = mitt(); type Wrapper = any; interface ComponentInstance { [key: string]: any; } interface State { value: any; } declare global { interface Window { [key: string]: any; } } let stateNamesHashes = new Map(); let createUUID = ()=>{} if (window && typeof window.crypto.randomUUID === 'function') { createUUID = window.crypto.randomUUID.bind(window.crypto) } else { createUUID = generateUniqueId } export function createScope( scopeId: string, scope: (e) => ComponentInstance, alias: string = "" ) { const $wrapper = document.getElementById(scopeId); if ($wrapper) { const scopeInstance = scope($wrapper); // @note handle data-ref handlerRefsInDom($wrapper as Wrapper, scopeInstance); // @note handle data-click handlerClickReactive($wrapper, scopeInstance); // @note handle data-class handlerClassesReactive($wrapper, scopeInstance); // @note handle input[data-value] handlerInputDataValueReactive($wrapper, scopeInstance); // @note handle data-change handlerChangeReactive($wrapper, scopeInstance); // @note handle data-input handlerInputReactive($wrapper, scopeInstance); if (alias !== "") { window[alias] = scopeInstance; } else { window[scopeId] = scopeInstance; } } else { console.warn("Not found wrapper: #" + scopeId); } } export function createComponent(wrapperClass: string, component: (e) => {}) { const wrappers = document.getElementsByClassName( wrapperClass ) as HTMLCollection; for (let $wrapper of wrappers) { if ($wrapper) { const componentInstance: ComponentInstance = component($wrapper); if (isObject(componentInstance)) { // @note handle data-ref handlerRefsInDom($wrapper as Wrapper, componentInstance); // @note handle data-click handlerClickReactive($wrapper as Wrapper, componentInstance); // @note handle data-class handlerClassesReactive($wrapper as Wrapper, componentInstance); // @note handle input[data-value] // ! @todo not tested!!! handlerInputDataValueReactive( $wrapper as Wrapper, componentInstance ); // @note handle data-change handlerChangeReactive($wrapper as Wrapper, componentInstance); // @note handle data-input handlerInputReactive($wrapper, componentInstance); } } else { console.warn("Not found wrapper: ." + wrapperClass); } } } export function ref(defaultValue: any): State { const stateNameHash = `state_${createUUID()}`; let state: State = { value: defaultValue }; const proxyState = new Proxy<State>(state, { set(stateTarget, prop, valueNew) { if (prop === "value") { if (stateTarget["value"] !== valueNew) { stateTarget["value"] = valueNew; emitter.emit(stateNameHash, stateTarget); } return true; } return false; }, }); stateNamesHashes.set(proxyState, stateNameHash); emitter.emit(stateNameHash, proxyState); return proxyState; } function handlerRefsInDom($wrapper: Wrapper, instance: ComponentInstance) { const refsInDomAll = findAllByAttr("data-ref", $wrapper); refsInDomAll.forEach(($refEl) => { const refName = $refEl.getAttribute("data-ref"); if (refName && instance[refName]) { instance[refName].value = $refEl; } else { console.warn("The data-ref name was not found in: ", $refEl); } }); } function handlerInputDataValueReactive( $wrapper: Wrapper, instance: ComponentInstance ) { const dataValues = findAllByAttr("data-value", $wrapper); dataValues.forEach(($dataValue) => { if ($dataValue instanceof HTMLInputElement) { const dataValue: string | null = $dataValue.getAttribute('data-value') || null; if (dataValue) { const jsExpressionWithPrefix: string = dataValue; let jsExpression = deleteWordPrefix(jsExpressionWithPrefix); const state = instance[jsExpression]; if (state) { $dataValue.value = state.value; const stateNameHash = stateNamesHashes.get(state); emitter.on(stateNameHash, (newState: any) => { $dataValue.value = newState.value; }); } } } }); } function handlerClickReactive($wrapper: Wrapper, instance: ComponentInstance) { const elClicks = findAllByAttr("data-click", $wrapper); if (elClicks.length) { elClicks.forEach(($elOnClick) => { let methodNameOnClick = $elOnClick.getAttribute('data-click'); if (methodNameOnClick) { // ! Это для убирания префикса например header. - оно пока не мешает в случае если его нет вообще const methodNameOnClickWithoutPrefix = deleteWordPrefix(methodNameOnClick); const methodOnClick = instance[methodNameOnClickWithoutPrefix]; if (methodOnClick) { $elOnClick.addEventListener("click", function (e) { methodOnClick(e); }); } } else { console.warn( "The name of the data-click method was not found in: ", $elOnClick ); } }); } } function handlerChangeReactive($wrapper: Wrapper, instance: ComponentInstance) { const elChanges = findAllByAttr("data-change", $wrapper); if (elChanges.length) { elChanges.forEach(($elOnchange) => { if ($elOnchange) { let methodNameOnChange = $elOnchange.getAttribute('data-change'); if (methodNameOnChange) { // ! Это для убирания префикса например header. - оно пока не мешает в случае если его нет вообще const methodNameOnChangeWithoutPrefix = deleteWordPrefix(methodNameOnChange); const methodOnChange = instance[methodNameOnChangeWithoutPrefix]; if (methodOnChange) { $elOnchange.addEventListener("change", function (e) { methodOnChange(e); }); } } } else { console.warn( "The name of the data-click method was not found in: ", $elOnchange ); } }); } } function handlerInputReactive($wrapper: Wrapper, instance: ComponentInstance) { const elInputs = findAllByAttr("data-input", $wrapper); if (elInputs.length) { elInputs.forEach(($elOnInput) => { if ($elOnInput) { let methodNameOnInput = $elOnInput.getAttribute('data-input'); if (methodNameOnInput) { // ! Это для убирания префикса например header. - оно пока не мешает в случае если его нет вообще const methodNameOnInputWithoutPrefix = deleteWordPrefix(methodNameOnInput); const methodOnInput = instance[methodNameOnInputWithoutPrefix]; if (methodOnInput) { $elOnInput.addEventListener("input", function (e) { methodOnInput(e); }); } } } else { console.warn( "The name of the data-click method was not found in: ", $elOnInput ); } }); } } function handlerClassesReactive( $wrapper: Wrapper, instance: ComponentInstance ) { const elClasses = findAllByAttr("data-class", $wrapper); elClasses.forEach(($el) => { handlerClassesReactiveSubFunc1($el); }); function handlerClassesReactiveSubFunc1($el: HTMLElement) { let jsonString = $el.getAttribute('data-class'); if (jsonString) { // @todo deletion is necessary in another place so that one component does not delete the data-class of another component. // $el.removeAttribute("data-class"); let parsedJson; try { parsedJson = JSON.parse(jsonString); } catch (error) { console.error("Error at JSON string: " + jsonString); console.error(error); } if (Array.isArray(parsedJson)) { for (let i in parsedJson) { let jsExpressionTernary = parsedJson[i]; const regex = /(.+?)\s*\?\s*(.+?)\s*:\s*(.+)/; const match = jsExpressionTernary.match(regex); let jsNameWithPrefix = match[1]; const classNameTrue = match[2]; const classNameFalse = match[3]; const className = [classNameTrue, classNameFalse]; handlerClassesReactiveSubFunc2( $el, className, jsNameWithPrefix ); } } else if (isObject(parsedJson)) { for (let className in parsedJson) { let jsNameWithPrefix = parsedJson[className]; handlerClassesReactiveSubFunc2( $el, className, jsNameWithPrefix ); } } } else { console.warn("The data-class JSON string was not found in: ", $el); } } function handlerClassesReactiveSubFunc2( $el: HTMLElement, className: string | string[], jsNameWithPrefix: string ) { let isRevertVal = false; if (jsNameWithPrefix[0] === "!") { isRevertVal = true; jsNameWithPrefix = jsNameWithPrefix.slice(1); } let jsExpression = deleteWordPrefix(jsNameWithPrefix); const isNotEqualExpression = jsExpression.includes("!="); const isEqualExpression = jsExpression.includes("=="); if (isNotEqualExpression || isEqualExpression) { if (isNotEqualExpression) { const regex = /!=/; const operator = "!="; splitExpressionAndCompareAndUpdateClassesReactive( jsExpression, regex, operator, isRevertVal, className, $el ); } else if (isEqualExpression) { const regex = /==/; const operator = "=="; splitExpressionAndCompareAndUpdateClassesReactive( jsExpression, regex, operator, isRevertVal, className, $el ); } } else { const operator = "=="; const jsName = jsExpression; const jsVal = true; compareAndUpdateClassesReactive( jsName, jsVal, operator, isRevertVal, className, $el ); } } function splitExpressionAndCompareAndUpdateClassesReactive( jsExpression: any, regex: any, operator: any, isRevertVal: any, className: any, $el: any ) { const res = splitExpression(jsExpression, regex); if (res && res.length === 2) { const [jsName, jsVal] = res; compareAndUpdateClassesReactive( jsName, jsVal, operator, isRevertVal, className, $el ); } } function compareAndUpdateClassesReactive( jsName: any, jsVal: any, operator: any, isRevertVal: any, className: any, $el: any ) { const state = instance[jsName]; if (!state) { showWarnIfRefNotFound($wrapper, jsName); } else { const isTrue = compare(state.value, jsVal, operator); toggleClass(isTrue, className, $el, isRevertVal); const stateNameHash = stateNamesHashes.get(state); emitter.on(stateNameHash, (newState: any) => { const isTrue = compare(newState.value, jsVal, operator); toggleClass(isTrue, className, $el, isRevertVal); }); } } function toggleClass( value: any, className: string | string[], where: HTMLElement, isRevertVal = false ) { try { const isTrue = (value && !isRevertVal) || (!value && isRevertVal); if (typeof className === "string") { if (isTrue) { where.classList.add(className); } else { where.classList.remove(className); } } else if (Array.isArray(className)) { const [classIfTrue, classIfFalse] = className; if (isTrue) { where.classList.remove(classIfFalse); where.classList.add(classIfTrue); } else { where.classList.remove(classIfTrue); where.classList.add(classIfFalse); } } } catch (error) { console.error(error); } } } function showWarnIfRefNotFound($wrapper: Wrapper, jsName: string) { const instanceId = "#" + $wrapper.getAttribute("id") || "." + $wrapper.getAttribute("class"); console.warn( `Ref ${jsName} is not exists at ${instanceId}. Perhaps the component is located in another component.` ); } // ! tools: function isObject(value: any) { return value !== null && typeof value === "object"; } function deleteWordPrefix(strWithprefixWithDot: string) { return strWithprefixWithDot.replace(/^\w+\./, ""); } function splitExpression( expression: string, regex: RegExp ): [string, string] | null { const parts = expression.split(regex); if (parts.length === 2) { return [parts[0].trim(), parts[1].trim()]; } return null; } function compare(value1: any, value2: any, operator: string) { switch (operator) { case "!=": return value1 != value2; case "==": return value1 == value2; case "<": return value1 < value2; case ">": return value1 > value2; case "<=": return value1 <= value2; case ">=": return value1 >= value2; default: throw new Error("Invalid operator"); } } function findAllByAttr(attr: string, $wrapper: HTMLElement) { const els = $wrapper.querySelectorAll(`[${attr}]`) as NodeListOf< HTMLElement | HTMLInputElement >; const elsAll = [...Array.from(els)]; if ($wrapper.dataset && $wrapper.getAttribute(attr)) { elsAll.push($wrapper); } return elsAll; } function generateUniqueId() { // 4 - версия UUID return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); // Установка варианта UUID return v.toString(16); }); }