Public
Edited
Dec 27, 2022
Importers
Insert cell
Insert cell
Insert cell
Insert cell
class Effector {
// Having `TYPE` makes is easier for debugging
static TYPE = "Effector";

constructor(inputs) {
this.inputs =
inputs instanceof DataTemplate ? inputs : new DataTemplate(inputs);
}

get type() {
return Object.getPrototypeOf(this).TYPE;
}

apply(
value,
state = undefined,
scope = undefined,
context = new RenderingContext(),
path = [],
onSignal = undefined
) {
if (state === undefined) {
return this.onCreate(value, state, scope, context, path, onSignal);
} else if (value === undefined || value === null) {
return this.onRemove(state, scope, context, path, onSignal);
} else {
return this.onUpdate(value, state, scope, context, path, onSignal);
}
}

onUpdate(value, state, scope, 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(value, state, scope, context, path, onSignal) {
return this.onUpdate(value, state, scope, context, path, onSignal);
}

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

onError(name, context) {
return onError(name, context);
}
}
Insert cell
class EffectorState {
constructor() {}
}
Insert cell
Insert cell
class TemplateEffector extends Effector {
constructor(inputs, template) {
super(new DataTemplate(inputs));
this.node = template instanceof Node ? template : template.toDOM();
this.effects = template instanceof Node ? [] : template.getEffects();
}

onUpdate(value, state, scope, context, path, onSignal) {
// We clone the template node if needed
const previous = state;
state = state
? state
: new TemplateEffectorState(this.node, value, this.effects);

// NOTE: 💡 This is maybe something we want to move to Effector.apply()?
context = context.update(value, this.inputs);

// NOTE: Here we could 💡 optimise by pre-extracting the values and mapping them
// to slots. Effectors would then not have to do the extraction, but that relies
// on extracts
for (let i = 0; i < this.effects.length; i++) {
const effect = this.effects[i];
state.effectStates[i] = effect.effector.apply(
value,
state.effectStates[i],
state.effectScopes[i],
context,
path,
onSignal
);
}

Rendering.Mount(state.node, scope);
return state;
}

onRemove(state, scope, context, path, onSignal) {
Rendering.Unmount(state.node);
return state;
}
}
Insert cell
class TemplateEffectorState extends EffectorState {
constructor(node, value, effects) {
super(value);
// Clones the node
this.node = node.cloneNode(true);
// Resolves the slot scopes, this becomes an array of node references.
this.effectScopes = effects.map(({ path }) => DataPath.Extract(node, path));
// The slot states will hold an array of the values for each slot.
this.effectStates = effects.map((_) => undefined);
}
}
Insert cell
{
const name = slot("name");
return render(
new TemplateEffector({ name }, H.div("Hello, my name is: ", name)),
{ name: "Danger" }
);
}
Insert cell
Insert cell
class ContentEffector extends Effector {
static TYPE = "ContentEffector";

onUpdate(value, state, scope, context, path, onSignal) {
const previous = state;
state = state ? state : new ContentEffectorState();
const XXXvalue = this.inputs.apply(context.slots);

console.log("RENDERING", {
value,
XXXvalue,
context,
inputs: this.inputs,
AAAA: context.slots.has(this.inputs.holes[0])
});
return state;
}
}
Insert cell
class ContentEffectorState extends EffectorState {
}
Insert cell
Insert cell
class AttributeEffector extends Effector {
static TYPE = "AttributeEffector";
}
Insert cell
class AttributeEffectorState extends EffectorState {}
Insert cell
Insert cell
class InteractionEffector extends Effector {
static TYPE = "InteractionEffector";
}
Insert cell
class InteractionEffectorState extends EffectorState {}
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
class Rendering {
// 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 (!node) {
return Rendering.Unmount(value);
} else 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) {
value?.nextSibling != 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);
}
}

}
Insert cell
class RenderingContext {
constructor(parent = null) {
this.parent = parent;
this.slots = new Map();
this.state = undefined;
}
derive() {
return new RenderingContext(this);
}

// --
// Update the rendering context slots by extracting slot mapping from the give value
// using the given template. This will return a different context object derived
// from the current one in case there's a slot conflict.
update(value, template) {
let context = this;
for (const [slot, extracted] of template.extract(value).entries()) {
if (context === this && context.slots.has(slot)) {
context = context.derive();
}
context.slots.set(slot, extracted);
}
return context;
}

has(key) {
return this.slots.has(key)
? true
: this.parent
? this.parent.has(key)
: false;
}
get(key) {
const res = this.slots.get(key);
return res !== undefined
? res
: this.parent
? this.parent.get(key)
: undefined;
}
}
Insert cell
class Renderer {
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.context = new RenderingContext();
this.state = {};
this.nodes = this.effectors.map((_) => {
const node = document.createComment("◎");
this.node.appendChild(node);
return node;
});
}

onSignal(signal, path, value) {
console.log("TODO: ON SIGNAL");
}

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=*/ this.context,
/*path=*/ [],
/*onSignal=*/ this.onSignal.bind(this)
)
: undefined;
});
return this;
}
}
Insert cell
render = (component, state, parent = null) => {
return new Renderer(
component instanceof Function && component.effector
? component.effector
: component instanceof TemplateElement
? new TemplateEffector(component.getEffectors(), component)
: component instanceof Effector
? component
: onError("render() expected component, template or effector", {
component
})
).apply(state).node;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class TemplateEffect {
constructor(effector, path, index) {
this.effector = effector;
this.path = path;
this.index = index;
}
}
Insert cell
class TemplateElement {
static XMLNS = {
svg: "http://www.w3.org/2000/svg",
xlink: "https://www.w3.org/1999/xlink"
};
static HTML = HTML.reduce((r, v) => {
r[v] = true;
return r;
}, {});
static SVG = SVG.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 Slot ||
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 Slot
? 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 Slot
? 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]));
}
});
}

getEffects(element = this) {
// This extracts the effectors defined in the template and returns a list list of effects.
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(new TemplateEffect(node, path, slots.length));
} else {
Object.entries(node.attributes || {}).forEach(([k, v]) => {
if (v instanceof Effector) {
slots.push(new TemplateEffect(v, [...path, k], slots.length));
}
});
}
});
return slots;
}

getXXXEffectors(element = this) {
throw new Error("This is not implemented");
return this.getSlots(element).reduce((r, _) => {
// FIXME: We're not using mapping inputs
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 Slot) {
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 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
H = proxy(
(name) =>
(...content) =>
content.length
? new TemplateElement(name).set(...content)
: new TemplateElement(name)
)
Insert cell
Insert cell
slots = (state = {}) => proxy((name) => new Slot(name), state)
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
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more