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

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