Professional Documents
Culture Documents
Ajdin Imsirovic
BIRMINGHAM - MUMBAI
Elm Web Development
Copyright © 2018 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system,
or transmitted in any form or by any means, without the prior written permission of the
publisher, except in the case of brief quotations embedded in critical articles or reviews.
Every effort has been made in the preparation of this book to ensure the accuracy of the
information presented. However, the information contained in this book is sold without
warranty, either express or implied. Neither the author, nor Packt Publishing or its
dealers and distributors, will be held liable for any damages caused or alleged to have
been caused directly or indirectly by this book.
Packt Publishing has endeavored to provide trademark information about all of the
companies and products mentioned in this book by the appropriate use of capitals.
However, Packt Publishing cannot guarantee the accuracy of this information.
Commissioning Editor: Smeet Thakkar
Acquisition Editor: Siddharth Mandal
Content Development Editor: Arun Nadar
Technical Editor: Rutuja Vaze
Copy Editor: Safis Editing
Project Coordinator: Sheejal Shah
Proofreader: Safis Editing
Indexer: Aishwarya Gangawane
Graphics: Jason Monteiro
Production Coordinator: Shantanu Zagade
First published: March 2018
Production reference: 1280318
Published by Packt Publishing Ltd.
Livery Place
35 Livery Street
Birmingham
B3 2PB, UK.
ISBN 978-1-78829-905-3
>www.packtpub.com
mapt.io
What is Elm?
Constraints as guarantees
Piped syntax
Easy refactoring
Increased productivity
Time-traveling debugger
multiply(8, "10");
multiplyNumber x y =
x * y
main =
text ( toString ( multiplyNumber 8 10 ) )
toString multiplyNumber 8 10
multiplyNumber x y =
x * y
main =
multiplyNumber 2 10
|> multiplyNumber 2
|> multiplyNumber 2
|> toString
|> text
multiplyNumber x y =
x * y
main =
multiplyNumber 8 10
|> toString
|> text
main =
multiplyNumber 8 10
|> toString
|> text
main =
text "Hello, World!"
main =
p [] [ text "Hello, Elm!" ]
main =
div []
[ p [] [text "1st paragraph" ]
, p [] [text "2nd paragraph" ]
]
main =
div [ class "danger" ]
[ p [] [text "1st paragraph" ]
, p [] [text "2nd paragraph" ]
]
elm-app start
main =
text "Hello, Elm!"
Summary
In this chapter, we have covered a number of
important topics, namely:
What is Elm?
Messages in Elm
view model =
div [] [ h1 [] [ text ("Fruit to eat: " ++ (toString mode
type Msg =
Decrement
-- MODEL
type alias Model =
Int
-- VIEW
view model =
div [] [ h1 [] [ text ("Fruit to eat: " ++ (toString mode
-- MESSAGE
type Msg =
Decrement
-- UPDATE
update msg model =
model - 1
NAMING ERROR
Line 23, Column 20
Cannot find variable onClick
-- UPDATE
update msg model =
model - 1
-- UPDATE
update msg model =
if model > 0 then model - 1 else model == 5
number
But the else branch is:
Bool
Hint: These need to match so that no matter which branch we t
main =
HTML.beginnerProgram
{ model = 5
, update = update
, view = view
}
-- MODEL
type alias Model =
Int
-- VIEW
view model =
div []
[ h1 [] [ text ("Fruit to eat: " ++ (toString model))
, button [ onClick Decrement ] [ text "Eat fruit" ]
]
-- MESSAGE
type Msg =
Decrement
-- UPDATE
update msg model =
if model > 0 then model - 1 else model + 5
'a' : Char
""" \
| This
| is
| a
| multi-line
| string
| """
" \n This \n is \n a \n multi-line \n string \n " : String
> 3.6
3.6 : Float
> true
-- NAMING ERROR ---------------------------------------------
3| true
^^^^
Maybe you want one of the following?
List.take
String.trim
3| [ 1, 2, 3, 'c' ]
^^^
The 3rd entry has this type:
number
Char
( "abc", True )
The 1st and 2nd entries in this list are different types of v
( Char, Char )
Char
('1','2','3','4','5','6','7','8','9','0')
dict =
Dict.fromList
[ ("keyOne", "valueOne")
, ("keyTwo", "valueTwo")
]
Dict.fromList [("keyOne","valueOne"),("keyTwo","valueTwo")]
> time = 24
24 : number
> if time < 12 then "morning" else "afternoon"
"afternoon" : String
example:
import HTML exposing (HTML, button, div, text)
import HTML.Events exposing (onClick)
main =
HTML.beginnerProgram { model = model, view = view, update =
-- MODEL
type alias Model = Int
model : Model
model =
0
-- UPDATE
type Msg = Increment | Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
-- VIEW
view : Model -> HTML Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick Increment ] [ text "+" ]
]
-- MESSAGE
type Msg =
Decrement
Decrement ->
model - 1
Decrement ->
model - 1
-- MODEL
type alias Model =
Int
-- VIEW
view model =
div []
[ h1 [] [ text ("Fruit to eat: " ++ (toString model))
, button [ onClick Decrement ] [ text "Eat fruit" ]
, button [ onClick Reset ] [ text "Reset counter" ]
]
-- MESSAGE
type Msg = Decrement | Reset
-- UPDATE
update msg model =
case msg of
Decrement ->
if model >= 1 then
model - 1
else
5
Reset ->
5
0 : Int
fizzBuzz = "FizzBuzz"
fizz = "Fizz"
buzz = "Buzz"
fizzBuzzInput value =
if value % 15 == 0 then
fizzBuzz
else if value % 3 == 0 then
fizz
else if value % 5 == 0 then
buzz
else (toString value)
main =
text (fizzBuzzInput 34567)
some operators
Using elm-make-app
Starting downloads...
● elm-lang/HTML 2.0.0
● elm-lang/virtual-dom 2.0.4
● elm-lang/core 5.1.1
elm-app start
Starts the development server.
elm-app build
Bundles the app into static files for production.
elm-app test
Starts the test runner.
elm-app eject
Removes this tool and copies build dependencies, configur
and scripts into the app directory. If you do this, you c
cd my-portfolio
elm-app start
8| h1 []
9| [ text "My Portfolio" ]
10|> div
11|> []
12|> [ text "Just a bunch of text here " ]
Maybe you forgot some parentheses? Or a comma?
Once that is done, save the index.HTML file and close it.
Back in Main.elm, we need to import the HTML.Attributes
module. We also need to specify the CSS classes to
use. We’ll begin by adding the .card class to the div
that wraps the ul tag.
The updated code should look like this:
module Main exposing (main)
<element> : List (Attribute msg) -> List (HTML msg) -> HTML m
About Me
Poems
Stories
Contact
About.elm
Stories.elm
Contact.elm
model =
""
type Msg
= FirstButtonMessage
| SecondButtonMessage
| ThirdButtonMessage
| FourthButtonMessage
SecondButtonMessage ->
"You've clicked the second button!"
ThirdButtonMessage ->
"You've clicked the third button!"
FourthButtonMessage ->
"You've clicked the fourth button!"
view model =
div []
[ h1 [ class "display-4 mt-5" ] [ text "My Portfolio"
, div [ class "container" ]
[ h2 [ class "strong" ] [ text "Just Another Writ
, hr [] []
, div [ class "mt-5" ]
[ button
[ class "btn btn-primary mr-1", onClick F
[ text "First Button" ]
, button
[ class "btn btn-primary", onClick Second
[ text "Second Button" ]
, button
[ class "btn btn-primary", onClick ThirdB
[ text "3rd Button" ]
, button
[ class "btn btn-primary", onClick Fourth
[ text "4th Button" ]
, p [ class "mt-5 lead" ] [ text model ]
]
]
]
main =
HTML.beginnerProgram
{ model = model
, view = view
, update = update
}
and a p function.
Each button’s attributes list takes two attributes;
the first one is the class to use on the button, and
the second one is the onClick event. Each onClick event
will emit a different message, ready to be accepted
and used by the update function.
Finally, the main function takes in HTML.beginnerProgram,
which was explained in Chapter 2, Building Your
First Elm App. Now that we understand just what
the preceding code is doing, we can improve it a
bit.
Improving our
messages
To begin this section, let’s look at the completed
code, with all the improvements made to it, and
after that, we’ll discuss the changes made to the
code:
model : String
model =
""
type Msg
= FirstButtonMessage
| SecondButtonMessage
| ThirdButtonMessage
| FourthButtonMessage
"""
SecondButtonMessage ->
"""
You've clicked the "My Poems" button!
ThirdButtonMessage ->
"""
You've clicked the "My Stories" button!
"""
FourthButtonMessage ->
"""
You've clicked the "Contact Me" button!
"""
model : Model
model =
...
view a =
...
, p [ class "mt-5 lead" ] [ text a ]
]
]
]
The fact that we replaced both msg and model with the
more generic a in the preceding code, and did not
break it, is great. The compiler happily performs its
duties, and we still have a working app.
Type alias is used to make it easier to read complex
type annotations. Also, type annotations are
capitalized by default, and so are union types.
Thus, both the Model and the Msg are capitalized in
our code, and we cannot change them.
Working with List.map
and List.filter
As discussed in previous chapters, Elm works with
immutable data structures. How do we then use the
existing immutable data structure to find its
members that satisfy only a certain condition or to
produce different values based on existing values?
To achieve these two goals, we can use the map and
filter functions. To keep things simple, we’ll look at
the List.map and List.filter functions, although .map
and .filter can also be used with some other data
structures in Elm.
Let’s say our goal is to take a List of numbers and find
only those that are divisible by 3. To begin, let’s
define a function that will take an Int and return a
Boolean (either True or False), based on whether the
ourList = [1,2,3,4,5,6,7,8,9,10]
fizzBuzzInput value =
if value % 15 == 0 then
fizzBuzz
else if value % 3 == 0 then
fizz
else if value % 5 == 0 then
buzz
else (toString value)
main =
text (fizzBuzzInput 34567)
ourList = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
main =
text (String.concat (List.map (fizzBuzzCheck "fizz" "buzz" "
ourList = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
main =
List.map (fizzBuzzCheck "fizz" "buzz" "fizz buzz") ourList
|> String.concat
|> text
Using elm-make-app
-- Model
init =
{}
-- Update
type Msg
= Nothing
-- View
view model =
div [] [ text "Everything will go here" ]
-- Main
main =
beginnerProgram
{ model = init
, view = view
, update = update
}
init =
{ unit1 = "Kilometers"
, unit2 = "Miles"
, ratio = 1.608
, convertedValue = 0.0
}
-- MODEL
init : Model
init =
{ unit1 = "Kilometers"
, unit2 = "Miles"
, ratio = 1.608
, convertedValue = 0.0
}
view model =
div []
[ div []
[ label [ for "unit1Input" ] [ text model.unit1 ]
, input [ id "unit1Input" ] []
, button [ onClick Swap ] [ text "Switch" ]
, label [ for "unit2" ] [ text model.unit2 ]
, div [ id "unit2Value" ] [ text "1234" ]
]
]
...
, div [ id "unit2Value" ] [ text (toString model.
]
]
view model =
div []
[ div []
[ label [ for "unit1Input" ] [ text model.unit1 ]
, input [ id "unit1Input", onInput Convert ] []
, button [ onClick Swap ] [ text "Switch" ]
, label [ for "unit2" ] [ text model.unit2 ]
, div [ id "unit2Value" ] [ text (toString model.
]
]
NAMING ERROR
Line 28, Column 9
Cannot find pattern Convert
NAMING ERROR
Line 36, Column 48
Cannot find variable Convert
-- Model
{--
type alias Model =
{ convertedValue : Float
, ratio : Float
, unit1 : String
, unit2 : String
}
init : Model
--}
TYPE MISMATCH
Line 49, Column 9
The argument to function beginnerProgram is causing a mismat
Function beginnerProgram is expecting the argument to be:
{ ...
, update :
Main.Msg
-> { ..., convertedValue : Float }
-> { ..., convertedValue : Float }
}
But it is:
{ ...
, update :
Main.Msg
-> { ..., convertedValue : Result.Result String Float }
-> { ..., convertedValue : Result.Result String Float }
}
Hint: Problem at update.convertedValue...
-- Model
type alias Model =
{ convertedValue : Float
, ratio : Float
, unit1 : String
, unit2 : String
}
init : Model
init =
{ unit1 = "Kilometers"
, unit2 = "Miles"
, ratio = 1.608
, convertedValue = 0.0
}
-- Update
type Msg
= Swap
| Convert String
-- View
view model =
div []
[ div []
[ label [ for "unit1Input" ] [ text model.unit1 ]
, input [ id "unit1Input", onInput Convert ] []
, button [ onClick Swap ] [ text "Switch" ]
, label [ for "unit2" ] [ text model.unit2 ]
, div [ id "unit2Value" ] [ text (toString model.
]
]
-- Main
main =
beginnerProgram
{ model = init
, view = view
, update = update
}
After the update, save the app and test the behavior
of the input field. Once you start typing a number,
you’ll see it immediately being converted to its
converted value in kilometers.
However, there is another improvement we need to
make. As our Result.withDefault is now 10, whenever a
user types anything else besides numbers, or even
before they start typing, we see the number 16.08
displayed on the screen.
This is an easy enough fix. Instead of 10, we’ll
simply use zero as the default Result. Update the
code to the following:
initModel : Model
initModel =
{ unit1 = "Kilometers"
, unit2 = "Miles"
, ratio = 1.608
, convertedValue = 0.0
}
main =
beginnerProgram { model = initModel, view = view, update
view model =
div []
[ div []
[ label [ for "unit1Input" ] [ text model.unit1 ]
, input [ id "unit1Input", onInput Convert ] []
, button [ onClick Swap ] [ text "Switch" ]
, label [ for "unit2" ] [ text model.unit2 ]
, div [ id "unit2Value" ] [ text (toString model.
]
]
type Msg
= Swap
| Convert String
initModel : Model
initModel =
{ unit1 = "Kilometers"
, unit2 = "Miles"
, ratio = 1.608
, convertedValue = 0.0
}
main =
beginnerProgram { model = initModel, view = view, update
view model =
div []
[ div [ class "col-lg-offset-3 col-lg-6 mt5 pt5" ]
[ h1 []
[ text "Unit Converter App" ]
, div [ class "input-group" ]
[ span [ class "input-group-btn" ]
[ button [ class "btn btn-secondary", type_ "button" ]
[ text model.unit1 ]
]
, input [ onInput Convert, class "form-control", placeholder
, span [ class "input-group-btn" ]
[ button [ onClick Swap, class "btn btn-primary", type_ "but
]
]
, div [ class "mt5 pt5" ] [ text model.unit2 ]
, div [ id "unit2Value" ] [ text (toString model.convertedVa
]
]
type Msg
= Swap
| Convert String
ourList = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
main =
List.map (fizzBuzzCheck "fizz" "buzz" "fizz buzz") ourLis
|> String.concat
|> text
-- Model
initModel =
{}
-- Update
type Msg
= Nothing
-- View
view model =
div [] [ text "Everything will go here" ]
-- Main
main =
beginnerProgram
{ model = initModel
, view = view
, update = update
}
initModel : Model
initModel =
{ inputValue = ""
, outputValue = 0.0
}
We can now save and run our app and observe how
it dynamically updates the text node of the div
under the input field, based on user inputs.
We can observe the following behavior:
initModel : Model
initModel =
{ inputValue = ""
, outputValue = ""
}
-- Update
type Msg
= DisplayInput String
-- Main
main =
beginnerProgram
{ model = initModel
, view = view
, update = update
}
Using Html.map
-- Model
initModel =
{}
-- Update
type Msg
= Nothing
-- View
view model =
div [] [ text "Everything will go here" ]
-- Main
main =
beginnerProgram
{ model = initModel
, view = view
, update = update
}
Now, we will improve it so that it can take multiple
input fields. The first improvement has to do with
separating the current unit converter into a
different module.
Separating
UnitConverter into a
different module
Create a new file, call it UnitConverter.elm, and save it
inside the src folder. Next, copy and paste the
barebones app there as well.
Back in Main.elm, change its imports section so that it
looks like this:
module Main exposing (..)
type Msg
= Swap
| Convert String
view model =
div []
[ UnitConverter.view model.lengthConverter |> Html.ma
, UnitConverter.view model.weightConverter |> Html.ma
]
view :
{ c
| lengthConverter : { b | convertedValue : a, unit1 :
, weightConverter : { b1 | convertedValue : a1, unit1
}
-> Html.Html Msg
type Msg
= LengthConverterMsg UnitConverter.Msg
| WeightConverterMsg UnitConverter.Msg
The 1st and 2nd entries in this list are different types of v
Html.Html UnitConverter.Msg
Html.Html Main.Msg
But it is:
{ ...
, view :
{ lengthConverter : UnitConverter.Model
, weightConverter : UnitConverter.Model
}
-> Html.Html UnitConverter.Msg
}
/*
* Header
*/
.masthead {
margin-bottom: 2rem;
}
.masthead-brand {
margin-bottom: 0;
}
.nav-masthead .nav-link {
padding: .25rem 0;
font-weight: 700;
background-color: transparent;
border-bottom: .25rem solid transparent;
}
.nav-masthead .nav-link:hover,
.nav-masthead .nav-link:focus {
border-bottom-color: rgba(255, 255, 255, .25);
}
.nav-masthead .active {
color: #111;
border-bottom-color: #fff;
}
/*
* Cover
*/
.cover {
padding: 0 1.5rem;
}
.cover .btn-lg {
padding: .75rem 1.25rem;
font-weight: 700;
}
/*
* Footer
*/
.mastfoot {
color: rgba(255, 255, 255, .5);
}
/*
* Affix and center
*/
@media (min-width: 40em) {
/* Pull out the header and footer */
.masthead {
position: fixed;
top: 0;
}
.mastfoot {
position: fixed;
bottom: 0;
}
</head>
<div class="site-wrapper">
<div class="site-wrapper-inner">
<div class="cover-container">
<footer class="mastfoot">
<div class="inner text-secondary">
<p>Cover template for <a href="https://getbootstrap.com/">Bo
</div>
</footer>
</div>
</div>
</div>
create-elm-app design-focused;
cd design-focused;
elm-app start
-- Model
initModel =
{}
-- Update
type Msg
= Nothing
-- View
view model =
div [] [ text "Everything will go here" ]
-- Main
main =
beginnerProgram
{ model = initModel
, view = view
, update = update
}
attrs =
[ attribute "rel" "stylesheet"
, attribute "property" "stylesheet"
, attribute "href" "//maxcdn.bootstrapcdn.com/boo
]
children =
[]
in
node tag attrs children
view model =
div [ class "site-wrapper" ]
[ bootstrapCss
, div [ class "site-wrapper-inner" ]
[ div [ class "cover-container" ]
[ header [ class "masthead clearfix" ]
[ div [ class "inner" ]
[ h3 [ class "masthead-brand" ]
[ text "Unit Converter" ]
, nav [ class "nav nav-masthead" ]
[ a [ class "nav-link text-secondary"
[ text "Home" ]
, a [ class "nav-link text-secondary"
[ text "Features" ]
, a [ class "nav-link text-secondary"
[ text "Contact" ]
]
]
]
, div [ class "inner cover" ]
[ h1 [ class "cover-heading" ]
[ text "Distance converter" ]
, div [ class "col-lg-12 mb-5" ]
[ div [ class "input-group mb-3" ]
[ div [ class "input-group-prepend" ]
[ button [ class "btn btn-seconda
[ text "Kilometers:" ]
]
, input [ attribute "aria-describedby
[]
, div [ class "input-group-append" ]
[ button [ class "btn btn-seconda
[ text "Miles:" ]
, button [ class "btn btn-seconda
[ text "0.0" ]
]
]
]
]
, div [ class "inner cover" ]
[ h1 [ class "cover-heading" ]
[ text "Weight converter" ]
, div [ class "col-lg-offset-12" ]
[ div [ class "input-group mb-3" ]
[ div [ class "input-group-prepend" ]
[ button [ class "btn btn-seconda
[ text "Kilograms:" ]
]
, input [ attribute "aria-describedby
[]
, div [ class "input-group-append" ]
[ button [ class "btn btn-seconda
[ text "Pounds:" ]
, button [ class "btn btn-seconda
[ text "0.0" ]
]
]
]
]
, footer [ class "mastfoot" ]
[ div [ class "inner text-secondary" ]
[ p []
[ text "Cover template for "
, a [ href "https://getbootstrap.com/
[ text "Bootstrap" ]
, text ", by "
, a [ href "https://twitter.com/mdo"
[ text "@mdo" ]
, text "."
]
]
]
]
]
]
coverTemplateCss =
let
tag =
"link"
attrs =
[ attribute "rel" "stylesheet"
, attribute "property" "stylesheet"
, attribute "href" "//rawgit.com/twbs/bootstrap/v
]
children =
[]
in
node tag attrs children
view model =
div [ class "site-wrapper" ]
[ bootstrapCss
, coverTemplateCss
, div [ class "site-wrapper-inner" ]
...
attrs =
[ attribute "rel" "stylesheet"
, attribute "property" "stylesheet"
, attribute "href" "//maxcdn.bootstrapcdn.com/boo
]
children =
[]
in
node tag attrs children
attrs =
[ attribute "rel" "stylesheet"
, attribute "property" "stylesheet"
, attribute "href" "//gitcdn.xyz/repo/twbs/bootst
]
children =
[]
in
node tag attrs children
update :
Msg
-> { b | convertedValue : Float, unit1 : a, unit2 : a, ra
-> { b | convertedValue : Float, unit1 : a, unit2 : a, ra
update msg model =
case msg of
Swap ->
{ model | unit1 = model.unit2, unit2 = model.unit
, div []
[ div [ class "input-group-prepend" ]
[ button [ class "btn btn-warning", type_ "button" ]
, input
[ attribute "aria-describedby" "basic-addon1"
, attribute "aria-label" ""
, class "form-control"
, placeholder "Enter a numerical value"
, type_ "text"
, onInput Convert
]
[]
, button [ onClick Swap ] [ text model.unit2 ]
, button [ id "unit2Value" ] [ text (toString model.c
]
]
]
attrs =
[ attribute "rel" "stylesheet"
, attribute "property" "stylesheet"
, attribute "href" "//maxcdn.bootstrapcdn.com/boo
]
children =
[]
in
node tag attrs children
attrs =
[ attribute "rel" "stylesheet"
, attribute "property" "stylesheet"
, attribute "href" "//gitcdn.xyz/repo/twbs/bootst
]
children =
[]
in
node tag attrs children
-- Model
initModel : Model
initModel =
{ lengthConverter = UnitConverter.init "Miles" "Kilometer
, weightConverter = UnitConverter.init "Kilograms" "Pound
}
-- Update
type Msg
= LengthConverterMsg UnitConverter.Msg
| WeightConverterMsg UnitConverter.Msg
update :
Msg
->
{ c
| lengthConverter :
{ b1
| convertedValue : Float
, ratio : Float
, unit1 : a1
, unit2 : a1
}
, weightConverter :
{ b
| convertedValue : Float
, ratio : Float
, unit1 : a
, unit2 : a
}
}
->
{ c
| lengthConverter :
{ b1
| convertedValue : Float
, ratio : Float
, unit1 : a1
, unit2 : a1
}
, weightConverter :
{ b
| convertedValue : Float
, ratio : Float
, unit1 : a
, unit2 : a
}
}
update msg model =
case msg of
LengthConverterMsg msg_ ->
let
newLengthConverter =
UnitConverter.update msg_ model.lengthCon
in
{ model | lengthConverter = newLengthConverter }
-- View
view :
{ c
| lengthConverter :
{ b | convertedValue : a, unit1 : String, unit2 :
, weightConverter :
{ b1 | convertedValue : a1, unit1 : String, unit2
}
-> Html Msg
view model =
div
[ style
[ ( "background", "#0575e6" )
, ( "background", "-webkit-linear-gradient(to rig
, ( "background", "linear-gradient(to right, #057
]
]
[ bootstrapCss
, coverTemplateCss
, div [ class "site-wrapper-inner" ]
[ div [ class "cover-container" ]
[ header [ class "masthead clearfix" ]
[ div [ class "inner" ]
[ h3 [ class "masthead-brand" ]
[ text "Unit Converter Site" ]
, nav [ class "nav nav-masthead" ]
[ a [ class "nav-link text-second
[ text "Home" ]
, a [ class "nav-link text-second
[ text "Features" ]
, a [ class "nav-link text-second
[ text "Contact" ]
]
]
]
, h1 [ class "cover-heading" ]
[ text "Distance and Weight converter" ]
, UnitConverter.view model.lengthConverter |>
, UnitConverter.view model.weightConverter |>
]
]
]
-- Main
type Msg
= Swap
| Convert String
update :
Msg
-> { b | convertedValue : Float, unit1 : a, unit2 : a, ra
-> { b | convertedValue : Float, unit1 : a, unit2 : a, ra
update msg model =
case msg of
Swap ->
{ model | unit1 = model.unit2, unit2 = model.unit
Using Html.map
Commands in Elm
Subscriptions in Elm
Subscribe to events
aTuple = (60,65,71,75,90)
let \
(a,b,c,d,e) = aTuple \
in \
a + b + c + d + e
let \
(a,b,c,d,e) = aTuple \
in \
(a+b+c) / 3
let \
(a, b) = aTupleOfStrings
in \
a ++ e
5| (a,e) = aTupleOfStrings
^^^^^^^^^^^^^^^
Based on its definition, `aTupleOfStrings` has this type:
( a, b )
let \
(a,_,_,_,e) = aTupleOfStrings \
in \
a ++ e
let \
(greeting,_,_,_,planetDescription) = aTupleOfStrings \
in \
greeting ++ planetDescription
("Hello", _) ->
text "Tuple contained: Hello "
(_, _) ->
text "There were neither 'Hello' nor 'World' in t
(_, _) ->
Now, compile the Ellie app, and you should get the
following:
n = 100
modulusTest = (n % 3, n % 5)
n = 70
modulusTest = (n % 3, n % 7)
(1,2,3,(5,10,15,(100,50,0)))
: ( number
, number1
, number2
, ( number3, number4, number5, ( number6, number7, numb
)
0 : number
averageRunnerTime record =
let
{first, second, third, fourth, fifth} = record
in
(first + second + third + fourth + fifth) / 5
72.2
90 : number
extractorFunction { fifth } =
fifth
this:
module Main exposing (main)
"'"
a field access like .name
an expression
an infix operator like (+)
end of input
more letters in this name
whitespace
main =
Html.beginnerProgram
{ model = model
, view = view
, update = update
}
-- Model
init =
{}
-- Update
type Msg
= Nothing
-- View
view model =
div [] [ text "Everything will go here" ]
-- Main
main =
beginnerProgram
{ model = init
, view = view
, update = update
}
-- Model
type alias Model =
{}
-- Update
type Msg
= Noop
-- View
view : Model -> Html Msg
view model =
div [] [ text "Everything will go here, this time using H
-- Subscriptions
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- Main
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
Install:
elm-lang/http 1.0.0
● elm-lang/http 1.0.0
import Date
Date.fromString "2018-02-18"
Date.fromString "abc"
Nothing
testingMaybe = Just 1
The REPL will return the following:
Just 1 : Maybe.Maybe number
main =
text <|
case tom.age of
Nothing ->
"No age for this person"
Just val ->
val
case sue.age of
Result.withDefault 0 (Ok 1)
1 : number
Maybe.withDefault {} Nothing
{} : {}
expression:
json = """
{
"coord":{
"lon":-87.62,
"lat":41.88
},
"weather":[
{
"id":802,
"main":"Clouds",
"description":"scattered clouds",
"icon":"03n"
}
],
"base":"stations",
"main":{
"temp":268.9,
"pressure":1025,
"humidity":86,
"temp_min":267.15,
"temp_max":270.15
},
"visibility":16093,
"wind":{
"speed":2.1,
"deg":260
},
"clouds":{
"all":40
},
"dt":1518941700,
"sys":{
"type":1,
"id":1030,
"message":0.0033,
"country":"US",
"sunrise":1518957687,
"sunset":1518996470
},
"id":4887398,
"name":"Chicago",
"cod":200
}
"""
weatherDataDecoder =
Json.Decode.map
WeatherData
(field "name" string)
json = """
{
"coord":{
"lon":-87.62,
"lat":41.88
},
"weather":[
{
"id":802,
"main":"Clouds",
"description":"scattered clouds",
"icon":"03n"
}
],
"base":"stations",
"main":{
"temp":268.9,
"pressure":1025,
"humidity":86,
"temp_min":267.15,
"temp_max":270.15
},
"visibility":16093,
"wind":{
"speed":2.1,
"deg":260
},
"clouds":{
"all":40
},
"dt":1518941700,
"sys":{
"type":1,
"id":1030,
"message":0.0033,
"country":"US",
"sunrise":1518957687,
"sunset":1518996470
},
"id":4887398,
"name":"Chicago",
"cod":200
}
"""
weatherDataDecoder =
Json.Decode.map2
WeatherData
(field "id" int)
(field "name" string)
weatherDataDecoder =
Json.Decode.map2
WeatherData
(field "id" int)
(field "name" string)
weatherDataDecoder =
Json.Decode.map3
WeatherData
(field "id" int)
(field "name" string)
(at ["sys", "country"] string)
getTemperature city =
let
url =
"http://api.openweathermap.org/data/2.5/weather?q
in
Http.send NewTemp (Http.getString url)
windSpeed =
Result.withDefault 0 (Decode.decodeString (Decode
temperature =
Result.withDefault 0 (Decode.decodeString (Decode
pressure =
Result.withDefault 0 (Decode.decodeString (Decode
humidity =
Result.withDefault 0 (Decode.decodeString (Decode
in
TemperatureInfo name windSpeed temperature pressure humid
view model =
div []
[ input [ placeholder "City", onInput CityInput ] []
, br [] []
, button [ onClick GetTemp ] [ text "Get temperature"
, br [] []
, div [] [ text "Name: ", text model.temperatureInfo.
, div [] [ text "Temp: ", text (toString model.temper
, div [] [ text "Wind: ", text (toString model.temper
, div [] [ text "Pressure: ", text (toString model.te
, div [] [ text "Humidity: ", text (toString model.te
]
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
windSpeed =
Result.withDefault 0 (Decode.decodeString (Decode
temperature =
Result.withDefault 0 (Decode.decodeString (Decode
pressure =
Result.withDefault 0 (Decode.decodeString (Decode
humidity =
Result.withDefault 0 (Decode.decodeString (Decode
in
TemperatureInfo name windSpeed temperature pressure h
Summary
In this chapter, we have covered a lot of new
concepts, and reinforced some that we have
learned about in the previous chapters. We have
looked at using Result and Maybe to deal with
operations that might fail and to deal with possibly
missing data. We looked at using decoders and
mapping over them.
We also looked at using the Http package to get
remote JSON data. Now that we have set up the
basics of our weather app, we will discuss ways of
improving it in the next chapter.
Adding More Features
to the Weather App
Welcome to Chapter 8, Adding More Features to the
Weather App. The goal of this chapter is to
improve our simple weather app by adding more
features to it.
The topics we will cover include:
elm-app start
import Material
import Material.Button as Button
import Material.Card as Card
import Material.Color as Color
import Material.Elevation as Elevation
import Material.Options as Options
import Material.Scheme
import Material.Textfield as Textfield
import Material.Typography as Typography
import Round exposing (..)
type Msg
= GetTemp
| CityInput String
| NewTemp (Result Http.Error String)
| Mdl (Material.Msg Msg)
GetTemp ->
( model, getTemperature model.city )
Button.render Mdl
[ 1 ]
model.mdl
[ Button.raised
, Button.colored
, Button.ripple
, Options.onClick GetTemp
]
[ text "Get temperature" ]
, br [] []
, Card.view
[ Options.css "width" "256px"
, Options.css "margin" "20px"
, Elevation.e8
, Elevation.transition 250
, Color.background (Color.color Color.LightBlue C
]
All the other content blocks (media, text, and actions) have
identical type signatures, save for the name of the
content block function.
x = 3.141592653589793
round 2 x -- "3.14"
, Card.actions []
[ div [] [ text "Wind: ", text (toString model.temperatur
, div [] [ text "Pressure: ", text (toString model.temper
, div [] [ text "Humidity: ", text (toString model.temper
]
leave all the other code as is, and the app will still
work. The only change that will happen has to do
with the compiled HTML structure of our weather
app. In order to see the change, we will need to
inspect the code in the dev tools, which can be
accessed in most major browsers by pressing the
F12 button.
If using Card.action in our Elm app, the resultant div
in the browser will have this CSS class attribute:
mdl-card__actions. If we instead switch to using Card.text
in our view, the resultant div of the running app in
the browser will have the CSS class attribute of mdl-
card__supporting-text.
the mdl CSS to our app. To find out more about this,
refer to the official documentation at http://package.el
m-lang.org/packages/debois/elm-mdl/8.1.0/Material-Scheme#top.
Adding color schemes
We can change the way our mdl-styled app looks,
almost effortlessly, by utilizing color schemes. To
understand how to do that, we will first need to
refer to the official documentation of the elm-mdl
package. More specifically, we are interested in
topWithScheme, a function that is made available in the
windSpeed =
Result.withDefault 0 (Decode.decodeString (Decode
temperature =
Result.withDefault 0 (Decode.decodeString (Decode
pressure =
Result.withDefault 0 (Decode.decodeString (Decode
humidity =
Result.withDefault 0 (Decode.decodeString (Decode
in
TemperatureInfo name windSpeed temperature pressure h
● debois/elm-dom 1.2.3
● eeue56/elm-html-query 3.0.0
● eeue56/elm-html-in-elm 5.2.0
● eeue56/elm-lazy 1.0.0
● eeue56/elm-lazy-list 1.0.0
● eeue56/elm-html-test 5.1.3
● eeue56/elm-shrink 1.0.0
● elm-community/elm-test 4.2.0
● elm-lang/core 5.1.1
● debois/elm-mdl 8.1.0
● elm-lang/dom 1.1.1
● elm-lang/html 2.0.0
● elm-lang/http 1.0.0
● elm-lang/mouse 1.0.1
● elm-lang/virtual-dom 2.0.4
● elm-lang/window 1.0.1
● mgold/elm-random-pcg 5.0.2
● myrho/elm-round 1.0.2
Packages configured successfully!
"source-directories": [
"..\\src",
"."
],
suite : Test
suite =
todo "Implement our first test. See http://package.elm-la
The code in this file follows the same rules as any
other Elm code. On the first line, we are exposing
the Example module. We follow it up with importing
Expect, Fuzz, and Test, which are all needed for our
tests to work.
It is helpful to be familiar with some of the jargon
of software testing, as it will give some more
context to our test writing. Tests are grouped into
test suites. A test suite holds test cases. A test case
is the smallest test unit. Therefore, appropriately,
we start our testing in Example.elm by specifying a suite
function, which will hold our tests. As we can see,
the suite function is of type Test. The official
documentation on Test can be found at http://package.
elm-lang.org/packages/elm-community/elm-test/latest/Test#Test.
Note that this is not the actual Elm code, but rather
an Elm-like pseudocode, written as an intermediate
step to better understand tests in Elm.
The test and Expect
functions
The test function is used to write the actual unit
test. The test function can have only one Expectation,
and returns a Test.
Expectation is a type alias. It can be one of the two: a
pass or a fail. Expect has several functions, such as
Expect.equal, Expect.notEqual, Expect.lessThan, and so on.
suite : Test
suite =
describe "Zero is equal to zero"
[ test "Zero is equal to one minus one" <|
\_ -> Expect.equal 0 (1 - 1)
, test "Zero is equal to two minus two" <|
\_ -> Expect.equal 0 (2 - 2)
]
elm-test tests/Example.elm
$ elm-test tests/Example.elm
Success! Compiled 0 modules.
Successfully generated /dev/null
Success! Compiled 1 module.
Successfully generated C:\Users\PC\Desktop\improved-weather-a
elm-test 0.18.12
----------------
Duration: 517 ms
Passed: 2
Failed: 0
( \_ -> Expect.equal 0 (1 - 1) )
suite : Test
suite =
describe "Zero is equal to zero"
[ test "Zero is equal to one minus one" <|
\_ -> Expect.equal 0 <| 1 - 1
, test "Zero is equal to two minus two" <|
\_ -> Expect.equal 0 <| 2 - 2
]
all : Test
all =
describe "A Test Suite"
[ test "Addition" <|
\_ ->
Expect.equal 10 (3 + 7)
, test "String.left" <|
\_ ->
Expect.equal "a" (String.left 1 "abcdefg")
, test "This test should fail" <|
\_ ->
Expect.fail "failed as expected!"
]
elm-test
elm-test 0.18.12
----------------
> Tests
> A Test Suite
> This test should fail
failed as expected!
Duration: 696 ms
Passed: 4
Failed: 1
This test failed, but as the console output reads,
that was expected. As can be seen in the code of the
Tests.elm file, when we need to write a failing test, we
simply use the Expect.fail function.
As we will continue working with the Tests.elm file in
the following sections of this chapter, a useful
command that we can use is elm-test --watch. If you
are familiar with a number of different utilities that
can be run in the console, the --watch flag watches
for changes in our code and reruns the command
that the --watch flag is appended to.
This means that we’ll only need to run the elm-test --
watch command once, and every time we save the
suite : Test
suite =
describe "A Test Suite"
[ describe "Testing addition"
[ test "Addition" <|
\_ ->
Expect.equal 10 (3 + 7)
]
, describe "Using let-in expression in a test suite"
[ test "Multiplication" <|
\_ ->
let
x = 2
y = 4
xy = x * y
in
xy
|> \_ -> Expect.equal 8 (2 * 4)
]
]
Running this Example.elm file with elm-test
tests/Example.elm will produce the following output to
the console:
Success! Compiled 1 module.
Successfully generated /dev/null
Success! Compiled 1 module.
Successfully generated C:\Users\PC\Desktop\improved-weather-a
elm-test 0.18.12
----------------
Duration: 334 ms
Passed: 2
Failed: 0
suite : Test
suite =
describe "Decoder test"
[ test "Test decoding valid string" <|
\_ ->
let
jsonInput =
"{\"main\":{\"temp\":303.15,\"pressur
expectedResult =
TemperatureInfo "Rome" 15.3 30.0 30.0
in
Expect.equal expectedResult (decodeTemperatur
{--
, test "Test decoding invalid string" <|
\_ ->
let
jsonInput =
"Scrabbled json message"
expectedResult =
TemperatureInfo "Error decoding data!
in
Expect.equal expectedResult (decodeTemperatur
, fuzz string "Fuzz test decoding invalid string" <|
\randomlyGeneratedString ->
let
expectedResult =
TemperatureInfo "Error decoding data!
in
Expect.equal expectedResult (decodeTemperatur
-}
]
elm-test 0.18.12
----------------
Duration: 663 ms
Passed: 1
Failed: 0
jsonInput =
"{\"main\":{\"temp\":303.15,\"pressure\":30,\"humidity\":20},
An alternative approach would be to enclose the
entire JSON string inside the opening and closing
multiline quotes, which, as mentioned earlier in the
book, are groupings of three double quotes, like
this: """. We will see an example of this approach in
the next section, when we build some custom
expectations.
Building custom
expectations while
decoding JSON
To build custom expectations, we can refer to the
previously-mentioned pass and fail functions. The
pass function will always pass, and the fail function
will fail with a message. To produce a custom
message, you can use the onFail function.
Let’s see an example by creating a new file inside
the tests folder. We’ll call this new
file CustomExpectations.elm and add the following code:
module CustomExpectations exposing (..)
playerDecoder =
map2 Player
(field "name" string)
(field "language" string)
suite : Test
suite =
describe "A Test Suite"
[ describe "A custom expectation with a custom decode
[ test "Decoding JSON strings" <|
\_ ->
let
json =
"""
{
"name" : "John Doe",
"language" : "English"
}
"""
in
case decodeString playerDecoder json of
Ok json ->
Expect.pass
output:
Success! Compiled 0 modules.
Successfully generated /dev/null
Success! Compiled 1 module.
Successfully generated C:\Users\PC\Desktop\elm-web-developmen
elm-test 0.18.12
----------------
Duration: 468 ms
Passed: 1
Failed: 0
suite : Test
suite =
describe "Zero is equal to zero"
[ test "Zero is equal to one minus one" <|
\_ -> Expect.equal 0 (1 - 1)
, test "Zero is equal to two minus two" <|
\_ -> Expect.equal 0 (2 - 2)
]
suite : Test
suite =
describe "Zero is equal to zero"
[ test "Zero is equal to one minus one" <|
\_ -> Expect.equal 0 (1 - 1)
, test "Zero is equal to two minus two" <|
\_ -> Expect.equal 0 (2 - 2)
]
suite : Test
suite =
describe "Zero is equal to zero"
[ fuzz int "Zero is equal to random number minus itse
\randomNumber -> Expect.equal 0 (randomNumber - r
]
a fuzzer
suite : Test
suite =
describe "Zero is equal to zero"
[ fuzz2 int int "Swiching positions on two numbers bein
\randomNumber1 randomNumber2 ->
Expect.equal (randomNumber1 + randomNumber2) (ran
]
elmrails Container
Development Stack with your custom installed tools
http://<containername>-<username><123456>.codeanyapp.com
https://<containername>-<username><123456>.codeanyapp.com
http://port-XX.<containername>-<username><123456>.codeanyapp.
http://<containername>-<username><123456>.codeanyapp.com
cabox@box-codeanywhere:~/workspace$
∨ elmrails
> simple
rails s
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
http://<containername>-<username><123456>.codeanyapp.com
document.addEventListener('DOMContentLoaded', () => {
const target = document.createElement('div')
document.body.appendChild(target)
Elm.Main.embed(target)
})
Rails.application.routes.draw do
# For details on the DSL available within this file, see ht
root 'application#index'
end
Install:
debois/elm-dom 1.2.3
debois/elm-mdl 8.1.0
elm-lang/dom 1.1.1
elm-lang/mouse 1.0.1
elm-lang/window 1.0.1
myrho/elm-round 1.0.2
● debois/elm-dom 1.2.3
● elm-lang/mouse 1.0.1
● elm-lang/dom 1.1.1
● debois/elm-mdl 8.1.0
● elm-lang/window 1.0.1
● myrho/elm-round 1.0.2
document.addEventListener('DOMContentLoaded', () => {
const target = document.getElementById('weather-app')
Elm.WeatherApp.embed(target);
})
<h1>
Successfully integrated Elm and Rails!
</h1>
<%= javascript_pack_tag "weather_app" %>
<div id="weather-app"></div>
<div></div>
rails s
https://github.com/bryanjenningz/25-elm-examples
https://www.smashingmagazine.com/2018/01/learning-elm-
drum-sequencer-part-1/
http://elmprogramming.com/
https://www.reddit.com/r/elm/
http://elmlang.herokuapp.com/
https://www.elmlog.com/meetup_groups
https://discourse.elm-lang.org/
Visit elmcasts.com
And finally, a brand new project started by the
author of this book. A website dedicated to learning
Elm.
In the tradition of sites such as railscasts.com and lara
casts.com, I’ve decided to name it elmcasts.com. The