HTML preprocessors can make writing HTML more powerful or convenient. For instance, Markdown is designed to be easier to write and read for text documents and you could write a loop in Pug.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. So you don't have access to higher-up elements like the <html>
tag. If you want to add classes there that can affect the whole document, this is the place to do it.
In CodePen, whatever you write in the HTML editor is what goes within the <body>
tags in a basic HTML5 template. If you need things in the <head>
of the document, put that code here.
The resource you are linking to is using the 'http' protocol, which may not work when the browser is using https.
CSS preprocessors help make authoring CSS easier. All of them offer things like variables and mixins to provide convenient abstractions.
It's a common practice to apply CSS to a page that styles elements such that they are consistent across all browsers. We offer two of the most popular choices: normalize.css and a reset. Or, choose Neither and nothing will be applied.
To get the best cross-browser support, it is a common practice to apply vendor prefixes to CSS properties and values that require them to work. For instance -webkit-
or -moz-
.
We offer two popular choices: Autoprefixer (which processes your CSS server-side) and -prefix-free (which applies prefixes via a script, client-side).
Any URL's added here will be added as <link>
s in order, and before the CSS in the editor. You can use the CSS from another Pen by using it's URL and the proper URL extention.
You can apply CSS to your Pen from any stylesheet on the web. Just put a URL to it here and we'll apply it, in the order you have them, before the CSS in the Pen itself.
You can also link to another Pen here (use the .css
URL Extension) and we'll pull the CSS from that Pen and include it. If it's using a matching preprocessor, use the appropriate URL Extension and we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
JavaScript preprocessors can help make authoring JavaScript easier and more convenient.
Babel includes JSX processing.
Any URL's added here will be added as <script>
s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.
You can apply a script from anywhere on the web to your Pen. Just put a URL to it here and we'll add it, in the order you have them, before the JavaScript in the Pen itself.
If the script you link to has the file extension of a preprocessor, we'll attempt to process it before applying.
You can also link to another Pen here, and we'll pull the JavaScript from that Pen and include it. If it's using a matching preprocessor, we'll combine the code before preprocessing, so you can use the linked Pen as a true dependency.
Search for and use JavaScript packages from npm here. By selecting a package, an import
statement will be added to the top of the JavaScript editor for this package.
Using packages here is powered by Skypack, which makes packages from npm not only available on a CDN, but prepares them for native JavaScript ES6 import
usage.
All packages are different, so refer to their docs for how they work.
If you're using React / ReactDOM, make sure to turn on Babel for the JSX processing.
If active, Pens will autosave every 30 seconds after being saved once.
If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.
If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.
Visit your global Editor Settings.
<!-- 20 - 24 April 2020 (lots of refactorings along the way) -->
<!-- goaded by https://www.youtube.com/watch?v=uNeVn15Ho14 -->
<!-- 21 May 2020 - added value attributes to nextOp and append buttons -->
<main>
<p data-note>This is a messy proof-of-concept only. You can visit a working demo with test suite on my blog, at <a data-note href="https://dfkaye.com/demos/calculator/">https://dfkaye.com/demos/calculator/</a>.
</p>
<ul command hidden>
<li>Start server: <code>$ live-server --open=index.html --port=1515 --browser=firefox</code>
<li><code>aria-live</code> <div> is visually hidden.
<li><code>role="alert"</code> required by screen readers on live regions.
<li>tested on Windows 10 with Narrator.
<li>TODO: add more operations (sqrt, modulo, inverse, etc.
</ul>
<div calculator>
<div equation></div>
<input display readonly value="[ Calculator not initialized. ]" />
<div role="alert" aria-live="polite" aria-atomic="false" visually-hidden a11y-display></div>
<div keypad>
<button action="reset" aria-label="Clear">C</button>
<button action="backspace" aria-label="Backspace">⋖</button>
<button action="nextOp" aria-label="divide-by" value="÷">÷</button>
<button value="7">7</button>
<button value="8">8</button>
<button value="9">9</button>
<button action="nextOp" aria-label="multiply-by" value="×">×</button>
<button value="4">4</button>
<button value="5">5</button>
<button value="6">6</button>
<button action="nextOp" aria-label="minus" value="−">−</button>
<button value="1">1</button>
<button value="2">2</button>
<button value="3">3</button>
<button action="nextOp" aria-label="plus" value="+">+</button>
<button value="negate" aria-label="positive-negative">±</button>
<button value="0">0</button>
<button value="." aria-label="decimal-separator">.</button>
<button action="calculate" aria-label="Equals">=</button>
</div>
</div>
</main>
/* calculator style.css */
main {
background-color: skyblue;
font-family: sans-serif;
font-size: 1em;
padding: 1em;
}
[data-note] {
color: white;
text-align: center;
}
/*
[command] code {
background-color: lightgray;
margin: 1em auto;
padding: 0.25em;
}
*/
[calculator] {
display: grid;
justify-content: center;
}
[visually-hidden] {
/* https://a11yproject.com/posts/how-to-hide-content/ */
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
/* clip: rect(1px, 1px, 1px, 1px); */
white-space: nowrap; /* added line */
}
[equation] {
background-color: transparent;
color: white;
font-family: Arial;
font-weight: normal;
grid-column: span 4;
min-height: 1.5em;
padding: 0 0.75em;
text-align: right;
}
[display] {
background-color: azure;
background-color: whitesmoke;
border: 1px solid deepskyblue;
border-radius: 5px;
font-size: 1.25em;
grid-column: span 4;
padding: 1em 0.5em 0.5em;
text-align: right;
}
[keypad] {
background-color: azure;
background-color: whitesmoke;
border: 1px solid skyblue;
border-radius: 5px;
grid-column: span 4;
padding: 2px;
/* control the button layout */
display: grid;
grid-gap: 2px 1.5px;
grid-template-columns: 1fr 1fr 1fr 1fr;
}
[keypad] button {
background-color: gainsboro;
background-color: #fcfcfc;
border: 1px solid lightgrey;
border-radius: 5px;
font-size: 1.25em;
min-height: 1.75em;
min-width: 3.5em;
padding: 1em;
}
[keypad] [action="reset"] {
grid-column: 1 / 3;
}
[keypad] [action]:not([action="negate"]) {
background-color: deepskyblue;
background-color: lightgray;
}
[keypad] :active,
[keypad] :hover,
[keypad] :focus {
background-color: lightblue;
}
[action]:not([action="negate"]):active,
[action]:not([action="negate"]):hover,
[action]:not([action="negate"]):focus {
background-color: lightskyblue;
}
[action][action="calculate"]:not(:empty) {
background-color: lightskyblue;
}
[action][action="calculate"]:active,
[action][action="calculate"]:hover,
[action][action="calculate"]:focus {
background-color: deepskyblue;
}
/* calculator app.js */
// 20-24 April, 25 April - 7 May 2020 - 22 May 2020
// "Simple" calculators on the web typically use "eval()" or "Function()" calls
// to show how "simple" it is to build something basic.
//
// As we are now well into the era of Content Security Policy, and due to other
// defects in those implementations, and due to my long interest in the SAM
// pattern, I wanted to see just how much thought goes into making more than a
// basic calculator, one that could mirror closely the Calculator app that ships
// with Windows 10.
//
// After 22 April, went through several rounds of refactoring and fixing - esp.
// getting the test sequences right, in order to match MS Calculator output (25
// April - 5 May). The display equation and operand shifting logic were the
// hardest.
//
// As of 5-7 May, the SAM pattern is coming into shape (view, state, model, and
// action) - comments updated, early return pattern in model methods done, view
// init and schedule done.
//
// Not bad at 3 hours a day.
// 21 May 2020
// Finally added keydown input and traversal/navigation, and moved handlers from
// the action to view. Now some things are handled only by the view. Keyboard
// input sends data to action.next(), whereas keyboard *navigation* sets the
// next focused item directly. I suppose that should be updated with an
// action or step modification to the state (i.e., which element to focus next)
// as a selector...
// Also TODO:
// - revisit Enter vs. Space (space should act as keypress, enter should apply
// the next value/op, I think...)
// - restrict the Arrow key left & right nav to be row-restricted
// - add instructional section on form for key shortcuts (C for clear, N for
// negate, backspace & delete for backspace...)
// - keep refactoring for clarity...
//
// 25 June 2020 - support handleEvent interface in view.init().
/*
Test sequences:
+ : 0 (no update) { display: 0 }
+ 1 : 1 (update) { display: 1 }
+ 1 + : 1 (no update) { display: 1 }
+ 1 + = : 2 (update) { display: 2 }
+ 1 + = = : 3 (update) { display: 3 }
+ 1 = : 1 (update) { display: 1 }
+ 1 = = : 2 (update) { display: 2 }
+ 1 = = = : 3 (update) { display: 3 }
+ 1 + = : 2 (update) { display: 1, last: 0 }
+ 1 + = + : 2 (update) { 2 + }
+ 1 + = + = : 4 (update) { 2 + }
nextOp assigns first operand (result) to second operand (after recalc)
2 + 3 + : 5
5 = : 10
10 = : 15
15 = : 20
equals increments first operand (result) by second operand
2 + 3 = : 5
5 = : 8
8 = : 11
11 = : 14
*/
/*
* Application logic follows the SAM pattern, with some adjustments, like so:
*
* action.next() -> model.propose() -> state.change() -> view.update()
*
* An action receives data, sends the useful parts to the model, which then
* calculates the next model state, sends that to the state controller, which
* constructs a representation of the updated model state for use by the view.
*
* Although the calculator app does not run a continuous loop, SAM semantics
* consider that case after the view update is called. If at that point, the
* state controller calculates that another step should be taken (such as
* decrementing a timer in a countdown app), the state would call action.next()
* to do that.
*
* For more information about SAM, see http://sam.js.org/
*/
/* The view is created with an IIFE to keep schedule function private. */
var view = (function () {
// The view API does not depend directly on the model, or state APIs,
// only action.next() and the DOM in this case...
function getAction(button) {
var action = (
button.getAttribute('action')
|| (button.value == 'negate' && 'negate')
|| 'append'
);
return action;
}
function getValue(button) {
return button.value;
}
function focusColumnSibling({ sibling, direction }) {
var i = 3;
while(i--) {
sibling = sibling[direction];
if (!sibling) {
return;
}
}
sibling.focus();
}
function isAction(action) {
return action && action != 'append' && action != 'negate';
}
function focusActionSibling({ button, shiftKey, preventDefault }) {
var direction = shiftKey
// move backwards to previous action button on Shift+Tab
? 'previousElementSibling'
: 'nextElementSibling';
var sibling = button;
var action = getAction(button);
var actionKey = isAction(action);
var previous;
while (sibling) {
var previous = sibling;
sibling = sibling[direction];
if (!sibling && actionKey) {
// console.warn("we're at the end of the actions");
focusNumericSibling({ button: previous, shiftKey, preventDefault });
return;
}
action = sibling.getAttribute('action');
if (actionKey && isAction(action)) {
break;
}
}
// Turn off the Tab key default behavior only if we have the next action
// sibling to focus.
sibling && (
preventDefault(),
sibling.focus()
);
}
function focusNumericSibling({ button, shiftKey, preventDefault }) {
// 21 MAY 2020
// TODO: REFACTOR NUMERIC TRAVERSAL VS ACTION TRAVERSAL
var buttons = Array.from(document.querySelectorAll('[calculator] button:not([action]'))
.sort(function(a,b) {
// Chrome requires -1 for a less than b; won't sort if only 0.
return a.value > b.value ? 1 : a.value < b.value ? -1 : 0;
});
// find decimal, put it between 9 and negate
var decimal = buttons.filter(button => button.value == '.')[0];
buttons.splice(buttons.indexOf(decimal), 1);
buttons.splice(buttons.length - 1, 0, decimal);
// debugging chrome sorting took some time...
// console.warn( buttons.map(b => b.value).join(', '));
var nextIndex = buttons.indexOf(button);
if (nextIndex == -1) {
nextIndex = shiftKey
? buttons.length - 1
: 0;
} else {
nextIndex = shiftKey
? nextIndex - 1
: nextIndex + 1;
}
var actions = Array.from(document.querySelectorAll('[calculator] button[action]'));
var next = buttons[nextIndex];
if (shiftKey && nextIndex == -1) {
// go to last action button
next = actions[actions.length - 1];
}
if (!shiftKey && nextIndex == buttons.length) {
// go to first focusable element (the input display).
next = document.querySelector('input[display]');
}
next && (
preventDefault(),
next.focus()
);
}
function schedule(handler) {
document.addEventListener('readystatechange', function () {
if (document.readyState == "complete") {
run(handler);
}
})
}
function run(handler) {
// Enable fn() both fn.handleEvent() interfaces.
typeof handler.handleEvent == "function"
? handler.handleEvent.call(handler)
: typeof handler == "function"
? handler.call(document)
: 0;
}
var api = {
/**
* Type annotations go here.
*/
init(handler) {
// Enables us to call init() multiple times, even after document is ready.
document.readyState == 'complete'
? run(handler)
: schedule(handler);
},
update(representation) {
var { displayValue, a11yContent, equation } = representation;
document.querySelector('[calculator] [display]').value = displayValue;
document.querySelector('[calculator] [a11y-display]').textContent = `
Display is ${ a11yContent }
`.trim();
document.querySelector('[calculator] [equation]').textContent = equation;
},
/*
* Handlers, maybe should be re-structured as
* handlers: {
* 'click': ...,
* etc
* }
*
* Alternative: put DOM event handlers on the view API instead?
*
* 21 May - did that.
*
*/
handlers: {
onclick(e) {
/*
* Good news: Enter and Space key events trigger click events on button
* elements, no separate key event handler needed for those two keys.
*/
if (e.target.nodeName.toLowerCase() != 'button') {
return;
}
action.next({
action: getAction(e.target),
value: getValue(e.target)
});
},
onkeydown(e) {
/*
* Handle numeric, decimal, backspace, negate, clear, and operator key
* presses first.
* Handle TAB and Arrow traversal further down.
*/
var key = e.key;
if (/^(N)$/i.test(key)) {
e.preventDefault();
action.next({
action: "negate"
});
}
if (/^(\d|\.)$/.test(key)) {
action.next({
action: "append",
value: key
});
}
// TODO - FIX OP KEY MAP WITH VERBS INSTEAD OF SYMBOLS
if (/^[\-\+\/\*]$/.test(key)) {
e.preventDefault();
var value = key == '*' ? '×'
: key == '-' ? '−'
: key == '/' ? '÷'
// : key == '-' ? '−'
: key;
action.next({
action: "nextOp",
value: value
});
}
if (/^\=$/.test(key)) {
action.next({
action: "calculate"
});
}
if (/^Backspace|Delete$/.test(key)) {
action.next({
action: "backspace"
});
}
if (/^C$/i.test(key)) {
action.next({
action: "reset"
});
}
/*
* Traversal by key press means Tab and Arrow key events triggered from
* buttons.
*
* Good news: Enter and Space key events trigger click events on button
* elements, already handled by onclick.
*/
if (e.target.nodeName.toLowerCase() == 'button') {
var button = e.target;
var shiftKey = e.shiftKey;
/*
* Handle arrow key traversal, allowing left and right to traverse up
* and down rows (because markup is a grid, not a table).
*/
if (/^(ArrowRight|ArrowLeft|ArrowUp|ArrowDown)$/.test(key)) {
var previous = button.previousElementSibling;
var next = button.nextElementSibling;
key == 'ArrowLeft' && previous && previous.focus();
key == 'ArrowRight' && next && next.focus();
key == 'ArrowUp' && focusColumnSibling({
sibling: previous,
direction: 'previousElementSibling'
});
key == 'ArrowDown' && focusColumnSibling({
sibling: next,
direction: 'nextElementSibling'
});
}
/*
* Handle tab and shift+tab traversal, mimicking the pattern followed
* by MS Calculator.
* display to actions,
* then through the actions,
* then through the values,
* then decimal,
* then positive-negative,
* then to display.
*/
if (/^(Tab)$/.test(key)) {
isAction(getAction(button))
? focusActionSibling({
button,
shiftKey,
preventDefault: () => e.preventDefault()
})
: focusNumericSibling({
button,
shiftKey,
preventDefault: () => e.preventDefault()
});
}
}
}
}
};
return api;
}());
/*
* The state is created with an IIFE to keep format and represent functions
* private.
*/
var state = (function () {
// state depends on the view (i.e., view.update()), and the action (i.e.,
// action.next()).
/*
* This one took some time to figure out.
*
* Format a numeric string with commas, preserving minus sign for negative
* numbers, and the decimal point.
*
* format(999.999) => "999.999"
* format(1000.999) => "1,000.999"
* format(-999.999) => "-999.999"
* format(-1000.999) => "-1,000.999"
*/
function format(value) {
if (Math.abs(value) < 1000 ) {
return value;
}
var decimal = value.indexOf('.');
var fractional = decimal > 0;
var fraction = fractional ? value.substring(decimal) : '';
//return new Intl.NumberFormat({ minimumFractionDigits: fraction.length })
//.format(value);
// Rolled by hand...
var integer = value.substring(0, fractional ? decimal : value.length);
var result = Array(integer.length);
integer.split('').forEach(function(v, i, integer) {
// Iterates the array of integer characters from index 0, but reads from
// integer string and inserts in result array at `length - 1 - index`.
var at = integer.length - i - 1;
var value = integer[at];
// Get value at next lower index. If current index is 0, next value is
// undefined.
var nextValue = integer[at - 1];
var offset = integer.length - at;
// Insert comma when value and nextValue are digits, and insertion offset
// is divisible by 3.
var digit = /\d/.test(value) && /\d/.test(nextValue) && offset % 3 == 0
? ',' + value
: value;
result[at] = digit;
});
return result.join('') + fraction;
}
function represent(data) {
// A representation is a subset of or an enhancement to the model data.
var displayValue = format(data.displayValue);
var equation = data.equation.join(' ');
// This is to mimic MS Calculator output when read by Narrator.
var a11yContent = data.equation.length == 2 // if ["6", "+"]
? equation // show "6 +"
: displayValue;
return {
displayValue,
a11yContent,
equation
};
}
/*
* TODO: act on this short essay (5-7 May)
*
* I don't like the "state.render()" semantic - prefer "state.change()" or
* "state.apply()" which are more state specific. The "render" semantic
* belongs on a view API. In fact, I prefer view.update() over view.render().
* And the view API should contain init() which calls action.next({ action:
* "reset" })`, and update({ name: 'view-name', data }), which updates a view
* or the whole view based on name - i.e., either update DOM nodes
* incrementally, or rewrite a DOM tree completely.
*/
var api = {
change(data) {
// Un-fancy guard clause - actually caught an error during the first week.
console.assert('displayValue' in data);
// Get next state representation and update the view.
var representation = represent(data);
view.update(representation);
/*
* Here you can calculate from the data whether to dispatch the next
* action call. For example,
*
* if (somethingTrue) {
* function next() { action.next({ action: 'decrement' }); },
*
* setTimeout(next, 1000);
*
* Or call an action that persists the current data to the DB, or to
* localStorage/sessionStorage.
*
*/
}
};
return api;
}());
/*
* The model is created with an IIFE to keep many things private. The entry
* is always model.propose() which delegates to the internal steps functions.
*/
var model = (function () {
// model depends on state API.
// Internal model. Changes to state are updated here.
var data = {};
// TODO:
// Add https://gist.github.com/dfkaye/c2210ceb0f813dda498d22776f98d48a
// for safer math operations.
// e.g., 1.1 * 1.1 => 1.2100000000000002
var ops = {
'÷': function(a, b) { return a / b; },
'×': function(a, b) { return a * b; },
'−': function(a, b) { return a - b; },
'+': function(a, b) { return Number(a) + Number(b); }
};
/* data helper methods */
function shiftOperands({ data, newValue }) {
var newOperands = data.operands.slice();
// Update only last added value in operands. No shifting required.
newOperands[newOperands.length - 1] = newValue;
return newOperands;
}
/**
* Type annotations go here.
* The following annotations using JSDoc will pass TypeScript with compiler options:
* allowJS: true, checkJS: true, noEmit: true
* See running TS playground example at
* https://www.typescriptlang.org/play?strictNullChecks=true&useJavaScript=true#code/PQKhCgAIUgBAHAhgJ0QW0gbwPYCMBWApgMYAuAvpCgOaQC0kyhAjgK4CWTAJlDAiuix4iZSjQB0XRKUT1GLDt15wkqDDgIkKVZNXHEAFogB21QgGc5TNp0I9oKgevOlk7U5AA+kY6zS5CZEoAbQlDEzNzSXZzeAAbRABPADVEONZCAF05bHhSdmxjNPBlWCZSVmRjSw0RCl5gcAAzVmMyAuNINECzAApMSCkZABpIcNMLSHIASiwoSAXgYEgAMU4XUcJjJuxkYkIqY0TIUkT4A-NEJsJT+RsmbuNSc3mF9ibIXoByLhj4pNS6UIX0g7jGRgm5lmmFeCzh40i0ViCRSaQykAAvJAAMqudzUXoIixI-6ooHTADcsPIJThkCWkAAsj0DkTLL8mGQ4sd3KRsINpIhxLDypVOgB5TRkcSIczmdjUYy9IaIUZsyngGngBnmAzYVhxLiQJBy8DdXSEfoCmQALiw5DVEMidoGv2RALRhDtACIAG6e71TKYa819V2Cl0O8ERCwuwZ-FGAjJ2gCMACYAMwAFiDMyp2uWuv1hsgTUQ7DiZpZVpVkcdMfMcbdpKTXpOyHRlDz4CAA
*
* @param {object} arg - required
* @param {object} arg.data - required
* @param {object} arg.changes - required
* @param {string | number} [arg.changes.displayValue] - optional
* @returns {object}
*/
function merge({ data, changes }) {
// First, enforce any type safety requirements
if ('displayValue' in changes) {
changes.displayValue = String(changes.displayValue);
}
// Merge changes directly into data.
return Object.assign(data, changes);
}
/*
* Collection of steps, or updaters. If data can be updated successfully, then
* each method returns the modified data object. If anything prevents an
* update, a method may return either nothing, or an object with a message
* field.
*/
var steps = {
reset() {
// Set initial model data.
var changes = {
displayValue: '0',
equalSign: '=',
equation: [],
last: '',
nextOp: '',
operands: []
};
return merge({ data, changes });
},
backspace() {
if (!Math.abs(data.displayValue)) {
// If absolute value is already 0, don't update.
return { message: 'Value is already 0' };
}
var displayValue = data.displayValue;
var newValue = displayValue.substring(0, displayValue.length - 1);
if (!newValue) {
// If the last digital character is removed, replace it with 0.
newValue = 0;
}
var newOperands = shiftOperands({ data, newValue });
var changes = {
displayValue: newValue,
last: newValue,
operands: newOperands
};
return merge({ data, changes });
},
nextOp(op) {
if (op === data.last) {
return { message: `Operation [${ op }] already pending.` };
}
if (!data.equation.length) {
// When user has selected an operation with the default value displayed.
// prefix that value to the display equation.
data.equation.unshift(data.displayValue);
}
if (/\d|\./.test(data.last)) {
// console.info('should calculate()', data.nextOp);
// Call calculate() if last entry was numeric.
// Remember, the data is mutated by this call.
steps.calculate();
}
var newValue = data.displayValue;
var lastIndex = data.equation.length - 1;
var lastEntry = data.equation[lastIndex];
// If the equation ends with an operator, replace it with the incoming
// operator. Otherwise, append it to the output equation.
var newEquation = !/\d(\.)?/.test(lastEntry)
? data.equation.slice(0, lastIndex).concat(op)
: data.equation.concat(op);
var changes = {
nextOp: op,
last: op,
// Assign the calculated value to the result field
// AND set the calculated value as the new right operand value.
operands: [newValue, newValue],
equation: newEquation
};
return merge({ data, changes });
},
negate() {
console.log('negate');
if (!Number(data.displayValue)) {
return { message: `Cannot negate value: ${ data.displayValue }.` };
}
var displayValue = data.displayValue;
var newValue = Number(displayValue) > 0
? displayValue * -1 // convert to negative
: displayValue.substring(1); // right-trim minus sign
var newOperands = shiftOperands({ data, newValue });
var changes = {
displayValue: newValue,
last: newValue,
operands: newOperands
};
return merge({ data, changes });
},
calculate() {
if (!data.nextOp) {
return { message: 'nextOp not entered yet.' }
}
// Destructuring to the rescue. Sure is nice here.
var [ left, right ] = data.operands;
var newValue = ops[data.nextOp](left, right).toString();
var lastIndex = data.equation.length - 1;
var lastEntry = data.equation[data.equation.length - 1];
var { equalSign } = data;
// Logic here is tricky. If the equation ends with '=', then we're ready to
// shorten its output, so replace the equation with a new array of
// operands and operators. Otherwise, append the right operand and the '='
// operator.
var newEquation = lastEntry === equalSign
? [left, data.nextOp, right, equalSign]
: data.equation.concat(right, equalSign);
var changes = {
displayValue: newValue,
// Assign the calculated value to the result field
// BUT leave the right operand value unchanged.
operands: [newValue, right],
equation: newEquation,
// append() tests this on first digit after calculate() call.
last: equalSign
};
return merge({ data, changes });
},
append(value) {
console.log('append', value);
if (data.last == data.equalSign) {
// If last action was calculate(), reinitialize to clean state.
steps.reset();
}
// If last entry is numeric, continue with it; otherwise, we have an
// operation request, so start a new operand.
var operand = /\d|\./.test(data.last)
? data.displayValue
: '';
// The value is digital, or it's decimal and no decimal has been entered.
var ok = /\d/.test(value) || (/\./.test(value) && !/\./.test(operand));
if (!ok) {
return { message: `Change [${value}] not applicable to [${operand}].` };
}
// If the current display value is non-zero, append the new character,
// otherwise replace it.
operand = Number(operand) || operand.length > 1
? operand + value
: value;
// If all we have is the decimal point, prefix '0' to it.
var newValue = operand == '.'
? ['0' + operand].join('')
: operand;
var newOperands = shiftOperands({ data, newValue });
var changes = {
displayValue: newValue,
last: newValue,
operands: newOperands
};
return merge({ data, changes });
}
};
var api = {
/**
* Type annotations go here.
*/
propose({action, value}) {
if (!action) {
return;
}
// TODO:
// steps should take { data, value } structured params.
var update = steps[action](value);
// Embrace mutability.
// On success we always mutate the data.
// On error we always return a different object.
if (data !== update) {
// Need to handle errors better. If no update, that's one thing, but if
// there are errors we need to tell the user about, we have to create an
// errors list and send that with the updated data, so the state can
// decide how to represent them and send them to the view.
//
// AND the view should check for presence/absence of errors listm etc.
var message = 'No update for ' + action + '(' + value + '): '
+ JSON.stringify(update);
console.log(message);
}
// Tell state to update. Send a copy so as not to mutate in the state.
state.change(Object.assign({}, data));
}
};
return api;
}());
/*
* The action is created with an IIFE (not necessary for this app), but with the
* idea that async operations should occur as a result of action calls - i.e.,
* fetch() data from storage, persist() updated data to storage.
*/
var action = (function () {
// action depends on model API.
var api = {
/*
* Possibly the single innovation on the SAM pattern worth maintaining from
* this example. Make next-action predicate a first class citizen as
* action.next(). Every proposed mutation should go through this method
* which then calls the model.propose() method. That allows other action
* handlers to make async calls or operations that then call action.next()
* when they resolve. Opens the way for async/await, and/or a
* generator/iterator approach, with or without Promises.
*
* tl;dr - call fetch() - or fetch wrappers - within actions.
*/
/**
* Type annotations go here.
*/
next({ action, value }) {
// For now, we'll map a keypad action and possible value to the model's
// steps API, and let the model reject invalid steps.
model.propose({ action, value });
}
};
return api;
}());
/* Schedule init. */
view.init(function() {
// The event interface depends on the action API.
// Essentially, view.init() call.
console.log('1st init');
document.querySelector('[calculator]')
.addEventListener('click', view.handlers.onclick);
document //.querySelector('body')
.addEventListener('keydown', view.handlers.onkeydown);
// Initialize everything.
action.next({ action: 'reset', value: '' });
// Test that init runs after DOM loaded
view.init(function() {
console.log('2nd init');
});
// Test handleEvent interface
view.init({
message: "handleEvent called.",
handleEvent: function() {
console.log(this.message);
}
});
});
Also see: Tab Triggers