Multiple Counters Exercise!
July 30, 2018 ยท View on GitHub
There are (at least) two ways of displaying multiple counters on the same page.
The easy way is to "instantiate" several counters
each within their own "container" (DOM) element. e.g:
<script src="counter.js" data-cover></script> <!-- load counter once -->
<div id="app"></div>
<div id="app1"></div>
<div id="app2"></div>
<script> // Mount as many apps as you like:
mount(0, update, view, 'app');
mount(1, update, view, 'app1');
mount(2, update, view, 'app2');
</script>
see:
/examples/multiple-counters-instances/index.html
This "works" and "satisfies the requirement"
of having multiple counters on the same "page".
However, it's not a "sustainable" way of "extending"
an app for the long term.
Almost no "real" web application uses an Integer as the model,
so the "complexity" of the model will be much greater.
We could leave the counter example model as an Integer
and move on to the next example (Todo List),
but as a "thought experiment",
let's try to implement multiple counters using an Array of Integers,
this is a good "refactoring" exercise.
1. Refactor Model from Integer to Object with Array
Using the code from
example/counter-reset
as a starting point,
refactor the model from Integer to an Object with an Array
called counters:
mount({counters:[0]}, update, view, 'app');
That will "break" the existing tests:

(I temporarily commented out all the other failing tests to reduce noise, but by the time we are done refactoring, all tests will pass!)
1.1 Make Tests Pass Again?
When refactoring the convention is to not touch the tests,
However the first test in our test.js file checks the state
of the model if no action is passed into the update function:
test('Test Update update(0) returns 0 (current state)', function(assert) {
var result = update(0);
assert.equal(result, 0);
});
This test is still relevant because the Elm Architecture always
returns the model unchanged if no action is given.
We need to update this test to reflect the change in the model signature:
test('update({counters:[0]}) returns {counters:[0]} (current state unmodified)', function(assert) {
var result = update({counters:[0]});
assert.equal(result.counters[0], 0);
});
Snapshot of the code/changes required to make tests pass again: https://github.com/dwyl/learn-elm-architecture-in-javascript/pull/41/commits/c65d491d69d2d68964df36817ccbff9de3275f0b
2. Render Multiple Counters using New Model
Updating the model was the start of our refactoring journey,
if we were to include multiple elements in the counters Array
now, before updating the view function,
we would still only see one
counter on the page because our view
does not yet "know" how to render multiple counters.
2.1 Update the view function
Given that we have updated the model to be a an Object
with a counters Array, we need to update our view function
to render as many counters as we have elements
in the counters Array.
First create a "container" DOM element so each counter (the increment, decrement and reset buttons and text display of the current counter value) can be "wrapped" together:
function container(index, elements) {
var con = document.createElement('section');
con.id = index;
con.className = 'counter';
elements.forEach(function(el) { con.appendChild(el) });
return con;
}
This container function will be used
in the re-worked view function (which we are modifying next!)
Let's modify the view function to accommodate
Before:
function view(signal, model, root) {
empty(root); // clear root element before
[ // Store DOM nodes in an array
button('+', signal, Inc), // then iterate to append them
div('count', model), // create div with stat as text
button('-', signal, Dec), // decrement counter
button('Reset', signal, Res) // reset counter
].forEach(function(el){ root.appendChild(el) }); // forEach is ES5 so IE9+
}
After:
function view(signal, model, root) {
empty(root); // clear root element before re-rendering the App (DOM).
model.counters.map(function(counter, index) { // one counter for each
return container(index, [ // wrap DOM nodes in an "container"
button('+', signal, Inc + '-' + index), // append index to action
div('count', counter), // create div w/ count as text
button('-', signal, Dec + '-' + index), // decrement counter
button('Reset', signal, Res + '-' + index) // reset counter
]);
}).forEach(function (el) { root.appendChild(el) }); // forEach is ES5 so IE9+
}
The key differences are:
- Wrapping the counter in a "container" DOM element.
- Appending the index (in the
model.countersArray) to eachactione.g:Inc + '-' + indexsuch that each button is unique and we can derive the exact counter that needs to be Incremented.
2.2 Refactor the update function
The update function needs to be updated to support
Before:
function update(model, action) { // Update function takes the current state
switch(action) { // and an action (String) runs a switch
case Inc: return model + 1; // add 1 to the model
case Dec: return model - 1; // subtract 1 from model
case Res: return 0; // reset state to 0 (Zero) git.io/v9KJk
default: return model; // if no action, return curent state.
} // (default action always returns current)
}
After:
function update(model, action) {
var parts = action ? action.split('-') : []; // e.g: inc-0 where 0 is the counter "id"
var act = parts[0];
var index = parts[1] || 0; // default to 0 (assume only one counter)
var new_model = JSON.parse(JSON.stringify(model)) // "clone" the model
switch(act) { // and an action (String) runs a switch
case Inc:
new_model.counters[index] = model.counters[index] + 1;
break;
case Dec:
new_model.counters[index] = model.counters[index] - 1;
break;
case Res: // use ES6 Array.fill to create a new array with values set to 0:
new_model.counters[index] = 0;
break;
default: return model; // if action not defined, return current state.
}
return new_model;
}
Try it: http://127.0.0.1:8000/examples/multiple-counters/?coverage

If you can simplify this code, we're happy to receive a Pull Request! Share your thoughts on: https://github.com/dwyl/learn-elm-architecture-in-javascript/issues/40