From ef56c490a48db8a900f1832d0cc373b75838b4d4 Mon Sep 17 00:00:00 2001 From: nelsonic Date: Wed, 8 Aug 2018 15:29:14 +0100 Subject: [PATCH] refactor counter-reset example to use the elmish mount function signaure and simplify view https://github.com/dwyl/learn-elm-architecture-in-javascript/issues/5 --- examples/counter-reset/counter.js | 22 +++++++++----- examples/todo-list/elmish.js | 13 ++++---- examples/todo-list/index.html | 45 ++++++++++------------------ examples/todo-list/todo-app.js | 20 ++++++------- test/counter-reset.test.js | 4 ++- test/elmish.test.js | 4 +++ todo-list.md | 50 +++++++++++++++++++++++++++++++ 7 files changed, 103 insertions(+), 55 deletions(-) diff --git a/examples/counter-reset/counter.js b/examples/counter-reset/counter.js index 9c55d091..f7910634 100644 --- a/examples/counter-reset/counter.js +++ b/examples/counter-reset/counter.js @@ -12,14 +12,13 @@ function update (action, model) { // Update function takes the current state } // (default action always returns current) } -function view(signal, model, root) { - empty(root); // clear root element before - [ // Store DOM nodes in an array +function view(model, signal) { + return container([ // 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+ + ]); // forEach is ES5 so IE9+ } // yes, for loop is "faster" than forEach, but readability trumps "perf" here! // Mount Function receives all MUV and mounts the app in the "root" DOM Element @@ -28,10 +27,11 @@ function mount(model, update, view, root_element_id) { function signal(action) { // signal function takes action return function callback() { // and returns callback model = update(action, model); // update model according to action - view(signal, model, root); // subsequent re-rendering + empty(root); + root.appendChild(view(model, signal)); // subsequent re-rendering }; }; - view(signal, model, root); // render initial model (once) + root.appendChild(view(model, signal)); // render initial model (once) } // The following are "Helper" Functions which each "Do ONLY One Thing" and are @@ -40,7 +40,7 @@ function mount(model, update, view, root_element_id) { // empty the contents of a given DOM element "node" (before re-rendering) function empty(node) { while (node.firstChild) { - node.removeChild(node.firstChild); + node.removeChild(node.firstChild); } } // Inspired by: stackoverflow.com/a/3955238/1148249 @@ -66,6 +66,14 @@ function div(divid, text) { return div; } +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/section +function container(elements) { + var con = document.createElement('section'); + con.className = 'counter'; + elements.forEach(function(el) { con.appendChild(el) }); + return con; +} + /* The code block below ONLY Applies to tests run using Node.js */ /* istanbul ignore else */ if (typeof module !== 'undefined' && module.exports) { diff --git a/examples/todo-list/elmish.js b/examples/todo-list/elmish.js index 54e3e054..0d98b492 100644 --- a/examples/todo-list/elmish.js +++ b/examples/todo-list/elmish.js @@ -1,4 +1,3 @@ -(function() { // scope elm(ish) functions to prevent conflicts if used elsewhere /** * `empty` the contents of a given DOM element "node" (before re-rendering). * This is the *fastest* way according to: stackoverflow.com/a/3955238/1148249 @@ -8,7 +7,7 @@ * const node = document.getElementById('app'); * empty(node); */ -function empty(node) { +function empty (node) { while (node.lastChild) { node.removeChild(node.lastChild); } @@ -21,18 +20,18 @@ function empty(node) { * @param {Function} view function that renders HTML/DOM elements with model. * @param {String} root_element_id root DOM element in which the app is mounted */ -function mount(model, update, view, root_element_id) { +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 var updatedModel = update(action, model); // update model for the action localStorage.setItem('elmish_store', JSON.stringify(updatedModel)); empty(root); // clear root el before rerender - view(signal, updatedModel, root); // subsequent re-rendering + root.appendChild(view(updatedModel, signal)); // subsequent re-rendering }; }; model = JSON.parse(localStorage.getItem('elmish_store')) || model; - view(signal, model, root); // render initial model (once) + root.appendChild(view(model, signal)) // render initial model (once) localStorage.setItem('elmish_store', JSON.stringify(model)); // save model! } @@ -58,7 +57,7 @@ function add_attributes (attrlist, node) { case 'checked': node.checked = (a[1] === 'true' ? true : false); case 'class': - node.className = a[1]; // apply CSS classes + node.className = a[1]; // apply one or more CSS classes break; case 'data-id': node.setAttribute('data-id', a[1]); // add data-id e.g: to
  • @@ -233,5 +232,3 @@ if (typeof module !== 'undefined' && module.exports) { ul: ul } } - -})(); diff --git a/examples/todo-list/index.html b/examples/todo-list/index.html index fd11ca1c..5a4e58c8 100644 --- a/examples/todo-list/index.html +++ b/examples/todo-list/index.html @@ -7,40 +7,27 @@ Elm(ish) Todo List! + + +
    + + + - - - -
    -
    - - - - - - - - diff --git a/examples/todo-list/todo-app.js b/examples/todo-list/todo-app.js index 5d196e0c..a39840aa 100644 --- a/examples/todo-list/todo-app.js +++ b/examples/todo-list/todo-app.js @@ -1,9 +1,9 @@ -(function() { // scope functions to prevent naming conflicts /* if require is available, it means we are in Node.js Land i.e. testing! */ /* istanbul ignore next */ -const { a, button, div, empty, footer, input, h1, header, label, li, mount, - route, section, span, strong, text, ul } = - (typeof require !== 'undefined') ? require('./elmish.js') : {}; +if (typeof require !== 'undefined' && this.window !== this) { + var { a, button, div, empty, footer, input, h1, header, label, li, mount, + route, section, span, strong, text, ul } = require('./elmish.js'); +} var initial_model = { todos: [], @@ -62,8 +62,11 @@ function render_item (item) { item.done ? "class=completed" : "" ], [ div(["class=view"], [ - input(["class=toggle", "type=checkbox", - item.done ? "checked=true" : ""], + input([ + item.done ? "checked=true" : "", + "class=toggle", + "type=checkbox" + ], []), // does not have any nested elements label([], [text(item.title)]), button(["class=destroy"]) @@ -142,7 +145,7 @@ function render_footer (model) { * // returns
    DOM element with other DOM els nested: * var DOM = view(model); */ -function view(model) { +function view (model) { return ( section(["class=todoapp"], [ // array of "child" elements header(["class=header"], [ @@ -174,6 +177,3 @@ if (typeof module !== 'undefined' && module.exports) { view: view } } - - -})(); // https://en.wikipedia.org/wiki/Immediately-invoked_function_expression diff --git a/test/counter-reset.test.js b/test/counter-reset.test.js index ace1db6b..60ee1079 100644 --- a/test/counter-reset.test.js +++ b/test/counter-reset.test.js @@ -42,6 +42,7 @@ test('Test Update decrement: update("dec", 1) returns 0', function (t) { test('click on "+" button (increment model by 1)', function (t) { // console.log('document', typeof document, document); + empty(document.getElementById(id)); document.body.appendChild(div(id)); mount(7, update, view, id); @@ -65,8 +66,9 @@ test('click on "+" button (increment model by 1)', function (t) { }); test('Click reset button resets state to 0', function (t) { - mount(7, update, view, id); var root = document.getElementById(id); + empty(root); + mount(7, update, view, id); var state7 = parseInt(root.getElementsByClassName('count')[0].textContent, 10) t.equal(state7, 7, "State is 7 after mounting with initial state=7"); var btn = root.getElementsByClassName("reset")[0]; // click reset button diff --git a/test/elmish.test.js b/test/elmish.test.js index 6825da51..ae95ce53 100644 --- a/test/elmish.test.js +++ b/test/elmish.test.js @@ -373,9 +373,13 @@ global.localStorage = { // globals are bad! but a "necessary evil" here ... }, setItem: function (key, value) { this[key] = value; + }, + removeItem: function (key) { + delete this[key] } } localStorage.setItem('hello', 'world!'); +localStorage.removeItem('elmish_store'); // console.log('localStorage (polyfil) hello', localStorage.getItem('hello')); // // Test mount's localStorage using view and update from counter-reset example diff --git a/todo-list.md b/todo-list.md index e4d76ac1..e78b5b56 100644 --- a/todo-list.md +++ b/todo-list.md @@ -1266,7 +1266,57 @@ When you run `npm test` you should see something like this: ![image](https://user-images.githubusercontent.com/194400/43782895-48496f22-9a58-11e8-9fde-dbb5554f43a0.png) +## Checkpoint! +So far we have made a _lot_ of progress with our Todo List App _quest_, +_however_ if we were to _stop_ working on this _now_ we would have +_nothing_ to show a "user". +Users can't _interact_ with functions, +even those with _great_ test coverage! + +What we _need_ is to start putting all the pieces together +into a functioning app! + +### Mount the App in `index.html` + +Open your **`/examples/todo-list/index.html`** file +and ensure that the following lines are in the **``**: + +```html + +
    + + + + + + + + + +
    + +``` + +For a "snapshot" of the `index.html` file here, +see: + + +If you run the project with `npm start` +and open: http://127.0.0.1:8000/examples/todo-list + +You should see: +![view-working](https://user-images.githubusercontent.com/194400/43786145-e476bdd0-9a5f-11e8-9043-cf997be615ae.png)