Public
Edited
Nov 9
2 forks
52 stars
Insert cell
Insert cell
Insert cell
Insert cell
viewof output = {
// we will make this construction 🎵sing🎵
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
dynamicSelect = juice(Inputs.select, {
options: "[0]", // "range" is first arg (index 0), the min is the 1st arg of the range array
result: "[1].value" // "result" can be set in the options.value, options being the 2nd arg (index 0)
})
Insert cell
Insert cell
Insert cell
Insert cell
viewof combinedView = view`<div>
<!-- Normal HTML provides us affordance to decorate CSS and labeling and arrangments -->
<h4>Simple Nested View Example</h4>
<style>
/* We can add CSS */
.horz form { display: inline-block; width: 30px; }
.horz { margin-left: 30%; }
</style>
<!-- Child 'x' is an off-the-shelf range view-->
${['x', Inputs.range([-10, 10], { label: "x" })]}
${['y', Inputs.range([-10, 10], { label: "y" })]}
<span class="horz">
<!-- Child 'a' is an off-the-shelf button view-->
${['a', Inputs.button("a")]}
${['b', Inputs.button("b")]}
<span>
</div>
</div>`
Insert cell
Insert cell
combinedView
Insert cell
Insert cell
viewof combinedView
Insert cell
Insert cell
Inputs.bind(
Inputs.range([-10, 10], {
label: "This is bound to just part of the UI above"
}),
viewof combinedView.x
)
Insert cell
Insert cell
Insert cell
Inputs.range()
Insert cell
Insert cell
Insert cell
naturalNumber = options =>
view`<div class="nnum"><style>
.nnum input[type=range] {
display: none;
}
</style>${[
'...', // Note the '...' moves the binding up to not being a child view
Inputs.range([0, 999999999], { value: 0, ...options, step: 1 })
]}`
Insert cell
viewof naturalNumExample = naturalNumber()
Insert cell
naturalNumExample
Insert cell
Insert cell
Insert cell
row = value => view`<li>${["...", Inputs.text({ value })]}</li>`
Insert cell
viewof exampleRow = row()
Insert cell
exampleRow
Insert cell
Insert cell
viewof staticListExample = view`<ul>${["children", [row(), row(), row()]]}`
Insert cell
Insert cell
staticListExample
Insert cell
Inputs.button("backwrite contents of rows", {
reduce: () => {
viewof staticListExample.children.forEach(
row => (row.value = Math.random())
);
viewof staticListExample.dispatchEvent(
new Event('input', { bubbles: true })
);
}
})
Insert cell
Insert cell
viewof dynamicListExample = view`<ul>${[
"children",
[row(Math.random()), row(Math.random()), row(Math.random())],
d => row(d)
]}`
Insert cell
Insert cell
Inputs.button("backwrite number and contents of rows", {
reduce: () => {
viewof dynamicListExample.value = {
children: Array.from(
{ length: Math.floor(Math.random() * 10) },
Math.random
)
};
viewof dynamicListExample.dispatchEvent(
new Event('input', { bubbles: true })
);
}
})
Insert cell
Insert cell
Insert cell
viewof hiddenExample = {
const c1 = Inputs.text();
const c2 = Inputs.range();
const c3 = Inputs.radio(["yes", "no"]);

return view`<div>
<section>${c1 /*Vanilla htl binding, does not link the subview*/} </section>
<section>${c2}${c3}</section>
<!-- end -->${[
'_...', // Binding children via an object collection, but not changing their DOM location
{
a: c1,
b: c2,
c: c3
}
]}</div>`;
}
Insert cell
hiddenExample
Insert cell
Insert cell
viewof hiddenExample.outerHTML
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function createShowable(child, { show = true } = {}) {
const showVariable = variable(show, { name: "show" });
const showable = view`<div>${["show", showVariable]}${["child", child]}`;

// The showable logic is to toggle the visibility of the enclosing div based
// on the show variable state
const updateDisplay = () => {
if (showVariable.value) {
showable.style.display = "inline";
} else {
showable.style.display = "none";
}
};
// Variables have additional assign event so presentation can be
// updated as soon as variables change but before dataflow
// because this is a pure presentation state it makes sense not to trigger
// dataflow so we do not use 'input' event
showVariable.addEventListener("assign", updateDisplay);

updateDisplay();
return showable;
}
Insert cell
viewof exampleShowable = createShowable(Inputs.text())
Insert cell
Inputs.button("toggle show", {
reduce: () => {
// note changing this updates immediately and without dataflow because of assign event
exampleShowable.show = !exampleShowable.show;
}
})
Insert cell
Insert cell
Insert cell
viewof yesAnd = {
const prompt = Inputs.radio(["no", "yes"], { label: "answer?", value: "no" });
const answer = createShowable(
Inputs.text({ placeholder: "well write your answer here" })
);
const result = variable(undefined);

bindOneWay(answer.show, prompt, {
transform: ans => ans === 'yes' // Convert radio text to boolean
});

// The result needs to update if the toggle or the text changes
bindOneWay(result, answer.child, {
transform: text => (answer.show.value ? text : "no answer")
});
bindOneWay(result, answer.show, {
transform: show => (show ? answer.child.value : "no answer")
});

return view`<div>
${prompt}
${answer}
<!-- this view is a singleton so its value is just the result -->
${["...", result]}
`;
}
Insert cell
Insert cell
Insert cell
Insert cell
createStorageView = key => {
const ui = htl.html`<div>${key}</span>
</div>`;
return Object.defineProperty(ui, 'value', {
get: () => localStorage.getItem(key),
set: value => {
localStorage.setItem(key, value);
},
enumerable: true
});
}
Insert cell
Insert cell
viewof exampleStorageView = createStorageView("exampleStorageKey")
Insert cell
exampleStorageView
Insert cell
Insert cell
Inputs.button("write random value", {
reduce: () => {
viewof exampleStorageView.value = Math.random();
viewof exampleStorageView.dispatchEvent(
new Event('input', { bubbles: true })
);
}
})
Insert cell
Insert cell
viewof persistedRange = Inputs.bind(
Inputs.range(),
createStorageView("rangeStoragekey")
)
Insert cell
persistedRange
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
rollDice = (sides, andThen) =>
async function*() {
const start = Date.now();
let val = undefined;
while (Date.now() < start + 1000) {
val = Math.floor(Math.random() * sides) + 1;
yield md`${val}`; // Not a view so its value is "undefined"
await Promises.delay(16); // 16ms
}
// final value should be the dice AND the value
const final = md`<mark>${val}</mark>`;
final.value = val;
yield final;
yield new Event('input', { bubbles: true }); // Notify the value has changed
await andThen;
}
Insert cell
Insert cell
viewof diceVal = (reroll, viewroutine(rollDice(6, invalidation)))
Insert cell
diceVal
Insert cell
Insert cell
viewof nDice = viewroutine(async function*() {
let sides = 6;
while (true) {
sides = Number.parseInt(
yield* ask(
Inputs.radio(["2", "4", "6", "8", "10", "20"], {
label: "Roll a dice with how many sides?"
})
)
);
yield* rollDice(sides)();
await Promises.delay(1000);
}
})
Insert cell
nDice
Insert cell
Insert cell
Insert cell
Insert cell
calculatorKey = ({ className, label } = {}) => {
const view = htl.html`<button class="calculator-key ${className}" onClick=${function press() {
view.value = className;
view.dispatchEvent(new Event('input', { bubbles: true }));
}}>${label}</button>`;
return view;
}
Insert cell
Insert cell
Insert cell
viewof multiplyKey = calculatorKey({
className: "key-multiply",
value: "MUL",
label: "×"
})
Insert cell
multiplyKey
Insert cell
Insert cell
autoScalingText = ({ child } = {}) => {
const scale = variable(1, { name: "scale" });
const node = view`<div class="auto-scaling-text">
<!-- Variables are HTML comments and have no appearance, but we want it to be a part of the parent view
so we still bind it -->
${['scale', scale]}
${['child', child]}
</div>`;

// If we change scale we want the div style.transform changed
scale.addEventListener("assign", evt => {
node.style.transform = `scale(${scale.value},${scale.value})`;
});

scale.value = 1;
const updateScale = () => {
const parentNode = node.parentNode;
if (!parentNode) return 1;

const availableWidth = parentNode.offsetWidth;
const actualWidth = node.offsetWidth;
const actualScale = availableWidth / actualWidth;

// In mjackson example there is a case that returns with no value to avoid a call to setState.
// This is coz this code is executed in React's ComponentDidUpdate which is done after
// placing the DOM node. This path is for concluding there is nothing to be done, otherwise,
// it calls setState causing another DOM adjsut and place loop
// Because in Observable we have to call dispatchEvent after setting the component, this code
// executes after placement kinda naturally.

if (actualScale < 1) {
return actualScale;
} else if (scale <= 1) {
return 1;
}
};

// When the child updates we want to change the scale, so we bind child -> scale with a transform
bindOneWay(scale, child, {
transform: updateScale
});

return node;
}
Insert cell
Insert cell
Insert cell
textNodeView = (value = '') => {
const node = document.createTextNode(value);
return Object.defineProperty(node, 'value', {
get: () => node.textContent,
set: val => (node.textContent = val),
enumerable: true
});
}
Insert cell
Insert cell
viewof exampleAutoScalingTextContainer = view`<div class="example" style="max-width:300px">
<style>
.example .auto-scaling-text {
position: absolute;
transform-origin: top left;
}
</style>
${[
'example',
autoScalingText({
child: textNodeView("press random number below")
})
]}`
Insert cell
Insert cell
Inputs.button("generate a random long number", {
reduce: () => {
const digits = Math.floor(Math.random() * 100);
const num = Array(digits)
.fill(null)
.reduce(str => str + Math.floor(Math.random() * 10), "");
viewof exampleAutoScalingTextContainer.example.child.value = num;
viewof exampleAutoScalingTextContainer.example.child.dispatchEvent(
new Event('input'),
{ bubbles: true }
);
}
})
Insert cell
Insert cell
calculatorDisplay = ({ value } = {}) => {
const valueVariable = variable(value, { name: "value" });

const format = val => {
// Coz in react the value is passed in as an attribute to the component its a string.
// But we can be flexible and accept both
const valStr = `${val}`;
const language = navigator.language || 'en-US';
let formattedValue = parseFloat(valStr).toLocaleString(language, {
useGrouping: true,
maximumFractionDigits: 6
});

// Add back missing .0 in e.g. 12.0
const match = valStr.match(/\.\d*?(0*)$/);

if (match) formattedValue += /[1-9]/.test(match[0]) ? match[1] : match[0];
return formattedValue;
};
const text = textNodeView();

const ui = view`<div class="calculator-display">
${['...', valueVariable]}
${autoScalingText({
child: text
})}
</div>`;
bindOneWay(text, valueVariable, {
transform: format
});

return ui;
}
Insert cell
Insert cell
viewof testDisplay = view`<div class="example">
<!-- We need a little styling for the auto resizing to kick in -->
<style>
.example .auto-scaling-text {
position: absolute;
transform-origin: top left;
}
.example .calculator-display {
max-width: 400px;
}
</style>
${['...', calculatorDisplay({ value: "100.0000" })]}
`
Insert cell
testDisplay
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
calculator = ({ className = '', displayValue = '0' } = {}) => {
// We will bind all keys to lastKey, so we can have a single handler
const lastKey = variable(undefined, { name: "lastKey" });
const placeKey = (className, label) => {
const key = calculatorKey({ className, label });
// To have a single handler we will bind everthing to lastKey
bindOneWay(lastKey, key, {
transform: d => {
return d;
}
});
return key;
};

const ui = view`<div class="calculator ${className}">
${['displayValue', calculatorDisplay()]}
<div class="calculator-keypad">
${['lastKey', lastKey]}
<div class="input-keys">
<div class="function-keys">
${placeKey("key-clear", "AC")}
${placeKey('key-sign', "±")}
${placeKey('key-percent', "%")}
</div>
<div class="digit-keys">
${placeKey('key-0', "0")}
${placeKey('key-dot', "●")}
${placeKey('key-1', "1")}
${placeKey('key-2', "2")}
${placeKey('key-3', "3")}
${placeKey('key-4', "4")}
${placeKey('key-5', "5")}
${placeKey('key-6', "6")}
${placeKey('key-7', "7")}
${placeKey('key-8', "8")}
${placeKey('key-9', "9")}
</div>
</div>
<div class="operator-keys">
${placeKey('key-divide', "÷")}
${placeKey('key-multiply', "×")}
${placeKey('key-subtract', "-")}
${placeKey('key-add', "+")}
${placeKey('key-equals', "=")}
</div>
</div>
</div>`;

// Now add business logic
const logic = calculatorLogic(ui);
lastKey.addEventListener('input', () => {
let match = undefined;
if ((match = /^key-(\d)$/.exec(lastKey.value))) {
logic.inputDigit(Number.parseInt(match[1]));
}
switch (lastKey.value) {
case 'key-dot':
return logic.inputDot();
case 'key-clear':
return logic.clearAll();
case 'key-sign':
return logic.toggleSign();
case 'key-percent':
return logic.inputPercent();
case 'key-divide':
return logic.performOperation('/');
case 'key-multiply':
return logic.performOperation('*');
case 'key-subtract':
return logic.performOperation('-');
case 'key-add':
return logic.performOperation('+');
case 'key-equals':
return logic.performOperation('=');
}
});

// Add keyboard
document.addEventListener('keydown', logic.handleKeyDown);
invalidation.then(() =>
document.removeEventListener('keydown', logic.handleKeyDown)
);

// initialize
logic.setState({
displayValue: displayValue
});

return ui;
}
Insert cell
Insert cell
viewof testCalculator = calculator({
displayValue: '0'
})
Insert cell
testCalculator
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
exampleStyledCalculator
Insert cell
Insert cell
Insert cell
Inputs.button("set display to random number", {
reduce: () => {
const digits = Math.floor(Math.random() * 20);
const num = Array(digits)
.fill(null)
.reduce(str => str + Math.floor(Math.random() * 10), "");
viewof exampleStyledCalculator.singleton.displayValue.value = num;
viewof exampleStyledCalculator.singleton.displayValue.dispatchEvent(
new Event('input'),
{ bubbles: true }
);
}
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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