Created
June 9, 2025 22:09
-
-
Save MrAntix/a8e47496b6861435f110f34b571c7767 to your computer and use it in GitHub Desktop.
Bind HTML Elements to a data object
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* https://antix.co.uk/bind-form.html */ | |
const bindForm = selector => { | |
const bindFields = root => { | |
const setters = {}, getters = {} | |
const binding = { | |
get: () => Object.keys(getters) | |
.reduce((data, key) => { | |
const getter = getters[key]; | |
if (getter) data[key] = getter.get(); | |
return data; | |
}, {}), | |
set: data => { | |
Object.keys(setters).forEach(key => { | |
const setter = setters[key]; | |
if (setter !== undefined) | |
setter.set(data[key] ?? null); | |
}); | |
}, | |
onChange: () => { } | |
}; | |
const toggleEmpty = el => | |
el.classList.toggle('is-empty', [null, undefined, ''].includes(el.value)); | |
const hasValueGetter = el => () => | |
el.value == '' ? null : el.value; | |
const hasValueSetter = el => value => { | |
el.value = value ?? null; | |
toggleEmpty(el); | |
} | |
root.querySelectorAll('[name]') | |
.forEach(el => { | |
const name = el.getAttribute('name'); | |
const onchangehandler = e => { | |
const value = getters[name].get(); | |
console.log('onchangehandler', name, value); | |
binding.onChange(e, name, value); | |
toggleEmpty(el); | |
}; | |
let get, set; | |
switch (el.tagName) { | |
case 'BUTTON': break; | |
case 'INPUT': | |
switch (el.type) { | |
case 'checkbox': | |
get = () => | |
el.checked | |
? el.hasAttribute('value') ? el.value : true | |
: false; | |
set = value => | |
el.checked = el.hasAttribute('value') | |
? el.value == value | |
: !!value; | |
break; | |
case 'radio': | |
get = () => | |
el.checked | |
? el.hasAttribute('value') ? el.value : null | |
: undefined; | |
set = value => | |
el.checked = el.hasAttribute('value') | |
? el.value == value | |
: value == null; | |
break; | |
default: | |
get = hasValueGetter(el); | |
set = hasValueSetter(el); | |
break; | |
} | |
el.onchange = onchangehandler; | |
toggleEmpty(el); | |
break; | |
case 'TEXTAREA': | |
case 'SELECT': | |
get = hasValueGetter(el); | |
set = hasValueSetter(el); | |
el.onchange = onchangehandler; | |
toggleEmpty(el); | |
break; | |
default: | |
if (el.hasAttribute('template')) { | |
const template = document.querySelector(`#${el.getAttribute('template')}`); | |
let bindings = []; | |
set = value => { | |
if (el.getAttribute('role') === 'list') { | |
let i = 0; | |
for (i = 0; i < value.length; i++) { | |
const item = value[i]; | |
let itemEl = el.children[i]; | |
if (itemEl == null) { | |
itemEl = document.createElement('div'); | |
itemEl.setAttribute('role', 'listitem'); | |
itemEl.dataset.index = i; | |
itemEl.appendChild(template.content.cloneNode(true)); | |
el.appendChild(itemEl); | |
bindings[i] = bindFields(itemEl); | |
bindings[i].onChange = (e, propertyName) => { | |
binding.onChange(e, `${name}[${i}].${propertyName}`); | |
} | |
} | |
bindings[i].set(item); | |
}; | |
bindings.length = i; | |
for (; i < el.children.length; i++) | |
el.children[i].remove(); | |
} else { | |
const item = value; | |
if (!el.children.length) { | |
const fragment = template.content.cloneNode(true); | |
bindings[0] = bindFields(fragment); | |
bindings[0].onChange = (e, name, value) => { | |
binding.onChange(e, `${name}.${name}`, value); | |
} | |
el.appendChild(fragment); | |
} | |
bindings[0].set(item); | |
} | |
}; | |
get = () => | |
el.getAttribute('role') === 'list' | |
? bindings.map(b => b.get()) | |
: bindings[0].get(); | |
break; | |
} | |
set = value => el.innerText = value; | |
break; | |
} | |
if (get) { | |
if (!getters[name]) | |
getters[name] = { | |
get: function () { | |
values = this.all.map(g => g()) | |
.filter(v => v !== undefined); | |
return values.length > 1 ? values : values[0]; | |
}, | |
all: [] | |
}; | |
getters[name].all.push(get); | |
} | |
if (set) { | |
if (!setters[name]) | |
setters[name] = { | |
set: function (value) { | |
this.all.forEach(s => s(value)) | |
}, | |
all: [] | |
}; | |
setters[name].all.push(set); | |
} | |
}); | |
return binding; | |
}; | |
const form = document.querySelector(selector); | |
const bindings = bindFields(form); | |
bindings.onChange = (e, name, value) => | |
controller.onChange(e, name, value); | |
const controller = | |
{ | |
...bindings, | |
onSubmit: () => { } | |
}; | |
form.onsubmit = e => { | |
e.preventDefault(); | |
const data = controller.get(); | |
controller.onSubmit(data, e.submitter); | |
} | |
return controller; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment