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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more