Public
Edited
Nov 9
2 forks
53 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

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