@techexp/data-bind
Version:
Easy to use, two-way data binding library, light-weight, and with no dependencies.
158 lines (133 loc) • 4.45 kB
JavaScript
/**
* Bind an object's property to a UI element or an attribute on a IU element
* @param {onChange} Call back fires only when property is changed thru API, not UI.
*/
export default function bind({obj = {}, prop, sel, attr, root = document,
getter, setter, onChange}) {
validateArgs(prop)
checkInitialValue(obj, prop)
const objNotBound = {}
if (!getter) {
getter = () => getValue({prop, sel, attr, root, objNotBound})
}
if (!setter) {
setter = (value) => setValue({prop, value, root, sel, attr, objNotBound})
}
return bindProp({obj, prop, getter, setter, onChange})
}
function bindProp({obj, prop, getter, setter, onChange}) {
const descriptor = {
get: () => getter(),
set: value => {
if (onChange) {
const oldValue = getter(prop)
if (oldValue !== value) {
onChange(oldValue, value)
}
}
setter(value)
},
configurable: true,
enumerable: true
}
Object.defineProperty(obj, prop, descriptor);
return obj;
}
const isCheckbox = el => el.type === 'checkbox';
const isRadio = el => el.type === 'radio';
const isSelect = el => el.nodeName.toLowerCase() === 'select';
const isInputField = el => el.nodeName.toLowerCase() === 'input-field'
const isInput = el => 'value' in el;
const toSet = v => new Set( Array.isArray(v) ? v : [v]);
function checkInitialValue(obj, prop) {
const oldValue = obj[prop]
if (oldValue !== undefined) {
console.info(`Property '${prop}' already exists in object. Will override previous definition but retain old value of ${oldValue}.`)
obj[prop] = oldValue
}
return oldValue
}
function getValue({prop, root, sel, attr, objNotBound}) {
if (sel) return getDomVal(root, sel, attr)
return objNotBound[prop]
}
function setValue({prop, value, root, sel, attr, objNotBound}) {
if (sel) {
setDomVal(root, sel, value, attr)
return
}
objNotBound[prop] = value
}
function getDomVal(root, sel, attr) {
const elements = findElements(root, sel);
if (elements.length === 0) return null;
let el = elements[0];
if (attr) return el.getAttribute(attr);
if (!isInput(el)) return el.innerHTML;
if (isCheckbox(el)) {
return elements.filter(e => isCheckbox(e) && e.checked)
.map(e => e.value === 'on'? e.name : e.value);
}
if (isSelect(el)) {
const opts = [... el.querySelectorAll('option')];
return opts.filter(op => op.selected).map(op => op.value);
}
if (isRadio(el)) {
el = elements.filter(isRadio).find(e => e.checked);
if (!el) return undefined
}
if (isInputField(el)) {
return el.getAttribute('value')
}
return el.value;
}
function setDomVal(root, sel, val, attr) {
const elements = findElements(root, sel);
if (elements.length === 0) return;
const el = elements[0];
if (isCheckbox(el)) {
const v = toSet(val);
elements.filter(isCheckbox).forEach(e => e.checked = v.has(e.value) || v.has(e.name));
return;
}
if (isSelect(el)) {
const v = toSet(val);
el.querySelectorAll('option').forEach(op => op.selected = v.has(op.value));
return;
}
if (isRadio(el)) {
elements.filter(isRadio).forEach(e => e.checked = e.value === val);
return;
}
elements.forEach(el => setElementValue(el, val, attr));
}
function setElementValue(el, val, attr) {
if (attr) {
el.setAttribute(attr, val);
} else if (isInput(el)) {
el.value = val;
} else if (isInputField(el)) {
el.setAttribute('value', val)
} else {
el.innerHTML = val;
}
}
function findElements(root, sel) {
const elements = root.querySelectorAll(sel);
if (elements.length === 0) {
console.warn(`No elements found matching selector ${sel}`);
}
return [... elements];
}
function validateArgs(prop) {
if (typeof prop !== 'string' || prop.length === 0) {
throw `'prop' argument must be a String defining the name a property.`
}
}
/*
References:
1. DaBi: https://gist.github.com/plugnburn/f9d3acf33bee8f3f7e2d
2. Simulacra.js: https://simulacra.js.org
3. Article: https://www.wintellect.com/data-binding-pure-javascript/
4. bind.js: https://github.com/remy/bind.js
*/