Skip to content

Latest commit

 

History

History
207 lines (166 loc) · 7.33 KB

multiple-counters.md

File metadata and controls

207 lines (166 loc) · 7.33 KB

Multiple Counters Exercise!

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>

elm-arch-multiple-counters-naive 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: counter-tests-broken

(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.counters Array) to each action e.g: Inc + '-' + index such 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

image

If you can simplify this code, we're happy to receive a Pull Request! Share your thoughts on: #40