Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel is required to process package imports. If you need a different preprocessor remove all packages first.

Add External Scripts/Pens

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.

+ add another resource

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <!-- 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>
	<ul command hidden>
		<li>Start server: <code>$ live-server --open=index.html --port=1515 --browser=firefox</code>
		<li><code>aria-live</code> &lt;div&gt; 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">&ltdot;</button>
      <button action="nextOp" aria-label="divide-by" value="&divide;">&divide;</button>

			<button value="7">7</button>
			<button value="8">8</button>
			<button value="9">9</button>
      <button action="nextOp" aria-label="multiply-by" value="&times;">&times;</button>

			<button value="4">4</button>
			<button value="5">5</button>
			<button value="6">6</button>
      <button action="nextOp" aria-label="minus" value="&minus;">&minus;</button>

			<button value="1">1</button>
			<button value="2">2</button>
			<button value="3">3</button>
      <button action="nextOp" aria-label="plus" value="&plus;">&plus;</button>

			<button value="negate" aria-label="positive-negative">&PlusMinus;</button>
			<button value="0">0</button>
			<button value=".">.</button>
			<button action="calculate" aria-label="Equals">&equals;</button>
		</div>
	</div>
</main>

              
            
!

CSS

              
                /* calculator style.css */

main {
  background-color: skyblue;
  padding: 1em;
}

/*
[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;
}

              
            
!

JS

              
                /* 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.
   */
  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);
    }
  });
});

              
            
!
999px

Console