- Notifications
You must be signed in to change notification settings - Fork22
🦄 Learn how to build web apps using the Elm Architecture in "vanilla" JavaScript (step-by-step TDD tutorial)!
License
dwyl/learn-elm-architecture-in-javascript
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Learn how to build web applications usingthe Elm ("Model Update View") Architecture in "plain" JavaScript.
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:
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!
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
Anyone who knows alittle bit of JavaScriptand wants to learn how to organize/structure
their code/app in asane, predictable and testable way.
- Basic JavaScript Knowledge.see:github.com/dwyl/Javascript-the-Good-Parts-notes
- Basic Understanding ofTDD. If you arecompletely new to TDD,please see:github.com/dwyl/learn-tdd
- A computer with a Web Browser.
- 30 minutes.
No other knowledge is assumed or implied.If you haveany questions,please ask:
github.com/dwyl/learn-elm-architecture-in-javascript/issues
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
performedby people andupdate
s 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 to
view
the Model (in the case of the first tutorial below,the counter) asHTML
rendered in a web browser.
If you're not into flow diagrams,here is a much more "user friendly" explanationof The Elm Architecture ("TEA"):
Kolja Wilcke's"View Theater" diagramCreative Commons LicenseAttribution 4.0 International (CC BY 4.0)
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)!
git clone https://github.com/dwyl/learn-elm-architecture-in-javascript.git&&cd learn-elm-architecture-in-javascript
Tip: if you havenode.js installed, simply run
npm 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:
Try clicking on the buttons to increase/decrease the counter.
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!
When you refresh the your Web Browser you will seethat the "initial state" is now9(or whichever number you changed the initial value to):
You have just seen how easy it is to set the "initial state"in an App built with the Elm Architecture.
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
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 beforemount
ing)
The first line inmount
is to get areference to the root DOM element;
we do thisonce in the entire application tominimize DOM lookups.
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"!
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
thesignal
would be executedimmediately when thebutton
isdefined.
Whereas we only want thesignal
(callback
) to be triggeredwhen the button isclicked.
Try removing thecallback
to see the effect:
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.
- https://developer.mozilla.org/en/docs/Web/JavaScript/Closures
- http://javascriptissexy.com/understand-javascript-closures-with-ease/
- ... if closures aren't "clicking",or you wantmore detail/examples,please ask!
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.
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!
Theupdate
function is a simpleswitch
statement 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).
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.
Theview
makes use of three "helper" (DOM manipulation) functions:
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}}
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)}
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: in
elm
land all of these "helper" functions are in theelm-html
package, 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!
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-server
which will auto-open yourdefault
browser:
Then you cannavigate to the desired file.e.g:http://127.0.0.1:8000/examples/counter-basic/
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:
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
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".
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.
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
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
It'seasy to getsuckeredinto thinking that the "impure" version of the counterexamples/counter-basic-impure/index.html
is "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:
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.
As you (hopefully) recall from ourStep-by-Step TDD Tutorial,when we craft code following the "TDD" approach,we go through the following steps:
- Read and understand the "user story"(e.g: in this case:issues/5)
- Make sure the "acceptance criteria" are clear(the checklist in the issue)
- Write your test(s) based on the acceptance criteria.(Tip: a single feature - in this case resetting the counter - canand often
should
have multiple tests to cover all cases.) - Write code to make the test(s) pass.
BEFORE
you continue, try and build the "reset"functionality yourself following TDD approach!
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);});
Watch the testfail in your Web Browser:
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)
caseRes:return0;
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});
Watch the UI tests go red in the browser:
Luckily, to makeboth these testspass requiresasingle line of code in theview
function!
button('Reset',signal,Res)
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!
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
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
- The Elm Architecture Simple, yet powerful – An overview by example:https://dennisreimann.de/articles/elm-architecture-overview.html(written in Elm so not much use for people who only know JS,but a good post for further reading!)
- What does it mean when something is "easy toreason about"?
http://stackoverflow.com/questions/18666821/what-does-the-term-reason-about-mean-in-computer-science - Elm Architecture with JQuery by @steos:https://medium.com/javascript-inside/elm-architecture-with-jquery-152cb98a62f(written in JQuery and no Tests sonot ideal for teaching beginners good habits, but still a v. good post!)
- Pure functions:https://en.wikipedia.org/wiki/Pure_function
- Higher Order Functions in #"http://eloquentjavascript.net/05_higher_order.html" rel="nofollow">http://eloquentjavascript.net/05_higher_order.html
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
i.e. itassumes that peoplealreadyunderstandthe (Core)ElmLanguage...
This is afair assumption given theordering of the Guidehowever... we have adifferent idea:
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!
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!
"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!!
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
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.