Published
Edited
May 8, 2020
Importers
8 stars
Insert cell
Insert cell
Insert cell
Insert cell
viewof beauitful_slider = compose.md`
#### Slider with title
${compose.default(html`<input type=range min=0 max=24 />`)}
*Pretty nice*
`
Insert cell
Insert cell
Insert cell
viewof options = compose.html`
<div>
<h4>Scrubber in my html</h4>

<div style="
display: flex;
flex-direction: row;
align-items: center;
">
<div style="margin-right: 32px">
${compose.viewof({ a_b_c: Scrubber([1, 2, 3], { delay: 500 }) })}
</div>
<small style="font-style: italic">Not bad right?</small>
</div>

<h4>Input in the same html</h4>
<div style="
display: flex;
flex-direction: row;
align-items: center;
">
${compose.viewof({ title: html`<input type=text placeholder=Title />` })}
<button>Submit</button>
</div>
</div>
`
Insert cell
Insert cell
Insert cell
viewof scrubber_with_title = with_title({
viewof: Scrubber([1, 2, 3], { delay: 500 }),
title: 'A slider',
description: 'This is useful'
})
Insert cell
with_title = ({ title, viewof: view, description }) => {
return compose.html`
<div>
${title &&
html`<div style="font: 700 0.9rem sans-serif;">${title}</div>`}
<div style="margin: 0">
${compose.default(view)}
</div>
${description &&
html`<div style="font-size: 0.85rem; font-style: italic;">${description}</div>`}
</div>
`;
}
Insert cell
Insert cell
Insert cell
viewof ultimate_test = compose.html`
<div style="border: solid black 2px; padding: 16px;">
${compose.viewof({
scrubber_with_title: with_title({
viewof: Scrubber([1, 2, 3], { delay: 500 }),
title: 'Hey there',
description: 'This is useful'
})
})}
</div>
`
Insert cell
viewof markdown_test = compose.md`
## Compose in markdown

### Where would you like to go?
${compose.default(worldMapCoordinates([-122.27, 37.87]))}
Choose wisely, once you click you will be teleported there immediately
`
Insert cell
Insert cell
Insert cell
Insert cell
viewof htl_html_test = compose(htl_html)`
<h3>I don't have inspiration for forms</h3>
<div style=${{ display: 'flex', flexDirection: 'row' }}>
${compose.viewof({ location: worldMapCoordinates([-122.27, 37.87]) })}
<div style=${{ display: 'flex', flexDirection: 'column' }}>
<h4>Slider 1</h4>
${compose.viewof({
slider1: htl_html`<input type=range min=0 max=100 />`
})}

<!--
// Not sure why but htl didn't like my
// <div style=\${{ height: 16 }}></div>
// here, so yeah not htl style for you div!
-->
<div style="height: 16px"></div>

<h4>Slider 2</h4>
${compose.viewof({
slider2: htl_html`<input type=range min=0 max=100 />`
})}

</div>
</div>
`
Insert cell
Insert cell
compose = {
// Wrappers so I can quickly/intuitively check for values created with
// `compose({ ... })` and `compose.default()`
class ComposeView {
constructor(name, view) {
this.name = name;
this.view = view;
}
}
class ComposeDefault {
constructor(view) {
this.view = view;
}
}

// The real powerhouse of this function
let compose_template = (template_fn, template_strings, ...input_cells) => {
// Special case where I want the .value of this element to not be an object, but just this value
let has_default = input_cells.some(cell => cell instanceof ComposeDefault);

// Setup
const runtime = new ObservableRuntime({});
const main = runtime.module();
let cells_with_index = Object.entries(input_cells);

// Define a "viewof_<x>" for every interpolated value.
// This is useful, because this way we can still use anything that would normally render in a cell.
// In normal html or md templates this is not the case, generators and promises do not "just work".
for (let [index, compose_view] of cells_with_index) {
let is_compose_value =
compose_view instanceof ComposeView ||
compose_view instanceof ComposeDefault;
let cell = is_compose_value ? compose_view.view : compose_view;

// Better safe than sorry
if (is_compose_value) {
precondition(
!(cell instanceof DocumentFragment),
`For now I do not allow DocumentFragment's in compose(...), as they would normally not be rendered in a notebook cell either`
);
}

// Define the cell as a constant.
// Turns out that if you put a function as interpolation,
// compose will actually treat it like a cell container 🙃
main.define(`viewof_${index}`, cell);
}

// Define the value cells, derive from the viewof cells
if (has_default) {
// In the has_default case this is really easy, and really most of the lines go to extra error checking.
// This thing is most likely buggy enough as it is, let me at least disallow anything I know is wrong.
precondition(
!input_cells.some(cell => cell instanceof ComposeView),
`When using a ComposeDefault, there are not ComposeValues allowed`
);
precondition(
input_cells.filter(cell => cell instanceof ComposeDefault).length === 1,
`You can't have multiple ComposeDefault's, that doesn't make sense!`
);

// Find the single default cell and derive value from it
let [default_cell_index, _] = cells_with_index.find(
([_, cell]) => cell instanceof ComposeDefault
);
main.define('value', [`viewof_${default_cell_index}`], view => {
return Generators.input(view);
});
} else {
// Define a "valueof_<x>" from "viewof_<x>" for every compose() value.
let compose_cells_with_index = cells_with_index.filter(
([_, view]) => view instanceof ComposeView
);
for (let [index, compose_value] of compose_cells_with_index) {
let name = compose_value.name;
// prettier-ignore
main.define(`valueof_${index}`, [`viewof_${index}`], async function*(view) {
// Can't just do Generators.input, because we need the name as well,
// and tried using Generators.map but it acted weirdly, but that doesn't support async 🤷‍
for await (let value of Generators.input(view)) {
yield [name, value];
}
});
}

// Collect all the valueof cells, and combine them into an object
main.define(
'value',
compose_cells_with_index.map(([index, _]) => `valueof_${index}`),
(...valueofs) => {
let value_object = {};
for (let [name, value] of valueofs) {
value_object[name] = value;
}
return value_object;
}
);
}

// Cell that collects all viewof cells in one array
main.define(
'viewofs',
cells_with_index.map(([index, _]) => `viewof_${index}`),
(...viewofs) => viewofs
);
// One last cell to rule them all.
// We need the "<x>_changed" properties, so we do not refresh the html when a value changed,
// because that would take a lot of processing and all inputs would be recreated and lose focus.
main.define('everything', ['viewofs', 'value'], function(viewofs, value) {
let previous = this || { viewofs: null, value: null };

return {
viewofs,
value,
viewofs_changed: viewofs !== previous.viewofs,
value_changed: value !== previous.value
};
});

// withDispose is like Generators.disposable, but when you want to return a generator
// (where Observable does not like it when you return a generator with Generators.disposable)
return disposable(
run(async function*() {
let container_element = null;

let generator = generator_from_observable_variable(main, 'everything');
for await (let {
value,
viewofs,
value_changed,
viewofs_changed
} of generator) {
// Generate the view first, so we can set the value on it below
if (viewofs_changed) {
container_element = template_fn(template_strings, ...viewofs);
}

if (value_changed) {
container_element.value = value;
container_element.dispatchEvent(new CustomEvent('input'));
}

// But only after we set the value, we yield
if (viewofs_changed) {
yield container_element;
}
}
}),
() => {
// Just get rid of the whole runtime,
// also invokes all disposables inside cells 👍🏾
runtime.dispose();
}
);
};

let compose = template_fn => {
return (...args) => compose_template(template_fn, ...args);
};

compose.default = value => new ComposeDefault(value);
compose.viewof = definition_object => {
let [name, ...other_keys] = Object.keys(definition_object);
precondition(
other_keys.length === 0,
'compose({ ... }) takes an object with only one key'
);
precondition(
name != null,
'compose({ ... }) takes an object with at least one key'
);

let value = definition_object[name];
precondition(value != null, `compose({ ... }) can not have null as value`);
return new ComposeView(name, value);
};

compose.html = compose(html);
compose.md = compose(md);
return compose;
}
Insert cell
Insert cell
run = fn => fn()
Insert cell
Insert cell
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