diff --git a/README.md b/README.md index 71e51ef4..e615f705 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,15 @@ very easy! [![everybodys-gotta-learn-sometime](https://cloud.githubusercontent.com/assets/194400/25806590/a1619644-33fb-11e7-8b84-1a21be188fb7.png)](https://www.youtube.com/results?q=The+Korgis+-+Everybody%27s+Gotta+Learn+Sometime) Anyone who knows a _little_ bit of JavaScript -and wants to learn how to organize/structure -their code in the most _sane_ and easy to understand way. +and wants to learn how to organize/structure
+their code/app in the most _sane_, predictable and testable way. ### _Prerequisites_? ![all-you-need-is-less](https://cloud.githubusercontent.com/assets/194400/25772135/a4230490-325b-11e7-9f12-da19fa4eb5e9.png) + _Basic_ JavaScript Knowledge. -see: https://github.com/iteles/Javascript-the-Good-Parts-notes +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 @@ -86,7 +86,7 @@ Start with a few definitions: ![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_), -it will all become clear when you start seeing it in _action_! +it will all become clear when you start seeing it in _action_ (_below_)! ## _How?_ @@ -103,7 +103,6 @@ When you open `examples/counter-basic/index.html` you should see: ![elm-architecture-counter](https://cloud.githubusercontent.com/assets/194400/25780607/d2251eac-3321-11e7-8e65-9abbfa204fb3.gif) - Try clicking on the buttons to increase/decrease the counter. ### 3. Edit Some Code! @@ -221,9 +220,11 @@ e.g: ### 7. Read the _Tests_! -In the _first_ example we kept everything in one file (`index.html`) -for simplicity. In order to write tests, we need to _split_ out the -JavaScript code from the HTML. +In the _first_ example we kept everything in +_one_ file (`index.html`) for simplicity.
+In order to write tests (_and collect coverage_), +we need to _separate_ out
+the JavaScript code from the HTML. For this example there are 3 _separate_ files: @@ -234,7 +235,7 @@ Let's start by opening the `/examples/counter-basic-test/index.html` file in a web browser:
http://127.0.0.1:8000/examples/counter-basic-test/?coverage -![counter-tests](https://cloud.githubusercontent.com/assets/194400/25776602/9df4b550-32ba-11e7-958b-25baaeeea212.png) +![counter-coverage](https://cloud.githubusercontent.com/assets/194400/25816673/b994d25a-341c-11e7-8fd1-52e136fb7152.png) Because all functions are "pure" testing the `update` function is _very_ easy: @@ -257,8 +258,8 @@ test('Test Update decrement: update(3, "dec") returns 2', function(assert) { ``` open: `examples/counter-basic-test/test.js` to see these and _other_ tests. -> The _reason_ why Apps built using the Elm Architecture are _**so -easy**_ to _understand_ +> The _reason_ why Apps built using the Elm Architecture +are _**so easy**_ to _understand_
(_or ["**reason about**"](http://stackoverflow.com/q/18666821)_) and _test_ is that all functions are "Pure". @@ -308,15 +309,96 @@ console.log(increment(increment(increment(counter)))); // 3 ``` see: https://repl.it/FIpV +#### 8.3 Counter Example written in "Impure" JS + +It's _easy_ to get +[_suckered_](http://www.urbandictionary.com/define.php?term=suckered) +into thinking that the "_impure_" version of the counter
+`examples/counter-basic-impure/index.html` +is "_simpler_" ...
+the _complete_ code (_including HTML and JS_) is ***8 lines***: + +```html + +
0
+ + +``` + + +This counter _does_ the same thing as +our Elm Architecture example (_above_),
+and to the _end-user_ the UI **_looks_ identical**: + +![counter-impure-665](https://cloud.githubusercontent.com/assets/194400/25816521/3a0e0722-341c-11e7-9afc-269abb4bb225.png) + + +The difference is that in the _impure_ example is "_mutating state_" +and it's impossible to predict what that state will be! + +> _Annoyingly, for the person explaining the benefits +of function "purity" and the virtues of the Elm Architecture
+the "impure" example is both **fewer lines of code** +(which means it **loads faster**!), takes less time to read
+and renders faster because only the `
` text content +is being updated on each update!
+> This is why it can often be **difficult to explain** to "**non-technical**" +**people** that code which has similar output
+on the **screen**(s) +might **not the same** quality "**behind the scenes**"!_
+> _Writing impure functions is like setting off on a marathon run after +[tying your shoelaces **incorrectly**](https://youtu.be/zAFcV7zuUDA) ...
+You might be "OK" for a while, but pretty soon your laces will come undone +and you will have to **stop** and **re-do** them._ + + To conclude: Pure functions doe not mutate a "global" state -and are thus predictable and thus easy to test; +and are thus predictable and easy to test; we _always_ use "Pure" functions in Apps built with the Elm Architecture. The moment you use "_impure_" functions you forfeit reliability. -### 9. Extend the Counter Example following "TDD" +### 9. Extend the Counter Example following "TDD": Reset the Count! + +As you (_hopefully_) recall from our +[Step-by-Step TDD Tutorial](https://github.com/dwyl/learn-tdd), +when we craft code following the "TDD" approach, +we go through the following steps: +1. Read and understand the "user story" (_e.g: [issues/5](https://github.com/dwyl/learn-elm-architecture-in-javascript/issues/5) in this case_) +![reset-counter-user-story](https://cloud.githubusercontent.com/assets/194400/25817522/84fdd9bc-341f-11e7-9efd-406d76a3b1f3.png)
+2. Make sure the "_acceptance criteria_" are clear +(_the checklist in the issue_) +3. Write your test(s) based on the acceptance criteria. +(_Tip: a single feature - in this case resetting the counter - can + and often `should` have multiple tests to cover all cases._) +4. Write code to make the test(s) pass. + +#### 9.1 Tests for Resetting the Counter + +We _always_ start with the Model test(s) +(_because they are the easiest_): + +```js +test('Test: reset counter returns 0', function(assert) { + var result = update(6, "res"); + assert.equal(result, 0); +}); +``` +#### 9.2 Watch it Fail! + +Watch the test _fail_ in your Web Browser:
+![reset-counter-failing-test](https://cloud.githubusercontent.com/assets/194400/25818566/e2c33152-3422-11e7-9c4c-9ecd9fa9ffc6.png) + +#### 9.3 Make it Pass (_writing the minimum code_) -As you (_hopefully_) recall from our TDD Tutorial +In the case of an App written with the Elm Architecture, +the minimum code is: ++ Action ++ Update (_case and/or function_)

@@ -389,7 +471,9 @@ to build something full-featured and easy/fast to read!! If you can build with "ES5" JavaScript:
a) you side-step the [_noise_](https://twitter.com/iamdevloper/status/610191865216786432) -and focus on core skills that _already_ work everywhere!
+and focus on core skills that _already_ work everywhere! +(_don't worry you can always "top-up" your +JS knowledge later with ES6, etc!)
b) you don't need to waste time installing [_**Two Hundred Megabytes**_](https://cloud.githubusercontent.com/assets/194400/13321493/39fcfa30-dbc7-11e5-8b05-f046675f9cb6.png) of dependencies just to run a simple project!
diff --git a/examples/counter-reset/counter.js b/examples/counter-reset/counter.js new file mode 100644 index 00000000..77edeeda --- /dev/null +++ b/examples/counter-reset/counter.js @@ -0,0 +1,65 @@ +// Mount Function receives all the elements and mounts the app +function mount(muv, id) { // state is encapsulated by mount function + var root = document.getElementById(id); + var update = muv.update; // make local copies of the init parameters + var state = muv.model; // initial state + var view = muv.view; // view is what renders the UI in Browser + + function signal(action) { // signal function takes action + return function callback() { // and returns callback + state = update(state, action); // update state according to action + view(signal, state, root); // subsequent re-rendering + }; + }; + view(signal, state, root); // render initial state (once) +} +// Define the Component's Actions: +var Inc = 'inc'; // increment the counter +var Dec = 'dec'; // decrement the counter + + +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 + default: return model; // if no action, return curent state. + } // (default action always returns current) +} + +function view(signal, model, root) { + empty(root); // clear root element before + return [ // Store DOM nodes in an array + button('+', signal, Inc), // then iterate to append them + div('count', model), // avoids repetition. + button('-', signal, Dec) + ].forEach(function(el){ root.appendChild(el) }); // forEach is ES5 so IE9+ +} // yes, for loop is "faster" than forEach, but readability trumps "perf" here! + +// 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 + +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; // use action as CSS class + 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; + if(text !== undefined) { // if text is passed in render it in a "Text Node" + var txt = document.createTextNode(text); + div.appendChild(txt); + } + return div; +} diff --git a/examples/counter-reset/index.html b/examples/counter-reset/index.html new file mode 100644 index 00000000..3833a776 --- /dev/null +++ b/examples/counter-reset/index.html @@ -0,0 +1,33 @@ + + + + + + Elm Architecture in JS - Counter Reset + + + + + +
+ + + + +
+
+ + + + + + + + + diff --git a/examples/counter-reset/test.js b/examples/counter-reset/test.js new file mode 100644 index 00000000..707b3a7d --- /dev/null +++ b/examples/counter-reset/test.js @@ -0,0 +1,54 @@ +var id = 'test-app' + +test('Test Update update(0) returns 0 (current state)', function(assert) { + var result = update(0); + assert.equal(result, 0); +}); + +test('Test Update increment: update(1, "inc") returns 2', function(assert) { + var result = update(1, "inc"); + assert.equal(result, 2); +}); + +test('Test Update decrement: update(3, "dec") returns 2', function(assert) { + var result = update(1, "dec"); + assert.equal(result, 0); +}); + +test('Test negative state: update(-9, "inc") returns -8', function(assert) { + var result = update(-9, "inc"); + assert.equal(result, -8); +}); + +test('mount({model: 7, update: update, view: view}, "' + + id +'") sets initial state to 7', function(assert) { + var init = {model: 7, update: update, view: view}; + mount(init, id); + var state = document.getElementById(id).textContent.replace(/-+/, ''); + assert.equal(state, 7); +}); + +test('empty("test-app") should clear DOM in root node', function(assert) { + empty(document.getElementById(id)); + var init = {model: 7, update: update, view: view}; + mount(init, 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)); + var init = {model: 7, update: update, view: view}; + mount(init, id); + document.getElementsByTagName('button')[2].click(); // there are 4 buttons + var state = document.getElementById(id).textContent.replace(/-+/, ''); + assert.equal(state, 8); // model was incremented successfully + empty(document.getElementById(id)); // clean up after tests +}); + +test('Test reset counter when model/state is 6 returns 0', function(assert) { + var result = update(6, "reset"); + assert.equal(result, 0); +}); diff --git a/examples/style.css b/examples/style.css index 73290445..1cde742d 100644 --- a/examples/style.css +++ b/examples/style.css @@ -15,6 +15,6 @@ button { background-color:#e74c3c; border-color: #c0392b; } -#qunit-header { - font-size: 0.5em !important; +#qunit-header { /* just cause the default style makes the header HUGE!! */ + font-size: 0.4em !important; }