Skip to content

Commit 8097e4b

Browse files
authored
Merge pull request #41 from dwyl/multiple-counters-issue#40
Multiple counters issue#40
2 parents 145202f + 7d6871e commit 8097e4b

File tree

8 files changed

+528
-16
lines changed

8 files changed

+528
-16
lines changed

README.md

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ When we encounter this type of "_what is the **right way**_?"
5353
question <br />
5454
we always follow [***Occam's Razor***](https://en.wikipedia.org/wiki/Occam%27s_razor) and _ask_:
5555
what is the ***simplest way***? <br />
56-
In the case of web application organization,
56+
In the case of web application organization,
5757
the ***answer*** is:
5858
the "**Elm _Architecture_**".
5959

@@ -63,13 +63,13 @@ When compared to _other_ ways of organizing your code,
6363
+ Easier to _understand_ what is going on in more advanced apps because there is no complex logic,
6464
only one basic principal
6565
and the "_flow_" is _always_ the same.
66-
+ ***Uni-directional data-flow*** means "state"
66+
+ ***Uni-directional data-flow*** means "state"
6767
of the app is always _predictable_;
6868
given a specific starting "state" and sequence of update actions
6969
the output/end state will _always_ be the same. This makes testing/testability
7070
very easy!
7171
+ There's **no** "***middle man***" to complicate things
72-
(_the way there is in other application architectures
72+
(_the way there is in other application architectures
7373
such as
7474
[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_.)
7575

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

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

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

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

106106
Start with a few definitions:
107107

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

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

118-
Don't worry if you don't understand this diagram (_yet_),
118+
<br />
119+
If you're not into flow diagrams, don't worry, there not everyone is, <br />
120+
a _much_ more "user friendly" _explanation_
121+
of **The Elm Architecture** ("TEA") <br />
122+
is
123+
[**Kolja Wilcke**'s](https://twitter.com/01k/status/986528602635358208?s=20) _fantastic_
124+
["View Theater" diagram](https://github.com/w0rm/creating-a-fun-game-with-elm/blob/001baf05b3879d12c0ff70075e9d25e8cc7c4656/assets/the-elm-architecture1.jpg):
125+
126+
<div align="center">
127+
128+
![elm-architecture-puppet-show](https://user-images.githubusercontent.com/194400/41206474-62d1a6a4-6cfc-11e8-8029-e27b7aa7f069.jpg)
129+
130+
Creative Commons License:
131+
[Attribution 4.0 International (CC BY 4.0)](https://twitter.com/01k/status/986528602635358208?s=20)
132+
133+
</div>
134+
<br />
135+
If this diagram is not clear (_yet_), again, don't panic,
119136
it will all become clear when you start seeing it in _action_ (_below_)!
120137

121138

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

686+
<br />
687+
688+
### 10. _Next Level: Multiple Counters_!
689+
690+
Now that you have _understood_ the Elm Architecture
691+
by following the basic (_single_) counter example,
692+
it's time to take the example to the next level:
693+
multiple counters on the same page!
694+
695+
#### Multiple Counters Exercise
696+
697+
Follow your _instincts_ and `try` to the following:
698+
699+
**1.** **Refactor** the "reset counter" example
700+
to use an `Object` for the `model` (_instead of an_ `Integer`) <br />
701+
**e.g**: `var model = { counters: [0] }` <br />
702+
where the value of the first element in the `model.counters` Array
703+
is the value for the _single_ counter example. <br />
704+
705+
**2.** **Display _multiple_ counters** on the **_same_ page**
706+
using the `var model = { counters: [0] }` approach. <br />
707+
708+
**3.** **Write tests** for the scenario where there
709+
are multiple counters on the same page.
710+
711+
Once you have had a go, checkout our solutions: `examples/multiple-counters`
712+
and corresponding writeup:
713+
[**multiple-counters.md**](https://github.com/dwyl/learn-elm-architecture-in-javascript/blob/master/multiple-counters.md)
669714

670715

671716
<br /> <br />

examples/counter-reset/counter.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ function init(doc){
7272

7373
/* The code block below ONLY Applies to tests run using Node.js */
7474
/* istanbul ignore next */
75-
if (typeof module !== 'undefined' && module.exports) { module.exports = {
75+
if (typeof module !== 'undefined' && module.exports) {
76+
module.exports = {
7677
view: view,
7778
mount: mount,
7879
update: update,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!DOCTYPE html>
2+
<html lang=”en-GB”>
3+
<head>
4+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5+
<meta name="viewport"
6+
content="width=device-width, initial-scale=1.0, user-scalable=yes">
7+
<title>Elm Architecture in JS - Counter Reset</title>
8+
<link rel="shortcut icon"
9+
href="http://www.dwyl.io/images/favicon.ico" type="image/x-icon">
10+
</head>
11+
<body>
12+
<script src="../counter-reset/counter.js" data-cover></script> <!-- load counter once -->
13+
<div id="app" class="counter"></div>
14+
<div id="app1" class="counter"></div>
15+
<div id="app2" class="counter"></div>
16+
<script> // Mount as many apps as you like:
17+
mount(0, update, view, 'app');
18+
mount(1, update, view, 'app1');
19+
mount(2, update, view, 'app2');
20+
</script>
21+
22+
<style>
23+
.counter {
24+
border: 1px solid black;
25+
width: 28%;
26+
float: left;
27+
margin: 1%;
28+
padding: 1%;
29+
}
30+
body{
31+
font-family: Courier, "Lucida Console", monospace;
32+
font-size: 4em;
33+
text-align: center;
34+
}
35+
button {
36+
font-size: 0.5em; color:white; border:5px solid; border-radius: 0.5em;
37+
padding:0.1em; margin: 0.2em auto;
38+
display: block; width: 90%;
39+
}
40+
.inc {
41+
background-color: #2ecc71; border-color: #27ae60;
42+
}
43+
.dec {
44+
background-color: #e74c3c; border-color: #c0392b;
45+
}
46+
.reset {
47+
background-color: #f39c12; border-color: #e67e22;
48+
}
49+
</style>
50+
51+
</body>
52+
</html>

examples/multiple-counters/counter.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Define the Component's Actions:
2+
var Inc = 'inc'; // increment the counter
3+
var Dec = 'dec'; // decrement the counter
4+
var Res = 'reset'; // reset counter: git.io/v9KJk
5+
6+
function update(model, action) { // Update function takes the current state
7+
var parts = action ? action.split('-') : []; // e.g: inc-0 where 0 is the counter "id"
8+
var act = parts[0];
9+
var index = parts[1] || 0;
10+
var new_model = JSON.parse(JSON.stringify(model)) // "clone" the model
11+
switch(act) { // and an action (String) runs a switch
12+
case Inc:
13+
new_model.counters[index] = model.counters[index] + 1;
14+
break;
15+
case Dec:
16+
new_model.counters[index] = model.counters[index] - 1;
17+
break;
18+
case Res: // use ES6 Array.fill to create a new array with values set to 0:
19+
new_model.counters[index] = 0;
20+
break;
21+
default: return model; // if action not defined, return curent state.
22+
}
23+
return new_model;
24+
}
25+
26+
function view(signal, model, root) {
27+
empty(root); // clear root element before re-rendering the App (DOM).
28+
model.counters.map(function(counter, index) {
29+
return container(index, [ // wrap DOM nodes in an "container"
30+
button('+', signal, Inc + '-' + index), // append index to action
31+
div('count', counter), // create div w/ count as text
32+
button('-', signal, Dec + '-' + index), // decrement counter
33+
button('Reset', signal, Res + '-' + index) // reset counter
34+
]);
35+
}).forEach(function (el) { root.appendChild(el) }); // forEach is ES5 so IE9+
36+
}
37+
38+
// Mount Function receives all MUV and mounts the app in the "root" DOM Element
39+
function mount(model, update, view, root_element_id) {
40+
var root = document.getElementById(root_element_id); // root DOM element
41+
function signal(action) { // signal function takes action
42+
return function callback() { // and returns callback
43+
model = update(model, action); // update model according to action
44+
view(signal, model, root); // subsequent re-rendering
45+
};
46+
};
47+
view(signal, model, root); // render initial model (once)
48+
}
49+
50+
// The following are "Helper" Functions which each "Do ONLY One Thing" and are
51+
// used in the "View" function to render the Model (State) to the Browser DOM:
52+
53+
// empty the contents of a given DOM element "node" (before re-rendering)
54+
function empty(node) {
55+
while (node.firstChild) {
56+
node.removeChild(node.firstChild);
57+
}
58+
} // Inspired by: stackoverflow.com/a/3955238/1148249
59+
60+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section
61+
function container(index, elements) {
62+
var con = document.createElement('section');
63+
con.id = index;
64+
con.className = 'counter';
65+
elements.forEach(function(el) { con.appendChild(el) });
66+
return con;
67+
}
68+
69+
function button(text, signal, action) {
70+
var button = document.createElement('button');
71+
var text = document.createTextNode(text); // human-readable button text
72+
button.appendChild(text); // text goes *inside* not attrib
73+
button.className = action.split('-')[0]; // use action as CSS class
74+
button.id = action;
75+
// console.log(signal, ' action:', action)
76+
button.onclick = signal(action); // onclick tells how to process
77+
return button; // return the DOM node(s)
78+
} // how to create a button in JavaScript: stackoverflow.com/a/8650996/1148249
79+
80+
function div(divid, text) {
81+
var div = document.createElement('div');
82+
div.id = divid;
83+
div.className = divid;
84+
if(text !== undefined) { // if text is passed in render it in a "Text Node"
85+
var txt = document.createTextNode(text);
86+
div.appendChild(txt);
87+
}
88+
return div;
89+
}

examples/multiple-counters/index.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!DOCTYPE html>
2+
<html lang=”en-GB”>
3+
<head>
4+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
5+
<meta name="viewport"
6+
content="width=device-width, initial-scale=1.0, user-scalable=yes">
7+
<title>Elm Architecture in JS - Counter Reset</title>
8+
<link rel="shortcut icon"
9+
href="http://www.dwyl.io/images/favicon.ico" type="image/x-icon">
10+
<!-- CSS Styles are 100% optional. but they make it look *much* nicer -->
11+
<link rel="stylesheet" href="../style.css">
12+
</head>
13+
<body>
14+
<div id="app"></div>
15+
<script src="counter.js" data-cover></script> <!-- load counter -->
16+
<script> mount({counters:[0, 1, 2]}, update, view, 'app'); </script>
17+
<!-- Below this point is all related to the Tests for the App -->
18+
<div id="test-app"></div> <!-- Create a test-app div to mount the app -->
19+
<div id="qunit"></div> <!-- test results are displayed here -->
20+
<!-- Load the QUnit CSS file from CDN - require to display our tests -->
21+
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.18.0.css">
22+
<!-- Load the QUnit Testing Framework from CDN - to run the tests -->
23+
<script src="https://code.jquery.com/qunit/qunit-1.18.0.js"></script>
24+
<!-- Load Blanket.js from CDN - for test coverage stats -->
25+
<script src="https://cdnjs.cloudflare.com/ajax/libs/blanket.js/1.1.4/blanket.js">
26+
</script>
27+
<script src="test.js"></script> <!-- always load test.js last -->
28+
</body>
29+
</html>

examples/multiple-counters/test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
var id = 'test-app';
2+
3+
test('update({counters:[0]}) returns {counters:[0]} (current state unmodified)',
4+
function(assert) {
5+
var result = update({counters:[0]});
6+
assert.equal(result.counters[0], 0);
7+
});
8+
9+
test('Test Update increment: update(1, "inc") returns 2', function(assert) {
10+
var result = update({counters: [1] }, "inc");
11+
console.log('result', result);
12+
assert.equal(result.counters[0], 2);
13+
});
14+
15+
16+
test('Test Update decrement: update(1, "dec") returns 0', function(assert) {
17+
var result = update({counters: [1] }, "dec");
18+
assert.equal(result.counters[0], 0);
19+
});
20+
21+
test('Test negative state: update(-9, "inc") returns -8', function(assert) {
22+
var result = update({counters: [-9] }, "inc");
23+
assert.equal(result.counters[0], -8);
24+
});
25+
26+
test('mount({model: 7, update: update, view: view}, "'
27+
+ id +'") sets initial state to 7', function(assert) {
28+
mount({counters:[7]}, update, view, id);
29+
var state = document.getElementById(id)
30+
.getElementsByClassName('count')[0].textContent;
31+
assert.equal(state, 7);
32+
});
33+
34+
test('empty("test-app") should clear DOM in root node', function(assert) {
35+
empty(document.getElementById(id));
36+
mount({counters:[7]}, update, view, id);
37+
empty(document.getElementById(id));
38+
var result = document.getElementById(id).innerHtml
39+
assert.equal(result, undefined);
40+
});
41+
42+
test('click on "+" button to re-render state (increment model by 1)',
43+
function(assert) {
44+
document.body.appendChild(div(id));
45+
mount({counters:[7]}, update, view, id);
46+
document.getElementById(id).getElementsByClassName('inc')[0].click();
47+
var state = document.getElementById(id)
48+
.getElementsByClassName('count')[0].textContent;
49+
assert.equal(state, 8); // model was incremented successfully
50+
empty(document.getElementById(id)); // clean up after tests
51+
});
52+
53+
// Reset Functionality
54+
55+
test('Test reset counter when model/state is 6 returns 0', function(assert) {
56+
var result = update({counters:[7]}, "reset");
57+
assert.equal(result.counters[0], 0);
58+
});
59+
60+
test('reset button should be present on page', function(assert) {
61+
var reset = document.getElementsByClassName('reset');
62+
assert.equal(reset.length, 3);
63+
});
64+
65+
test('Click reset button resets state to 0', function(assert) {
66+
mount({counters:[7]}, update, view, id);
67+
var root = document.getElementById(id);
68+
assert.equal(root.getElementsByClassName('count')[0].textContent, 7);
69+
var btn = root.getElementsByClassName("reset")[0]; // click reset button
70+
btn.click(); // Click the Reset button!
71+
var state = root.getElementsByClassName('count')[0].textContent;
72+
assert.equal(state, 0); // state was successfully reset to 0!
73+
empty(root); // clean up after tests
74+
});

0 commit comments

Comments
 (0)