Giter Site home page Giter Site logo

dwyl / learn-elm-architecture-in-javascript Goto Github PK

View Code? Open in Web Editor NEW
209.0 16.0 19.0 394 KB

:unicorn: Learn how to build web apps using the Elm Architecture in "vanilla" JavaScript (step-by-step TDD tutorial)!

Home Page: https://todomvc-app.herokuapp.com

License: GNU General Public License v2.0

JavaScript 100.00%
elm-architecture javascript tutorial tests es5-javascript beginner-friendly elm howto

learn-elm-architecture-in-javascript's Introduction

Learn Elm Architecture in Plain JavaScript

Learn how to build web applications using the Elm ("Model Update View") Architecture in "plain" JavaScript.

Build Status test coverage dependencies Status devDependencies Status contributions welcome HitCount

We think Elm is the future of Front End Web Development
for all the reasons described in: github.com/dwyl/learn-elm#why
However we acknowledge that Elm is "not everyone's taste"!

What many Front-End Developers are learning/using is React.js.
Most new React.js apps are built using Redux which "takes cues from"
(takes all it's best ideas/features from) Elm:
redux-borrows-elm

Therefore, by learning the Elm Architecture, you will intrinsically understand Redux
which will help you learn/develop React apps.

This step-by-step tutorial is a gentle introduction to the Elm Architecture,
for people who write JavaScript and want a functional, elegant and fast
way of organizing their JavaScript code without having the learning curve
of a completely new (functional) programming language!

Why?

simple-life

Organizing code in a Web (or Mobile) Application is really easy to over-complicate,
especially when you are just starting out and there are dozens of competing ideas all claiming to be the "right way"...

When we encounter this type of "what is the right way?" question,
we always follow Occam's Razor and ask: what is the simplest way?
In the case of web application organization, the answer is: the "Elm Architecture".

When compared to other ways of organizing your code, "Model Update View" (MUV) has the following benefits:

  • 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 the "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 such as Model-view-Presenter or "Model-View-ViewModel" (MVVM) which is "overkill" for most apps).

Note: don't panic if any of the terms above are strange or even confusing to you right now. Our quest is to put all the concepts into context. And if you get "stuck" at any point, we are here to help! Simply open a question on GitHub: github.com/dwyl/learn-elm-architecture-in-javascript/issues

Who? (Should I Read/Learn This...?)

everybodys-gotta-learn-sometime

Anyone who knows a little bit of JavaScript and wants to learn how to organize/structure
their code/app in a sane, predictable and testable way.

Prerequisites?

all-you-need-is-less

No other knowledge is assumed or implied. If you have any questions, please ask:
github.com/dwyl/learn-elm-architecture-in-javascript/issues

What?

image

A Complete Beginner's Guide to "MUV"

Start with a few definitions:

  • Model - or "data model" is the place where all data is stored; often referred to as the application's state.
  • Update - how the app handles actions performed by people and updates the state, usually organised as a switch with various case statements corresponding to the different "actions" the user can take in your App.
  • View - what people using the app can see; a way to view the Model (in the case of the first tutorial below, the counter) as HTML rendered in a web browser.

elm-muv-architecture-diagram


If you're not into flow diagrams, here is a much more "user friendly" explanation of The Elm Architecture ("TEA"):

In the "View Theatre" diagram, the:

  • model is the ensamble of characters (or "puppets")
  • update is the function that transforms ("changes") the model (the "puppeteer").
  • view what the audience sees through "view port" (stage).

If this diagram is not clear (yet), again, don't panic, it will all be clarified when you start seeing it in action (below)!

How?

1. Clone this Repository

git clone https://github.com/dwyl/learn-elm-architecture-in-javascript.git && cd learn-elm-architecture-in-javascript

2. Open Example .html file in Web Browser

Tip: if you have node.js installed, simply run npm install! That will install live-server which will automatically refresh your browser window when you make changes to the code! (makes developing faster!)

When you open examples/counter-basic/index.html you should see:

elm-architecture-counter

Try clicking on the buttons to increase/decrease the counter.

3. Edit Some Code

In your Text Editor of choice, edit the initial value of the model (e.g: change the initial value from 0 to 9). Don't forget to save the file!

elm-architecture-code-update

4. Refresh the Web Browser

When you refresh the your Web Browser you will see that the "initial state" is now 9 (or whichever number you changed the initial value to):

update-initial-model-to-9

You have just seen how easy it is to set the "initial state" in an App built with the Elm Architecture.

5. Read Through & Break Down the Code in the Example

You may have taken the time to read the code in Step 3 (above) ...
If you did, well done for challenging yourself and getting a "head start" on reading/learning!
Reading (other people's) code is the fastest way to learn programming skills and the only way to learn useful "patterns".
If you didn't read through the code in Step 3, that's ok! Let's walk through the functions now!

As always, our hope is that the functions are clearly named and well-commented,
please inform us if anything is unclear please ask any questions as issues:
github.com/dwyl/learn-elm-architecture-in-javascript/issues

5.1 mount Function Walkthrough

The mount function "initializes" the app and tells the view how to process a signal sent by the user/client.

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 mount function receives the following four arguments:

  • model: "initial state" of your application (in this case the counter which starts at 0)
  • update: the function that gets executed when ever a "signal" is received from the client (person using the app).
  • view: the function that renders the DOM (see: section 5.3 below)
  • root_element_id is the id of the "root DOM element"; this is the DOM element
    where your app will be "mounted to". In other words your app will be contained within this root element.
    (so make sure it is empty before mounting)

The first line in mount is to get a reference to the root DOM element;
we do this once in the entire application to minimize DOM lookups.

mount > signal > callback ?

The interesting part of the mount function is signal (inner function)!
At first this function may seem a little strange ...
Why are we defining a function that returns another function?
If this your first time seeing this "pattern", welcome to the wonderful world of "closures"!

What is a "Closure" and Why/How is it Useful?

A closure is an inner function that has access to the outer (enclosing) function's variables—scope chain. The closure has three scope chains: it has access to its own scope (variables defined between its curly brackets), it has access to the outer function's variables, and it has access to the global variables.

In the case of the callback function inside signal, the signal is "passed" to the various bits of UI and the callback gets executed when the UI gets interacted with. If we did not have the callback the signal would be executed immediately when the button is defined.
Whereas we only want the signal (callback) to be triggered when the button is clicked.
Try removing the callback to see the effect:

range-error-stack-exceeded

The signal is triggered when button is created, which re-renders the view creating the button again. And, since the view renders two buttons each time it creates a "chain reaction" which almost instantly exceeds the "call stack" (i.e. exhausts the allocated memory) of the browser!

Putting the callback in a closure means we can pass a reference to the signal (parent/outer) function to the view function.

Further Reading on Closures

5.1.1 mount > render initial view

The last line in the mount function is to render the view function for the first time, passing in the signal function, initial model ("state") and root element. This is the initial rendering of the UI.

5.2 Define the "Actions" in your App

The next step in the Elm Architecture is to define the Actions that can be taken in your application. In the case of our counter example we only have two (for now):

// Define the Component's Actions:
var Inc = 'inc';                     // increment the counter
var Dec = 'dec';                     // decrement the counter

These Actions are used in the switch (i.e. decide what to do) inside the update function.

Actions are always defined as a String.
The Action variable gets passed around inside the JS code
but the String representation is what appears in the DOM
and then gets passed in signal from the UI back to the update function.

One of the biggest (side) benefits of defining actions like this is that it's really quick to see what the application does by reading the list of actions!

5.3 Define the update Function

The update function is a simple switch statement that evaluates the action and "dispatches" to the required function for processing.

In the case of our simple counter we aren't defining functions for each case:

function update(model, action) {     // Update function takes the current model
  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 current model.
  }                                  // (default action always returns current)
}

However if the "handlers" for each action were "bigger", we would split them out into their own functions e.g:

// define the handler function used when action is "inc"
function increment(model) {
  return model + 1
}
// define handler for "dec" action
function decrement(model) {
  return model - 1
}
function update(model, action) {     // Update function takes the current state
  switch(action) {                   // and an action (String) runs a switch
    case Inc: return increment(model);  // add 1 to the model
    case Dec: return decrement(model);  // subtract 1 from model
    default: return model;           // if no action, return current state.
  }                                  // (default action always returns current)
}

This is functionally equivalent to the simpler update (above)
But does not offer any advantage at this stage (just remember it for later).

5.4 Define the view Function

The view function is responsible for rendering the state to the DOM.

function view(signal, model, root) {
  empty(root);                                 // clear root element before
  [                                            // Store DOM nodes in an array
    button('+', signal, Inc),                  // create button (defined below)
    div('count', model),                       // show the "state" of the Model
    button('-', signal, Dec)                   // button to decrement counter
  ].forEach(function(el){ root.appendChild(el) }); // forEach is ES5 so IE9+
}

The view receives three arguments:

  • signal defined above in mount tells each (DOM) element how to "handle" the user input.
  • model a reference to the current value of the counter.
  • root a reference to the root DOM element where the app is mounted.

The view function starts by emptying the DOM inside the root element using the empty helper function.
This is necessary because, in the Elm Architecture, we re-render the entire application for each action.

See note on DOM Manipulation and "Virtual DOM" (below)

The view creates a list (Array) of DOM nodes that need to be rendered.

5.4.1 view helper functions: empty, button and div

The view makes use of three "helper" (DOM manipulation) functions:

  1. empty: empty the root element of any "child" nodes. Essentially delete the DOM inside whichever element's passed into empty.
function empty(node) {
  while (node.firstChild) { // while there are still nodes inside the "parent"
      node.removeChild(node.firstChild); // remove any children recursively
  }
}
  1. button: creates a <button> DOM element and attaches a "text node" which is the visible contents of the button the "user" sees.
function button(buttontext, signal, action) {
  var button = document.createElement('button');  // create a button HTML node
  var text = document.createTextNode(buttontext); // human-readable button text
  button.appendChild(text);                       // text goes *inside* button
  button.className = action;                      // use action as CSS class
  button.onclick = signal(action);                // onclick sends signal
  return button;                                  // return the DOM node(s)
}
  1. div: creates a <div> DOM element and applies an id to it, then if some text was supplied in the second argument, creates a "text node" to display that text. (in the case of our counter the text is the current value of the model, i.e. the count)
function div(divid, text) {
  var div = document.createElement('div'); // create a <div> DOM element
  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;
}

Note: in elm land all of these "helper" functions are in the elm-html package, but we have defined them in this counter example so there are no dependencies and you can see exactly how everything is "made" from "first principals".

Once you have read through the functions (and corresponding comments),
take a look at the tests.

Pro Tip: Writing code is an iterative (repetitive) process, manually refreshing the web browser each time you update some code gets tedious quite fast, Live Server to the rescue!

6. (Optional) Install "Live Server" for "Live Reloading"

Note: Live Reloading is not required, e.g. if you are on a computer where you cannot install anything, the examples will still work in your web browser.

Live Reloading helps you iterate/work faster because you don't have to
manually refresh the page each time.
Simply run the following command:

npm install && npm start

This will download and start live-server which will auto-open your default browser:
Then you can navigate to the desired file. e.g: http://127.0.0.1:8000/examples/counter-basic/

7. Read the Tests!

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:

test-example-files

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-coverage

Because all functions are "pure", testing the update function is very easy:

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);
});

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
(or "reason about") and test is that all functions are "Pure".

8. What is a "Pure" Function? (Quick Learning/Recap)

Pure Functions are functions that always return the same output for a given input.
Pure Functions have "no side effects", meaning they don't change anything they aren't supposed to,
they just do what they are told; this makes them very predictable/testable. Pure functions "transform" data into the desired value, they do not "mutate" state.

8.1 Example of an Impure Function

The following function is "impure" because it "mutates" i.e. changes the counter variable which is outside of the function and not passed in as an argument:

// this is an "impure" function that "mutates" state
var counter = 0;
function increment () {
  return ++counter;
}
console.log(increment()); // 1
console.log(increment()); // 2
console.log(increment()); // 3

see: https://repl.it/FIot/1

8.2 Example of an Pure Function

This example is a "pure" function because it will always return same result for a given input.

var counter = 0;
function increment (my_counter) {
  return my_counter + 1;
}
// counter variable is not being "mutated"
// the output of a pure function is always identical
console.log(increment(counter)); // 1
console.log(increment(counter)); // 1
console.log(increment(counter)); // 1
// you can "feed" the output of one pure function into another to get the same result:
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 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:

<button class='inc' onclick="incr()">+</button>
<div id='count'>0</div>
<button class='dec' onclick="decr()">-</button>
<script>
  var el = document.getElementById('count')
  function incr() { el.innerHTML = parseInt(el.textContent, 10) + 1 };
  function decr() { el.innerHTML = parseInt(el.textContent, 10) - 1 };
</script>

This counter does the same thing as our Elm Architecture example (above),
and to the end-user the UI looks identical:

counter-impure-665

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 <div> 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 ...
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 do not mutate a "global" state 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": Reset the Count!

As you (hopefully) recall from our Step-by-Step TDD Tutorial, when we craft code following the "TDD" approach, we go through the following steps:

  1. Read and understand the "user story" (e.g: in this case: issues/5) reset-counter-user-story
  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.

BEFORE you continue, try and build the "reset" functionality yourself following TDD approach!




9.1 Tests for Resetting the Counter (Update)

We always start with the Model test(s) (because they are the easiest):

test('Test: reset counter returns 0', function(assert) {
  var result = update(6, "reset");
  assert.equal(result, 0);
});

9.2 Watch it Fail!

Watch the test fail in your Web Browser:
reset-counter-failing-test

9.3 Make it Pass (writing the minimum code)

In the case of an App written with the Elm Architecture, the minimum code is:

  • Action in this case var Res = 'reset';
  • Update (case and/or function) to "process the signal" from the UI (i.e. handle the user's desired action)
case Res: return 0;

reset-counter-test-passing

9.4 Write View (UI) Tests

Once we have the Model tests passing we need to give the user something to interact with!
We are going to be "adventurous" and write two tests this time!
(thankfully we already have a UI test for another button we can "copy")

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

test('Click reset button resets model (counter) to 0', function(assert) {
  mount(7, update, view, id); // set initial state
  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;
  empty(document.getElementById(id)); // Clear the test DOM elements
});

9.5 Watch View/UI Tests Fail!

Watch the UI tests go red in the browser:

reset-counter-failing-tests

9.6 Make UI Tests Pass (writing the minimum code)

Luckily, to make both these tests pass requires a single line of code in the view function!

button('Reset', signal, Res)

reset-counter


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)
e.g: var model = { counters: [0] }
where the value of the first element in the model.counters Array is the value for the single counter example.

2. Display multiple counters on the same page using the var model = { counters: [0] } approach.

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


11. Todo List!

The ultimate test of whether you learned/understood something is
applying your knowledge to different context from the one you learned in.

Let's "turn this up to eleven" and build something "useful"!

GOTO: todo-list.md


Futher/Background Reading




tl;dr

Flattening the Learning Curve

The issue of the "Elm Learning Curve" was raised in: github.com/dwyl/learn-elm/issues/45
and scrolling down to to @lucymonie's list we see the Elm Architecture at number four ...
this seems fairly logical (initially) because the Elm Guide uses the Elm Language to explain the Elm Architecture: https://guide.elm-lang.org/architecture

elm-architecture

i.e. it assumes that people already understand the (Core) Elm Language...
This is a fair assumption given the ordering of the Guide however ... we have a different idea:

Hypothesis: Learn (& Practice) Elm Architecture before Learning Elm?

We hypothesize that if we explain the Elm Architecture (in detail) using a language
people are already familiar with (i.e JavaScript) before diving into the Elm Language
it will "flatten" the learning curve.

Note: Understanding the Elm Architecture will give you a massive headstart
on learning Redux which is the "de facto" way of structuring React.js Apps.
So even if you decide not to learn/use Elm, you will still gain great frontend skills!

Isn't DOM Manipulation Super Slow...?

DOM manipulation is the slowest part of any "client-side" web app.
That is why so many client-side frameworks (including Elm, React and Vue.js) now use a "Virtual DOM". For the purposes of this tutorial, and for most small apps Virtual DOM is total overkill!
It's akin to putting a jet engine in a go kart!

What is "Plain" JavaScript?

"Plain" JavaScript just means not using any frameworks or features that require "compilation".

The point is to understand that you don't need anything more than "JavaScript the Good Parts"
to build something full-featured and easy/fast to read!!

babel

If you can build with "ES5" JavaScript:
a) you side-step the noise 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 of dependencies just to run a simple project!
c) You save time (for yourself, your team and end-users!) because your code is already optimized to run in any browser!

learn-elm-architecture-in-javascript's People

Contributors

dwylbot avatar finnhodgkin avatar iteles avatar macintoshhelper avatar nelsonic avatar simonlab avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

learn-elm-architecture-in-javascript's Issues

render_item renders the DOM for a Single Todo List item

Given the following HTML:

    <li data-id="1533501855500" class="completed">
      <div class="view">
        <input class="toggle" type="checkbox">
        <label>Learn Elm Architecture</label>
        <button class="destroy"></button>
      </div>
    </li>

Use Elm(ish) DOM functions (li, div, input, label and button)
to render a single Todo List item.

Acceptance Criteria

  • Each Todo List item <li> should contain a <div> with a class="view" which "wraps":
    • <input class="toggle" type="checkbox"> - the "checkbox" that people can "Toggle" to change the "state" of the Todo item from "active" to "done" (_which updates the model From: model.todos[id].done=false To: model.todos[id].done=true
    • <label> - the text content of the todo list item
    • <button class="destroy"> - the button the person can click/tap to delete a Todo item.

Sample Test:

test.only('render_item HTML for a single Todo Item', function (t) {
  const model = {
    todos: [
      { id: 1, title: "Learn Elm Architecture", done: true },
    ],
    hash: '#/' // the "route" to display
  };
  // render the ONE todo list item:
  document.getElementById(id).appendChild(app.render_item(model.todos[0]))

  const done = document.querySelectorAll('.completed')[0].textContent;
  t.equal(done, 'Learn Elm Architecture', 'Done: Learn "TEA"');

  const checked = document.querySelectorAll('input')[0].checked;
  t.equal(checked, true, 'Done: ' + model.todos[0].title + " is done=true");

  elmish.empty(document.getElementById(id)); // clear DOM ready for next test
  t.end();
});

render_item-test-failing

Breaking It Down #2: Javascript the Hard Parts, Closure, Scope, Execution Context, Callbacks, and Higher Order Functions

As stated elsewhere, my main purpose of studying the learn-em-architecture-in-javascript code was using the code to help me deliberately backfill gaps in my JavaScript knowledge, and after learning basic dom stuff, my main stumbling block was centered around how JavaScript actually executes code, i.e. what gets run and when and why. To that end, this free version of a Front Masters course is the best resource so far for answering my questions: https://www.youtube.com/playlist?list=PLWrQZnG8l0E5hMTpzCK8WjP3nJ93jUEyk

Too, it is a single coherent resource for defining the topics of closure, scope, execution context, callbacks, and higher order functions; topics without which it will not be possible to truly understand the code. Instead of someone having to go read numerous resources, and then gather them together, and then apply them to the code, it is all in one place within a single narrative.

And here is the FrontEnd masters workshop: https://frontendmasters.com/courses/javascript-hard-parts/

Can we load the test-related Styles/Scripts using a single "loader" script?

e.g:

function div(divid, text) {
  console.log(divid, text)
  var div = document.createElement('div');
  div.id = divid;
  if(text !== undefined) {
    var txt = document.createTextNode(text);
    div.appendChild(txt);
  }
  console.log(div);
  return div;
}

function script(src){
  var script = document.createElement('script');
  script.type = 'text/javascript';
  script.setAttribute('src', src);
  console.log(script);
  return script;
}

function style(href) {
  var link = document.createElement('link');
  link.type = 'text/css';
  link.rel = 'stylesheet';
  link.href = href;
  return link;
}
document.body.appendChild(div('qunit'));
document.body.appendChild(div('qunit-fixture'));
  document.body.appendChild(script('//cdnjs.cloudflare.com/ajax/libs/blanket.js/1.1.4/blanket.js'));
(function() {
  // Load the QUnit CSS file from CDN - Require to display our tests
  document.body.appendChild(style('//code.jquery.com/qunit/qunit-1.18.0.css'));
  document.body.appendChild(script('//code.jquery.com/qunit/qunit-1.18.0.js'));

  document.body.appendChild(script('test.js'));
})();

TDD Reset the Counter

Adding the Reset action is good opportunity to write a test first!
As a user (counting things)
I want to reset the counter
So that I can start counting again from zero.

  • Clicking the reset button in the UI resets the count to 0 (zero).

typo on the cloning project command

On the clone-this-repository section

git clone https://github.com/dwyl/learn-elm-architecture-in-javascript.git && learn-elm-architecture-in-javascript
should be (adding change directory commandcd ):

git clone https://github.com/dwyl/learn-elm-architecture-in-javascript.git && cd learn-elm-architecture-in-javascript

Review with "Beginner's Mind" #22

"@edwardcodes please can you read through the instructions in this tutorial, try the examples and open any issues when anything is unclear? (thanks!)"

@nelsonic i´ve read through the instructions in this (awesome) tutorial (_the reasons to learn elm are very clear and very captivating) and tried the examples (explaining why the code is there and the meaning with the step-by-step comments is MEGA useful 🥇) but there´s a few things i didn't understand:

"9.1 Tests for Resetting the Counter (Update)"
"9.4 Write View (UI) Tests"

I didn't´t understand were do i put/write the tests in the script.. besides that everything is clear, each function (mount, signal, update, empty, button and view) has their own purpose in the app, we should use "pure" functions so we can have more control of the outputs (the "impure" js can be tricky).

Unclear sections in the TDD portion

In section 9.1 the workshop suggests the reset action be named 'res', but then for the rest of the text lists it as 'reset'. There's a premade 'reset' css class so it definitely makes more sense to just keep it as 'reset' the whole way through.

Test instructions

Then in sections 9.4 and 9.5 when writing tests for the reset button UI, the expected behaviour is for the two newly written tests to fail, but what actually happens is test number 5 fails too:

Failing test #5

Although sorting the test is an easy fix, it's not mentioned in the text so could easily be misconstrued by beginners as being due to them messing something else up.

Happy to make a quick PR if you want.

Elm(ish) onClick Event/Function Attribute

In order to simplify event listeners in our TEA example apps,
I'm thinking that adding an onclick attribute to a given node and passing in the signal(action)
is the simplest way of keeping the view declarative.

This will be useful for both "delete" a todo item (which is a button) and incrementing a counter.

  • add_attribute([function]) >> onclick=signal(action)

Do we *need* to "init"?

While re-reading the code (and preparing for "elmish") It occurred to me that we might be over-complicating things ...

Do we need an init function to initialise the DOM in:

function init(doc){
document = doc; // this is used for instantiating JSDOM. ignore!
}
/* The code block below ONLY Applies to tests run using Node.js */
/* istanbul ignore next */
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
view: view,
mount: mount,
update: update,
div: div,
button: button,
empty: empty,
init: init
}
} else { init(document); }

e.g:
init(document); // pass the JSDOM into counter.js

If we use https://github.com/rstacruz/jsdom-global in our tests, we should not need an init function.
I'm going to experiment with updating the counter-reset tests and see if I can remove the init function completely.

Clarity on essence of TEA vs how it's implemented here

This is a really nice introduction to the Elm Architecture!

I have a couple of related questions:

  1. Should it be made clear that the implementation here is provided as a concrete example of the Elm Architecture? I find that beginners sometimes get confused between whether an idea is some specific code (the implementation) vs something that is the pattern behind lots of pieces of code

  2. There are a couple of places where the tutorial has to change the way it does things compared to how Elm does them. For example here we read

Actions are always defined as a String

This is the way Redux does it, and arguably makes sense in JS-land, but would be really awful in Elm-land. A bit of clarity on how this is an implementation of the core ideas + also a shift of language here ("We will always define actions as Strings) might stop some potential confusion

Multiple Counters on the Same Page 😮

It occurred to me while applying the Elm Architecture to my little "side project": dwyl/time-mvp-node#33
That there's a "gap" in this tutorial, namely: how do we have a more "complex" model
(all the examples in the tutorial, currently have model as an int)

I propose that we: write an "exercise for the reader" to:
a) refactor the model in the "reset counter" example to use an Object
e.g: var model = { counters:[0] }
where the value of the first element in the model.counters Array
is the value for the single counter example.
b) using the var model = { counters:[0] } approach, display multiple counters on the same page.
c) write tests for the scenario where there are multiple counters on the same page.

Main Todo List View <section class="main">

The "Main" view in the Todo List Application displays the actual list.
Try it: http://todomvc.com/examples/vanillajs/
todomvc-main-section-todo-list-html

This is the HTML copied directly from the browser:

<section class="main" style="display: block;">
  <input class="toggle-all" type="checkbox">
  <label for="toggle-all">Mark all as complete</label>
  <ul class="todo-list">
    <li data-id="1533501855500" class="completed">
      <div class="view">
        <input class="toggle" type="checkbox">
        <label>Learn Elm Architecture</label>
        <button class="destroy"></button>
      </div>
    </li>
    <li data-id="1533501861171" class="">
      <div class="view">
        <input class="toggle" type="checkbox">
        <label>Build Todo List App</label>
        <button class="destroy"></button>
      </div>
    </li>
    <li data-id="1533501867123" class="">
      <div class="view">
        <input class="toggle" type="checkbox">
        <label>Win the Internet!</label>
        <button class="destroy"></button>
      </div>
    </li>
  </ul>
</section>

Acceptance Criteria

  • Todo List items should be displayed as list items <li> in an unordered list <ul>.
  • Each Todo List item <li> should contain a <div> with a class="view" which "wraps":
    • <input class="toggle" type="checkbox"> - the "checkbox" that people can "Toggle" to change the "state" of the Todo item from "active" to "done" (_which updates the model From: model.todos[id].done=false To: model.todos[id].done=true
    • <label> - the text content of the todo list item
    • <button class="destroy"> - the button the person can click/tap to delete a Todo item.

Note: the "toggle-all" (above the "main" list) is a separate issue/feature: #50

This issue is part of the TodoMVC Feature List [Epic] #48

use of gifs / images

The two biggest pngs + the two biggest gifs totalled 1.7MB out of 3MB total for downloading the dwyl/learn-elm-architecture page. They definitely have advantages, but maybe consider

a) Linking to images
b) Linking to two different pages in a doc folder with or without images as the viewer prefers (I read github on mobile data a lot)

"Local copy" confusion

This comment reads "make local copies of the init parameters", but I believe that this is inaccurate.

In the codebase as written, I think it makes a local copy of model, but not update or view. The function probably shouldn't know that model will be pass-by-value anyway, and so should consider the possiblity it's not making a copy of any of those values.

Review with "Beginner's Mind"

We need this tutorial to be reviewed by a person who is reasonably new to both JavaScript and the Elm Architecture so that we can:

  • confirm that the steps are clear
  • check code is well explained
  • ensure nothing is implied/assumed that should be explicit

@edwardcodes please can you read through the instructions in this tutorial, try the examples and open any issues when anything is unclear? (thanks!)

UX Feature Hitting the [Enter] (Return) key creates a new Todo List item

Technically there is no other way of adding a todo list item as there are no "submit" buttons.
So we need to have an event listener on the [Enter] key in order for the Todo List to function at all.

Acceptance Criteria

This issue is part of the TodoMVC Feature List [Epic] #48
Depends on: #57

Tests won't load on local filesystem

GET file://code.jquery.com/qunit/qunit-1.18.0.css net::ERR_FILE_NOT_FOUND error is given if loading the test html page from local file system (not from a live-server).

This is because // is used in the script sources which will use file:// instead of http://.

https:// could be used instead of //.

<script src="https://code.jquery.com/qunit/qunit-1.18.0.js"></script>

instead of //code.jquery...

1. No Todos, should hide #footer and #main

When there are No Todos, do not display the #main (list of todos) or footer (navigation):
image

refer to the 1st test on this list: https://github.com/tastejs/todomvc/tree/master/tests#example-output

Test

Add the following test to your test/todo-app.test.js file:

test.only('No Todos, should hide #footer and #main', function (t) {
  // render the view and append it to the DOM inside the `test-app` node:
  document.getElementById(id).appendChild(app.view({todos: []})); // No Todos

  const main_display = window.getComputedStyle(document.getElementById('main'));
  t.equal('none', main_display._values.display, "No Todos, hide #main");

  const main_footer= window.getComputedStyle(document.getElementById('footer'));
  t.equal('none', main_footer._values.display, "No Todos, hide #footer");

  elmish.empty(document.getElementById(id)); // clear DOM ready for next test
  t.end();
});

Toggle All Todo List Items

  <input class="toggle-all" type="checkbox">
  <label for="toggle-all">Mark all as complete</label>

Acceptance Criteria

  • clicking/tapping the "toggle-all" checkbox will set the done=true on all todo list items.
  • When toggle-all is set to checked then clicking/tapping it again will set all items done=false

This issue is part of the TodoMVC Feature List [Epic] #48

Breaking It Down #1: Organizing Code with Model, View, and Octupus!

I am using the study of @nelsonic implementation of the Elm architecture in Javascript to motiviate myself to back-fill, extend, organize, and apply knowledge about constructing GUIs in the browser environment using Javascript.

First question in looking at the material for me is: if the Elm architecture with its update, model, view organization is the answer, then what is the question? To that end, the Udacity course on patterns of organising the code for GUI javascript apps, provided much needed background context: Javascript Design Patterens. The course starts with restructuring a naive sphaghetti-code implementation of a simple app with a just-using-Javascript implementation/interpretation of model-view-controller, before looking at using Knockout.js. If someone has little time, then there is an excellent summary of the basic ideas/principles of code organisation with the video Universal Organizational Concepts.

Incidentally, just watching the videos means that a lot more dwyl tutorials make sense with a straight read through, which indicates that the material above is information I was very much needing to backfill. One of the basic mistakes beginner resources make along with the abscence of teaching specification and testing is there are no words about organization. Thus one of the primary benefits of learning Elm for a front-end developer might just be the benefits of working within its tight constraints as a way of disciplining the mind. About the only introductory Javascript book which discusses code organisation is Get Programming with Javascript which implements model-view-controller from scratch through chapters 14. 15. 16. Just watching the Udacity course means I can now get a lot more out of those chapters than my first glance-through many months ago!

Also Gordon Zhu's free course Practical Javascript teaches Javascript from the ground up using seperation of concerns. Too, if my memory serves me right Leo Verou in construcing a JS/HTML/CSS implementation of Conway's Game of Life also used proper structure from the beginning in the Pluralsight course Play by Play: HTML, CSS, and JavaScript with Lea Verou, starting out with data structures (model) in the console and later on adding HTML and CSS (view)!

use of the word model/state

Hi Nelson, Just wanted to raise a question on here.

Why call the argument in the update function model and then when calling the function have it defined as the state, could we just not call it model when calling it?

It is a minor thing but it may bamboozle a few beginner or am I being too cautious? what are your thoughts?

screen shot 2017-05-09 at 12 26 42

DELETE a Todo List item using the "cross" (button)

  • should show the <button class="destroy"> on hover (over the item)
  • Clicking/tapping the <button class="destroy"> sends the signal('DELETE', todo.id)
  • DELETE update case receives the todo.id and removes it from the model.todos Array.

Typo in section 5.3

In section 5.3 there is a typo switch is written as swicth.

Happy to make a quick PR to fix this, but don't have rights on this repo.

add CONTRIBUTING.md file to repo to encourage contributions

As a person who is new to the DWYL Org/Community 🆕
I need to know how to contribute to the project effectively 💭
so that I can start my journey towards Doing What I Love with my Life! ❤️ ✅ 😂

Markdown:

_**Please read** our_ [**contribution guide**](https://github.com/dwyl/contributing) (_thank you_!)

Note: these are line-separated but in the actual rendered page it's all one line.
see: https://github.com/dwyl/contributing/blob/master/CONTRIBUTING.md

Abstract superlatives

Mileage may vary, but I find language like

Anyone who knows a little bit of JavaScript and wants to learn how to organize/structure
their code/app in the most sane, predictable and testable way.

(from here) quite off-putting.

It's hard to provide evidence for things like "most sane", and anyway, the most sane way to organise something probably depends on the specifics of the thing you're trying to organise. There are definitely tonnes of advantages to the Elm Architecture, but I'm not sure it's the "most" anything.

Also, as a user, I would probably stop reading at this point because it looks quite dogmatic.

Remove the callback()

Removing the callback might not be a simple thing for some beginners. Edit suggestion below:

  function signal(action) {          
    // return function callback() {     // comment out to remove callback 
      model = update(model, action); 
      view(signal, model, root);     
    // };                               // comment out to remove callback.
  };

Then refresh the browser and check the console.

Subscriptions: How to "Listen" for Events and What to do ...

In order to have "Event Listeners" (so users can hit the [Enter] Key to create a Todo Item #55)
We need to add "Subscriptions" to Elm(ish).
see: https://www.elm-tutorial.org/en/03-subs-cmds/01-subs.html

Todo

  • investigate/research if we need to register events each time the DOM is re-rendered
  • write a test for subscriptions ... e.g:
    • [Up] & [Down] keyboard key press increases/decreases the counter.
  • add subscriptions parameter to mount function
  • invoke the subscriptions in mount depending on the outcome of initial research

required for: #55

Do we need to clarify "Uni-directional data-flow"?

In the Why? section, when explaining the benefits of the Model Update View architecture "Uni-directional data flow" is mentioned.

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!

I am not sure, if, to somebody who just fills the prerequisites for this tutorial (basic JS knowledge, basic TDD knowledge, a computer, 30 minutes), that it is clear what uni-directional data flow is.

Do we need to clarify what it is further?
If we do think that it needs more clarification for someone of this level, is it important to have that information in this repo, or would a link to a separate explanation suffice?

Would love to get feedback from people going through this tutorial for the first time on this question!

view function in Todo List App

Now that we have the individual ("lower order") functions
render_main #51, render_item #52, and render_footer #53
for rendering the sections of the todo app,
we can write the view function to render the entire app!

With the main and footer "partial" views built, the overall view is rediculously simple:

todoapp-view

To save on repetition, and illustrate just how simple the view is, this is the "HTML"
with the <section class"main"> and <footer class="footer"> partials replaced by invocations to the respective functions render_main #51 and render_footer #53:

<section class="todoapp">
  <header class="header">
    <h1>todos</h1>
    <input class="new-todo" placeholder="What needs to be done?" autofocus="">
  </header>
  render_main(model)
  render_footer(model)
</section>

Acceptance Criteria

View displays:

  • <h1> containing the title text "todos".
  • <input class="new-todo"> has placeholder text "What needs to be done?"
  • <ul class="todo-list"> list of todo items has zero items by default.
  • <footer> count is Zero when the app is first rendered with no todos in the model.

"Elmish" + Todo List Example?

Can we split the DOM-creation and mount code into it's own file elmish.js and use it as an opportunity to create a basic Todo List using TodoMVC styles?

Tasks

  • Split any generic functions into a separate file called elmish.js (independently tested)
    • empty - Empty all DOM nodes
    • mount - mount the app to the root DOM node.
  • DOM Creation Functions
    • <section>
    • <div>
    • <ul>
    • <li>
    • <input>
    • text
    • <h1>
    • <header>
    • <label>
    • <footer>
    • <span>
    • <strong>
    • <a>
  • Router: e.g: /#/active #46
  • localStorage #47
  • "Helpers"
    • Attributes
      • id
      • class
      • data-id
      • for
      • type
      • autofocus
      • style
      • placeholder
      • href
      • checked
    • Child Nodes
  • Biz Logic! #48

Rudimentary Routing in a Client-side Web Application: Why? What? How?

Routing in Web Application is surprisingly simple.
There are only a couple of functions we need to write but we want to achieve the following goals:

Acceptance Criteria

  • URL (hash) should change to reflect navigation in the app
  • History of navigation should be preserved and
    • Browser "back button" should work.
  • Pasting or Book-marking a URL should display the desired content when the "page" is loaded

For: #44 "Elmish" Todo List Example

[Epic] TodoMVC Feature List

Todo List Basic Functionality

A todo list has only 2 basic functions:

  • Add a new item to the list
    • new list item created when the [Enter] key is pressed #55
  • Check-off an item as "completed" (done/finished)
    • Click/tap on checkbox next to todo item

Add item and Check-off is exactly the "functionality"
you would have in a paper-based Todo List.

TodoMVC "Advanced" Functionality

In addition to these basic functions,
TodoMVC has the ability to:

  • Un-check an item as to make it "active" (still to be done)
  • Double-click/tap on todo item description to edit it.
  • Mark all as complete
  • Click X on item row to remove from list.

<footer> Menu > GOTO: #53

below the main interface there is a <footer>
with a count, 3 view toggles and one action:
image

  • "{count} item(s) left":

    {store.items.filter(complete==false)} item{store.items.length > 1 ? 's' : '' } left
  • Show All
  • Show Active
  • Show Completed
  • Clear Completed > delete item.done === true

Routing / Navigation

Finally, if you click around the <footer> toggle menu,
you will notice that the Web Bowser Address bar
changes to reflect the chosen view.

tea-todomvc-routing

When a route link is clicked

  • update invokes route(model, title, hash) function which updates model.hash to the value of hash

  • filter view based on model.hash

    • set <footer> view button as active
    • filter todo items based on selected view:
      • ALL: no filter
      • active: !item.done
      • complete: item.done === true
  • Subscriptions for event handling e.g: keyboard press & navigation changes #57

render_footer renders the <footer> element of Todo App

Todo App <footer> Element

Referring to the rendered HTML on http://todomvc.com/examples/vanillajs as our "guide":
image
there is:

  • a <footer> element with
    • a <span> element which contains
      • a text node with: "{count} item(s) left".
    • a <ul> containing
      • 3 <li> elements each with
      • a link (<a>) which allow the "user" to filter which items appear in the <view>.
    • a <button class="clear-completed"> which will Clear all Completed items when clicked.

Dev Tools > Elements (inspector)

todo-list-mvc-

Copy-paste the rendered HTML

I "copy-pasted" of the rendered HTML from the Dev Tools:
todo-list-mvc-copy-html

<footer class="footer" style="display: block;">
  <span class="todo-count">
    <strong>2</strong> items left
  </span>
  <ul class="filters">
    <li>
      <a href="#/" class="selected">All</a>
    </li>
    <li>
      <a href="#/active">Active</a>
    </li>
    <li>
      <a href="#/completed">Completed</a>
    </li>
  </ul>
  <button class="clear-completed" style="display: block;">
    Clear completed
  </button>
</footer>

Technical Acceptance Criteria

  • render_footer returns a <footer> DOM element which can be rendered directly to the document or nested in another DOM element.
  • <footer> contains:
    • <span class="todo-count"> which contains
      • a text node with: "{count} item(s) left".
        pseudocode:
        {model.todos.filter(done==false)} item{model.todo.length > 1 ? 's' : '' } left
    • <ul> containing 3 <li> with the following links (<a>):
      • Show All: <a href="#/" class="selected">All</a>
        • class="selected" should only appear on the selected menu/navigation item.
          this should be "driven" by the model.hash property.
      • Show Active: <a href="#/active">Active</a>
      • Show Completed: <a href="#/completed">Completed</a>
    • <button class="clear-completed" style="display: block;"> will Clear all Completed items.
      pseudocode:
      var new_model = model.todos.filter(function(item) { return item.done === false})

This issue is part of the TodoMVC Feature List [Epic] #48

Prepare this Tutorial for "Publication"

We need to be a lot more systematic in how we share the content we produce.

Todo

  • Tidy up the content removing anything superfluous!
  • Produce the 5 min video
  • Prepare the tweet with link to video and image

Question (for Inês...)

@iteles should we do our CMS/Blog MVP before publicising this content so that we get SEO from it?

The reason I have not previously prepared the "Content Launch Checklist" is because it requires a chunk of work from me where I tend to get pretty exhausted from writing the initial tutorial and just want to get "back" to my "real work" ... 😖 ⏳

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.