Public
Edited
Nov 15
Importers
98 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof composite = view`<div style="display: flex; justify-content:space-between; ">
<div style="display: flex-column;">
<div>${["r1", Inputs.range([0, 10])]}</div>
<div>${["r2", Inputs.range([0, 3])]}</div>
<div>${[
"text",
Inputs.text({
label: "Enter some text"
})
]}</div>
</div>
<img width="150"src="https://media.giphy.com/media/2vobTwCkFg88ZUnilt/giphy-downsized.gif"></img>
</div>
`
Insert cell
Insert cell
htl.html`<button onclick=${() => {
viewof composite.value = {
r1: Math.random() * 10,
r2: Math.random() * 3,
text: `${Math.random()}`
};
viewof composite.dispatchEvent(new Event('input'));
}}> randomize composite`
Insert cell
Insert cell
viewof singleton = view`<div><h4>My control</h4>${['...', Inputs.range()]}`
Insert cell
singleton
Insert cell
viewof singleton
Insert cell
Insert cell
viewof arrayCollection = view`<div>${[
"elements",
Array.from({ length: 5 }, () => Inputs.range())
]}`
Insert cell
viewof arrayCollection.elements
Insert cell
Insert cell
Inputs.button("Add a slider", {
reduce: () => {
viewof arrayCollection.elements = [
...viewof arrayCollection.elements,
Inputs.range() // Add another viewof
];
// dispatch the input event so dataflow gets updated
viewof arrayCollection.dispatchEvent(new Event("input"));
}
})
Insert cell
arrayCollection.elements
Insert cell
Insert cell
Insert cell
viewof dynamicArrayCollection = view`<div>${[
'elements',
[],
val => Inputs.range([0, 1], { value: val }) // rowBuilder
]}`
Insert cell


Insert cell
Inputs.button("Remove a slider", {
reduce: () => {
dynamicArrayCollection.elements.pop();
// dispatch the input event so dataflow gets updated
viewof dynamicArrayCollection.elements.dispatchEvent(new Event("input"));
}
})
Insert cell
Insert cell
viewof objectCollection = view`${[
'...',
{
number: Inputs.range(),
text: Inputs.text()
}
]}`
Insert cell
objectCollection
Insert cell
Insert cell
viewof dynamicObjectCollection = view`<div>${[
'...',
{},
txt => Inputs.text({ value: txt })
]}`
Insert cell
dynamicObjectCollection
Insert cell
viewof dynamicObjectCollection
Insert cell
Inputs.button("Pick one of three keys and randomize their value", {
reduce: () => {
const key = "k" + Math.floor(Math.random() * 3);
viewof dynamicObjectCollection.value = {
...viewof dynamicObjectCollection.value,
[key]: key + " " + Math.random()
};
viewof dynamicObjectCollection.dispatchEvent(new Event('input', {bubbles: true}))
}
})
Insert cell
Inputs.button("Delete a random key", {
reduce: () => {
const copy = { ...viewof dynamicObjectCollection.value };
const key = "k" + Math.floor(Math.random() * 3);
delete copy[key];
viewof dynamicObjectCollection.value = copy;
viewof dynamicObjectCollection.dispatchEvent(
new Event('input', { bubbles: true })
);
}
})
Insert cell
viewof dynamicObjectCollection.value
Insert cell
Insert cell
viewof hiddenView = view`<div><h4>My hidden control</h4>${[
'_hidden',
viewof singleton
]}`
Insert cell
{
viewof hiddenView.hidden.value = 0.60;
viewof hiddenView.hidden.dispatchEvent(new Event("input", { bubble: true }));
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
viewof cautiousNestedDemo = view`
${[
"c1",
cautious(
(apply, reset) => view`<div>
${['foo', Inputs.range([0, 100], { label: 'Foo', step: 1 })]}
${['bar', Inputs.text({ value: 'change me', label: 'Bar' })]}
<hr style="margin:0;padding:10px;max-width:360px">
<button onclick=${apply}>Apply</button>
<button onclick=${reset}>Reset</button>`
)
]}
${[
"c2",
cautious(
(apply, reset) => view`<div>
${['baz', Inputs.range([0, 100], { label: 'Baz', step: 1 })]}
${['bat', Inputs.text({ value: 'change me', label: 'Bat' })]}
<hr style="margin:0;padding:10px;max-width:360px">
<button onclick=${apply}>Apply</button>
<button onclick=${reset}>Reset</button>`
)
]}

`
Insert cell
Insert cell
Insert cell
viewof levels = bindOneWay(
Inputs.radio(["0", "low", "high"], { disabled: true }),
viewof slider,
{
transform: v => (v === 0 ? "0" : v < 5 ? "low" : "high")
}
)
Insert cell
viewof levelsText = bindOneWay(Inputs.text({ disabled: true }), viewof levels, {
transform: l => `The level is ${l}`
})
Insert cell
Insert cell
Insert cell
function variable(value, { name = "variable" } = {}) {
const self = document.createComment(name);
return Object.defineProperties(self, {
value: {
get: () => value,
set: newValue => {
value = newValue;
self.dispatchEvent(new CustomEvent('assign', { detail: newValue }));
},
enumerable: true
},
toString: {
value: () => `${value}`
}
});
}
Insert cell
exmple_variable = variable(5)
Insert cell
(exmple_variable.value = 44)
Insert cell
{
let resolve = null;
exmple_variable.addEventListener('assign', evt => resolve(evt.detail));
while (true) {
yield new Promise(r => (resolve = r));
}
}
Insert cell
Insert cell
Insert cell
Insert cell
function wrap(fn, strings, ...exprs) {
let singleton = undefined;
let start = undefined; // To know where to start dynamic objects
let builder = undefined; // For new keys are added dynamically
const views = {};

const pexpr = exprs.map((exp) => {
// All special functions are [key, ...]
// Otherwise we pass through
if (!Array.isArray(exp) || typeof exp[0] !== "string") {
return exp;
}

const hidden = exp[0].startsWith("_");
const key = hidden ? exp[0].substring(1) : exp[0];
if (key === "value") throw new Error("Cannot use 'value' as a key");
let presentation;

if (exp.length === 2 && exp[1] instanceof EventTarget) {
// SINGLE VIEW PASSED IN
if (key === "...") {
// SINGLETON!
singleton = exp[1];
} else {
// look for [key, HTML] entries
views[key] = exp[1];
}
presentation = exp[1];
} else if (
// ARRAY PASSED IN (NO BUILDER)
exp.length === 2 &&
Array.isArray(exp[1]) &&
exp[1].every((e) => e instanceof EventTarget)
) {
if (key === "...") throw new Error("Spread not supported for arrays ATM");
/*
const start = document.createComment(key);
arrayViews[key] = {
start,
array: exp[1]
};
presentation = [start, ...exp[1]];*/
presentation = arrayView({
name: key,
initial: exp[1]
});
views[key] = presentation;
} else if (
// ARRAY PASSED IN (WITH BUILDER)
exp.length === 3 &&
Array.isArray(exp[1]) &&
exp[1].every((e) => e instanceof EventTarget) &&
typeof exp[2] === "function"
) {
if (key === "...") throw new Error("Spread not supported for arrays ATM");
/*
const start = document.createComment(key);
arrayViews[key] = {
start,
array: exp[1],
builder: exp[2]
};
presentation = [start, ...exp[1]];*/
presentation = arrayView({
name: key,
initial: exp[1],
builder: exp[2]
});
views[key] = presentation;
} else if (
// SPREAD OBJECT (NO BUILDER)
exp.length === 2 &&
key === "..." &&
typeof exp[1] === "object" &&
Object.keys(exp[1]).every((e) => typeof e === "string") &&
Object.values(exp[1]).every((e) => e instanceof EventTarget)
) {
Object.entries(exp[1]).forEach((e) => (views[e[0]] = e[1]));
presentation = Object.values(exp[1]);
} else if (
// SPREAD OBJECT (WITH BUILDER)
exp.length === 3 &&
key === "..." &&
typeof exp[1] === "object" &&
Object.keys(exp[1]).every((e) => typeof e === "string") &&
Object.values(exp[1]).every((e) => e instanceof EventTarget) &&
typeof exp[2] === "function"
) {
Object.entries(exp[1]).forEach((e) => (views[e[0]] = e[1]));
start = document.createComment(key);
builder = exp[2];
presentation = [start, ...Object.values(exp[1])];
} else {
presentation = exp;
}

if (hidden) {
const forwardEvent = (evt) => {
const clone = new evt.constructor(evt.type, evt);
self.dispatchEvent(clone);
};
if (presentation.addEventListener) {
presentation.addEventListener("input", forwardEvent);
} else if (Array.isArray(presentation)) {
presentation.forEach((p) => {
// The first element can be the start event sometimes
if (p.addEventListener) {
p.addEventListener("input", forwardEvent);
}
});
} else {
throw new Error("Not sure how to deal with this hidden element");
}

return undefined; // No DOM representation
} else {
return presentation; // Places in DOM
}
});
const self = fn(strings, ...pexpr);

if (singleton) {
if (Object.keys(views).length !== 0)
throw new Error("Singleton defined but additional properties supplied");

// Users are expected to call dispatchEvent on view, so the inner singleton
// need to know about these events for the view to work
// => events need to be copied over, if originating from here
self.addEventListener("input", (evt) => {
if (evt.target === self) {
const clone = new evt.constructor(evt.type, evt);
singleton.dispatchEvent(clone);
}
});

return Object.defineProperties(self, {
value: {
get: () => singleton.value,
set: (val) => (singleton.value = val),
configurable: true
},
singleton: {
value: singleton,
enumerable: true
}
});
}
// Non-singleton (Object or Array)
return Object.defineProperties(self, {
value: {
get() {
return Object.defineProperties(
{},
Object.keys(views).reduce((acc, key) => {
acc[key] = {
get: () => views[key].value,
set: (v) => (views[key].value = v),
enumerable: true
};
return acc;
}, {})
);
},
set(newValues) {
Object.entries(newValues).forEach(([key, newValue]) => {
if (views[key]) {
views[key].value = newValue; // Update of existing child value
} else if (start && builder) {
// Adding a new key
const parent = start.parentNode;
const newView = builder(newValue);
views[key] = newView;
parent.appendChild(newView);
// Add top level entry too
Object.defineProperty(self, key, {
value: newView,
enumerable: true,
configurable: true
});
}
});

// If we are a dynamic Object, we need to remove keys too
Object.entries(views).forEach(([key, oldValue]) => {
if (!newValues.hasOwnProperty(key)) {
// It needs to go
const oldView = views[key];
delete views[key];
if (oldView.remove) oldView.remove();
delete self[key];
}
});
},
configurable: true
},
...Object.keys(views).reduce(
// Add top level field to access the subviews in the parent viewof
(acc, key) => {
acc[key] = {
get: () => views[key],
set: (newView) => {
const oldView = views[key];
delete views[key];
if (oldView.remove) oldView.remove();

// assigning an arrayView (special cased)
if (oldView.length && newView.length) {
newView = arrayView({
initial: newView,
builder: oldView.builder
});
}

views[key] = newView;
if (newView instanceof Node) self.appendChild(newView);
},
enumerable: true,
configurable: true
};
return acc;
},
{}
)
});
}
Insert cell
Insert cell
function arrayView({
name = "arrayNode" + DOM.uid().id,
value = [],
initial = [],
builder
} = {}) {
if (value.length > 0 && !builder)
throw new Error(
"You cannot initialize an arrayView with data without a builder"
);

const frag = new DocumentFragment();

const subviewToFragmentEventCloner = (e) => {
const new_e = new e.constructor(e.type, e);
frag.dispatchEvent(new_e);
};

const _builder = builder
? (arg) => {
const subview = builder(arg);
subview.addEventListener("input", subviewToFragmentEventCloner);
return subview;
}
: undefined;

const unbuilder = (subview) => {
subview.removeEventListener("input", subviewToFragmentEventCloner);
};

initial.forEach((subview) =>
subview.addEventListener("input", subviewToFragmentEventCloner)
);

const start = document.createComment("START:" + name);
const end = document.createComment("END:" + name);
let subviews = (_builder ? value.map(_builder) : []).concat(initial);
frag.append(...[start, ...subviews, end]);

frag.addEventListener("input", (e) => {
// https://stackoverflow.com/questions/11974262/how-to-clone-or-re-dispatch-dom-events
const new_e = new e.constructor(e.type, e);
start.dispatchEvent(new_e);
});

const getIndexProperty = (index) => ({
get: () => subviews[index],
enumerable: true,
configurable: true
});

const customSplice = (startIndex, deleteCount, ...items) => {
const parent = start.parentNode;
startIndex = Math.floor(startIndex);
const removedData = [];
// sync the splice with the DOM
let node = start;
// Forward to begining of the splice
for (let i = 0; i < startIndex && i < subviews.length; i++)
node = node.nextSibling;
// delete 'deleteCount' times
for (let i = 0; i < deleteCount && i < subviews.length; i++) {
const toDelete = node.nextSibling;
removedData.push(toDelete.value);
unbuilder(toDelete);
toDelete.remove();
}
// add additional items
const itemViews = [];
for (let i = items.length - 1; i >= 0; i--) {
const subview = _builder(items[i]);
Object.defineProperty(frag, i, getIndexProperty(i));
let presentation =
subview instanceof HTMLElement ? subview : htl.html`${subview}`;
itemViews.unshift(subview);
parent.insertBefore(presentation, node.nextSibling);
}

// Apply to cache
subviews.splice(startIndex, deleteCount, ...itemViews);
// Let flow upwards to array too
return removedData;
};
// We intercept operations to the data array and use it to drive DOM operations too.
const dataArrayProxyHandler = {
get: function (target, prop, receiver) {
const args = arguments;

if (prop === "splice") {
return customSplice;
} else if (prop === "push") {
return (...elements) => {
customSplice(subviews.length, 0, ...elements);
return subviews.length;
};
} else if (prop === "pop") {
return () => {
return customSplice(subviews.length - 1, 1)[0];
};
} else if (prop === "shift") {
return () => {
return customSplice(0, 1)[0];
};
} else if (prop === "unshift") {
return (...elements) => {
customSplice(0, 0, ...elements);
return subviews.length;
};
}
return Reflect.get(...args);
},
set(obj, prop, value) {
if (!isNaN(+prop)) {
// we also need to set the view
customSplice(+prop, 1, value);
}
return Reflect.set(...arguments);
}
};

// Add data channel
Object.defineProperties(frag, {
value: {
get: () =>
new Proxy(
subviews.map((sv) => sv.value),
dataArrayProxyHandler
),
set: (newArray) => {
const vArr = _.cloneDeep(newArray);
const parent = start.parentNode;

if (builder) {
// We should be true to the operation and tear of the DOM and then replace it.
subviews.forEach((sv) => (sv.remove ? sv.remove() : undefined));
subviews = vArr.map((data) => {
const subview = _builder(data);
let presentation =
subview instanceof HTMLElement ? subview : htl.html`${subview}`;
parent.insertBefore(presentation, end);
return subview;
});
} else {
// We have to work around the limitations and try to do the operation without
// building, so this only can work if you are setting it to something smaller
vArr.forEach((v, i) => {
if (i < subviews.length) {
subviews[i].value = v; // mutate inplace
} else {
let built = _builder(v); // append additional
subviews[i] = built;
if (!(built instanceof HTMLElement)) built = htl.html`${built}`;
parent.appendChild(built);
}
});

for (var i = subviews.length - 1; i >= vArr.length; i--) {
// delete backwards
const deleted = subviews.pop();
if (deleted.remove) deleted.remove();
}
}
}
}
});

// Add presentation channel
return Object.defineProperties(frag, {
remove: {
value: () => {
const toRemove = [];
for (var node = start; node !== end; node = node.nextSibling) {
toRemove.push(node);
}
toRemove.push(end);
toRemove.forEach((n) => n.remove());
}
},
length: {
get: () => subviews.length,
enumerable: true,
configurable: true
},
[Symbol.iterator]: {
value: () => {
let index = 0;
return {
next() {
if (index < subviews.length) {
let val = subviews[index];
index++;
return { value: val, done: false };
} else return { done: true };
}
};
}
},
...subviews.reduce((acc, sv, index) => {
acc[index] = getIndexProperty(index);
return acc;
}, {})
});
}
Insert cell
Insert cell
Insert cell
Insert cell
viewof arrayViewTests = testing.createSuite({
name: "arrayView Tests",
timeout_ms: 1000
})
Insert cell
arrayViewTests.test("arrayView dispatchEvent bubbles to container", (done) => {
const av = arrayView({ builder: (v) => Inputs.input(v) });
const container = view`<div>${av}`;
container.addEventListener("input", () => done());
av.dispatchEvent(new Event("input", { bubbles: true }));
})
Insert cell
arrayViewTests.test("arrayView subview events bubble to arrayView", (done) => {
const av = arrayView({
value: [1],
builder: (v) => Inputs.input(v)
});
av.addEventListener("input", () => done());
av[0].dispatchEvent(new Event("input", { bubbles: true }));
})
Insert cell
arrayViewTests.test("arrayView initialization (data + builder)", () => {
const av = arrayView({
value: [1],
builder: (number) => Inputs.input("foo")
});
expect(av[0].value).toBe("foo");
expect(av.value[0]).toBe("foo");
})
Insert cell
arrayViewTests.test("arrayView initialization (initial)", () => {
const av = arrayView({ initial: [Inputs.input("foo")] });
expect(av[0].value).toBe("foo");
expect(av.value[0]).toBe("foo");
})
Insert cell
arrayViewTests.test("arrayView write element", () => {
const av = arrayView({
initial: [Inputs.input("foo")],
builder: Inputs.input
});
expect(av[0].value).toBe("foo");
expect(av.value[0]).toBe("foo");

av.value[0] = "bar";

expect(av[0].value).toBe("bar");
expect(av.value[0]).toBe("bar");
})
Insert cell
arrayViewTests.test("arrayView splice (delete)", () => {
const av = arrayView({ initial: [Inputs.input("foo")] });
expect(av[0].value).toBe("foo");
expect(av.value[0]).toBe("foo");
expect(av.length).toBe(1);
expect(av.value.length).toBe(1);

av.value.splice(0, 1);

expect(av[0]).toBe(undefined);
expect(av.value[0]).toBe(undefined);
expect(av.value.length).toBe(0);
expect(av.length).toBe(0);
})
Insert cell
arrayViewTests.test("arrayView splice out of bounds (delete)", () => {
const av = arrayView({ initial: [Inputs.input("foo")] });
expect(av[0].value).toBe("foo");
expect(av.value[0]).toBe("foo");
expect(av.length).toBe(1);
expect(av.value.length).toBe(1);

av.value.splice(1, 1);

expect(av[0].value).toBe("foo");
expect(av.value[0]).toBe("foo");
expect(av.length).toBe(1);
expect(av.value.length).toBe(1);
})
Insert cell
arrayViewTests.test("arrayView splice (add)", () => {
const av = arrayView({ builder: (v) => Inputs.input(v) });
av.value.splice(0, 0, 1);
expect(av[0].value).toBe(1);
expect(av.value[0]).toBe(1);
})
Insert cell
arrayViewTests.test("arrayView unshift", () => {
const av = arrayView({ builder: (v) => Inputs.input(v) });
av.value.unshift(1);
expect(av[0].value).toBe(1);
expect(av.value[0]).toBe(1);
})
Insert cell
arrayViewTests.test("arrayView shift", () => {
const av = arrayView({ value: [1], builder: (v) => Inputs.input(v) });
av.value.shift(1);
expect(av[0]).toBe(undefined);
expect(av.value[0]).toBe(undefined);
})
Insert cell
arrayViewTests.test("arrayView pop", () => {
const av = arrayView({ value: [1], builder: (v) => Inputs.input(v) });
av.value.pop();
expect(av[0]).toBe(undefined);
expect(av.value[0]).toBe(undefined);
})
Insert cell
arrayViewTests.test("arrayView push", () => {
const av = arrayView({ builder: (v) => Inputs.input(v) });
av.value.push(1);
expect(av[0].value).toBe(1);
expect(av.value[0]).toBe(1);
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
suite.test("Singleton spread reads from delagate", async () => {
const v = view`<div>${["...", variable(1)]}`;
expect(v.value).toEqual(1);
})
Insert cell
suite.test("Singleton spread write propagates", async () => {
const delegate = variable();
const v = view`<div>${["...", delegate]}`;
v.value = 4;
expect(delegate.value).toEqual(4);
})
Insert cell
suite.test(
"Singleton events propagate from container to inner singleton",
async (done) => {
const delegate = variable();
delegate.addEventListener("input", (evt) => {
done();
});
const v = view`<div>${["...", delegate]}`;
v.dispatchEvent(new Event("input"));
}
)
Insert cell
suite.test("Hidden write propagates upstream", async () => {
const delegate = variable();
const v = view`<div>${["_hidden", delegate]}`;
v.hidden.value = 4;
expect(delegate.value).toEqual(4);
})
Insert cell
suite.test("Hidden events propogate to self", async (done) => {
const delegate = variable();
const v = view`<div>${["_hidden", delegate]}`;
v.addEventListener("input", (evt) => {
done();
});
delegate.dispatchEvent(new Event("input"));
})
Insert cell
suite.test(
"Hidden object collection member events propogate to self",
async (done) => {
const delegate = variable();
const v = view`<div>${["_...", { a: delegate }]}`;
v.addEventListener("input", (evt) => {
done();
});
delegate.dispatchEvent(new Event("input"));
}
)
Insert cell
suite.test("Nested write on arrayView replaces presentation", async () => {
const v = view`<div>${["array", [html`<input id=nwoa1 value="foo">`]]}`;
expect(v.querySelector("#nwoa1")).not.toBe(null);
expect(v.querySelector("#nwoa2")).toBe(null);
expect(v.array.value).toEqual(["foo"]);

v.array = [html`<input id=nwoa2 value="fum">`];
expect(v.querySelector("#nwoa1")).toBe(null);
expect(v.querySelector("#nwoa2")).not.toBe(null);
expect(v.array.value).toEqual(["fum"]);
})
Insert cell
suite.test(
"Composite write spreads to array subproperty (deletion)",
async () => {
const v = view`<div>${["array", [Inputs.input()]]}`;
v.value = { array: [] };
expect([...v.array]).toEqual([]);
expect(v.value.array).toEqual([]);
}
)
Insert cell
suite.test(
"Composite write spreads to array subproperty (addition) (via destructuring assignment)",
async () => {
const v = view`<div>${["array", [Inputs.input()], (v) => Inputs.input(v)]}`;
v.value = { array: [1, 2] };
expect(v.value.array).toEqual([1, 2]);
expect(v.array).toContainEqual(Inputs.input(1));
expect(v.array).toContainEqual(Inputs.input(2));
}
)
Insert cell
suite.test(
"Composite write spreads to array subproperty (addition) (via view.value assignment)",
async () => {
const v = view`<div>${["array", [Inputs.input()], (v) => Inputs.input(v)]}`;
v.array.value = [1, 2]; // Should work but doesn't, we need some kind of ArrayView type
expect(v.value.array).toEqual([1, 2]);
expect(v.array).toContainEqual(Inputs.input(1));
expect(v.array).toContainEqual(Inputs.input(2));
}
)
Insert cell
suite.test(
"Composite write spreads to array subproperty (addition) (via data assignment)",
async () => {
const v = view`<div>${["array", [Inputs.input()], (v) => Inputs.input(v)]}`;
v.value.array = [1, 2];
expect(v.value.array).toEqual([1, 2]);
expect(v.array).toContainEqual(Inputs.input(1));
expect(v.array).toContainEqual(Inputs.input(2));
}
)
Insert cell
suite.test("Array get", async () => {
const v = view`<div>${["array", [Inputs.input(1)]]}`;
expect(v.value.array).toEqual([1]);
expect(v.array[0]).toEqual(Inputs.input(1));
expect([...v.array]).toEqual([Inputs.input(1)]);
})
Insert cell
suite.test("Array write with builder creates new elements", async () => {
const v = view`<div>${["array", [Inputs.input()], (v) => Inputs.input(v)]}`;
v.value.array = [1, 2];
expect(v.value.array).toEqual([1, 2]);
expect(v.array).toContainEqual(Inputs.input(1));
expect(v.array).toContainEqual(Inputs.input(2));
})
Insert cell
suite.test("Array write remove elements", async () => {
const v = view`<div>${["array", [Inputs.input(0), Inputs.input(2)]]}`;
v.value.array = [1];
expect(v.value.array).toEqual([1]);
expect(v.array).toContainEqual(Inputs.input(1));
})
Insert cell
suite.test("Array in-place splice support (delete), no builder", async () => {
const v = view`<div>${["array", [Inputs.input(0), Inputs.input(2)]]}`;
v.value.array.splice(0, 1);
expect(v.value.array).toEqual([2]);
})
Insert cell
suite.test("Array in-place splice support (delete), with builder", async () => {
const v = view`<div>${[
"array",
[Inputs.input(0), Inputs.input(2)],
(v) => Inputs.input(v)
]}`;
v.value.array.splice(0, 1);
expect(v.value.array).toEqual([2]);
})
Insert cell
suite.test(
"Array in-place splice support (addition) with builder",
async () => {
const v = view`<div>${[
"array",
[Inputs.input(0)],
(v) => Inputs.input(v)
]}`;
v.value.array.splice(1, 0, 1);
expect(v.value.array).toEqual([0, 1]);
expect([...v.array]).toEqual([Inputs.input(0), Inputs.input(1)]);
}
)
Insert cell
suite.test("Dynamic Object value property assignment", async () => {
const v = view`<div>${["field", Inputs.input()]}`;
v.value = { field: 1 };
expect(v.field.value).toEqual(1);
expect(v.value.field).toEqual(1);
})
Insert cell
suite.test("Dynamic Object view property assignment", async () => {
const v = view`<div>${["field", Inputs.input()]}`;
v.field = Inputs.input("2");
expect(v.field.value).toEqual("2");
expect(v.value.field).toEqual("2");
})
Insert cell
suite.test(
"Dynamic Object write with builder creates new elements",
async () => {
const v = view`<div>${["...", {}, (v) => Inputs.text({ value: v })]}`;
v.value = { a: "b" };
expect(v.value).toEqual({ a: "b" });
expect(v.a).toHaveProperty("name"); // It's a DOM node
}
)
Insert cell
suite.test("Dynamic Object write deletes old elements", async () => {
const v = view`<div>${["...", { a: Inputs.text() }]}`;
expect(v.value).toEqual({ a: "" });
expect(v.a).toHaveProperty("name"); // It's a DOM node
v.value = {};
expect(v.value).toEqual({});
expect(v.a).toBeUndefined();
})
Insert cell
suite.test("Collection object creates matching keys", async () => {
const v = view`<div>${[
"...",
{
a: Inputs.input()
}
]}`;
expect(v.value).toHaveProperty("a");
})
Insert cell
Insert cell
//import { footer } from "@tomlarkworthy/footer"
Insert cell
//footer
Insert cell
import { exporter } from "@tomlarkworthy/exporter"
Insert cell
exporter()
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