// URL: https://observablehq.com/@mootari/mutable-forms
// Title: Mutable Forms
// Author: Fabian Iwand (@mootari)
// Version: 610
// Runtime version: 1
const m0 = {
id: "6b56a2a00cbb4676@610",
variables: [
{
inputs: ["md"],
value: (function(md){return(
md`# Mutable Forms
Mutate form widgets from the [Inputs notebook](https://beta.observablehq.com/@jashkenas/inputs) by Jeremy Ashkenas.
`
)})
},
{
name: "mutableForm",
inputs: ["html","linkProperties","HTMLInputElement","RadioNodeList","Event"],
value: (function(html,linkProperties,HTMLInputElement,RadioNodeList,Event){return(
function mutableForm(form) {
const view = html`
${form}`;
linkProperties(form, view, ['output'], ['value']);
view.form = form;
const input = form.input;
const setBoxes = (n, v) => {
const checked = new Set(v);
n.forEach(n => n.checked = checked.has(n.value));
};
const inputType = input instanceof HTMLInputElement ? input.type : null;
const eventName = input instanceof RadioNodeList || inputType === 'checkbox' ? 'change' : 'input';
const setValue = input instanceof RadioNodeList && input[0].type === 'checkbox'
? setBoxes
: inputType === 'checkbox'
? (n, v) => {n.checked = !!v}
: (n, v) => {n.value = v};
Object.defineProperty(view, 'value', {
enumerable: true,
get: () => form.value,
set: v => {
setValue(input, v);
form.dispatchEvent(new Event(eventName, {bubbles: true}));
}
});
return view;
}
)})
},
{
name: "linkProperties",
value: (function(){return(
function linkProperties(source, target, attribs = [], exclude = []) {
const props = Object.keys(source).filter(k => !exclude.includes(k));
for(let k of attribs) if(source.hasAttribute(k) && !props.includes(k)) props.push(k);
for(let k of props) {
Object.defineProperty(target, k, {
enumerable: true,
get: () => source[k],
set: v => source[k] = v
});
}
}
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Checkbox
Note: Mutating cells must pass an array of **all** checked values.
`
)})
},
{
name: "viewof test_checkbox",
inputs: ["mutableForm","checkbox"],
value: (function(mutableForm,checkbox){return(
mutableForm(checkbox({options:['a', 'b', 'c'],value:['a']}))
)})
},
{
name: "test_checkbox",
inputs: ["Generators","viewof test_checkbox"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_checkbox"],
value: (function(test_checkbox){return(
test_checkbox
)})
},
{
name: "test_checkbox_mutate",
inputs: ["viewof test_checkbox"],
value: (function($0){return(
$0.value = ['b', 'c']
)})
},
{
inputs: ["assert","test_checkbox","test_checkbox_mutate"],
value: (function(assert,test_checkbox,test_checkbox_mutate){return(
assert(test_checkbox, test_checkbox_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`Special case: single checkbox.`
)})
},
{
name: "viewof test_checkbox_single",
inputs: ["mutableForm","checkbox"],
value: (function(mutableForm,checkbox){return(
mutableForm(checkbox({options:['a']}))
)})
},
{
name: "test_checkbox_single",
inputs: ["Generators","viewof test_checkbox_single"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_checkbox_single"],
value: (function(test_checkbox_single){return(
test_checkbox_single
)})
},
{
name: "test_checkbox_single_mutate",
inputs: ["viewof test_checkbox_single"],
value: (function($0){return(
$0.value = true, $0.value
)})
},
{
inputs: ["assert","test_checkbox_single","test_checkbox_single_mutate"],
value: (function(assert,test_checkbox_single,test_checkbox_single_mutate){return(
assert(test_checkbox_single, test_checkbox_single_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Color
`
)})
},
{
name: "viewof test_color",
inputs: ["mutableForm","color"],
value: (function(mutableForm,color){return(
mutableForm(color('#555555'))
)})
},
{
name: "test_color",
inputs: ["Generators","viewof test_color"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_color"],
value: (function(test_color){return(
test_color
)})
},
{
name: "test_color_mutate",
inputs: ["viewof test_color"],
value: (function($0){return(
$0.value = '#ffffff'
)})
},
{
inputs: ["assert","test_color","test_color_mutate"],
value: (function(assert,test_color,test_color_mutate){return(
assert(test_color, test_color_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Date
`
)})
},
{
name: "viewof test_date",
inputs: ["mutableForm","date"],
value: (function(mutableForm,date){return(
mutableForm(date('1990-12-31'))
)})
},
{
name: "test_date",
inputs: ["Generators","viewof test_date"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_date"],
value: (function(test_date){return(
test_date
)})
},
{
name: "test_date_mutate",
inputs: ["viewof test_date"],
value: (function($0){return(
$0.value = '2000-01-01'
)})
},
{
inputs: ["assert","test_date","test_date_mutate"],
value: (function(assert,test_date,test_date_mutate){return(
assert(test_date, test_date_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Number
`
)})
},
{
name: "viewof test_number",
inputs: ["mutableForm","number"],
value: (function(mutableForm,number){return(
mutableForm(number({min:0,max:10,step:'any',value:5}))
)})
},
{
name: "test_number",
inputs: ["Generators","viewof test_number"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_number"],
value: (function(test_number){return(
test_number
)})
},
{
name: "test_number_mutate",
inputs: ["viewof test_number"],
value: (function($0){return(
$0.value = 9.5
)})
},
{
inputs: ["assert","test_number","test_number_mutate"],
value: (function(assert,test_number,test_number_mutate){return(
assert(test_number, test_number_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Radio
`
)})
},
{
name: "viewof test_radio",
inputs: ["mutableForm","radio"],
value: (function(mutableForm,radio){return(
mutableForm(radio({options:['a', 'b', 'c'], value:'a'}))
)})
},
{
name: "test_radio",
inputs: ["Generators","viewof test_radio"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_radio"],
value: (function(test_radio){return(
test_radio
)})
},
{
name: "test_radio_mutate",
inputs: ["viewof test_radio"],
value: (function($0){return(
$0.value = 'b'
)})
},
{
inputs: ["assert","test_radio","test_radio_mutate"],
value: (function(assert,test_radio,test_radio_mutate){return(
assert(test_radio, test_radio_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Select
`
)})
},
{
name: "viewof test_select",
inputs: ["mutableForm","select"],
value: (function(mutableForm,select){return(
mutableForm(select({
options: ['a', {value:'b', label:'B'}, 'c'],
value: 'a'
}))
)})
},
{
name: "test_select",
inputs: ["Generators","viewof test_select"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_select"],
value: (function(test_select){return(
test_select
)})
},
{
name: "test_select_mutate",
inputs: ["viewof test_select"],
value: (function($0){return(
$0.value = 'b'
)})
},
{
inputs: ["assert","test_select","test_select_mutate"],
value: (function(assert,test_select,test_select_mutate){return(
assert(test_select, test_select_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Slider
`
)})
},
{
name: "viewof test_slider",
inputs: ["mutableForm","slider"],
value: (function(mutableForm,slider){return(
mutableForm(slider({min:0,max:10,step:'any',value:5}))
)})
},
{
name: "test_slider",
inputs: ["Generators","viewof test_slider"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_slider"],
value: (function(test_slider){return(
test_slider
)})
},
{
name: "test_slider_mutate",
inputs: ["viewof test_slider"],
value: (function($0){return(
$0.value = 9.5
)})
},
{
inputs: ["assert","test_slider","test_slider_mutate"],
value: (function(assert,test_slider,test_slider_mutate){return(
assert(test_slider, test_slider_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Text
`
)})
},
{
name: "viewof test_text",
inputs: ["mutableForm","text"],
value: (function(mutableForm,text){return(
mutableForm(text({value:'foo'}))
)})
},
{
name: "test_text",
inputs: ["Generators","viewof test_text"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_text"],
value: (function(test_text){return(
test_text
)})
},
{
name: "test_text_mutate",
inputs: ["viewof test_text"],
value: (function($0){return(
$0.value = 'bar'
)})
},
{
inputs: ["assert","test_text","test_text_mutate"],
value: (function(assert,test_text,test_text_mutate){return(
assert(test_text, test_text_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Textarea
`
)})
},
{
name: "viewof test_textarea",
inputs: ["mutableForm","textarea"],
value: (function(mutableForm,textarea){return(
mutableForm(textarea('foo\nfoo'))
)})
},
{
name: "test_textarea",
inputs: ["Generators","viewof test_textarea"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_textarea"],
value: (function(test_textarea){return(
test_textarea
)})
},
{
name: "test_textarea_mutate",
inputs: ["viewof test_textarea"],
value: (function($0){return(
$0.value = 'bar\nbar'
)})
},
{
inputs: ["assert","test_textarea","test_textarea_mutate"],
value: (function(assert,test_textarea,test_textarea_mutate){return(
assert(test_textarea, test_textarea_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Autoselects (unsupported)
`
)})
},
{
name: "viewof test_autoSelect",
inputs: ["mutableForm","autoSelect"],
value: (function(mutableForm,autoSelect){return(
mutableForm(autoSelect({options: ['foo', 'bar', 'baz'], value: 'foo'}))
)})
},
{
name: "test_autoSelect",
inputs: ["Generators","viewof test_autoSelect"],
value: (G, _) => G.input(_)
},
{
inputs: ["test_autoSelect"],
value: (function(test_autoSelect){return(
test_autoSelect
)})
},
{
name: "test_autoSelect_mutate",
inputs: ["viewof test_autoSelect"],
value: (function($0){return(
$0.value = 'baz'
)})
},
{
inputs: ["assert","test_autoSelect","test_autoSelect_mutate"],
value: (function(assert,test_autoSelect,test_autoSelect_mutate){return(
assert(test_autoSelect, test_autoSelect_mutate)
)})
},
{
inputs: ["md"],
value: (function(md){return(
md`---`
)})
},
{
name: "assert",
inputs: ["html"],
value: (function(html){return(
function assert(a, b) {
const match = JSON.stringify(a) === JSON.stringify(b);
const out = html`
`;
out.textContent = 'Test ' + (match ? 'passed' : 'failed');
out.style.color = match ? 'green' : 'red';
return out;
}
)})
},
{
from: "@jashkenas/inputs",
name: "checkbox",
remote: "checkbox"
},
{
from: "@jashkenas/inputs",
name: "color",
remote: "color"
},
{
from: "@jashkenas/inputs",
name: "date",
remote: "date"
},
{
from: "@jashkenas/inputs",
name: "number",
remote: "number"
},
{
from: "@jashkenas/inputs",
name: "radio",
remote: "radio"
},
{
from: "@jashkenas/inputs",
name: "select",
remote: "select"
},
{
from: "@jashkenas/inputs",
name: "slider",
remote: "slider"
},
{
from: "@jashkenas/inputs",
name: "text",
remote: "text"
},
{
from: "@jashkenas/inputs",
name: "textarea",
remote: "textarea"
},
{
from: "@jashkenas/inputs",
name: "autoSelect",
remote: "autoSelect"
},
{
inputs: ["md"],
value: (function(md){return(
md`---
## Changelog
- **2020-11-30:** Bugfix: Dispatched events don't bubble.
`
)})
}
]
};
const m1 = {
id: "@jashkenas/inputs",
variables: [
{
name: "checkbox",
inputs: ["input","html"],
value: (function(input,html){return(
function checkbox(config = {}) {
let {
value: formValue,
title,
description,
submit,
disabled,
options
} = Array.isArray(config) ? { options: config } : config;
options = options.map(o =>
typeof o === "string" ? { value: o, label: o } : o
);
const form = input({
type: "checkbox",
title,
description,
submit,
getValue: input => {
if (input.length)
return Array.prototype.filter
.call(input, i => i.checked)
.map(i => i.value);
return input.checked ? input.value : false;
},
form: html`
`
});
form.output.remove();
return form;
}
)})
},
{
name: "color",
inputs: ["input"],
value: (function(input){return(
function color(config = {}) {
const { value = "#000000", title, description, disabled, submit, display } =
typeof config === "string" ? { value: config } : config;
const form = input({
type: "color",
title,
description,
submit,
display,
attributes: { disabled, value }
});
// The following two lines are a bugfix for Safari, which hopefully can be removed in the future.
form.input.value = '';
form.input.value = value;
if (title || description) form.input.style.margin = "5px 0";
return form;
}
)})
},
{
name: "date",
inputs: ["input"],
value: (function(input){return(
function date(config = {}) {
const { min, max, value, title, description, disabled, display } =
typeof config === "string" ? { value: config } : config;
return input({
type: "date",
title,
description,
display,
attributes: { min, max, disabled, value }
});
}
)})
},
{
name: "number",
inputs: ["input"],
value: (function(input){return(
function number(config = {}) {
const {
value,
title,
description,
disabled,
placeholder,
submit,
step = "any",
min,
max
} =
typeof config === "number" || typeof config === "string"
? { value: +config }
: config;
const form = input({
type: "number",
title,
description,
submit,
attributes: {
value,
placeholder,
step,
min,
max,
autocomplete: "off",
disabled
},
getValue: input => input.valueAsNumber
});
form.output.remove();
form.input.style.width = "auto";
form.input.style.fontSize = "1em";
return form;
}
)})
},
{
name: "radio",
inputs: ["input","html"],
value: (function(input,html){return(
function radio(config = {}) {
let {
value: formValue,
title,
description,
submit,
options,
disabled
} = Array.isArray(config) ? { options: config } : config;
options = options.map(o =>
typeof o === "string" ? { value: o, label: o } : o
);
const form = input({
type: "radio",
title,
description,
submit,
getValue: input => {
if (input.checked) return input.value;
const checked = Array.prototype.find.call(input, radio => radio.checked);
return checked ? checked.value : undefined;
},
form: html`
`
});
form.output.remove();
return form;
}
)})
},
{
name: "select",
inputs: ["input","html"],
value: (function(input,html){return(
function select(config = {}) {
let {
value: formValue,
title,
description,
disabled,
submit,
multiple,
size,
options
} = Array.isArray(config) ? { options: config } : config;
options = options.map(o =>
typeof o === "object" ? o : { value: o, label: o }
);
const form = input({
type: "select",
title,
description,
submit,
attributes: { disabled },
getValue: input => {
const selected = Array.prototype.filter
.call(input.options, i => i.selected)
.map(i => i.value);
return multiple ? selected : selected[0];
},
form: html`
`
});
form.output.remove();
return form;
}
)})
},
{
name: "slider",
inputs: ["input"],
value: (function(input){return(
function slider(config = {}) {
let {
min = 0,
max = 1,
value = (max + min) / 2,
step = "any",
precision = 2,
title,
description,
disabled,
getValue,
format,
display,
submit
} = typeof config === "number" ? { value: config } : config;
precision = Math.pow(10, precision);
if (!getValue)
getValue = input => Math.round(input.valueAsNumber * precision) / precision;
return input({
type: "range",
title,
description,
submit,
format,
display,
attributes: { min, max, step, disabled, value },
getValue
});
}
)})
},
{
name: "text",
inputs: ["input"],
value: (function(input){return(
function text(config = {}) {
const {
value,
title,
description,
disabled,
autocomplete = "off",
maxlength,
minlength,
pattern,
placeholder,
size,
submit,
getValue
} = typeof config === "string" ? { value: config } : config;
const form = input({
type: "text",
title,
description,
submit,
getValue,
attributes: {
value,
autocomplete,
maxlength,
minlength,
pattern,
placeholder,
size,
disabled
}
});
form.output.remove();
form.input.style.fontSize = "1em";
return form;
}
)})
},
{
name: "textarea",
inputs: ["input","html"],
value: (function(input,html){return(
function textarea(config = {}) {
const {
value = "",
title,
description,
autocomplete,
cols = 45,
rows = 3,
width,
height,
maxlength,
placeholder,
spellcheck,
wrap,
submit,
disabled,
getValue
} = typeof config === "string" ? { value: config } : config;
const form = input({
form: html`
`,
title,
description,
submit,
getValue,
attributes: {
autocomplete,
cols,
rows,
maxlength,
placeholder,
spellcheck,
wrap,
disabled
}
});
form.output.remove();
if (width != null) form.input.style.width = width;
if (height != null) form.input.style.height = height;
if (submit) form.submit.style.margin = "0";
if (title || description) form.input.style.margin = "3px 0";
return form;
}
)})
},
{
name: "autoSelect",
inputs: ["input","html"],
value: (function(input,html){return(
function autoSelect(config = {}) {
const {
value,
title,
description,
disabled,
autocomplete = "off",
placeholder,
size,
options,
list = "options"
} = Array.isArray(config) ? { options: config } : config;
const optionsSet = new Set(options);
const form = input({
type: "text",
title,
description,
attributes: { disabled },
action: fm => {
fm.value = fm.input.value = value || "";
fm.onsubmit = e => e.preventDefault();
fm.input.oninput = function(e) {
e.stopPropagation();
fm.value = fm.input.value;
if (!fm.value || optionsSet.has(fm.value))
fm.dispatchEvent(new CustomEvent("input"));
};
},
form: html`
`
});
form.output.remove();
return form;
}
)})
},
{
name: "input",
inputs: ["html","d3format"],
value: (function(html,d3format){return(
function input(config) {
let {
form,
type = "text",
attributes = {},
action,
getValue,
title,
description,
format,
display,
submit,
options
} = config;
const wrapper = html`
`;
if (!form)
form = html`
`;
Object.keys(attributes).forEach(key => {
const val = attributes[key];
if (val != null) form.input.setAttribute(key, val);
});
if (submit)
form.append(
html`
`
);
form.append(
html`
`
);
if (title)
form.prepend(
html`
${title}
`
);
if (description)
form.append(
html`
${description}
`
);
if (format)
format = typeof format === "function" ? format : d3format.format(format);
if (action) {
action(form);
} else {
const verb = submit
? "onsubmit"
: type == "button"
? "onclick"
: type == "checkbox" || type == "radio"
? "onchange"
: "oninput";
form[verb] = e => {
e && e.preventDefault();
const value = getValue ? getValue(form.input) : form.input.value;
if (form.output) {
const out = display ? display(value) : format ? format(value) : value;
if (out instanceof window.Element) {
while (form.output.hasChildNodes()) {
form.output.removeChild(form.output.lastChild);
}
form.output.append(out);
} else {
form.output.value = out;
}
}
form.value = value;
if (verb !== "oninput")
form.dispatchEvent(new CustomEvent("input", { bubbles: true }));
};
if (verb !== "oninput")
wrapper.oninput = e => e && e.stopPropagation() && e.preventDefault();
if (verb !== "onsubmit") form.onsubmit = e => e && e.preventDefault();
form[verb]();
}
while (form.childNodes.length) {
wrapper.appendChild(form.childNodes[0]);
}
form.append(wrapper);
return form;
}
)})
},
{
name: "d3format",
inputs: ["require"],
value: (function(require){return(
require("d3-format@1")
)})
}
]
};
const notebook = {
id: "6b56a2a00cbb4676@610",
modules: [m0,m1]
};
export default notebook;