Public
Edited
Dec 25, 2022
Importers
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
// The component(..) function takes a function that transforms properties
// into a DOM template.
const TodoItem = component(({ label, isChecked, onRemove }) =>
// DOM template nodes are created using the `H` object.
H.span(
{ style: { display: "flex" } },
H.input({
type: "checkbox",
// Props can be assigned as values directly
checked: isChecked,
// Handler are written like `onXXX` (XXX PascalCase), and in this
// case the handler will get the `isChecked` value and return an
// updated version for it.
onInput: (event) => ({
isChecked: event.target.value
})
}),
H.span(
{ style: { flexGrow: 1 } },
label
// FIXME: That fails
//$(label).then(label).else("No label")
),

// TODO: We're missing that communication to the parent
H.button({ onClick: onRemove }, "Remove")
)
);

const TodoList = component(({ label, items }) => {
// This is the handler to add an item. We return an object that will
// be merged with the current state
const onAddItem = ({ items, label }) => ({
items: [...items, { label: label, isChecked: false }]
});

// This is how an item get removed
const onRemoveItem = () => null;

return H.div(
{ style: { width: "40ch" } },
H.h4("Todo List Example"),
// Here we'll have a conditional rendering when there is no items.
$(items)
.then(
H.ul(
// Use $(items) to select the list of items, and map each item
// to an li tag, and instanciating a TodoItem there.
//
// TODO: We should do H.li(TodoItem({..._, onRemove:onRemoveItem}))
$(items).map((_) => H.li(TodoItem({ onRemove: onRemoveItem, _ })))
)
)
.else(H.em("Nothing to do!")),
H.div(
H.input({
placeholder: "New todo item",
onInput: (event) => {
// Here the handler does not depend on the state, and as result
// we can use the regular form (event) => result instead of () => (props) => result.
return { label: event.target.value };
}
}),
H.button({ onClick: () => onAddItem }, "Add")
)
);
});

// We render the TodoList component with a default state.
return render(TodoList, { items: [{ label: "Buy wine", isChecked: false }] });
}
Insert cell
Insert cell
Insert cell
Insert cell
class Cell {
static Expand(value, state) {
if (value && (value instanceof Array || value instanceof Object)) {
for (let k in value) {
const v = value[k];
if (v instanceof Cell) {
value[k] = state[v.name];
} else {
Cell.Expand(v, state);
}
}
}
return value;
}
constructor(name = undefined) {
this.name = name;
}
}
Insert cell
Self = new Cell("self")
Insert cell
Insert cell
Insert cell
RawObjectPrototype = Object.getPrototypeOf({})
Insert cell
class DataTemplate {
static RE_NUMBER;

// --
// Walks the given `value`, calling `callback(value, path, isCell)`
static Walk(value, callback, path = []) {
if (value === undefined || value === null) {
callback(value, path, false);
} else if (value instanceof Cell) {
callback(value, path, true);
} else if (value instanceof Array) {
for (let k = 0; k < value.length; k++) {
DataTemplate.Walk(
value[k],
callback,
path && path.length ? [...path, k] : [k]
);
}
} else if (
typeof value === "object" &&
Object.getPrototypeOf(value) === RawObjectPrototype
) {
for (let k in value) {
DataTemplate.Walk(
value[k],
callback,
path && path.length ? [...path, k] : [k]
);
}
}
}

// --
// Patches the given `data` structure with the `value` at the given `path`.
// When `value` is `null`, the element will be deleted.
static Patch(data, value, path = []) {
if (path.length == 0) {
return value;
}
data = data || {};
let scope = data;
let n = path.length;
let k = undefined;
let i = 0;
for (; i < n - 1; i++) {
if (scope[(k = path[i])] === undefined) {
const w = typeof path[i + 1] === "number" ? [] : {};
if (typeof k === "number" && scope instanceof Array) {
while (scope.length < k) {
scope.push(undefined);
}
}
scope = scope[k] = w;
} else {
scope = scope[k];
}
}
k = path[i];
if (value === null) {
if (typeof k === "number" && scope instanceof Array) {
scope.splice(k, 1);
} else {
delete scope[k];
}
} else {
scope[k] = value;
}
return data;
}

static Holes(template) {
return "XXXX";
const res = new Map();
return res;
DataTemplate.Walk(
template,
(value, path, isCell) => isCell && res.set(value, path)
);
return res;
}

static Capture(template, value) {}

static Expand(template, mapped) {}

constructor(template) {}
}
Insert cell
{
return 10;
}
Insert cell
// TODO: This should really be rehtough/reworked

class Mapping {
// This will extract input Cells from any data structure, returning them
// as a structure.
static Inputs(value, res = []) {
if (value === undefined || value === null) {
// pass
} else if (value instanceof Cell) {
res.push(value);
} else if (
value instanceof Array ||
(typeof value === "object" &&
Object.getPrototypeOf(value) === RawObjectPrototype)
) {
for (let k in value) {
Mapping.Inputs(value[k], res);
}
}
return res;
}

// --
// Takes the given `template`, which may contain cells and expands it
// by replacing cell references with the value applied from `mapped`.
// NOTE: This is not clear.
static Expand(template, mapped = null) {
if (template instanceof Cell) {
return mapped ? mapped.get(template) : undefined;
} else if (template instanceof Array) {
return template.map((_) => Mapping.Expand(_, mapped));
} else if (
typeof template === "object" &&
Object.getPrototypeOf(template) === RawObjectPrototype
) {
const res = {};
for (let k in template) {
res[k] = Mapping.Expand(template[k], mapped);
}
return res;
} else {
return template;
}
}

constructor(inputs) {
this.inputs = inputs;
// We keep track of the input type for speed
this.type =
inputs instanceof Cell
? 1
: inputs instanceof Array
? 2
: inputs instanceof Object
? 3
: 0;
// We keep the cells as an array
this.cells =
inputs instanceof Cell
? [inputs]
: inputs instanceof Array
? inputs
: inputs instanceof Object
? Object.values(inputs)
: [];
}

// --
// Extracts the cell values from values like `{[cell.name]:value}`
extract(values, mapped = new Mapped()) {
return this.cells.reduce((r, cell) => {
const v = values[cell.name];
if (v !== undefined) {
mapped.set(cell, v);
}
return r;
}, mapped);
}

expand(value, mapped) {
return Mapping.Expand(
value,
mapped ? (mapped instanceof Mapped ? mapped : this.apply(mapped)) : null
);
}

// --
// Maping.apply(input) will return a `Map[Cell,Value]` based on the application of `this.inputs`
// to `input`, matching Cell in `this.inputs` to values in `input`.
apply(input, mapped = new Mapped()) {
switch (this.type) {
case 0:
return null;
case 1:
mapped.set(this.inputs, input);
break;
case 2:
for (let i in input) {
mapped.set(this.inputs[i], input[i]);
}
break;
case 3:
for (let k in input) {
mapped.set(this.inputs[k], input[k]);
}
break;
default:
throw new Error(`Mapping.apply: Unsupported type: ${this.type}`);
}
return mapped;
}

// Takes the given mapped (`Map[Cell]`) and converts the `.inputs` by expanding
// the Cell references with the mapped cells.
unapply(mapped) {
if (!(mapped instanceof Map)) {
onError(
`Mapping.unapply: Unsupported mapping '${typeof mapped}, expected Map: ${mapped}`,
{ mapped }
);
}
switch (this.type) {
case 0:
return null;
case 1:
return mapped.get(this.inputs);
case 2:
return this.inputs.map((v) => mapped.get(v));
case 3:
const res = {};
for (let k in this.inputs) {
res[k] = mapped.get(this.inputs[k]);
}
return res;
default:
onError(`Mapping.unapply: Unsupported mapping type '${this.type}`, {
type: this.type,
mapped
});
}
}
}
Insert cell
// NOTE: Not sure that we need that
class Mapped extends Map {}
Insert cell
{
const m = new Mapping(new Cell("name"));
const v = "John";
return { input: v, applied: m.apply(v), unapplied: m.unapply(m.apply(v)) };
}
Insert cell
{
const m = new Mapping([new Cell("name"), new Cell("email")]);
const v = ["John", "john@email.com"];
return { input: v, applied: m.apply(v), unapplied: m.unapply(m.apply(v)) };
}
Insert cell
{
const m = new Mapping({ name: new Cell("name"), email: new Cell("email") });
const v = {
name: "John",
email: "john@email.com"
};
return { input: v, applied: m.apply(v), unapplied: m.unapply(m.apply(v)) };
}
Insert cell
{
const name = new Cell("name");
const email = new Cell("email");
const m = new Mapping({ name, email });
return m.expand([name, ":", email], {
name: "John",
email: "john@email.com"
});
}
Insert cell
Insert cell
Insert cell
class Effector {
// Mounting and Unmounting are important functions of effectors and their state. We define these
// as static, as they don't rely on internal state.
static Mount(value, node) {
if (value instanceof Array) {
for (let child of value) {
Effector.Mount(child, node);
}
} else if (value instanceof EffectorState) {
return value.mount(node);
} else if (value instanceof Node) {
node?.parentElement.insertBefore(value, node);
}
}

static Unmount(value) {
if (value instanceof Array) {
for (let child of value) {
Effector.Unmount(child);
}
} else if (value instanceof EffectorState) {
return value.unmount();
} else if (value instanceof Node) {
value?.parentElement?.removeChild(value);
}
}

constructor(inputs, useContext = false) {
this.mapping = new Mapping(inputs);
// An effector that declares that it uses the context will always be triggered for each update,
// as its inputs are not all explicit. For instance the CompositionEffector can't know before
// runtime which parts of the context are used by the composed effector.
this.useContext = useContext;
}

apply(
value,
state = undefined,
scope = undefined,
context = undefined,
path = [],
onSignal = undefined
) {
context = context ? context : value;
const mapped = value instanceof Map ? value : this.mapping.apply(value);
if (state === undefined) {
return this.onCreate(state, scope, mapped, context, path, onSignal);
} else if (value === undefined || value === null || mapped.size === 0) {
return this.onRemove(state, scope, context, path, onSignal);
} else {
return this.onUpdate(state, scope, mapped, context, path, onSignal);
}
}

onUpdate(state, scope, value, context, path, onSignal) {
// NOTE: It's super important to return the state here, otherwise it may
// create all the time without removing.
return state;
}

onCreate(state, scope, value, context, path, onSignal) {
return this.onUpdate(state, scope, value, context, path, onSignal);
}

onRemove(state, scope, context, path, onSignal) {
return this.onUpdate(state, scope, undefined, context, path, onSignal);
}

onError(name, context) {
return onError(name, context);
}

// Internal
}
Insert cell
Insert cell
class EffectorState {
constructor(value) {
this.value = value;
}

// Effector state can be moutned and unmounted
mount(node) {}
unmount() {}
}
Insert cell
Insert cell
Insert cell
class TemplateEffector extends Effector {
// A special value to denote the context
static ContextCell = new Cell("__context__");

// NOTE: Not sure if it still makes sense to pass "inputs" here as template has a `getEffectors()` method.
constructor(inputs, template) {
// Template Effector uses the context
super(inputs, true);
this.node = template instanceof Node ? template : template.toDOM();
this.slots = template instanceof Node ? [] : template.getSlots();
this.slotsByCell = this.slots.reduce((r, { effect }, i) => {
if (effect.useContext) {
r.has(TemplateEffector.ContextCell)
? r.get(TemplateEffector.ContextCell).push(i)
: r.set(TemplateEffector.ContextCell, [i]);
} else if (effect.mapping.cells) {
for (let cell of effect.mapping.cells) {
if (!r.has(cell)) {
r.set(cell, [i]);
} else {
r.get(cell).push(i);
}
}
}
return r;
}, new Map());
}

onUpdate(state, scope, value, context, path, onSignal) {
// We clone the template node if needed
const updated = state
? state
: new TemplateEffectorState(this.node, value, this.slots);
if (scope && (!state || updated.node.nextSibling != scope)) {
scope.parentElement.insertBefore(updated.node, scope);
}

// And if we have children, we trigger them, but only once per change.
if (this.slotsByCell) {
// The first loop is to get the list of slots that are affected
// by the change
const toUpdate = (
this.slotsByCell.get(TemplateEffector.ContextCell) || []
).reduce((r, i) => {
r.set(i, true);
return r;
}, new Map());
// FIXME: The problem here is that this means that the parent needs to know
// which slots the child uses.
if (value) {
for (let cell of value.keys()) {
const indexes = this.slotsByCell.get(cell);
if (indexes) {
for (let i of indexes) {
toUpdate.set(i, true);
}
}
}
}
// And now for each slot, we trigger an update.
for (let i of toUpdate.keys()) {
const slot = this.slots[i];
updated.slotStates[i] = slot.effect.apply(
value,
updated.slotStates[i],
updated.slotScopes[i],
context,
path,
onSignal
);
}
}
return updated;
}

onRemove(state, scope, context, path, onSignal) {
state?.node?.parentElement?.removeChild(state.node);
// TODO: Should we do something about an unmount?
return state;
}
}
Insert cell
class TemplateEffectorState extends EffectorState {
constructor(node, value, slots) {
super(value);
// Clones the node
this.node = node.cloneNode(true);
// Resolves the slot scopes, this becomes an array of node references.
this.slotScopes = slots.map(({ path }) =>
path.reduce(
(r, v) =>
// NOTE: If 'v' is a string, then it's an attribute, but we don't always
// have an attribute node, for instance the `style` node has none.
typeof v === "string"
? v === "style"
? r
: r.getAttributeNode(v)
: r.childNodes[v],
this.node
)
);
// The slot states will hold an array of the values for each slot.
this.slotStates = slots.map((_) => undefined);
}

mount(node) {
return Effector.Mount(this.node, node);
}
unmount() {
return Effector.Unmount(this.node);
}
}
Insert cell
{
const name = new Cell("name");
const email = new Cell("email");
const tmpl = new TemplateEffector(
{ name, email },
H.table(
H.tbody(
H.tr(H.th("User"), H.td(name)),
H.tr(H.th("Email"), H.td(H.a({ href: email }, email)))
)
)
);
return tmpl.apply({ name: "John Smith", email: "john.smith@email.com" }).node;
}
Insert cell
Insert cell
Insert cell
class ContentEffector extends Effector {
constructor(inputs, transform = undefined) {
super(inputs);
if (transform && !(transform instanceof Function)) {
onError(
`ContentEffector: transform should be function, got ${typeof transform}: ${transform}`,
{ transform }
);
}
this.transform = transform;
}

onUpdate(state, scope, value, context, path, onSignal) {
const unapplied = this.mapping.unapply(value);
const current = this.transform ? this.transform(unapplied) : unapplied;
const updated = state ? state : new ContentEffectorState();
const previous = updated.value;
const previousStates = updated.states;

// We only do something if the value has changed or if it's an array.
// If it's an array, we have to take into consideration that we may have not
// had an array before.
const previousStatesIsArray = previousStates instanceof Array;
if (current instanceof Array) {
const updatedStates = current.map((v, i) =>
this.renderNode(
previousStatesIsArray ? previousStates[i] : null,
scope,
v,
previousStatesIsArray ? previous[i] : null,
context,
path ? [...path, i] : [i],
onSignal
)
);
// TODO: we should have a Remount that only mounts the difference
Effector.Unmount(previousStates);
updated.states = previousStates;
} else if (current !== previous) {
updated.states = this.renderNode(
previousStates,
scope,
current,
previous,
context,
path,
onSignal
);
updated.states !== previousStates && Effector.Unmount(previousStates);
}

// TODO: We should implement a Effector.Remount(current, previous, node)
Effector.Mount(updated.states, scope);
return updated;
}

// This returns a node a or ana effector
renderNode(
state,
scope,
value,
previous = undefined,
context = undefined,
path,
onSignal
) {
const t = typeof value;
if (value === null || value == undefined) {
return null;
} else if (t === "number" || t === "string") {
// The value can be rendered as a string
if (value !== previous || !scope) {
const text = `${value}`;
if (scope && scope.nodeType != Node.TEXT_NODE) {
scope = document.createTextNode(text);
} else if (!scope) {
scope = document.createTextNode(text);
} else {
scope.data = text;
}
}
return scope;
} else if (value instanceof Node) {
return value;
} else if (value instanceof RenderingContext) {
// We can render a rendering context -- although in this case this means that the
// component should likely not be shared.
return this.renderNode(
state,
previous?.node,
value.node,
previous,
context,
path,
onSignal
);
} else if (t === "object" && value.node instanceof Element) {
// This would be a TemplateElement
return this.renderNode(
state,
previous?.node,
value.node,
previous,
context,
path,
onSignal
);
} else if (value instanceof Effector) {
// FIXME: We should investigate the semantics of that, as it's possible
// going to be quite wasteful. In particular if state is null, it's going
// to be create all the time.
return value.apply(
context, // value -- we pass the context, as value is the effector
state, // state
scope, // scope
context, // context
path, // path
onSignal // onSignal
);
} else {
this.onError(
`ContentEffector: Unsupported value type '${typeof value}': ${value}`,
{ value: value }
);
return null;
}
}
}
Insert cell
class ContentEffectorState extends EffectorState {
constructor(value = undefined) {
super(value);
this.states = undefined;
}

mount(node) {
}
unmount() {
}
}
Insert cell
Insert cell
class AttributeEffector extends Effector {
constructor(inputs, name) {
super(inputs);
this.name = name;
}

onUpdate(state, scope, value, context, path, pub) {
const v = this.mapping.unapply(value);
const attr = this.name;
state = state ? state : new AttributeEffectorState();

const isFirst = !state.scope;
if (isFirst) {
// NOTE: The scope may be an AttributeNode (most cases), but the Element itself
// for special attributes (style).
const parentName =
scope && scope.nodeType === Node.ATTRIBUTE_NODE && scope.ownerElement
? scope.ownerElement.nodeName
: null;
state.type =
attr === "style"
? 1
: (attr === "value" &&
(parentName === "INPUT" || parentName === "SELECT")) ||
(attr === "checked" && parentName === "INPUT")
? 2
: 0;
state.scope = scope
? scope.nodeType === Node.ATTRIBUTE_NODE
? scope.ownerElement
: scope
: null;
// We clean up the attribute node
if (
scope &&
scope.nodeType === Node.ATTRIBUTE_NODE &&
scope.ownerElement
) {
scope.ownerElement.removeAttribute(attr);
}
}

if (isFirst || state.value !== v) {
state.value = v;

if (state.scope) {
switch (state.type) {
case 1: // Style
// TODO: Should check that the style is actually an object
state.scope.setAttribute("style", "");
Object.assign(state.scope.style, v || {});
break;
case 2: // Node attribute (value, disabled, etc)
state.scope[attr] = v;
break;
default: // Generic attribute
// TODO: We should support namespaces as well
state.scope.setAttribute(
this.name,
v === undefined || v === null ? "" : `${v}`
);
break;
}
} else {
this.onError(
`AttributeEffector: state.scope is null for attribute ${this.name}`,
{ state, scope }
);
}
}

return state;
}
}
Insert cell
class AttributeEffectorState {
constructor(scope, value, type) {
this.scope = scope;
this.value = value;
this.type = type;
}
}
Insert cell
Insert cell
class InteractionEffector extends Effector {
constructor(inputs, callback) {
super(inputs, true);
this.callback = callback;
}

onCreate(state, scope, value, context, path, onSignal) {
const callback = this.callback
? this.callback
: this.mapping.unapply(value);

state = state
? state
: new InteractionEffectorState(
null,
// The callback can come from a cell
scope.nodeName,
callback,
path,
onSignal
);

// We update the path in case it changes;
state.path = path;

if (state.callback !== callback) {
state.callback = callback;
}

if (!state.scope) {
state.scope = scope.ownerElement;
state.scope.addEventListener(state.event, state.handler);
state.scope.removeAttribute(scope.nodeName);
}
return state;
}

onRemove(state, scope, context, path, onSignal) {
if (state) {
state.scope.removeEventListener(state.event, state?.callback);
// TODO: Should we restore the removed attribute?
}
return null;
}
}
Insert cell
class InteractionEffectorState {
constructor(scope, name, callback, path, pub) {
this.scope = scope;
this.name = name;
this.event = name.substring(2).toLowerCase();
this.path = path;
if (!name) {
onError("InteractionEffectorState: missing name", {
scope,
name,
callback,
path,
pub
});
}
this.callback = callback;
this.handler = (event) => {
const value = this.callback(event, this.path);
pub && pub(value && value.signal ? value.signal : "update", this.path, value);
};
}
}
Insert cell
Insert cell
class CompositionEffector extends Effector {
constructor(inputTemplate, effector = null) {
super(Mapping.Inputs(inputTemplate), true);
// NOTE: This inputTemplate will define how the state is managed in a composed effector:
// - Non cell inputs are considered as defaults
// - Cell inputs are considered the source of truth
// - When updated, cell inputs will be updated as well

// TODO: this.inputTemplate["…"] means it captures context
this.inputTemplate = inputTemplate;
this.defaults;
this.effector = effector;
this.effectorType =
effector instanceof Cell ? 2 : effector instanceof Effector ? 1 : 0;
}

resolveEffector(mapped) {
switch (this.effectorType) {
case 1:
return this.effector;
case 2:
const cell = this.effector;
// ## Dynamic composition
// TODO: Should check if the effector has changed or not
const v = mapped.get(cell);
if (!v) {
onError(
`CompositionEffector: effector cell ${cell.name} does not resolve in value`,
{ mapped, cell }
);
} else if (v instanceof Effector) {
// When we have an effector, we simply forward everything to it. This supports
// dynamic composition. Note how we apply the "input" here as opposed to the value.
// We want the whole context.
return v;
} else if (v) {
onError(
`CompositionEffector: unsupported value type '${typeof v}', expecting Effector: ${v}`,
{ value: v }
);
}
default:
return null;
}
}

onCreate(state, scope, value, context, path, onSignal) {
state = this.onUpdate(
state ? state : new CompositionEffectorState(this),
scope,
value,
context,
path,
onSignal
);
// We dispatch mount/unmount events
// TODO: I am not 100% sure about where we should put mount/umount.
const node = state?.effectorState?.node;
node &&
node.dispatchEvent(
new CustomEvent("mount", { detail: { node, path, context } })
);
return state;
}

onRemove(state, scope, context, path, onSignal) {
const node = state?.effectorState?.node;
node &&
node.dispatchEvent(
new CustomEvent("unmount", {
detail: { node, path, context }
})
);
// We clear the state
return this.onUpdate(state, scope, null, context, path, onSignal);
}

onUpdate(state, scope, value, context, path, onSignal) {
const composed = state.apply(value, context, path, onSignal);
const effector = this.resolveEffector(value);
state.substate = effector
? effector.apply(
// This will transform the template based on the input template
state.composed,
state.substate,
scope,
context,
path,
state.onSignal
)
: null;
return state;
}
}
Insert cell
class CompositionEffectorState {
constructor(compositionEffector) {
this.compositionEffector = compositionEffector;
this.composed = {};
this.substate = undefined;
this.path = undefined;
this.context = undefined;
this.originalOnSignal = null;
this.onSignal = this.onComposedSignal.bind(this);
}

onComposedSignal(type, path, value) {
const patch = Patch.Make(value, path, this.composed);
const abspath = this.path ? [...this.path, ...path] : path;
}

apply(value, context, path, onSignal) {
this.path = path;
this.context = context;
this.originalOnSignal = onSignal;
this.composed = Object.assign(
this.composed,
// NOTE: Should we always blend the context in?
context,
Mapping.Expand(this.compositionEffector.inputTemplate, value)
);

return this.composed;
}
}
Insert cell
Insert cell
class ConditionalEffector extends Effector {
constructor(inputs, branches) {
super(inputs, true);
this.branches = branches;
}
onUpdate(state, scope, value, context, path, onSignal) {
let active = -1;
state = state ? state : new ConditionalEffectorState(this.branches);
// TODO: If there is no change in the inputs, then we should forward to the matchin branch
for (let i in this.branches) {
const { mapping, predicate } = this.branches[i];
if (
predicate === true ||
(predicate && predicate(mapping.unapply(value)))
) {
active = i;
break;
}
}
// If we have one active branch, we forward the context -- we need to do this anyway
// as we don't know which fields the branch will use.
if (active >= 0) {
const effector = this.branches[active].effector;
state.branches[active] = effector.apply(
context,
state.branches[active],
scope,
context,
path,
onSignal
);
}
// We keep track of all the previous branch states, so that a switch can happen very quickly;
const previousActive = state.active;
if (active !== previousActive) {
const previousNode = state.branches[previousActive]?.node;
const currentNode = state.branches[active]?.node;

// We unmount/remount nodes
if (previousNode !== currentNode) {
previousNode?.parentElement?.removeChild(previousNode);
currentNode &&
scope &&
scope.parentElement.insertBefore(currentNode, scope);
}

// We update the state
state.active = active;
state.node = currentNode;
}

return state;
}
}
Insert cell
class ConditionalEffectorBranch {
constructor(inputs, predicate, effector) {
this.mapping = new Mapping(inputs);
if (
!(predicate === true || !predicate || typeof predicate === "function")
) {
onError(
`ConditionalEffectorBranch: predicate should be null,undefined,false,true or a function, got ${typeof predicate}: ${predicate}`,
{ predicate }
);
}
this.predicate = predicate;
if (!(effector instanceof Effector)) {
onError(
`ConditionalEffectorBranch: effector should be Effector subclass, got ${typeof effector} ${
Object.getPrototypeOf(effector).name
}: ${effector}`,
{ effector, type: Object.getPrototypeOf(effector).name }
);
}
this.effector = effector;
}
}
Insert cell
class ConditionalEffectorState {
constructor(branches) {
this.active = -1;
this.branches = branches.map(() => undefined);
this.node = undefined;
}

mount(node) {
return Effector.Mount(this.node, node);
}
unmount() {
return Effector.Unmount(this.node);
}
}
Insert cell
Insert cell
class MappingEffector extends Effector {
constructor(inputs, effector) {
super(inputs, true);
this.effector = effector;
this.key = this.mapping.type === 1 ? this.mapping.cells[0].name : null;
}

onUpdate(state, scope, value, context, path, onSignal) {
const items = this.mapping.unapply(value);
state = state ? state : new MappingEffectorState();
const previous = state ? state.items : null;
// FIXME: This won't work for key reordering, and successive updates will likely
// shuffle the order of the items. Also if the order is moved, we do want to
// preserve the existing state.
// Use cases: interactively re-ordering a list of elements that take a long time to render.
if (previous === null || previous === undefined) {
for (let k in items) {
const v = items[k];
state.items[k] = this.effector.apply(
v,
state.items[k],
scope,
// NOTE: We could also use setPrototype for that, may be performance degrading
v instanceof Object ? { ...context, ...v } : { ...context, _: v },
[...path, k],
onSignal
);
}
} else if (items === null || items === undefined) {
for (let k in previous) {
state.items[k] = this.effector.apply(
null,
state.items[k],
scope,
context,
[...path, this.key, k],
onSignal
);
}
} else {
for (let k in items) {
const v = items[k];
state.items[k] = this.effector.apply(
v,
state.items[k],
scope,
// NOTE: We could also use setPrototype for that, may be performance degrading
v instanceof Object ? { ...context, ...v } : { ...context, _: v },
[...path, this.key, k],
onSignal
);
}
for (let k in previous) {
if (items[k] === undefined) {
state.items[k] = this.effector.apply(
null,
state.items[k],
scope,
context,
[...path, this.key, k],
onSignal
);
}
}
}

return state;
}
}
Insert cell
class MappingEffectorState extends EffectorState {
constructor(value = undefined) {
super(value);
this.items = {};
}

// TODO: Mount/Unmount
}
Insert cell
Insert cell
Insert cell
class DebugEffector extends Effector {
constructor(effector, preHandler, postHandler) {
super(
effector
? effector instanceof Effector
? effector.mapping.inputs
: effector instanceof Cell
? effector
: null
: null,
true
);
this.effector =
effector instanceof Cell ? new ContentEffector(effector) : effector;
this.preHandler = preHandler;
this.postHandler = postHandler;
}
apply(
value,
state = undefined,
scope = undefined,
context = undefined,
path = [],
onSignal = undefined
) {
context = context ? context : value;
const mapped = value instanceof Map ? value : this.mapping.apply(value);
this.preHandler && this.preHandler("pre", this);
const result = this.effector
? this.effector.apply(value, state, scope, context, path, onSignal)
: state;
this.postHandler &&
this.postHandler(
state === undefined
? "onCreate"
: value === undefined || value === null || mapped.size === 0
? "onRemove"
: "onUpdate",
{
mapping: this.mapping,
extracted: this.mapping.unapply(value),
value,
state,
scope,
context,
path,
onSignal,
result
}
);
return result;
}
}
Insert cell
Insert cell
Insert cell
class RenderingContext {
constructor(effectors, parent) {
this.node = parent ? parent : document.createElement("div");
this.effectors = effectors instanceof Array ? effectors : [effectors];
this.states = new Array(this.effectors.length);
this.state = {};
this.nodes = this.effectors.map((_) => {
const node = document.createComment("◎");
this.node.appendChild(node);
return node;
});
}

onSignal(signal, path, value) {
// TODO: We shoulď take care of the state
switch (signal) {
case "update":
if (value !== undefined) {
const patch = Patch.Make(value, path.this.state);
if (patch instanceof Patch) {
patch.apply(this.state);
} else {
Patch.Apply(this.state, patch, path);
}
this.render();
}
break;
}
}

apply(value) {
this.state = Object.assign(this.state, value);
return this.render(this.state);
return this;
}

render(state = this.state) {
this.effectors.forEach((v, i) => {
this.states[i] = v
? v.apply(
/*value=*/ state,
/*state=*/ this.states[i],
/*scope=*/ this.nodes[i],
/*context=*/ undefined,
/*path=*/ [],
/*onSignal=*/ this.onSignal.bind(this)
)
: undefined;
});
return this;
}
}
Insert cell
class Patch {
static RE_NUMBER = new RegExp("^\\d+$");

static Make(value, path, state) {
return value instanceof Function
? value(
Patch.Resolve(state, path).scope,
(v, p = path) => new Patch(v, p)
)
: value === null
? value
: Cell.Expand(value, Patch.Resolve(state, path).scope);
}

static Resolve(data, path) {
let parent = undefined;
let key = undefined;
let scope = data;
if (path) {
for (let k of path) {
if (scope === undefined) {
break;
}
parent = scope;
key = k;
scope = parent[k];
}
}
return { parent, key, scope };
}

static Apply(data, value, path) {
const { parent, key, scope } = Patch.Resolve(data, path);
const isNumber = Patch.RE_NUMBER.test(key);
if (value === null) {
// This is a removal
if (parent) {
if (parent instanceof Array) {
parent.splice(key, 1);
} else {
delete parent[key];
}
}
} else {
scope;
if (!scope) {
// TODO
} else {
if (scope instanceof Array && isNumber) {
while (scope.length < key) {
scope.push(undefined);
}
scope[key] = value;
} else {
Object.assign(scope, value);
}
}
}
}

constructor(value, path) {
this.value = value;
this.path = path;
}

apply(data) {
return Patch.Apply(data, this.value, this.path);
}
}
Insert cell
Insert cell
Insert cell
Insert cell
{
const { title } = cells();

const TitleEditor = new TemplateEffector(
{ title },
H.div(
H.input({
type: "text",
value: title,
placeholder: "Edit title",
onInput: (event) => {
return { title: event.target.value };
}
}),
H.pre(title)
)
);

const state = {};
const onSignal = (action, path, value) => {
render(value);
};
const render = (update = {}) => {
state.value = TitleEditor.apply(
update,
state.value,
undefined,
undefined,
[],
onSignal
);
return state.value.node;
};
return render();
}
Insert cell
Insert cell
{
const { isEdited, isChecked, label, editedLabel } = cells();
const UneditedTodoItem = new TemplateEffector(
// FIXME: I don't event think we need the explicit cells here
{ label, isChecked },
H.li(
H.input({
type: "checkbox",
checked: isChecked,
onInput:
() =>
({ isChecked }) => ({
isChecked: !isChecked
})
}),

H.span(
{
onClick: () => ({
isEdited: true,
editedLabel: label
})
},
label
)
)
);

const EditedTodoItem = new TemplateEffector(
{ isEdited, isChecked, editedLabel },
H.li(
H.input({
type: "checkbox",
checked: isChecked,
placeholder: "Type item label",
onClick:
() =>
({ isChecked }) => ({
isChecked: !isChecked
})
}),
H.input({
type: "text",
value: editedLabel,
onInput: (_) => ({ editedLabel: _.target.value })
}),
H.button(
{
onClick: () => ({ isEdited: false, label: editedLabel })
},
"Save"
),
H.button(
{
onClick: () => ({ isEdited: false })
},
"Cancel"
)
)
);

// We use the fully expanded API here
const TodoItem = new ConditionalEffector({ isEdited }, [
new ConditionalEffectorBranch(
isEdited,
(_) => (_ ? true : false),
EditedTodoItem
),
new ConditionalEffectorBranch(null, true, UneditedTodoItem)
]);

return new RenderingContext(TodoItem).apply({
isEdited: false,
label: "Do the dishes"
}).node;
}
Insert cell
Insert cell
{
const { title, items, label } = cells();
const TodoItem = new TemplateEffector(
{ label },
H.li(H.input({ type: "checkbox" }), H.span(label))
);
const TodoList = new TemplateEffector(
{ title, items },
H.div(H.h3(title), H.ul(new MappingEffector(items, TodoItem)))
);
return TodoList.apply({
title: "John's Todo List",
items: [
{ label: "Do the dishes" },
{ label: "Clean the kitchen" },
{ label: "Make the laundry" }
]
}).node;
}
Insert cell
Insert cell
{
const { name, email, title, contents } = cells();

const User = new TemplateEffector(
{ name, email },
H.div(
"Nested template, name=",

H.span(name),
", email=",
H.a({ href: email }, email)
)
);

const UserCard = new TemplateEffector(
// NOTE: If we forget to list contents (for instance), the mapping won't work
{ title, contents },
H.div(
{
style: {
border: "1px solid #E0E0E0",
borderRadius: "4px",
padding: "12px",
margin: "12px"
}
},
H.h3(title),
H.div(
// We could have render(contents, {name:...., email:...}
new CompositionEffector(null, User)
)
)
);

return UserCard.apply({
title: "User: John",
name: "John Smith",
email: "john.smith@email.com",
contents: User
}).node;
}
Insert cell
Insert cell
Insert cell
{
const { name, email, title, view } = cells();

const Card = new TemplateEffector(
// NOTE: If we forget to list contents (for instance), the mapping won't work
{ title, view },
H.div(
{
style: {
border: "1px solid #E0E0E0",
borderRadius: "4px",
padding: "12px",
margin: "12px"
}
},
H.h3(title),
H.div(
// FIXME: Not sure this works
// We could have render(contents, {name:...., email:...}
new CompositionEffector(null, view)
)
)
);

const User = new TemplateEffector(
{ name, email },
H.div(
"Nested template, name=",

H.span(name),
", email=",
H.a({ href: email }, email)
)
);
return Card.apply({
title: "User: Johnn",
name: "John Smith",
email: "john.smith@email.com",
view: User
}).node;
}
Insert cell
Insert cell
class Selector {
constructor(inputs) {
this.inputs = inputs;
this.templates = [];
}
compile() {
onError("Selector.compile(): Selection compilation not implemented", {
selector: this
});
}
}
Insert cell
Insert cell
RE_ATTRIBUTE_HANDLER = new RegExp("^on([A-Z][a-z]+)+$")
Insert cell
Insert cell
class TemplateElement {
static XMLNS = {
svg: "http://www.w3.org/2000/svg",
xlink: "https://www.w3.org/1999/xlink"
};
static HTML = [
"a",
"abbr",
"acronym",
"address",
"applet",
"area",
"article",
"aside",
"audio",
"b",
"base",
"basefont",
"bdo",
"big",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"center",
"cite",
"code",
"col",
"colgroup",
"datalist",
"dd",
"del",
"dfn",
"div",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"font",
"footer",
"form",
"frame",
"frameset",
"head",
"header",
"h1 to h6",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"label",
"legend",
"li",
"link",
"main",
"map",
"mark",
"meta",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"p",
"param",
"pre",
"progress",
"q",
"s",
"samp",
"script",
"section",
"select",
"small",
"source",
"span",
"strike",
"strong",
"style",
"sub",
"sup",
"table",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"u",
"ul",
"var",
"video",
"wbr"
].reduce((r, v) => {
r[v] = true;
return r;
}, {});
static SVG = [
"a",
"altGlyph",
"altGlyphDef",
"altGlyphItem",
"animate",
"animateColor",
"animateMotion",
"animateTransform",
"circle",
"clipPath",
"color-profile",
"cursor",
"defs",
"desc",
"ellipse",
"feBlend",
"feColorMatrix",
"feComponentTransfer",
"feComposite",
"feConvolveMatrix",
"feDiffuseLighting",
"feDisplacementMap",
"feDistantLight",
"feFlood",
"feFuncA",
"feFuncB",
"feFuncG",
"feFuncR",
"feGaussianBlur",
"feImage",
"feMerge",
"feMergeNode",
"feMorphology",
"feOffset",
"fePointLight",
"feSpecularLighting",
"feSpotLight",
"feTile",
"feTurbulence",
"filter",
"font",
"font-face",
"font-face-format",
"font-face-name",
"font-face-src",
"font-face-uri",
"foreignObject",
"g",
"glyph",
"glyphRef",
"hkern",
"image",
"line",
"linearGradient",
"marker",
"mask",
"metadata",
"missing-glyph",
"mpath",
"path",
"pattern",
"polygon",
"polyline",
"radialGradient",
"rect",
"script",
"set",
"stop",
"style",
"svg",
"switch",
"symbol",
"text",
"textPath",
"title",
"tref",
"tspan",
"use",
"view",
"vkern"
].reduce((r, v) => {
if (TemplateElement.HTML[v] === undefined) {
r[v] = "http://www.w3.org/2000/svg";
}
return r;
}, {});

constructor(name, attributes = {}, children = []) {
this.name = name;
this.namespace = attributes?.xmlns || TemplateElement.SVG[name];
this.attributes = {};
this.children = [];
// We need to go through .set() as the contents need to be normalised.
this.set(attributes, ...children);
this._node = undefined;
}

get node() {
this._node = this._node ? this._node : this.toDOM();
return this._node;
}

set(attributes, ...rest) {
if (attributes instanceof Array) {
return this.set({}, attributes.concat(rest));
} else if (
attributes instanceof Node ||
attributes instanceof TemplateElement ||
attributes instanceof Cell ||
attributes instanceof Effector ||
attributes instanceof Selector
) {
return this.set({}, [attributes, ...rest]);
} else if (!(attributes instanceof Object)) {
return this.set({}, [attributes, ...rest]);
} else {
// We normalize child slots to be effects
this.attributes = Object.entries(attributes || {}).reduce((r, [k, v]) => {
r[k] =
v instanceof Cell
? k.match(RE_ATTRIBUTE_HANDLER)
? // The attribute is a handler, and the handler callback come from a cell
new InteractionEffector(v, null)
: new AttributeEffector(v, k)
: v instanceof Function
? new InteractionEffector(null, v)
: v;
return r;
}, this.attributes);
this.children = rest
.flat()
.map((v, k) =>
v instanceof Cell
? new ContentEffector(v)
: v instanceof Selector
? v.compile()
: v
);
return this;
}
}

walk(functor, path = []) {
functor(this, path);
for (let k in this.attributes) {
const v = this.attributes[k];
if (v instanceof Effector) {
functor(path.concat([k]));
}
}
this.children.forEach((v, k) => {
if (v instanceof TemplateElement) {
v.walk(functor, path.concat([k]));
} else if (v instanceof Effector) {
functor(v, path.concat([k]));
// TODO: We should probably recurse with the effectors's sub effectors.
/*v.templates.forEach((t, i) => {
t.walk(functor, path.concat([k, i]));
});*/
} else {
functor(v, path.concat([k]));
}
});
}

getSlots(element = this) {
// This extracts the slots defined in the template
const slots = [];
element.walk((node, path) => {
if (
node === null ||
node === undefined ||
typeof node === "string" ||
typeof node === "number"
) {
// Nothing, it's a straight up node.
} else if (node instanceof Effector) {
slots.push({ effect: node, path, index: slots.length });
} else {
Object.entries(node.attributes || {}).forEach(([k, v]) => {
if (v instanceof Effector) {
slots.push({
effect: v,
path: path.concat([k]),
index: slots.length
});
}
});
}
});
return slots;
}

getEffectors(element = this) {
return this.getSlots(element).reduce((r, _) => {
const m = _.effect.mapping.inputs;
if (!m) {
// Nothing
} else if (m instanceof Array) {
r = m.reduce((r, v) => {
r[v.name] = v;
return r;
}, r);
} else if (m instanceof Cell) {
r[m.name] = m;
} else {
r = Object.entries(m).reduce((r, [k, v]) => {
r[k] = v;
return r;
}, r);
}
return r;
}, {});
}

toDOM(element = this) {
return TemplateElement.ToDOM(element);
}

static ToDOM(element) {
let node = null;
if (typeof element === "string" || typeof element === "number") {
node = document.createTextNode(`${element}`);
} else if (element === null || element === undefined) {
node = document.createComment("");
} else if (element instanceof Effector) {
node = document.createComment("◌");
} else if (element instanceof TemplateElement) {
node = element.namespace
? document.createElementNS(
TemplateElement.XMLNS[element.namespace] || element.namespace,
element.name
)
: document.createElement(element.name);
Object.entries(element.attributes || {}).forEach(([k, v]) => {
switch (k) {
case "style":
if (!(v instanceof Effector)) {
Object.assign(node.style, v);
}
break;
// FIXME: We should probably have jsut one
case "class":
case "className":
case "_":
if (!(v instanceof Effector)) {
node.className = `${v}`;
}
break;
default:
const qualname = k.split(":");
qualname.length === 1
? node.setAttribute(k, v instanceof Effector ? "" : `${v}`)
: node.setAttributeNS(
TemplateElement.NS[qualname[0]] || qualname[0],
qualname[1],
v instanceof Effector ? "" : `${v}`
);
}
});
} else if (element instanceof RenderingContext) {
return element.node;
} else if (element instanceof Node) {
return element;
} else {
onError(
`TemplateElement: unsupported child of type '${typeof element}': ${element}`,
{ element }
);
}
return (element.children || []).reduce((r, v) => {
r.appendChild(TemplateElement.ToDOM(v));
return r;
}, node);
}
}
Insert cell
class StyledElement extends TemplateElement {
constructor(name, style) {
super(name);
this.attributes["class"] = `${
this.attributes["class"] ? this.attributes["class"] + " " : ""
}${name}`;
this.attributes["style"] = style;
}
}
Insert cell
Insert cell
Insert cell
class EffectSelector extends Selector {
constructor(inputs, ...rest) {
super();
this.inputs = inputs;
}

// Map effector
map(creator) {
let effector = null;
if (creator instanceof TemplateElement) {
effector = new TemplateEffector(creator.getSlots(), creator);
} else if (creator instanceof TemplateEffector) {
effector = creator;
} else if (creator instanceof Function) {
const inputs = {};

const template = creator(cells(inputs));
effector =
template instanceof Effector
? template
: new TemplateEffector(inputs, template);
} else {
onError(
`EffectSelector.map: expected function, TemplateElement or TemplateEffector, got '${typeof creator}'`,
{ creator }
);
}
return new MappingEffector(this.inputs, effector);
}

// Branch selector
then(effector) {
return this.when(undefined, effector);
}
when(predicate, effector) {
return new BranchSelector().when(this.inputs, predicate, effector);
}
}
Insert cell
class BranchSelector extends Selector {
constructor(inputs = null) {
super();
this.inputs = inputs;
this.branches = [];
this.fallback = undefined;
}
then(effector) {
return this.when(undefined, undefined, effector);
}
when(inputs, predicate, effector) {
this.branches.push(
new ConditionalEffectorBranch(
inputs === undefined ? this.inputs : inputs,
predicate === undefined ? isTrueish : predicate,
asEffector(effector)
)
);
return this;
}
elif(predicate, effector) {
this.branches.push(
new ConditionalEffectorBranch(
this.branches.length
? this.branches[this.branches.length - 1].mapping.inputs
: null,
predicate,
asEffector(effector)
)
);
return this;
}
else(effector) {
this.fallback = new ConditionalEffectorBranch(
null,
true,
asEffector(effector)
);
return this;
}
compile() {
return new ConditionalEffector(
this.branches.reduce((r, v) => {
const inputs = v && v.mapping && v.mapping.inputs;
if (inputs) {
if (inputs instanceof Cell) {
if (r.indexOf(inputs)) {
r.push(inputs);
}
} else {
for (let c of inputs) {
if (r.indexOf(c) === -1) {
r.push(c);
}
}
}
return r;
}
}, []),
this.fallback ? [...this.branches, this.fallback] : [...this.branches]
);
}
}
Insert cell
Insert cell
Insert cell
component = (definition) => {
const template = definition($.state());
const effector = new TemplateEffector(template.getEffectors(), template);
return Object.assign(
(...args) => {
// If the first args is the proxy, it means we've invoked the component
// like so: $(inputs).map((_) => MyComponent(_))
if (args.length === 0 || (args[0] && args[0].__isProxy__)) {
// This means we take the whole context.
return new CompositionEffector({}, effector);
} else {
// This extracts the inputs. If one of the inputs is a proxy (meaning
// it's mean to represent all state cells), then we'll add "…" to the
// inputs. This is then handled by the mapping to mean "the rest".
const inputs = args.reduce((r, v) => {
if (
typeof v === "object" &&
// Oddly enough, the prototype may not be Object!
Object.getPrototypeOf(v)?.constructor?.name === "Object"
) {
for (let k in v) {
const w = v[k];
if (w && w.__isProxy__) {
r["…"] = true;
} else {
r[k] = w;
}
}
return r;
} else if (r.children) {
r.children.push(v);
} else {
r.children = [v];
}
return r;
}, {});
return new CompositionEffector(inputs, effector);
}
},
{ effector, template }
);
}
Insert cell
Insert cell
Insert cell
cells = (state = {}) => proxy((name) => new Cell(name), state)
Insert cell
{
const { user, name, email, ...rest } = cells();
return { user, name, email, rest };
}
Insert cell
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
H = proxy(
(name) =>
(...content) =>
content.length
? new TemplateElement(name).set(...content)
: new TemplateElement(name)
)
Insert cell
Insert cell
Insert cell
$ = Object.assign(
(inputs, attrs = undefined, ...rest) => {
if (inputs instanceof TemplateElement) {
const t = new TemplateEffector(inputs.getEffectors(), inputs);
return attrs === undefined ? t : new RenderingContext(t).apply(attrs);
} else if (inputs.template instanceof TemplateElement) {
// This is a new template effector
const props = asProps(attrs, ...rest);
return new CompositionEffector(
props,
new TemplateEffector(inputs.template.getEffectors(), inputs.template)
);
} else if (attrs instanceof Function) {
return new ContentEffector(inputs, attrs);
} else {
return new EffectSelector(inputs, attrs, ...rest);
}
},
{
state: cells,
patch: (value, path) => new Patch(value, path),
map: (inputs, effector = new ContentEffector()) =>
new MappingEffector(inputs, effector)
}
)
Insert cell
Insert cell
render = (component, state, parent = null) => {
return new RenderingContext(
component instanceof Function && component.effector
? component.effector
: component instanceof TemplateElement
? new TemplateEffector(component.getEffectors(), component)
: component
).apply(state).node;
}
Insert cell
Insert cell
isSpecial = value =>
value instanceof Node ||
value instanceof Effector ||
value instanceof TemplateElement ||
value instanceof RenderingContext ||
value instanceof Cell
? true
: false;
Insert cell
asProps = (attrs, ...rest) => {
const special = isSpecial(attrs);
const isArray = !special && attrs instanceof Array ? true : false;
const isObject =
!special && !isArray && typeof attrs === "object" ? true : false;
const hasRest = rest.length > 0;
return isObject
? Object.assign({}, attrs, hasRest ? { children: rest } : undefined)
: { children: (isArray ? attrs : [attrs]).concat(rest) };
}
Insert cell
// Ensures that the given value is an effector.
asEffector = (value, ...rest) => {
if (value instanceof Effector) {
return value;
} else if (value instanceof TemplateElement) {
const t = new TemplateEffector(value.getEffectors(), value);
return rest[0] === undefined ? t : new RenderingContext(t).apply(...rest);
} else if (value.template instanceof TemplateElement) {
const props = asProps(...rest);
return new CompositionEffector(
props,
new TemplateEffector(value.template.getEffectors(), value.template)
);
} else {
return new TemplateEffector(null, TemplateElement.ToDOM(value));
}
}
Insert cell
isTrueish = (value) =>
value
? value instanceof Array
? value.length > 0
? true
: false
: true
: false
Insert cell
onError = (name, context) => {
console.error(name, context);
throw new Error(name);
}
Insert cell
proxy = (
creator = (_) => _,
state = {},
// These symbols need to be skipped, at least in the Observable console
skip = ["then", "next", "coords", "latitude"]
) =>
new Proxy(state, {
get: (state, prop, receiver) => {
if (
skip.indexOf(prop) >= 0 ||
prop == Symbol.toStringTag ||
prop == Symbol.iterator ||
(typeof prop === "string" && prop.startsWith("@"))
) {
return state[prop];
} else if (prop === "__self__") {
return state;
} else if (prop === "__isProxy__") {
return true;
}

if (!state[prop]) {
state[prop] = creator(prop);
}
return state[prop];
}
})
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