Skip to content

Instantly share code, notes, and snippets.

@MrAntix
Created June 9, 2025 22:09
Show Gist options
  • Save MrAntix/a8e47496b6861435f110f34b571c7767 to your computer and use it in GitHub Desktop.
Save MrAntix/a8e47496b6861435f110f34b571c7767 to your computer and use it in GitHub Desktop.
Bind HTML Elements to a data object
/* 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