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

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