compose = {
class ComposeView {
constructor(name, view) {
this.name = name;
this.view = view;
}
}
class ComposeDefault {
constructor(view) {
this.view = view;
}
}
let compose_template = (template_fn, template_strings, ...input_cells) => {
let has_default = input_cells.some(cell => cell instanceof ComposeDefault);
const runtime = new ObservableRuntime({});
const main = runtime.module();
let cells_with_index = Object.entries(input_cells);
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;
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;
}