UNPKG

data-context-binding

Version:

Simple and lightweight solution for binding data to DOM elements.

1,034 lines (701 loc) 36.3 kB
/** Copyright (c) 2024, Manuel Lõhmus (MIT License). */ "use strict"; (function () { exportModule("data-context-binding", ["data-context"], function factory(DC) { var globalScope = this, { createDataContext, parse, stringify } = DC; //TODO [ x ] default bind ... //TODO [ x ] template binding //TODO [ x ] Event > : 'new', 'set', 'reposition', 'delete', '-change', 'bind', 'unbind' //TODO [ x ] DOM Attribute: 'path', 'bind', 'unbinded' var DataContextBinding = Object.defineProperties(bindDataContext, { bind: { value: false, writable: true, configurable: false, enumerable: false }, bindAllElements: { value: bindAllElements, writable: false, configurable: false, enumerable: false }, bindElement: { value: bindElement, writable: false, configurable: false, enumerable: false }, bindingContext: { value: bindingContext, writable: false, configurable: false, enumerable: false }, change: { value: changeBind, writable: false, configurable: false, enumerable: false }, innerHTML: { value: innerHTMLBind, writable: true, configurable: false, enumerable: false }, value: { value: valueBind, writable: true, configurable: false, enumerable: false }, getvalue: { value: getValueBind, writable: true, configurable: false, enumerable: false }, setvalue: { value: setValueBind, writable: true, configurable: false, enumerable: false }, check: { value: checkBind, writable: true, configurable: false, enumerable: false }, hidden: { value: hiddenBind, writable: true, configurable: false, enumerable: false }, enabled: { value: enabledBind, writable: true, configurable: false, enumerable: false }, disabled: { value: disabledBind, writable: true, configurable: false, enumerable: false }, input: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_checkbox: { value: checkBind, writable: true, configurable: false, enumerable: false }, input_color: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_date: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_datetime_local: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_email: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_file: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_hidden: { value: hiddenBind, writable: true, configurable: false, enumerable: false }, input_month: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_number: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_password: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_radio: { value: checkBind, writable: true, configurable: false, enumerable: false }, input_range: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_search: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_tel: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_text: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_time: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_url: { value: valueBind, writable: true, configurable: false, enumerable: false }, input_week: { value: valueBind, writable: true, configurable: false, enumerable: false }, select: { value: valueBind, writable: true, configurable: false, enumerable: false }, textarea: { value: valueBind, writable: true, configurable: false, enumerable: false } }); waitForReadyState("complete", bindDataContext); return DataContextBinding; /** * Get-Set DOM html root data context * @param {any} value * @returns */ function bindDataContext(value) { var htmlElement = document.querySelector("html"); if (htmlElement) { if (value !== undefined) { htmlElement.datacontext = value._isDataContext ? value : createDataContext(value); waitForReadyState("complete", bindAllElements.bind(this, htmlElement, true)); } if (!htmlElement.datacontext) { value = document.querySelector("script#data")?.innerText; htmlElement.datacontext = createDataContext(value && parse(value) || {}); waitForReadyState("complete", bindAllElements.bind(this, htmlElement)); } } return htmlElement?.datacontext; } function waitForReadyState(state, cb) { if (document.readyState == state) setTimeout(cb, 50); else setTimeout(waitForReadyState, 0, state, cb); } function bindAllElements(rootElement, rebinding = false) { if (!rootElement) { rootElement = document.querySelector("html"); } bindElement(rootElement, rebinding); rootElement.querySelectorAll("[bind]:not(template>*):not([link]>*),[onbind]:not(template>*):not([link]>*),[onunbind]:not(template>*):not([link]>*),[template]:not(template>*):not([link]>*),[templates]:not(template>*):not([link]>*),[rebinding]:not(template>*):not([link]>*),[link]:not(template>*):not([link]>*)") .forEach(function (element) { if (!element.contextValue || rebinding) { Promise.resolve(1).then((function (element, rebinding) { bindElement(element, rebinding); })(element, rebinding)); } }); } function rebindAllElements(rootElement) { if (!rootElement) { rootElement = document.body; } bindElement(rootElement); rootElement.querySelectorAll("[rebinding]") .forEach(function (element) { bindElement(element); }); // TODO [ ] test document.head.querySelectorAll("[rebinding]") .forEach(function (element) { bindElement(element); }); } function bindElement(element, rebinding = false) { if (!element instanceof HTMLTemplateElement || !element?.isConnected || ( !element?.attributes?.template && !element?.attributes?.templates && !element?.attributes?.onbind && !element?.attributes?.onunbind && !element?.attributes?.bind && !element?.attributes?.rebinding && !element?.attributes?.link )) { return; } if (element.attributes?.rebinding) { element.removeAttribute("rebinding"); rebinding = true; } if (element.contextValue && !rebinding) { return; } if (element.bindingContext?.isActive) { element.bindingContext.isActive(false); } // for rebinding if (element.attributes.link && !element.linked) {// if true then linked _link(element.attributes.link.value); return; } var _bindingContext = bindingContext(element, rebinding); if (!rebinding && element.attributes.template) { _template(element.attributes.template.value); return; } if (!rebinding && element.attributes.templates) { _template(element.attributes.templates.value, true); return; } _tryBind(); return; function _link(path) { var { CreateLink } = globalScope?.modules?.['ws-user']; if (CreateLink) { element.wsLink = WsUser.CreateLink(path, element); element.wsLink.bindAllElements = function (elem) { bindAllElements(elem, rebinding); }; element.setAttribute("linked", path); element.removeAttribute("link"); } } function _template(selectors, isMultiple = false) { if (!selectors) { _tryBind(); return; } //remove element children var i = 0; while (i < element.childElementCount) { if (!(element.children[i] instanceof HTMLTemplateElement || element.children[i] instanceof HTMLHtmlElement || element.children[i] instanceof HTMLBodyElement) && element.children[i].isTemplate) { element.children[i].remove(); } else { i++; } } var templateElement = null; try { templateElement = element.querySelector(selectors) || document.querySelector(selectors); } catch { templateElement = null; } if (!templateElement) { return fetchContent(); } bindTemplate(); return; function fetchContent() { fetch(selectors).then(function (res) { res.text().then(function (textContent) { templateElement = document.createElement("template"); selectors.endsWith('.html') ? templateElement.innerHTML = textContent : templateElement.innerHTML = '<div>' + encodeHTML(textContent) + '</div>'; bindTemplate(); }); }); function encodeHTML(str) { return str.replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;') .replace(/ /g, "&nbsp;") .replace(/\r\n/g, '<br>') // Windows-style newlines .replace(/\r/g, '<br>') // Old Mac-style newlines .replace(/\n/g, '<br>'); // Unix-style newlines } } function bindTemplate() { if (isMultiple) { let keys = Object.keys(_bindingContext.value);//.sort(); for (var i = 0; i < keys.length; i++) { if (!appendTemplate(keys[i])) { break; } } if (i !== 0) { setTimeout(function () { if (_bindingContext.source && typeof _bindingContext.source.on === "function") { _bindingContext.source.on( "-", function addTemplate(event) { if (event.eventName === "new") { return appendTemplate(event.propertyPath.at(-1), event.target); } return true; }, element ); _bindingContext.source.on( "-", function removeTemplate(event) { if (event.eventName === "delete") { element.querySelectorAll(`:scope > [path="${event.propertyPath.at(-1)}"]`).forEach(function (e) { e.remove(); }); } return true; }, element ); } }); return; } } else { return appendTemplate(); } return; function appendTemplate(key, target) { var ret = false; for (var i = 0; i < templateElement.content.childElementCount; i++) { ret = true; var child = templateElement.content.children[i].cloneNode(true); child.isTemplate = true; if (target && typeof target === "object" && !Array.isArray(target)) { var keys = Object.keys(target);//.sort(); var index = keys.indexOf(key); if (index < 0 || index === keys.length - 1) { element.append(child); } else { var referenceKey = keys[index + 1]; for (const referenceChild of element.children) { if (referenceChild.attributes.path?.value.startsWith(referenceKey)) { element.insertBefore(child, referenceChild); break; } } } } else { element.append(child); } if (key) { if (child.attributes.path?.value) { child.setAttribute("path", key + "." + child.attributes.path.value); } else { child.setAttribute("path", key); } } bindAllElements(child); } return ret; } } } function _tryBind() { if (element.attributes.bind) { try { var fnName = element.attributes.bind.value; if (!fnName) { fnName = element.tagName.toLowerCase(); if (element.attributes.type) { fnName += "_" + element.attributes.type.value.replace(/-/g, "_"); } } _bind(DataContextBinding[fnName] || DataContextBinding.innerHTML); } catch (err) { return _error("[ bind ] ", err); } } if (element.attributes.onbind && DataContextBinding.bind) { try { _bind(DataContextBinding.bind("event", element.attributes.onbind.value)); } catch (err) { return _error("[ onbind ] ", err); } } return; function _bind(fnBind) { if (_bindingContext.source === undefined) { var event = { eventName: "unbind", target: undefined, propertyPath: [_bindingContext.property], oldValue: undefined, newValue: undefined }; element.setAttribute("unbinded", ""); fnBind.call(element, event); if (element.attributes.onunbind && DataContextBinding.bind) { try { (DataContextBinding.bind("event", element.attributes.onunbind.value)).call(element, event); } catch (err) { return _error("[ onunbind ] ", err); } } } else if (fnBind.call(element, { eventName: "bind", target: _bindingContext.source, propertyPath: [_bindingContext.property], oldValue: _bindingContext.value, newValue: _bindingContext.value })) { if (_bindingContext.source && typeof _bindingContext.source.on === "function") { element.removeAttribute("unbinded"); _bindingContext.source.on( _bindingContext.property, function (event) { return fnBind.call(element, event); }, element ); } } } } function _error(...args) { console.log(args, element); } } /** Get-Set binding context */ function bindingContext(elem, rebinding) { if (!elem instanceof Node) { _error("[ bind context ] ", new Error("Invalid HTML element.")); return null; } var d = { rootElement: elem, rootSource: null, source: null, property: null, arrPath: [] }; _findPathAndSource(); _selectSource(); var _bindingContext = Object.create(null, { element: { value: d.rootElement, writable: false, configurable: false, enumerable: false }, source: { configurable: false, enumerable: false, get: function () { return d.arrPath.reduce(function (s, k) { return s[k]; }, d.rootSource); } }, property: { value: d.property || "-change", writable: false, configurable: false, enumerable: false }, path: { value: d.arrPath.reverse(), writable: false, configurable: false, enumerable: false }, value: { configurable: false, enumerable: false, get: function () { var s = this.source; if (s && s[d.property] !== undefined) { return s[this.property]; } //root if (s && s._parent === null) { return s; } return; }, set: function (val) { var s = this.source; s && d.property !== undefined && (s[d.property] = val); } }, contextValue: { value: _contextValue, writable: false, configurable: false, enumerable: false }, isActive: { value: _isActive, writable: false, configurable: false, enumerable: false }, remove: { value: _remove, writable: false, configurable: false, enumerable: false }, }); elem.contextValue = _bindingContext.contextValue; elem.bindingContext = _bindingContext; return _bindingContext; function _findPathAndSource() { do { if (d.rootElement?.attributes?.path?.value) { d.arrPath = d.arrPath.concat( d.rootElement.attributes.path.value .split(/[.\[\]]/) .filter(function (s) { return s; }) .reverse() ); } if (d.rootElement.datacontext !== undefined) { d.source = d.rootElement.datacontext; } } while (!d.source && d.rootElement.parentElement && (d.rootElement = d.rootElement.parentElement)) d.rootSource = d.source || (d.source = {}); if (d.rootSource?._events && ( !d.rootSource._events["-"] || !d.rootSource._events["-"].find(function (fn) { return fn.name.includes("bindHandler"); }) )) { d.rootSource.on("-", bindHandler.bind(d.rootElement), d.rootElement); } } function _selectSource() { d.property = d.arrPath.shift(); var arrPath = Object.values(d.arrPath); d.source = d.source?._isDataContext ? d.source : createDataContext(d.source || {}); if (!d.rootElement.datacontext) { d.rootElement.datacontext = d.source; } while (arrPath.length && d.source) { d.source = _nextSource(d, arrPath.pop(), {}); } _nextSource(d, d.property, ""); } function _nextSource(d, property, defValue) { if (rebinding) { defValue = undefined; } if (property === undefined) { return d.source; } if (_isNumber(property) && d.source[property] === undefined && !rebinding) { if (getType(d.source) !== "array") { if (d.rootSource === d.source) { d.rootSource = d.source = _sourceToArray(d.source); } else { d.source = _sourceToArray(d.source); } } if (d.source[property] === undefined && !rebinding) { d.source.push(createDataContext({})); } if (d.source[property] === undefined && getType(d.source[property]) !== getType(defValue) && defValue !== undefined) { d.source[property] = createDataContext(defValue); } return d.source[property]; } if (!d.source || !d.source._isDataContext) { return; } if (d.source[property]?._isDataContext) { return d.source[property]; } if (d.source[property] !== undefined) { d.source[property] = createDataContext(d.source[property]); return d.source[property]; } if (!rebinding) { d.source[property] = createDataContext(defValue); } return d.source[property]; function _isNumber(str) { return !isNaN(str); } function _sourceToArray(source) { var arr = createDataContext([]); var parent = source?._parent; var property = source?._propertyName; if (parent instanceof Object && property !== undefined) { parent[property] = arr; } else if (d.rootElement?.datacontext) { d.rootElement.datacontext = arr; } if (source) { Object.keys(source).forEach(function (k) { arr[k] = source[k]; if (source?._events && source._events[k]) arr._events[k] = source._events[k]; }); } return arr; } } /** * Set-Get data-context value. * @param {any} value * @returns {any} */ function _contextValue(value) { if (value !== undefined) { _bindingContext.value = value; return value; } return _bindingContext.value; } /** * Set-Get isActive value. * @param {boolean} activate Default: true * @returns {boolean} */ function _isActive(activate = true) { if (!activate) { if (_bindingContext.source?._events) { Object.entries(_bindingContext.source._events) .forEach(function ([k, arr]) { var index = 0; while (index < arr.length) { if (arr[index].isActive === elem && !arr[index].name.includes("Template")) { arr.splice(index, 1); } else { index++; } } }); } delete elem.contextValue; delete elem.bindingContext; } return elem.bindingContext === _bindingContext; } function _remove() { if (Array.isArray(_bindingContext.source)) { var index = _bindingContext.source.indexOf(_bindingContext.value); if (index > -1) { _bindingContext.source.splice(index, 1); } } else { delete _bindingContext.source[_bindingContext.property] } } function _error(...args) { console.log(args, d.rootElement); } } function bindHandler(event) { if (event.eventName === "delete") { //debugger; rebinding(event.oldValue); return true; } if (!event.newValue?._events) { return true; } if (event.eventName === "set") { //debugger; event.newValue._events = event.oldValue._events || {}; event.oldValue._events = {}; emitProperties(event.newValue, event.oldValue, event.eventName); } else if (event.eventName === "reposition") { //debugger; rebinding(event.newValue); rebinding(event.oldValue); } //else if (event.eventName === "new") { debugger; } return true; function emitProperties(newValue, oldValue, eventName) { if (newValue?.emit) { Object.keys(newValue).forEach(function (k) { newValue.emit(k, { eventName, target: newValue, propertyPath: [k], oldValue: oldValue[k], newValue: newValue[k] }); emitProperties(newValue[k], oldValue[k], eventName); }); } } function rebinding(val) { if (val?._events) { Object.keys(val._events).forEach(function (k) { val._events[k].forEach(function (l) { if (l.isActive instanceof Node) { l.isActive.setAttribute("rebinding", ""); l.isActive = false; } }); }); val._events = {}; } clearTimeout(rebindAllElements.timeout); rebindAllElements.timeout = setTimeout(function () { //console.log("RebindingAll"); rebindAllElements(); }); } } function getType(v) { return v === null ? "null" : Array.isArray(v) ? "array" : typeof v; } //#region *** Default bind *** function innerHTMLBind(event) { if (event.eventName === "unbind") { this.innerHTML = ''; // I am dead! return false; } var val = this.contextValue(); this.innerHTML = val; // I am alive! return val === undefined ? false : true; } function valueBind(event) { if (event.eventName === "unbind") { this.onchange = null; this.onkeydown = null; this.value = ''; // I am dead! return false; } this.value = this.contextValue(); // Init if (event.eventName === "bind") { this.onchange = function () { this.contextValue(this.value); }; this.onkeydown = function (ev) { if (ev.keyCode === 27) { this.value = his.contextValue(); } }; } // I am alive! return true; } function getValueBind(event) { if (event.eventName === "unbind") { this.onchange = null; this.onkeydown = null; this.value = ''; // I am dead! return false; } this.value = this.contextValue(); // I am alive! return true; } function setValueBind(event) { if (event.eventName === "unbind") { this.onchange = null; this.onkeydown = null; this.value = ''; // I am dead! return false; } // Init if (event.eventName === "bind") { this.onchange = function () { this.contextValue(this.value); }; this.onkeydown = function (ev) { if (ev.keyCode === 27) { this.value = this.contextValue(); } }; } // I am alive! return true; } function checkBind(event) { if (event.eventName === "unbind") { this.onchange = null; // I am dead! return false; } this.checked = this.contextValue(); this.checked ? this.setAttribute("checked", "") : this.removeAttribute("checked"); // Init if (event.eventName === "bind") { this.onchange = function () { this.contextValue(this.checked); }; } // I am alive! return this.contextValue() === undefined ? false : true; } function hiddenBind(event) { if (event.eventName === "unbind") { // I am dead! return false; } this.contextValue() ? this.setAttribute("hidden", "") : this.removeAttribute("hidden"); // I am alive! return this.contextValue() === undefined ? false : true; } function changeBind(event) { if (event.eventName === "unbind") { // I am dead! return false; } // Initial setup if (event.eventName === "bind") { // Set listener this.contextValue().on('-change', function () { // Let's wait a bit and let the operations finish. setTimeout(function (_this) { // Get context data var dc = _this.contextValue(); // Read the changes var str = dc.stringifyChanges(null, 2); // Put the value on the screen _this.value = str; }, 50, this); //TODO [ x ] I am dead! return this.bindingContext?.isActive(); }, // Set DOM element. // I am monitoring the attribute 'isConnected'. // If it's true, it's alive. this ); this.onchange = function () { // Let's take the context of the data. this.contextValue() // We will overvalue the modified JSON string data. .overwritingData(this.value); }; } // I am alive! return true; } function enabledBind(event) { if (event.eventName === "unbind") { this.setAttribute('disabled', 'disabled'); // I am dead! return false; } if (this.contextValue()) { this.removeAttribute('disabled'); } else { this.setAttribute('disabled', 'disabled'); } // I am alive! return true; } function disabledBind(event) { if (event.eventName === "unbind") { this.setAttribute('disabled', 'disabled'); // I am dead! return false; } if (this.contextValue()) { this.setAttribute('disabled', 'disabled'); } else { this.removeAttribute('disabled'); } // I am alive! return true; }; //#endregion }); /** * Exporting the library as a module. * @param {string} exportIdentifier Export identifier * @param {string[]} importIdentifierArray Import identifier array * @param {any} factory Factory function * @returns {void} */ function exportModule(exportIdentifier, importIdentifierArray, factory) { var thisScope = "undefined" != typeof globalThis ? globalThis : "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof self ? self : {}; if (!thisScope.modules) { thisScope.modules = {}; } // Browser waitModules(); function waitModules() { if (importIdentifierArray.length) { for (let i = 0; i < importIdentifierArray.length; i++) { if (!thisScope.modules[importIdentifierArray[i]]) { return setTimeout(waitModules, 10); } } } thisScope.modules[exportIdentifier] = factory.call(thisScope, ...importIdentifierArray.map(function (id) { return thisScope.modules[id]; })); } } })();