Public
Edited
Jul 8, 2024
Importers
9 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof exampleSurveyEditor = view`<div class="brand-font bg-near-white">
<div style="overflow-y: auto; max-height:600px;">
${["...", surveyEditor(await FileAttachment("surveyUiInput.json").json())]}
</div>
</div>`
Insert cell
Insert cell
Inputs.button("Overwrite suvery editor data", {
reduce: () => {
viewof exampleSurveyEditor.value = viewof anotherSurveyEditorData.value;
viewof exampleSurveyEditor.applyValueUpdates();
}
})
Insert cell
surveyEditor = ({metadata, pages = []} = {}) => {
const pageBuilder = (args) => page({
...args,
onDelete: (id) => {
const index = ui.value.pages.findIndex(pages => pages.id === id);
ui.value.pages.splice(index, 1);
ui.pages.dispatchEvent(new Event('input', {bubbles: true}));
},
onDown: (id) => {
const index = ui.value.pages.findIndex(pages => pages.id === id);
var element = ui.value.pages[index];
ui.value.pages.splice(index, 1);
ui.value.pages.splice(index + 1, 0, element);
ui.pages.dispatchEvent(new Event('input', {bubbles: true}));
},
onUp: (id) => {
const index = ui.value.pages.findIndex(page => page.id === id);
var element = ui.value.pages[index];
ui.value.pages.splice(index, 1);
ui.value.pages.splice(Math.max(index - 1, 0), 0, element);
ui.pages.dispatchEvent(new Event('input', {bubbles: true}));
},
})

const summary = view`<p class="ma0">
${['questions', textNodeView("0 questions")]} across ${['pages', textNodeView("0 pages")]}.
</p>`

const updateSummary = () => {
// console.log('updateSummary');
const pages = ui.value?.pages?.length || 0;
const questions = ui.value?.pages?.reduce((count, page) => {
const cells = filterCellsNotOfType(page.cells || [], 'section');
return count + cells.length
}, 0);

summary.value.pages = pages === 1 ? "1 page" : `${pages} pages`;
summary.value.questions = questions === 1 ? "1 question" : `${questions} questions`;
}

const ui = view`<div class="space-y-3">
${['metadata', surveyMetadata(metadata)]}
<div class="space-y-3">
${['pages', pages.map(pageBuilder), pageBuilder]}
</div>
<div>
${Inputs.button('Add Page', {
reduce: () => {
ui.value.pages.push({
title: "page " + randomId()
});
ui.pages.dispatchEvent(new Event('input', {bubbles: true}));
}
})}
</div>

<div class="sticky bottom-1">
${summaryCard(summary, html`<div class="flex space-x-2">${Inputs.button('Preview')} ${Inputs.button('Save')}`)}
</div>
</div>`

ui.addEventListener('input', updateSummary);

// When values assigned to `viewof surveyEditor().value`, without dispatching `input` event
// call `viewof surveyEditor().applyValueUpdates()` to update UIs like summary.
ui.applyValueUpdates = () => {
updateSummary();
};
updateSummary();

return ui;
}
Insert cell
filterCellsNotOfType = (cells, type) => cells.filter(c => c?.inner?.type != type)
Insert cell
Insert cell
surveyMetadata = ({title} = {}) => view`<div class="card solid-shadow-1 space-y-3">
<h2>Survey Metadata</h2>
${['title', Inputs.text({value: title, label: "Survey title"})]}
</div>`
Insert cell
viewof exampleSurveyMetadata = surveyMetadata()
Insert cell
Insert cell
Insert cell
Insert cell
examplePage
Insert cell
Inputs.button("test backwritability of a cell", {
reduce: () => {
debugger;
examplePage.cells[0] = ({
id: "md",
inner: {
type: "md",
result: {
content: Math.random()
}
}
})
}
})
Insert cell
page = ({
id = randomId(),
title,
cells = [],
onDelete = (id) => {},
onUp = (id) => {},
onDown = (id) => {},
} = {}) => {
const cellBuilder = (args) => cell({
...args,
onDelete: (id) => {
const index = ui.value.cells.findIndex(cell => cell.id === id);
ui.value.cells.splice(index, 1);
ui.cells.dispatchEvent(new Event('input', {bubbles: true}));
},
onDown: (id) => {
const index = ui.value.cells.findIndex(cell => cell.id === id);
const element = ui.value.cells[index];
ui.value.cells.splice(index, 1);
ui.value.cells.splice(index + 1, 0, element);
ui.cells.dispatchEvent(new Event('input', {bubbles: true}));
},
onUp: (id) => {
const index = ui.value.cells.findIndex(cell => cell.id === id);
const element = ui.value.cells[index];
ui.value.cells.splice(index, 1);
ui.value.cells.splice(Math.max(index - 1, 0), 0, element);
ui.cells.dispatchEvent(new Event('input', {bubbles: true}));
}
});

const ui = view`<div class="[ page card ][ solid-shadow-1 space-y-3 ]">
${['_id', Inputs.input(id)]}
<div class="flex justify-between">
<div class="w-100 pr4">${['title', Inputs.text({label: "Page title", value: title})]}</div>
<div class="flex space-x-2">
${Inputs.button(buttonLabel({ariaLabel: "Delete", iconLeft: "trash-2", iconLeftClass: "icon--danger"}), {reduce: () => onDelete(id)})}
<div class="button-group">
${Inputs.button(buttonLabel({ariaLabel: "Move up", iconLeft: "arrow-up"}), {reduce: () => onUp(id)})}
${Inputs.button(buttonLabel({ariaLabel: "Move down", iconLeft: "arrow-down"}), {reduce: () => onDown(id)})}
</div>
</div>
</div>
<div class="box space-y-3">
${['cells', cells.map(cellData => cellBuilder(cellData)), cellBuilder]}
</div>
<div class="flex space-x-3">
<div class="button-group">
${Inputs.button('Add Question', {
reduce: () => {
ui.value.cells.push({inner: {type: "textarea"}})
ui.cells.dispatchEvent(new Event('input', {bubbles: true}));
}
})}
${Inputs.button('Add Text', {
reduce: () => {
ui.value.cells.push({inner: {type: "md"}});
ui.cells.dispatchEvent(new Event('input', {bubbles: true}));
}
})}
</div>
${Inputs.button('Add Section', {
reduce: () => {
ui.value.cells.push({inner: {type: "section"}});
ui.cells.dispatchEvent(new Event('input', {bubbles: true}));
}
})}
</div>
</div>`

return ui;
}
Insert cell
Insert cell
viewof exampleCell = cell({
id: "cell1",
inner: {
type: "md",
result: {
content: "hi!"
}
},
connections: { // Ugly double nesting (simplest implementation)
connections: [{
set: "g1",
role: "ifyes"
}]
}
})

Insert cell
exampleCell
Insert cell
Inputs.button("test backwritability", {
reduce: () => {
viewof exampleCell.value = ({
id: "cellTest",
inner: {
type: "md",
result: {
content: Math.random()
}
},
connections: { // Ugly double nesting (simplest implementation)
connections: [{
set: "test2",
role: "scored"
}]
}
})
}
})
Insert cell
cell = ({
id = randomId(),
inner = {
type: "radio",
result: {
}
},
connections,
onDelete = (id) => {},
onDown = (id) => {},
onUp = (id) => {}
} = {}) => {
const typeSelector = Inputs.select(types, {value: inner?.type, label: "Question Type"});
const innerTypeUI = typeUI(inner);
Inputs.bind(innerTypeUI.type, typeSelector);

const ui = view`<section class="[ cell ]" data-cell-type="${typeSelector.value}">
<div class="[ cell__section ][ flex ]">
<div>
${typeSelector}
</div>

<div class="ml-auto">
<div class="flex space-x-2">
${Inputs.button(buttonLabel({ariaLabel: "Delete", iconLeft: "trash-2", iconLeftClass: "icon--danger"}), {reduce: () => onDelete(id)})}
<div class="button-group">
${Inputs.button(buttonLabel({ariaLabel: "Move up", iconLeft: "arrow-up"}), {reduce: () => onUp(id)})}
${Inputs.button(buttonLabel({ariaLabel: "Move down", iconLeft: "arrow-down"}), {reduce: () => onDown(id)})}
</div>
</div>
</div>
</div>
<div class="[ cell__section ][ space-y-3 ]">
<div class="flex">${['id', Inputs.text({value: id, "label": "Question ID"})]}</div>
</div>
<div>${['inner', innerTypeUI]}</div>
<div class="[ cell__section cell__section--separated ][ space-y-3 ]">
<details>
<summary>Connections</summary>
<div class="pv2">${['connections', connectionsUI(connections)]}</div>
</details>
</div>
</section>`
typeSelector.addEventListener('input', () => {
ui.dataset.cellType = typeSelector.value;
});
return ui;
}
Insert cell
Insert cell
viewof exampleType = Inputs.select(types, {value: "fallback"})
Insert cell
viewof exampleTypeUI = {
const view = typeUI({
result: {
content: "foo"
}
})
Inputs.bind(view.type, viewof exampleType); // type is backwriteable
return view;
}
Insert cell
typeUI({
type: "md",
result: {
content: "foo"
}
})

Insert cell
exampleTypeUI
Insert cell
viewof exampleTypeUIRadio = typeUI({
type: "radio",
result: {
options: [{
id: "my id",
label: "a label",
score: 10000,
}]
}
})
Insert cell
exampleTypeUIRadio
Insert cell
typeUIFactories = ({
"radio": radioUI,
"checkbox": checkboxUI,
"table": tableUI,
"md": mdUI,
"text": textUI,
"number": numberUI,
"textarea": textareaUI,
"file_attachment": fileAttachmentUI,
"summary": summaryUI,
"section": sectionUI,
"aggregate": aggregateSummaryUI
})
Insert cell
typeUI = juice((arg0) => {
const factory = typeUIFactories[arg0?.type];
if (factory) {
return factory(arg0?.result)
}
return fallbackUI(arg0?.result)
}, {
"type": "[0].type"
})
Insert cell
Insert cell
typeof "" === 'string'
Insert cell
fallbackUI = (args = {}) => { // Main purpose is to convert into <map>format. Main UI is in fallbackUIEntries
const entriesUI = fallbackUIEntries({
entries: Object.entries(args).map(([k, v]) => {
if (!v) {
return {key: k, value_: undefined};
} if (v.outerHTML) {
return {key: k + "_md", value_: v.outerHTML};
} else if (typeof v === 'object') {
return {key: k + "_js", value_: JSON.stringify(v)};
} else {
return {key: k, value_: v};
}
})
});
const result = bindOneWay(Inputs.input(undefined), entriesUI, {
transform: (entriesUi) => Object.fromEntries(entriesUi.entries.map(e => {
return [e.key, e.value_]
}))
})
const ui = view`<div class="[ cell__section ][ pb3 ]">
<form class="${ns}"><label>Attributes</label>
${entriesUI}
${['_...', result]}
</form>
</div>`

return ui;
}
Insert cell
md``.outerHTML
Insert cell
viewof exampleFallbackUI = fallbackUI({
key1: "v2",
key2: md`<mark>we need to convert to string</mark>`,
key3: {object: true}
})
Insert cell
exampleFallbackUI
Insert cell
fallbackUIEntries = ({entries = []}) => {
const newAttributeInput = kvRowBuilder({
placeholder: "Add attribute",
onEnter: () => {
ui.entries.value.push(newAttributeInput.value)
newAttributeInput.value = {key: "", value_: ""};
ui.dispatchEvent(new Event('input', {bubbles: true}))
}
});
const kvRowBuilderWithDelete = (args) => kvRowBuilder({...args, onDelete: (id) => {
const index = ui.entries.value.find(option => option.id === id)
ui.entries.value.splice(index, 1); // delete in-place
ui.dispatchEvent(new Event('input', {bubbles: true}));
}})
const ui = view`<table class="ma0">
<tr>
<th>Attribute</th>
<th>Value</th>
</tr>
${['entries', entries.map(kvRowBuilderWithDelete), kvRowBuilderWithDelete]}
<tfoot>
${newAttributeInput}
</tfoot>
</table>`
return ui;
}
Insert cell
viewof exampleFallbackUIEntries = fallbackUIEntries({
entries: [{key: "type", value_: "random"}]
})
Insert cell
exampleFallbackUIEntries
Insert cell
kvRowBuilder = ({
placeholder,
key, value_,
onEnter, onDelete
}) => {
const keyInput = Inputs.text({placeholder, value: key});
const valueInput = Inputs.text({value: value_});
if (onEnter) {
keyInput.addEventListener('keyup', (evt) => {
if (evt.keyCode === 13) {
onEnter(evt);
}
});
valueInput.addEventListener('keyup', (evt) => {
if (evt.keyCode === 13) {
onEnter(evt);
}
});
}
if (onDelete) { // If user editable rows
keyInput.addEventListener('keyup', (evt) => {
// BACKSPACE
if (evt.keyCode === 8 && keyInput.value.length == 0) {
onDelete(keyInput.value);
}
});
}
const ui = view`<tr>
<td>${['key', keyInput]}</td>
<td>${['value_', valueInput]}</td>
</tr>`
return ui;
}
Insert cell
viewof exampleKvRow = kvRowBuilder ({
key: "k1",
value: "val"
})
Insert cell
Insert cell
viewof sampleMdUI = mdUI({
content: "## content"
})
Insert cell
sampleMdUI
Insert cell
mdUI = ({content, rows = 20} = {}) => {
return view`<div class="[ cell__section ][ pb3 ]">
${['content', Inputs.textarea({value: content, label: "Content", rows})]}
</div>`
}
Insert cell
Insert cell
Insert cell
viewof sampleTextUI = textUI()
Insert cell
sampleTextUI
Insert cell
textUI = ({title,description} = {}) => {
return view`<div class="[ cell__section ][ pb2 ]">
${['title', Inputs.text({value: title, label: "Question"})]}
</div>
<div class="[ cell__section cell__section--separated ]">
<details>
<summary>Options</summary>
<div class="pv2">${['description', Inputs.text({label: "Description", value: description})]}</div>
</details>
</div>`
}
Insert cell
Insert cell
viewof exampleRadioUI = radioUI({
title: "How far along is your process?",
options: [{
id: "dsds",
label: "We are planning a process",
score: 4
},{
id: "asdas",
label: "Do you have process in place?",
score: 10
}],
includeAllOption: {
score: 5,
label: "All of the above"
},
description: "Select the statements that best describes your case"
})
Insert cell
exampleRadioUI
Insert cell
radioUI = ({title, options = [], connections = [], description,includeAllOption} = {}) => {
const newOptionInput = optionsRowBuilder({
placeholder: "Add new row",
id: randomId(),
onEnter: () => {
ui.value = {
...ui.value,
options: [...ui.value.options, newOptionInput.value]
}
newOptionInput.value.label = "";
newOptionInput.value.id = randomId();
ui.dispatchEvent(new Event('input', {bubbles: true}))
}
});
const radioOptionBuilderWithDelete = (args) => optionsRowBuilder({...args, onDelete: (id) => {
ui.value = {
...ui.value,
options: ui.value.options.filter(option => option.id !== id)
}
ui.dispatchEvent(new Event('input', {bubbles: true}));
}})
const ui = view`<div class="[ cell__section ][ space-y-3 ph2 pb3 ]">
${['title', Inputs.text({label: "Question", value: title})]}
<form class="${ns}">
<label>Radio Options</label>
<table class="ma0">
<tr>
<th>Label</th>
<th>Score</th>
<th>ID</th>
</tr>
${['options', options.map(radioOptionBuilderWithDelete), radioOptionBuilderWithDelete]}
<tfoot>
${newOptionInput}
</tfoot>
</table>
</form>
</div>
<div class="[ cell__section cell__section--separated ]">
<details>
<summary>Options</summary>
<div class="pv2 space-y-2">
${['includeAllOption', includeOptionalAttributesUI(includeAllOption)]}
${['description', descriptionUI(description)]}
</div>
</details>
</div>`

return ui;

}
Insert cell
optionsRowBuilder = ({
label,
score,
id,
placeholder,
onEnter,
onDelete,
} = {}) => {
const labelInput = Inputs.text({value: label, placeholder})
if (onEnter) {
labelInput.addEventListener('keyup', (evt) => {
if (evt.keyCode === 13) {
onEnter(evt);
}
});
}
if (onDelete) { // If user editable rows
labelInput.addEventListener('keyup', (evt) => {
// BACKSPACE
if (evt.keyCode === 8 && labelInput.value.length == 0) {
onDelete(id);
}
});
}
return view`<tr>
<td>${['label', labelInput]}</td>
<td>${['score', Inputs.text({value: score})]}</td>
<td>${['id', Inputs.text({value: id})]}</td>
</tr>`
}
Insert cell
Insert cell
Insert cell
viewof exampleCheckboxUI = checkboxUI({
title: "Your business focus",
options: [{
id: "eg",
label: "Electricity Generation",
score: 10
},{
id: "et",
label: "Electricity Transmission",
score: 8
},{
id: "ed",
label: "Electricity Distribution",
score: 7
},{
id: "other",
label: "Other",
score: 3
}],
includeAllOption: {
score: 5,
label: "All of the above"
},
description: "Select a statement that best describes your case",
})
Insert cell
exampleCheckboxUI
Insert cell
checkboxUI = ({title, options = [], includeAllOption, includeNoneOption, description} = {}) => {
const newOptionInput = optionsRowBuilder({
placeholder: "Add new row",
id: randomId(),
onEnter: () => {
ui.value = {
...ui.value,
options: [...ui.value.options, newOptionInput.value]
}
newOptionInput.value.label = "";
newOptionInput.value.id = randomId();
ui.dispatchEvent(new Event('input', {bubbles: true}))
}
});
const radioOptionBuilderWithDelete = (args) => optionsRowBuilder({...args, onDelete: (id) => {
ui.value = {
...ui.value,
options: ui.value.options.filter(option => option.id !== id)
}
ui.dispatchEvent(new Event('input', {bubbles: true}));
}})

const selectNoneUI = includeOptionalAttributesUI(includeNoneOption, "Add option to select none", "None of the above");

const ui = view`<div class="[ cell__section ][ space-y-3 ph2 pb3 ]">
${['title', Inputs.text({label: "Question", value: title})]}
<form class="${ns}">
<label>Checkbox Options</label>
<table class="ma0">
<tr>
<th>Label</th>
<th>Score</th>
<th>ID</th>
</tr>
${['options', options.map(radioOptionBuilderWithDelete), radioOptionBuilderWithDelete]}
<tfoot>
${newOptionInput}
</tfoot>
</table>
</form>
</div>
<div class="[ cell__section cell__section--separated ]">
<details>
<summary>Options</summary>
<div class="pv2 space-y-2">
${['includeAllOption', includeOptionalAttributesUI(includeAllOption)]}
${['includeNoneOption', selectNoneUI]}
${['description', descriptionUI(description)]}
</div>
</div>
</details>
</div>`

return ui;

}
Insert cell
typeUI({
type: "checkbox",
result: {
title: "Your business focus",
options: [{
id: "a",
label: "a_label",
score: 10
},{
id: "b",
label: "b_label",
score: 8
}],
includeAllOption: {
score: 5,
label: "all"
},
}
})
Insert cell
Insert cell
viewof exampleNumberUI = numberUI({
title: "How old is your company?",
description: "In years",
min: 1,
// max: 21,
step: 2
})
Insert cell
exampleNumberUI
Insert cell
numberUI = ({title, description, min = 0, max, step = 1} = {}) => {
const attributes = [
{
id: "min",
label: "Minimum",
value: min,
},
{
id: "max",
label: "Maximum",
value: max
},
{
id: "steps",
label: "Steps",
value: step
}
];

return view`<div class="[ cell__section ][ space-y-3 ph2 pb3 ]">
${['title', Inputs.text({label: "Question", value: title})]}
</div>
<div class="[ cell__section cell__section--separated ]">
<details>
<summary>Options</summary>
<div class="pv2 space-y-2">
${['description', descriptionUI(description)]}
<div class="space-y-2 w-25-ns">
${['...', {
...attributes.reduce((acc,{id, label, value}) => {
return {...acc, [id]: Inputs.number([-Infinity, Infinity], {label, value}) }
}, {})
}]}
</div>
</div>
</details>
</div>`;
}
Insert cell
#### Textarea Question
Insert cell
viewof exampleTextareaUI = textareaUI({
title: "Tell us about the work your company does?",
description: "Include you primary and secondary revenue streams",
placeholder: "Please be elaborate",
rows: 6
})
Insert cell
exampleTextareaUI
Insert cell
textareaUI = ({title, placeholder, description, rows = 4} = {}) => {
const attributes = [
{
id: "rows",
label: "Rows",
value: rows,
}
];

return view`<div class="[ cell__section ][ ph2 pb3 ]">
${['title', Inputs.text({label: "Question", value: title})]}
</div>
<div class="[ cell__section cell__section--separated ]">
<details>
<summary>Options</summary>
<div class="pv2 space-y-2">
${['description', descriptionUI(description)]}
${['placeholder', Inputs.text({label: "Placeholder", value: placeholder})]}
<div class="space-y-2 w-25-ns">
${['...', {
...attributes.reduce((acc,{id, label, value}) => {
return {...acc, [id]: Inputs.number([-Infinity, Infinity], {label, value}) }
}, {})
}]}
</div>
</div>
</details>
</div>`
}
Insert cell
#### File Attachment Question
Insert cell
viewof exampleFileAttachmentUI = fileAttachmentUI({
title: "File attachments",
description: "You can provide documents such as policies, brochure, etc.",
placeholder: "No file chosen"
})
Insert cell
exampleFileAttachmentUI
Insert cell
fileAttachmentUI = ({title, description, placeholder} = {}) => {
return view`
<div class="[ cell__section ][ space-y-3 ph2 pb3 ]">
${['title', Inputs.text({label: "Question", value: title})]}
</div>
<div class="[ cell__section cell__section--separated ]">
<details>
<summary>Options</summary>
<div class="pv2 space-y-2">
${['description', Inputs.text({label: "Description", value: description})]}
${['placeholder', Inputs.text({label: "Placeholder", value: placeholder})]}
</div>
</details>
</div>`
}
Insert cell
Insert cell
viewof exampleTableUI = tableUI({
title: "Gender distribution across teams",
columns: [
{ key: "w", label: "Women", total: "women" },
{ key: "m", label: "Men", total: "men" },
{ key: "unknown", label: "No Data", total: "no data" },
],
rows: [
{ key: "board", label: "Board" },
{ key: "management", label: "Management" },
{ key: "tech", label: "Technical / Engineering Staff" },
{ key: "staff", label: "Non-Technical Staff" },
{ key: "admin", label: "Administrative / Support Staff" },
{ key: "customerservice", label: "Customer Service Staff" },
{ key: "other", label: "Other Staff" },
{ key: "day", label: "Non-Contractual/Informal Day Workers" },
],
caption: "_Some caption_",
user_rows: true,
table_total: "Total workforce",
table_total_label: "people",
});
Insert cell
viewof exampleTableUI2 = tableUI();
Insert cell
Insert cell
tableUI = ({title, rows = [], columns = [], caption, user_rows, table_total, table_total_label} = {}) => {
const newRowInputBuilder = (valueKey, hideTotal) => {
const newRowInput = tableHeaderRowBuilder({
placeholder: "Add new row",
key: randomId(),
hideTotal,
onEnter: () => {
ui.value = {
...ui.value,
[valueKey]: [...ui.value[valueKey], newRowInput.value]
}
newRowInput.value.label = "";
newRowInput.value.key = randomId();
ui.dispatchEvent(new Event('input', {bubbles: true}))
}
})
return newRowInput;
};

const tableHeaderRowBuilderWithDelete = (valueKey, hideTotal) => (args) => tableHeaderRowBuilder({...args, hideTotal, onDelete: (key) => {
console.log("ui.value", ui.value);
ui.value = {
...ui.value,
[valueKey]: ui.value[valueKey].filter(option => option.key !== key)
}
ui.dispatchEvent(new Event('input', {bubbles: true}));
}})

const ui = view`
<div class="[ cell__section ][ space-y-3 ph2 pb3 ]">
${['title', Inputs.text({label: "Question", value: title})]}
<form class="${ns}">
<label>Columns</label>
<table class="ma0">
<tr>
<th>Label</th>
<th>Key</th>
<th>Total</th>
</tr>
${['columns', columns.map(tableHeaderRowBuilderWithDelete("columns")), tableHeaderRowBuilderWithDelete("columns")]}
<tfoot>
${newRowInputBuilder("columns")}
</tfoot>
</table>
</form>
<form class="${ns}">
<label>Rows</label>
<table class="ma0">
<tr>
<th>Label</th>
<th>Key</th>
</tr>
${['rows', rows.map(tableHeaderRowBuilderWithDelete("rows", true)), tableHeaderRowBuilderWithDelete("rows", true)]}
<tfoot>
${newRowInputBuilder("rows", true)}
</tfoot>
</table>
</form>
</div>

<div class="[ cell__section cell__section--separated ]">
<details>
<summary>Options</summary>
<div class="pv2 space-y-2">
${['user_rows', Inputs.toggle({label: "Allow to edit row labels", value: user_rows})]}
${['table_total', Inputs.text({label: "Label for total", value: table_total})]}
${['table_total_label', Inputs.text({label: "Unit for total", value: table_total_label})]}
${['caption', descriptionUI(caption)]}
</div>
</details>
</div>`

return ui;
}
Insert cell
tableHeaderRowBuilder = ({
label,
key,
total,
placeholder,
onEnter,
onDelete,
hideTotal
} = {}) => {
const labelInput = Inputs.text({value: label, placeholder})
if (onEnter) {
labelInput.addEventListener('keyup', (evt) => {
if (evt.keyCode === 13) {
onEnter(evt);
}
});
}
if (onDelete) { // If user editable rows
labelInput.addEventListener('keyup', (evt) => {
// BACKSPACE
if (evt.keyCode === 8 && labelInput.value.length == 0) {
onDelete(key);
}
});
}

const row = {
label: view`<td>${['...', labelInput]}</td>`,
key: view`<td>${['...', Inputs.text({value: key})]}</td>`
}

if (!hideTotal) {
row.total = view`<td>${['...', Inputs.text({value: total})]}</td>`
}

return view`<tr>
${['...', row]}
</tr>`
}
Insert cell
Insert cell
viewof sampleSummaryUI = summaryUI({
label: "Policy/commitment to promoting gender equality and women's empowerment",
counter_group: "A",
counter_value: "4"
})
Insert cell
summaryUI = ({label, counter_group, counter_value} = {}) => {
return view`<div class="[ cell__section ][ space-y-3 ph2 pb3 ]">
${['label', Inputs.text({value: label, label: "Label"})]}
${['counter_group', Inputs.text({label: "Group", value: counter_group})]}
${['counter_value', Inputs.text({label: "Value", value: counter_value})]}
</div>`
}
Insert cell
#### Aggregate Summary
Insert cell
viewof exampleAggregateSummaryUI = aggregateSummaryUI({
label: "Organizational Policies"
})
Insert cell
aggregateSummaryUI = ({label, counter_group} = {}) => {
return view`<div class="[ cell__section ][ space-y-3 ph2 pb3 ]">
${['label', Inputs.text({value: label, label: "Label"})]}
${['counter_group', Inputs.text({label: "Group", value: counter_group})]}
</div>`
}
Insert cell
Insert cell
viewof sampleSectionUI = sectionUI({
title: "External operations"
})
Insert cell
sectionUI = ({title} = {}) => {
// class="[ pa3 mh-3 ][ bt bw1 b--light-silver" ]"
return view`<div class="[ cell__section pb3 ]">
${['title', Inputs.text({value: title, label: "Title"})]}
</div>`
}
Insert cell
Insert cell
Insert cell
viewof sampleIncludeOptionalAttributesUI = includeOptionalAttributesUI({
score: 5,
label: "Everything"
})
Insert cell
viewof sampleIncludeOptionalAttributesUINoLabel = includeOptionalAttributesUI({
score: 2
})
Insert cell
sampleIncludeOptionalAttributesUINoLabel
Insert cell
sampleIncludeOptionalAttributesUI
Insert cell
viewof sampleIncludeOptionalAttributesUI3 = includeOptionalAttributesUI({
label: "This is included in our regulations, internal policies/strategies, guidelines, and/or mechanisms; we are planning to implement.",
score: 3,
id: "planning"
}, "Add option to select none", "None", true)
Insert cell
sampleIncludeOptionalAttributesUI3
Insert cell
viewof sampleIncludeOptionalAttributesUI4 = includeOptionalAttributesUI({
score: 3,
id: "planning"
}, "Add option to select none", "None of the above", true)
Insert cell
includeOptionalAttributesUI = (option = {}, toggleLabel = "Add option to select all", defaultOptionLabel = "All of the above", showId = false) => {
const {score, label, id} = option;
const showUI = Boolean(score || label);
const toggleUI = Inputs.toggle({label: toggleLabel, value: showUI});

let optionRow = {
label: view`<td>${['...', Inputs.text({value: label})]}</td>`,
score: view`<td>${['...', Inputs.text({value: score})]}</td>`
}

if (showId) {
optionRow.id = view`<td>${['...', Inputs.text({value: id})]}</td>`
}
const table = view`<table class="mt0 dn">
<tr>
<th>Label</th>
<th>Score</th>
${showId ? html`<th>ID</th>` : ""}
</tr>
<tr>
${['...', optionRow]}
</tr>`;

const handler = (val) => {
if (val) {
table.classList.remove('dn')
table.value.label = table.value.label || label || defaultOptionLabel;
table.value.score = table.value.score || score;
table.value.id = table.value.id || id;
} else {
table.classList.add('dn');
table.value.label = undefined;
table.value.score = undefined;
table.value.id = undefined;
}
}
bindOneWay(Inputs.input(undefined), toggleUI, {
transform: handler
})

handler(showUI);
return view`<div>
${toggleUI}
${['...', table]}
</div>`;
}
Insert cell
Insert cell
FileAttachment("image@4.png").image()
Insert cell
viewof exampleInitializedConnectionsUI = connectionsUI({connections: [
{set:"g1", role:"r1"},
{set:"g2", role:"r2"}
]})
Insert cell
viewof exampleConnectionsUI = connectionsUI()
Insert cell
exampleConnectionsUI
Insert cell
exampleConnectionsUIBackwritingExample = {
viewof exampleConnectionsUI.value = {connections: [
{set:"g1", role:"r1"},
{set:"g2", role:"r2"}
]}
viewof exampleConnectionsUI.dispatchEvent(new Event('input', {bubbles: true}))
}
Insert cell
connectionsUI = ({connections = []} = {}) => {
const newRoleInput = connectionRowBuilder({
placeholder: "Add new connection",
id: randomId(),
role: "scored",
onEnter: () => {
ui.value = {
...ui.value,
connections: [...ui.value.connections, newRoleInput.value]
}
newRoleInput.value.group = "";
newRoleInput.value.role = "scored";
ui.dispatchEvent(new Event('input', {bubbles: true}))
}
});
const connectionRowBuilderWithDelete = (args) => connectionRowBuilder({...args, onDelete: (group) => {
console.log("deleting", group, ui.value.connections.filter(option => option.group !== group))
ui.value = {
...ui.value,
connections: ui.value.connections.filter(option => option.group !== group)
}
ui.dispatchEvent(new Event('input', {bubbles: true}))
}})
const ui = view`<table class="ma0">
<tr>
<th>Group</th>
<th>Role</th>
</tr>
${['connections', connections.map(connectionRowBuilderWithDelete), connectionRowBuilderWithDelete]}
<tfoot>
${newRoleInput}
</tfoot>
</table>`

return ui;

}
Insert cell
connectionRowBuilder = ({
set,
role,
placeholder,
onEnter,
onDelete
} = {}) => {
const setInput = Inputs.text({value: set, placeholder})
if (onEnter) {
setInput.addEventListener('keyup', (evt) => {
if (evt.keyCode === 13) {
onEnter(evt);
}
});
}
if (onDelete) { // If user editable rows
setInput.addEventListener('keyup', (evt) => {
// BACKSPACE
if (evt.keyCode === 8 && setInput.value.length == 0) {
onDelete(setInput.value);
}
});
}
return view`<tr>
<td>${['set', setInput]}</td>
<td>${['role', Inputs.select(roles, {value: role})]}</td>
</tr>`
}
Insert cell
viewof exampleConnectionRowBuilder = connectionRowBuilder()
Insert cell
exampleConnectionRowBuilder
Insert cell
Insert cell
descriptionUI("Some description")
Insert cell
descriptionUI = (description, rows = 2) => view`<div>
${['...', Inputs.textarea({label: "Description", value: description, rows})]}
<form class="${ns}"><p class="[ align-observable-inputs ][ ma0 ][ mid-gray ]">Supports Markdown</p></form>
</div>`
Insert cell
Insert cell
viewof exampleSettingsCard = settingsCard()
Insert cell
exampleSettingsCard
Insert cell
settingsCard = () => {
return view`<div class="[ card card--compact ][ solid-shadow-1 ]">
<div class="[ space-y-3 ]">
<h2 class="mt0 f4">Settings</h2>
${['showResults', Inputs.toggle({label: "Show results to respondents"})]}
</div>
`
}
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Insert cell
summaryCard('30 questions across 4 pages');
Insert cell
summaryCard('30 questions across 4 pages', html`<div class="flex space-x-2">${Inputs.button('Preview')} ${Inputs.button('Save')}`);
Insert cell
summaryCard = (title, actionsHtml) => {
const actions = actionsHtml ? html`<div>${actionsHtml}` : "";

return html`<div class="[ card card--compact ][ b--light-blue solid-shadow-1 ]">
<div class="[ flex flex-wrap justify-between items-center ][ f6 mid-gray ]">
<p class="ma0">${title}</p>
${actions}
</div>
`
}
Insert cell
Insert cell
inputLabel("A sample label")
Insert cell
inputLabel("A sample label", true)
Insert cell
inputLabel = (label, optional) => html`<span>${label}${ optional ? ` <span class="mid-gray">Optional</span` : "" }</span>`
Insert cell
Insert cell
styles = html`${commonComponentStyles}
<style>
.mh-3 {
margin-right: calc(-1 * var(--spacing-medium));
margin-left: calc(-1 * var(--spacing-medium));
}

tr:not(:last-child) {
border: 0;
}

.cell {
border: 1px solid var(--border-color-light);
border-radius: var(--border-radius-2);
}

.cell[data-cell-type="section"] {
margin-left: -1rem;
margin-right: -1rem;
border-top-width: 0.25rem;
border-left-width: 0;
border-right-width: 0;
border-radius: 0;
}

.cell__section {
padding-top: var(--spacing-medium);
padding-left: var(--spacing-medium);
padding-right: var(--spacing-medium);
}

.cell__section--separated {
padding-top: var(--spacing-extra-small);
padding-bottom: var(--spacing-extra-small);
border-top: 1px solid var(--border-color-light);
}

.cell__section summary {
color: #555; /* mid-gray */
cursor: pointer;
font-size: .875rem; /* .f6 */
}

.align-observable-inputs {}

@media screen and (min-width: 30em) {
.align-observable-inputs {
margin-left: calc(var(--label-width) + var(--length2));
}
}
</style>`
Insert cell
Insert cell
html`
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,400;0,700;1,400&display=swap" rel="stylesheet">
<style type="text/css" media="screen, print">
body {
font-family: var(--brand-font);
}
</style>
`
Insert cell
tachyons = tachyonsExt({
colors: {
brand: mainColors[900], // or, provide and color hex code
accent: accentColors[900], // or, provide and color hex code
// The color of text which are usually displayed on top of the brand or accent colors.
"text-on-brand": "#ffffff",
},
fonts: {
"brand-font": `"Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"`
}
})
Insert cell
Insert cell
randomId = () => Math.random().toString(16).substring(2)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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