Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

License

NotificationsYou must be signed in to change notification settings

dwyl/learn-elm-architecture-in-javascript

Repository files navigation

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

Build Statustest coveragedependencies StatusdevDependencies Statuscontributions welcomeHitCount

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

WhatmanyFront-End Developersare learning/using isReact.js.
Most newReact.js apps are built usingRedux which "takes cues from"
(takesall it'sbest ideas/features from) Elm:
redux-borrows-elm

Therefore, by learning the Elm Architecture,you willintrinsically understand Redux
which will help you learn/developReactapps.

Thisstep-by-step tutorial is agentle introduction tothe Elm Architecture,
for people who write JavaScript and wantafunctional,elegant andfast
way of organizing their JavaScript codewithouthaving the learning curve
of a completely new (functional) programming language!

Why?

simple-life

Organizingcode in a Web (or Mobile) Applicationisreally easy toover-complicate,
especially when you are just starting out and therearedozens of competing ideasallclaiming to be the "right way"...

When we encounter this type of "what is theright way?"question,
we always followOccam's Razorandask:what is thesimplest way?
In the case of web application organization,theanswer is:the "ElmArchitecture".

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

  • Easier tounderstand what is going on in more advanced apps because there is no complex logic,only one basic principaland the "flow" isalways the same.
  • Uni-directional data flow means the "state"of the app is alwayspredictable;given a specific starting "state" and sequence of update actions,the output/end state willalways be the same. This makes testing/testabilityvery easy!
  • There'sno "middle man" to complicate things(the way there is in other application architecturessuch asModel-view-Presenter or "Model-View-ViewModel" (MVVM) which is "overkill" for most apps).

Note:don't panic if any of the terms above are strangeor even confusing to you right now.Ourquest is to put all the concepts intocontext.And if you get "stuck" at any point, we are here to help!Simplyopen 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 alittle bit of JavaScriptand wants to learn how to organize/structure
their code/app in asane, predictable and testable way.

Prerequisites?

all-you-need-is-less

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

What?

image

AComplete 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'sstate.
  • Update - how the app handlesactions performedby people andupdates thestate,usually organised as aswitch with variouscase statements correspondingto the different "actions" the user can take in your App.
  • View - what people using the app cansee;a way toview the Model (in the case of the first tutorial below,the counter) asHTML rendered in a web browser.

elm-muv-architecture-diagram


If you're not into flow diagrams,here is a much more "user friendly" explanationof 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") themodel(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 inaction (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 havenode.js installed, simply runnpm install!That will installlive-server which willautomatically refreshyour browser window when you make changes to the code!(makes developing faster!)

When you openexamples/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 theinitial 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 seethat the "initial state" is now9(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

Youmay have taken the time to read the code in Step 3 (above) ...
If you did,well done for challenging yourselfand getting a "head start" on reading/learning!
Reading (other people's) code is thefastest wayto learn programming skills andtheonly way to learn useful "patterns".
If you didn't read through the code in Step 3, that's ok!Let's walk through the functionsnow!

As always, ourhope is that the functionsare clearly named and well-commented,
please inform us if anything is unclear pleaseask any questions asissues:
github.com/dwyl/learn-elm-architecture-in-javascript/issues

5.1mount Function Walkthrough

The mount function "initializes" the app and tells theviewhow to process asignal sent by the user/client.

functionmount(model,update,view,root_element_id){varroot=document.getElementById(root_element_id);// root DOM elementfunctionsignal(action){// signal function takes actionreturnfunctioncallback(){// and returns callbackmodel=update(model,action);// update model according to actionview(signal,model,root);// subsequent re-rendering};};view(signal,model,root);// render initial model (once)}

Themount function receives the followingfour 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 theid of the "root DOM element"; this is the DOM element
    where your app will be "mounted to". In other words your appwill becontained within this root element.
    (so make sure it is empty beforemounting)

The first line inmount is to get areference to the root DOM element;
we do thisonce in the entire application tominimize DOM lookups.

mount >signal >callback ?

Theinteresting part of themount function issignal (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" andWhy/How is it Useful?

Aclosure is an inner function that has accessto 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 tothe outer function's variables, and it has access to the global variables.

In the case of thecallback function insidesignal,thesignal is "passed" to the various bits of UIand thecallback gets executed when the UI gets interacted with.If we did not have thecallback thesignalwould be executedimmediately when thebutton isdefined.
Whereas we only want thesignal (callback) to be triggeredwhen the button isclicked.
Try removing thecallback to see the effect:

range-error-stack-exceeded

Thesignal is triggered when button iscreated, whichre-renderstheview creating the button again. And, since theview renderstwobuttons each time it creates a "chain reaction" which almostinstantlyexceeds the "call stack"(i.e. exhausts the allocated memory) of the browser!

Putting thecallback in aclosure means we can pass areferenceto thesignal (parent/outer) function to theview function.

Further Reading on Closures

5.1.1mount > render initial view

The last line in themount function is torender theview functionfor the first time, passing in thesignal function, initial model ("state")and root element. This is theinitial rendering of the UI.

5.2 Define the "Actions" in your App

The next step in the Elm Architecture is todefine the Actionsthat can be taken in your application. In the case of ourcounterexample we only havetwo (for now):

// Define the Component's Actions:varInc='inc';// increment the countervarDec='dec';// decrement the counter

TheseActions are used in theswitch (i.e. decide what to do)inside theupdate function.

Actions are always defined as aString.
The Actionvariable gets passed around inside the JS code
but theString representation is what appears in the DOM
and then gets passed insignal from the UI back to theupdate function.

One of the biggest (side) benefits of defining actions like thisis that it's really quick to see what the applicationdoesbyreading the list of actions!

5.3 Define theupdate Function

Theupdate function is a simpleswitchstatement that evaluates theaction and "dispatches"to the required function for processing.

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

functionupdate(model,action){// Update function takes the current modelswitch(action){// and an action (String) runs a switchcaseInc:returnmodel+1;// add 1 to the modelcaseDec:returnmodel-1;// subtract 1 from modeldefault:returnmodel;// if no action, return current model.}// (default action always returns current)}

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

// define the handler function used when action is "inc"functionincrement(model){returnmodel+1}// define handler for "dec" actionfunctiondecrement(model){returnmodel-1}functionupdate(model,action){// Update function takes the current stateswitch(action){// and an action (String) runs a switchcaseInc:returnincrement(model);// add 1 to the modelcaseDec:returndecrement(model);// subtract 1 from modeldefault:returnmodel;// if no action, return current state.}// (default action always returns current)}

This isfunctionally equivalent to the simplerupdate (above)
But does not offer anyadvantage at this stage (just remember it for later).

5.4 Define theview Function

Theview function is responsibleforrendering thestate to the DOM.

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

Theview receives three arguments:

  • signal defined above inmount tells each (DOM) elementhow to "handle" the user input.
  • model a reference to thecurrent value of the counter.
  • root a reference to the root DOM element where the app ismounted.

Theview function starts byemptyingthe DOM inside theroot element using theempty helper function.
This isnecessary because, in the Elm Architecture, were-rendertheentire application for each action.

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

Theview creates alist (Array) of DOM nodes that need to be rendered.

5.4.1view helper functions:empty,button anddiv

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

  1. empty: empty theroot element of any "child" nodes.Essentiallydelete the DOM inside whichever element's passed intoempty.
functionempty(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 thevisible contents of the button the "user" sees.
functionbutton(buttontext,signal,action){varbutton=document.createElement('button');// create a button HTML nodevartext=document.createTextNode(buttontext);// human-readable button textbutton.appendChild(text);// text goes *inside* buttonbutton.className=action;// use action as CSS classbutton.onclick=signal(action);// onclick sends signalreturnbutton;// return the DOM node(s)}
  1. div: creates a<div> DOM element and applies anid to it,then if sometext was supplied in thesecond argument,creates a "text node" to display that text.(in the case of our counter thetext is the current value of the model,i.e. the count)
functiondiv(divid,text){vardiv=document.createElement('div');// create a <div> DOM elementdiv.id=divid;if(text!==undefined){// if text is passed in render it in a "Text Node"vartxt=document.createTextNode(text);div.appendChild(txt);}returndiv;}

Note: inelm land all of these "helper" functions are in theelm-htmlpackage, but we have defined them in this counter exampleso there areno dependencies and you can seeexactlyhow everything is "made" from "first principals".

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

Pro Tip: Writing code is aniterative (repetitive) process,manually refreshing the web browser each time you updatesome code getstedious 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 startlive-serverwhich will auto-open yourdefault browser:
Then you cannavigate to the desired file.e.g:http://127.0.0.1:8000/examples/counter-basic/

7. Read theTests!

In thefirst example we kept everything inone file (index.html) for simplicity.
In order to write tests (and collect coverage),we need toseparate outthe JavaScript code from the HTML.

For this example there are 3separate files:

test-example-files

Let's start by opening the/examples/counter-basic-test/index.htmlfile in a web browser:
http://127.0.0.1:8000/examples/counter-basic-test/?coverage

counter-coverage

Because all functions are "pure", testingtheupdate function isvery easy:

test('Test Update update(0) returns 0 (current state)',function(assert){varresult=update(0);assert.equal(result,0);});test('Test Update increment: update(1, "inc") returns 2',function(assert){varresult=update(1,"inc");assert.equal(result,2);});test('Test Update decrement: update(3, "dec") returns 2',function(assert){varresult=update(1,"dec");assert.equal(result,0);});

open:examples/counter-basic-test/test.js to see these andother tests.

Thereason why Apps built using the Elm Architectureareso easy tounderstand
(or"reason about")andtest is that all functions are "Pure".

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

Pure Functions are functions thatalwaysreturn thesame output for agiven 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 anImpure Function

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

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

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

8.2 Example of anPure Function

This example is a "pure" function because it willalways returnsame result for a given input.

varcounter=0;functionincrement(my_counter){returnmy_counter+1;}// counter variable is not being "mutated"// the output of a pure function is always identicalconsole.log(increment(counter));// 1console.log(increment(counter));// 1console.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'seasy to getsuckeredinto thinking that the "impure" version of the counter
examples/counter-basic-impure/index.htmlis "simpler" ...
thecomplete code (including HTML and JS) is8 lines:

<buttonclass='inc'onclick="incr()">+</button><divid='count'>0</div><buttonclass='dec'onclick="decr()">-</button><script>varel=document.getElementById('count')functionincr(){el.innerHTML=parseInt(el.textContent,10)+1};functiondecr(){el.innerHTML=parseInt(el.textContent,10)-1};</script>

This counterdoes the same thing asour Elm Architecture example (above),
and to theend-user the UIlooks identical:

counter-impure-665

The difference is that in theimpure example is "mutating state"and it's impossible to predict what that state will be!

Annoyingly, for the person explaining the benefitsof function "purity" and the virtues of the Elm Architecture
the "impure" example is bothfewer lines of code(which means itloads faster!), takes less time to read
and renders faster because only the<div> text contentis being updated on each update!
This is why it can often bedifficult to explain to "non-technical"people that code which has similar output
on thescreen(s)mightnot the same quality "behind the scenes"!

Writing impure functions is like setting off on a marathon run aftertying your shoelacesincorrectly ...
You might be "OK" for a while, but pretty soon your laces will come undoneand you will have tostop andre-do them.

To conclude: Pure functions do not mutate a "global" stateand are thus predictable and easy to test;wealways 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 ourStep-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 - canand oftenshould 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)

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

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

9.2 Watch it Fail!

Watch the testfail 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 casevar Res = 'reset';
  • Update (case and/or function) to "process the signal" from the UI(i.e. handle the user's desired action)
caseRes:return0;

reset-counter-test-passing

9.4 Write View (UI) Tests

Once we have the Model tests passingwe need to give theuser something to interact with!
We are going to be "adventurous" and writetwo 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){varreset=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 statevarroot=document.getElementById(id);assert.equal(root.getElementsByClassName('count')[0].textContent,7);varbtn=root.getElementsByClassName("reset")[0];// click reset buttonbtn.click();// Click the Reset button!varstate=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 makeboth these testspass requiresasingle line of code in theview function!

button('Reset',signal,Res)

reset-counter


10.Next Level: Multiple Counters!

Now that you haveunderstood the Elm Architectureby 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 yourinstincts andtry to the following:

1.Refactor the "reset counter" exampleto use anObject for themodel (instead of anInteger)
e.g:var model = { counters: [0] }
where the value of the first element in themodel.counters Arrayis the value for thesingle counter example.

2.Displaymultiple counters on thesame pageusing thevar model = { counters: [0] } approach.

3.Write tests for the scenario where thereare 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!

Theultimate test of whether youlearned/understood something is
applying your knowledge todifferent 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'slistwe see theElmArchitecture at number four ...
this seems fairly logical (initially) because theElmGuideuses theElmLanguage to explain theElmArchitecture:https://guide.elm-lang.org/architecture

elm-architecture

i.e. itassumes that peoplealreadyunderstandthe (Core)ElmLanguage...
This is afair assumption given theordering of the Guidehowever... we have adifferent idea:

Hypothesis: Learn (& Practice) Elm Architecturebefore Learning Elm?

Wehypothesize that if weexplain theElm Architecture(in detail) using alanguage
people arealready familiar with (i.eJavaScript)before diving into the Elm Language
it will"flatten"thelearning curve.

Note: Understanding theElm Architecturewill give you amassive headstart
onlearningReduxwhich is the "de facto" way of structuring React.js Apps.
So even if youdecide not to learn/use Elm, you will still gaingreat frontend skills!

Isn't DOM Manipulation Super Slow...?

DOM manipulation is theslowestpart of any "client-side" web app.
That is why so many client-side frameworks(includingElm, React and Vue.js) now use a "Virtual DOM".For the purposes ofthis tutorial, and formost small appsVirtual DOM is totaloverkill!
It's akin to putting ajet engine in ago kart!

What is "Plain" JavaScript?

"Plain" JavaScript just means not usingany frameworksor features that require "compilation".

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

babel

If you can build with "ES5" #"https://twitter.com/iamdevloper/status/610191865216786432" rel="nofollow">noiseand focus on core skills thatalready work everywhere!
(don't worry you can always "top-up" yourJS knowledge later with ES6, etc!)
b) youdon't need towaste time installingTwo Hundred Megabytesof dependencies just to run a simple project!
c) Yousave time (for yourself, your team and end-users!)because your code isalready optimized to run inany browser!

About

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

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp