Skip to content

Multiple counters issue#40 #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b2354d5
adds @w0rm "View Theater" Elm Architecture diagram. closes https://gi…
nelsonic Jun 10, 2018
e8a3212
add Section 10 with instructions for Multiple Counters on the Same Pa…
nelsonic Jun 10, 2018
4c44077
new line in example/counter-reset (line length linting)
nelsonic Jun 10, 2018
b11473c
borrow code from example/counter-reset for Multiple Counters example #40
nelsonic Jun 10, 2018
c65d491
Step 10.1 refactor example/counter-reset to use model = { counters: […
nelsonic Jun 10, 2018
ca87db0
add correct Creative Commons License Attribution 4.0 International (C…
nelsonic Jun 11, 2018
f2ae57b
update link to tweet https://twitter.com/01k/status/98652860263535820…
nelsonic Jun 11, 2018
50ab6b3
fix attirbution to Kolja Wilcke https://twitter.com/01k/status/986528…
nelsonic Jun 11, 2018
0cc51b2
add multiple instances example for multiple counters quest #40
nelsonic Jun 12, 2018
781772f
[WiP] busy refactoring for multiple counters #40
nelsonic Jun 14, 2018
4a543ca
refactor view and update functions to accomodate multiple counters fo…
nelsonic Jun 15, 2018
ced7ab9
add CSS to float multiple counters for #40
nelsonic Jun 15, 2018
ff85e59
set default values for action and index in multiple-countes/counter.j…
nelsonic Jun 15, 2018
2f19809
finish writeup on multiple counters for #40
nelsonic Jun 16, 2018
78fcd81
adds link to https://github.com/dwyl/learn-elm-architecture-in-javasc…
nelsonic Jun 16, 2018
45475fe
re-use counter.js from example/counter-reset in examples/multiple-cou…
nelsonic Jun 16, 2018
7d6871e
re-use counter.js from example/counter-reset in examples/multiple-cou…
nelsonic Jun 16, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 60 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ When we encounter this type of "_what is the **right way**_?"
question <br />
we always follow [***Occam's Razor***](https://en.wikipedia.org/wiki/Occam%27s_razor) and _ask_:
what is the ***simplest way***? <br />
In the case of web application organization,
In the case of web application organization,
the ***answer*** is:
the "**Elm _Architecture_**".

Expand All @@ -63,13 +63,13 @@ When compared to _other_ ways of organizing your code,
+ Easier to _understand_ what is going on in more advanced apps because there is no complex logic,
only one basic principal
and the "_flow_" is _always_ the same.
+ ***Uni-directional data-flow*** means "state"
+ ***Uni-directional data-flow*** means "state"
of the app is always _predictable_;
given a specific starting "state" and sequence of update actions
the output/end state will _always_ be the same. This makes testing/testability
very easy!
+ There's **no** "***middle man***" to complicate things
(_the way there is in other application architectures
(_the way there is in other application architectures
such as
[Model-view-Presenter](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) or "Model-View-ViewModel" (MVVM) which is "overkill" for most apps_.)

Expand All @@ -85,12 +85,12 @@ their code/app in a _sane_, predictable and testable way.

![all-you-need-is-less](https://cloud.githubusercontent.com/assets/194400/25772135/a4230490-325b-11e7-9f12-da19fa4eb5e9.png)

+ _Basic_ JavaScript Knowledge.
see: [github.com/iteles/**Javascript**-the-**Good-Parts**-notes](https://github.com/iteles/Javascript-the-Good-Parts-notes)
+ _Basic_ Understanding of TDD. If you are _completely_ new to TDD,
please see: https://github.com/dwyl/learn-tdd
+ A computer
+ 30 minutes.
+ **_Basic_ JavaScript Knowledge**.
see: [github.com/dwyl/**Javascript**-the-**Good-Parts**-notes](https://github.com/iteles/Javascript-the-Good-Parts-notes)
+ _Basic_ Understanding of **TDD**. If you are _completely_ new to TDD,
please see: [github.com/dwyl/**learn-tdd**](https://github.com/dwyl/learn-tdd)
+ A computer with a Web Browser.
+ **30 minutes**.

> No other knowledge is assumed or implied.
If you have **_any_ questions**, ***please ask***: <br />
Expand All @@ -105,17 +105,34 @@ If you have **_any_ questions**, ***please ask***: <br />

Start with a few definitions:

+ **M**odel - or "data model" is the place where all data
is often referred to as the application's `state`.
+ **M**odel - or "data model" is the place where all data stored;
often referred to as the application's `state`.
+ **U**pdate - how the app handles `actions` performed
by people and `update` the `state`.
+ **V**iew - what the people using the app can _see_;
a way to `view` the Model (counter) as `HTML`
rendered by the web browser.
+ **V**iew - what people using the app can _see_;
a way to `view` the Model (counter) as `HTML`
rendered in a web browser.

![elm-muv-architecture-diagram](https://cloud.githubusercontent.com/assets/194400/25773775/b6a4b850-327b-11e7-9857-79b6972b49c3.png)

Don't worry if you don't understand this diagram (_yet_),
<br />
If you're not into flow diagrams, don't worry, there not everyone is, <br />
a _much_ more "user friendly" _explanation_
of **The Elm Architecture** ("TEA") <br />
is
[**Kolja Wilcke**'s](https://twitter.com/01k/status/986528602635358208?s=20) _fantastic_
["View Theater" diagram](https://github.com/w0rm/creating-a-fun-game-with-elm/blob/001baf05b3879d12c0ff70075e9d25e8cc7c4656/assets/the-elm-architecture1.jpg):

<div align="center">

![elm-architecture-puppet-show](https://user-images.githubusercontent.com/194400/41206474-62d1a6a4-6cfc-11e8-8029-e27b7aa7f069.jpg)

Creative Commons License:
[Attribution 4.0 International (CC BY 4.0)](https://twitter.com/01k/status/986528602635358208?s=20)

</div>
<br />
If this diagram is not clear (_yet_), again, don't panic,
it will all become clear when you start seeing it in _action_ (_below_)!


Expand Down Expand Up @@ -666,6 +683,34 @@ button('Reset', signal, Res)
```
![reset-counter](https://cloud.githubusercontent.com/assets/194400/25822128/82eb7a8e-342f-11e7-9cd0-1a69d95ee878.gif)

<br />

### 10. _Next Level: Multiple Counters_!

Now that you have _understood_ the Elm Architecture
by following the basic (_single_) counter example,
it's time to take the example to the next level:
multiple counters on the same page!

#### Multiple Counters Exercise

Follow your _instincts_ and `try` to the following:

**1.** **Refactor** the "reset counter" example
to use an `Object` for the `model` (_instead of an_ `Integer`) <br />
**e.g**: `var model = { counters: [0] }` <br />
where the value of the first element in the `model.counters` Array
is the value for the _single_ counter example. <br />

**2.** **Display _multiple_ counters** on the **_same_ page**
using the `var model = { counters: [0] }` approach. <br />

**3.** **Write tests** for the scenario where there
are multiple counters on the same page.

Once you have had a go, checkout our solutions: `examples/multiple-counters`
and corresponding writeup:
[**multiple-counters.md**](https://github.com/dwyl/learn-elm-architecture-in-javascript/blob/master/multiple-counters.md)


<br /> <br />
Expand Down
3 changes: 2 additions & 1 deletion examples/counter-reset/counter.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ function init(doc){

/* The code block below ONLY Applies to tests run using Node.js */
/* istanbul ignore next */
if (typeof module !== 'undefined' && module.exports) { module.exports = {
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
view: view,
mount: mount,
update: update,
Expand Down
52 changes: 52 additions & 0 deletions examples/multiple-counters-instances/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang=”en-GB”>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Elm Architecture in JS - Counter Reset</title>
<link rel="shortcut icon"
href="http://www.dwyl.io/images/favicon.ico" type="image/x-icon">
</head>
<body>
<script src="../counter-reset/counter.js" data-cover></script> <!-- load counter once -->
<div id="app" class="counter"></div>
<div id="app1" class="counter"></div>
<div id="app2" class="counter"></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>

<style>
.counter {
border: 1px solid black;
width: 28%;
float: left;
margin: 1%;
padding: 1%;
}
body{
font-family: Courier, "Lucida Console", monospace;
font-size: 4em;
text-align: center;
}
button {
font-size: 0.5em; color:white; border:5px solid; border-radius: 0.5em;
padding:0.1em; margin: 0.2em auto;
display: block; width: 90%;
}
.inc {
background-color: #2ecc71; border-color: #27ae60;
}
.dec {
background-color: #e74c3c; border-color: #c0392b;
}
.reset {
background-color: #f39c12; border-color: #e67e22;
}
</style>

</body>
</html>
89 changes: 89 additions & 0 deletions examples/multiple-counters/counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Define the Component's Actions:
var Inc = 'inc'; // increment the counter
var Dec = 'dec'; // decrement the counter
var Res = 'reset'; // reset counter: git.io/v9KJk

function update(model, action) { // Update function takes the current state
var parts = action ? action.split('-') : []; // e.g: inc-0 where 0 is the counter "id"
var act = parts[0];
var index = parts[1] || 0;
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 curent state.
}
return new_model;
}

function view(signal, model, root) {
empty(root); // clear root element before re-rendering the App (DOM).
model.counters.map(function(counter, index) {
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+
}

// Mount Function receives all MUV and mounts the app in the "root" DOM Element
function mount(model, update, view, root_element_id) {
var root = document.getElementById(root_element_id); // root DOM element
function signal(action) { // signal function takes action
return function callback() { // and returns callback
model = update(model, action); // update model according to action
view(signal, model, root); // subsequent re-rendering
};
};
view(signal, model, root); // render initial model (once)
}

// The following are "Helper" Functions which each "Do ONLY One Thing" and are
// used in the "View" function to render the Model (State) to the Browser DOM:

// empty the contents of a given DOM element "node" (before re-rendering)
function empty(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
} // Inspired by: stackoverflow.com/a/3955238/1148249

// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section
function container(index, elements) {
var con = document.createElement('section');
con.id = index;
con.className = 'counter';
elements.forEach(function(el) { con.appendChild(el) });
return con;
}

function button(text, signal, action) {
var button = document.createElement('button');
var text = document.createTextNode(text); // human-readable button text
button.appendChild(text); // text goes *inside* not attrib
button.className = action.split('-')[0]; // use action as CSS class
button.id = action;
// console.log(signal, ' action:', action)
button.onclick = signal(action); // onclick tells how to process
return button; // return the DOM node(s)
} // how to create a button in JavaScript: stackoverflow.com/a/8650996/1148249

function div(divid, text) {
var div = document.createElement('div');
div.id = divid;
div.className = divid;
if(text !== undefined) { // if text is passed in render it in a "Text Node"
var txt = document.createTextNode(text);
div.appendChild(txt);
}
return div;
}
29 changes: 29 additions & 0 deletions examples/multiple-counters/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang=”en-GB”>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Elm Architecture in JS - Counter Reset</title>
<link rel="shortcut icon"
href="http://www.dwyl.io/images/favicon.ico" type="image/x-icon">
<!-- CSS Styles are 100% optional. but they make it look *much* nicer -->
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div id="app"></div>
<script src="counter.js" data-cover></script> <!-- load counter -->
<script> mount({counters:[0, 1, 2]}, update, view, 'app'); </script>
<!-- Below this point is all related to the Tests for the App -->
<div id="test-app"></div> <!-- Create a test-app div to mount the app -->
<div id="qunit"></div> <!-- test results are displayed here -->
<!-- Load the QUnit CSS file from CDN - require to display our tests -->
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.18.0.css">
<!-- Load the QUnit Testing Framework from CDN - to run the tests -->
<script src="https://code.jquery.com/qunit/qunit-1.18.0.js"></script>
<!-- Load Blanket.js from CDN - for test coverage stats -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/blanket.js/1.1.4/blanket.js">
</script>
<script src="test.js"></script> <!-- always load test.js last -->
</body>
</html>
74 changes: 74 additions & 0 deletions examples/multiple-counters/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
var id = 'test-app';

test('update({counters:[0]}) returns {counters:[0]} (current state unmodified)',
function(assert) {
var result = update({counters:[0]});
assert.equal(result.counters[0], 0);
});

test('Test Update increment: update(1, "inc") returns 2', function(assert) {
var result = update({counters: [1] }, "inc");
console.log('result', result);
assert.equal(result.counters[0], 2);
});


test('Test Update decrement: update(1, "dec") returns 0', function(assert) {
var result = update({counters: [1] }, "dec");
assert.equal(result.counters[0], 0);
});

test('Test negative state: update(-9, "inc") returns -8', function(assert) {
var result = update({counters: [-9] }, "inc");
assert.equal(result.counters[0], -8);
});

test('mount({model: 7, update: update, view: view}, "'
+ id +'") sets initial state to 7', function(assert) {
mount({counters:[7]}, update, view, id);
var state = document.getElementById(id)
.getElementsByClassName('count')[0].textContent;
assert.equal(state, 7);
});

test('empty("test-app") should clear DOM in root node', function(assert) {
empty(document.getElementById(id));
mount({counters:[7]}, update, view, id);
empty(document.getElementById(id));
var result = document.getElementById(id).innerHtml
assert.equal(result, undefined);
});

test('click on "+" button to re-render state (increment model by 1)',
function(assert) {
document.body.appendChild(div(id));
mount({counters:[7]}, update, view, id);
document.getElementById(id).getElementsByClassName('inc')[0].click();
var state = document.getElementById(id)
.getElementsByClassName('count')[0].textContent;
assert.equal(state, 8); // model was incremented successfully
empty(document.getElementById(id)); // clean up after tests
});

// Reset Functionality

test('Test reset counter when model/state is 6 returns 0', function(assert) {
var result = update({counters:[7]}, "reset");
assert.equal(result.counters[0], 0);
});

test('reset button should be present on page', function(assert) {
var reset = document.getElementsByClassName('reset');
assert.equal(reset.length, 3);
});

test('Click reset button resets state to 0', function(assert) {
mount({counters:[7]}, update, view, id);
var root = document.getElementById(id);
assert.equal(root.getElementsByClassName('count')[0].textContent, 7);
var btn = root.getElementsByClassName("reset")[0]; // click reset button
btn.click(); // Click the Reset button!
var state = root.getElementsByClassName('count')[0].textContent;
assert.equal(state, 0); // state was successfully reset to 0!
empty(root); // clean up after tests
});
Loading