Skip to content

Commit

Permalink
refactor counter-reset example to use the elmish mount function signa…
Browse files Browse the repository at this point in the history
…ure and simplify view #5
  • Loading branch information
nelsonic committed Aug 8, 2018
1 parent 470ad9f commit ef56c49
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 55 deletions.
22 changes: 15 additions & 7 deletions examples/counter-reset/counter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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) {
Expand Down
13 changes: 5 additions & 8 deletions examples/todo-list/elmish.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -8,7 +7,7 @@
* const node = document.getElementById('app');
* empty(node);
*/
function empty(node) {
function empty (node) {
while (node.lastChild) {
node.removeChild(node.lastChild);
}
Expand All @@ -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!
}

Expand All @@ -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 <li>
Expand Down Expand Up @@ -233,5 +232,3 @@ if (typeof module !== 'undefined' && module.exports) {
ul: ul
}
}

})();
45 changes: 16 additions & 29 deletions examples/todo-list/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,27 @@
<title>Elm(ish) Todo List!</title>
<link rel="shortcut icon"
href="http://www.dwyl.io/images/favicon.ico" type="image/x-icon">
</head>
<body>
<div id="app"></div>
<!-- CSS Styles are 100% optional. but they make it look *much* nicer -->
<link rel="stylesheet" href="todomvc-common-base.css">
<link rel="stylesheet" href="todomvc-app.css">
<script src="elmish.js"></script>
<script src="todo-app.js"></script>
<script>
var model = {
todos: [
{ id: 1, title: "Learn Elm Architecture", done: true },
{ id: 2, title: "Build Todo List App", done: false },
{ id: 3, title: "Win the Internet!", done: false }
],
hash: '#/' // the "route" to display
};
mount(model, update, view, 'app');
</script>

</head>
<body>

<!--
<script src="todo-app.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 -->
<script>
setTimeout(function () { // delay for 1 second then run:
console.log('window.location.href:', window.location.href);
var base = window.location.href.split('#')[0];
var active = '#/active';
window.location.href = base + active;
console.log('window.location.href:', window.location.href, 'updated!');
console.log('window.history.length:', window.history.length);
window.history.pushState(null, 'Active', active);
console.log('window.history.length:', window.history.length);
}, 1000)
</script>
</body>
</html>
20 changes: 10 additions & 10 deletions examples/todo-list/todo-app.js
Original file line number Diff line number Diff line change
@@ -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: [],
Expand Down Expand Up @@ -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"
],
[]), // <input> does not have any nested elements
label([], [text(item.title)]),
button(["class=destroy"])
Expand Down Expand Up @@ -142,7 +145,7 @@ function render_footer (model) {
* // returns <section class="todo-app"> 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"], [
Expand Down Expand Up @@ -174,6 +177,3 @@ if (typeof module !== 'undefined' && module.exports) {
view: view
}
}


})(); // https://en.wikipedia.org/wiki/Immediately-invoked_function_expression
4 changes: 3 additions & 1 deletion test/counter-reset.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions test/elmish.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions todo-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 **`<body>`**:

```html
<body>
<div id="app"></div>
<!-- CSS Styles are 100% optional. but they make it look *much* nicer -->
<link rel="stylesheet" href="todomvc-common-base.css">
<link rel="stylesheet" href="todomvc-app.css">

<script src="elmish.js"></script>
<script src="todo-app.js"></script>
<script>
var model = {
todos: [
{ id: 1, title: "Learn Elm Architecture", done: true },
{ id: 2, title: "Build Todo List App", done: false },
{ id: 3, title: "Win the Internet!", done: false }
],
hash: '#/' // the "route" to display
};
mount(model, 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 -->
</body>
```

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)

<!--
Expand Down

0 comments on commit ef56c49

Please sign in to comment.