Published
Edited
Jun 3, 2022
Importers
Insert cell
Insert cell
Insert cell
Insert cell
formAccessor = newFormAccessor();
Insert cell
formValidator = {
const { getValues, setValues, listenValues } = formAccessor;

function updateResets(_form, element, info) {
element.disabled = !info.dirty;
}

function updateSubmits(_form, element, info) {
element.disabled = !info.valid;
}

function updateErrors(form, errors) {
[...form.querySelectorAll(`[data--form-error]`)].forEach((errorElement) => {
errorElement.innerHTML = "";
errorElement.classList.remove("error");
errorElement.style.display = "none";
});
for (const [name, error] of Object.entries(errors)) {
const errorElement = form.querySelector(`[data--form-error="${name}"]`);
if (!error || !errorElement) continue;
errorElement.innerHTML =
typeof error === "object" ? error.message : error;
errorElement.classList.add("error");
errorElement.style.display = "";
}
}

function deepEqual(a, b) {
if (a === b) return true;
if (typeof a !== "object" || typeof b !== "object") return false;
if (a === null || b === null) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
const len = a.length;
for (let i = 0; i < len; i++) {
if (!deepEqual(a[i], b[i])) return false;
}
} else {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!deepEqual(a[key], b[key])) return false;
}
}
return true;
}

function formValidator(
form,
{
value,
onSubmit = () => {},
onUpdate = () => {},
validate = () => {},
updateResetView = updateResets,
updateSubmitView = updateSubmits,
updateErrorViews = updateErrors,
submitSelector = '[type="submit"]',
resetSelector = '[type="button"]',
equal = deepEqual
} = {}
) {
const submitRegistrations = [
...form.querySelectorAll(submitSelector)
].map((button) => addButtonListener(button, "click", handleSubmit));

const resetRegistrations = [
...form.querySelectorAll(resetSelector)
].map((button) => addButtonListener(button, "click", handleReset));

value = value || getFormValues();
setValues(form, value);

const formListenerRegistration = listenValues(form, (values) => {
_updateFormStateView(values, true);
});

return {
resetForm,
getFormValues,
getFormState,
unregister
};

function addEventListener(element, event, handler) {
element.addEventListener(event, handler);
return () => element.removeEventListener(event, handler);
}

function addButtonListener(element, event, handler) {
return { element, unregister: addEventListener(element, event, handler) };
}

function handleSubmit(event) {
const element = event.target;
event.preventDefault();
event.stopPropagation();
const formState = getFormState();
formState.submit = true;
if (formState.valid) {
resetForm((value = formState.values));
formState.dirty = false;
}
onSubmit(formState, element);
}

function handleReset(event) {
event.preventDefault();
resetForm();
}

function buildFormState(values) {
const errors = validate(values) || {};
const dirty = !equal(values, value);
return {
errors,
values,
valid: Object.keys(errors).length === 0,
submit: false,
dirty
};
}

function _updateFormStateView(values, notify) {
const formState = buildFormState(values);
updateErrorViews(form, formState.errors);
submitRegistrations.forEach(({ element }) =>
updateSubmitView(form, element, formState)
);
resetRegistrations.forEach(({ element }) =>
updateResetView(form, element, formState)
);
notify && onUpdate(formState);
}

function setFormValues(values, notify) {
setValues(form, values);
values = getFormValues();
_updateFormStateView(values, notify);
}

function resetForm(values) {
setFormValues((value = values || value), true);
}

function getFormValues() {
return getValues(form);
}

function getFormState() {
const values = getFormValues();
return buildFormState(values);
}

function unregister() {
formListenerRegistration();
submitRegistrations.forEach((r) => r.unregister());
resetRegistrations.forEach((r) => r.unregister());
}
}
return formValidator;
}
Insert cell
newFormAccessor = {
const { newGetter, newSetter } = getset;
return newFormAccessor;

// Provides get/set methods for each input form element.
// * The "get" method retrieves the input value and stores it in the object.
// The path to the field with input values is defined by the name of the input.
// For example values from the input "address.city"="Paris" is stored
// as {"address":{"city":"Paris"}}
// * "set" method retrives values from the specified object and sets them
// in the input field.
function newFormAccessor(cache) {
cache = cache || new WeakMap();

function getInputAccessor(input) {
let slot = cache.get(input);
if (!slot) {
slot = newInputAccessor(input);
cache.set(input, slot);
}
return slot;
}

function newInputAccessor(input) {
if (
input.disabled ||
(!input.hasAttribute("name") && !input.hasAttribute("id"))
)
return {};
const name = input.name || input.id || "";
const getter = newGetter(name);
const setter = newSetter(name);
let get,
set = (obj) => {
const value = getter(obj);
input.value = value !== undefined ? value : "";
};
switch (input.type) {
case "range":
case "number": {
get = (obj) => setter(obj, input.valueAsNumber);
break;
}
case "date": {
get = (obj) => setter(obj, input.valueAsDate);
break;
}
case "radio": {
get = (obj) => input.checked && setter(obj, input.value);
set = (obj) => (input.checked = getter(obj) === input.value);
break;
}
case "checkbox": {
get = (obj) => !(input.name in obj) && setter(obj, !!input.checked);
set = (obj) => (input.checked = getter(obj));
break;
}
case "file": {
get = input.multiple
? (obj) => setter(obj, input.files)
: (obj) => setter(obj, input.files[0]);
set = undefined; // It is impossible to change the file input
break;
}
case "select-multiple": {
get = (obj) =>
setter(
obj,
Array.from(input.selectedOptions, (option) => option.value)
);
set = (obj) => {
const values = getter(obj);
const index = (!values
? []
: Array.isArray(values)
? values
: [values]
).reduce((idx, val) => ((idx[val] = true), idx), {});
const options = input.querySelectorAll("option");
options.forEach((option) => {
option.selected = option.value in index;
});
};
break;
}
default: {
get = (obj) => setter(obj, input.value);
break;
}
}
return { get, set };
}

function setValues(form, values) {
for (const input of form.elements) {
const { set } = getInputAccessor(input);
set && set(values);
}
form.dispatchEvent(new CustomEvent("change"));
return this;
}

function getValues(form, values = {}) {
for (const input of form.elements) {
const { get } = getInputAccessor(input);
get && get(values);
}
return values;
}

function listenValues(form, onChange) {
const registrations = [];
const addListener = (event, listener) => {
form.addEventListener(event, listener);
registrations.push(() => form.removeEventListener(event, listener));
};
const notify = () => onChange(getValues(form));
const cleanup = () => registrations.forEach((r) => r && r());
try {
addListener("submit", (event) => event.preventDefault());
// addListener("change", () => notify());
addListener("input", (event) => event.bubbles && notify());
notify();
return cleanup;
} catch (error) {
cleanup();
throw error;
}
}
return {
getValues,
setValues,
listenValues,
cache,
getInputAccessor,
newInputAccessor
};
}
}
Insert cell
// Compiles getters / setters for the specified paths
getset = {
return {
newGetter, newSetter, newCloneSetter,
toPath, escape
}
function newGetter(path) {
path = toPath(path);
let code = path.reduce((code, segment) => {
segment = escape(segment);
if (!segment) return code;
return `${code}["${segment}"]`;
}, '');
code = `try{ return obj${code}; } catch (err) {}`;
return new Function(['obj'], `"use strict";\n${code}`);
}

function newSetter(path) {
path = toPath(path);
let code = 'obj = obj || {};\n';
if (path.length) {
code += `var o = obj;\n`;
for (let i = 0; i < path.length; i++) {
const segment = escape(path[i]);
if (!segment) continue;
const next = `o["${segment}"]`;
if (i < path.length - 1) {
code += `o = ${next} = (typeof ${next} !== "object") ? {} : ${next}\n`;
} else {
code += `if (value === undefined) `;
code += `{\n delete ${next}; \n} else {\n ${next} = value; \n}\n`;
}
}
}
code += `return obj;`
return new Function(['obj', 'value'], `"use strict";\n${code}`);
}

function newCloneSetter(path) {
path = toPath(path);
let code = `var newObj = Object.assign({}, obj || {});\n`;
if (path.length) {
code += `var o = newObj;\n`;
for (let i = 0; i < path.length; i++) {
const segment = escape(path[i]);
if (!segment) continue;
const next = `o["${segment}"]`;
if (i < path.length - 1) {
code += `o = ${next} = (typeof ${next} === "object")\n`
+ ` ? Object.assign({}, ${next})\n`
+ ` : {};\n`
} else {
code += `if (value === undefined) `;
code += `{\n delete ${next}; \n} else {\n ${next} = value; \n}\n`;
}
}
}
code += `return newObj;`
return new Function(['obj', 'value'], `"use strict";\n${code}`);
}

function toPath(path) {
if (!path)
return [];
if (typeof path === 'string')
return path.split('.');
return path;
}

function escape(segment) {
segment = segment ? segment.trim() : '';
return segment
.replace(/(["'])/gim, '\\$1')
.replace(/\r/gim, '\\r')
.replace(/\n/gim, '\\n');
}
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more