Yesterday we got the structure (HTML) and styles (CSS) completed.
Today we're going to port the HTML to Elm and organize the code in such a way that it will be convenient to add features moving forward.
Port the HTML to Elm
Go to your project's root directory and run elm init
.
$ cd path/to/random-quote-machine
$ elm init
Press ENTER at the prompt to have an elm.json
file and an empty src
directory created for you.
The elm.json
file tracks dependencies and other metadata for your app.
The src
directory is where you'll place all the Elm files comprising your app. In this case you'll need one file, src/Main.elm
.
N.B. You can read https://elm-lang.org/0.19.0/init to learn more about elm init
.
Go ahead and create that file now.
$ touch src/Main.elm
And, edit it to contain the following:
module Main exposing (main)
import Html exposing (Html, a, blockquote, button, cite, div, footer, i, p, span, text)
import Html.Attributes exposing (autofocus, class, href, target, type_)
main : Html msg
main =
div [ class "background" ]
[ div []
[ div [ class "quote-box" ]
[ blockquote [ class "quote-box__blockquote"]
[ p [ class "quote-box__quote-wrapper" ]
[ span [ class "quote-left" ]
[ i [ class "fa fa-quote-left" ] [] ]
, text "I am not a product of my circumstances. I am a product of my decisions."
]
, footer [ class "quote-box__author-wrapper" ]
[ text "\u{2014} "
, cite [ class "author" ] [ text "Stephen Covey" ]
]
]
, div [ class "quote-box__actions" ]
[ div []
[ a [ href "https://twitter.com/intent/tweet?hashtags=quotes&text=%22I%20am%20not%20a%20product%20of%20my%20circumstances.%20I%20am%20a%20product%20of%20my%20decisions.%22%20%E2%80%94%20Stephen%20Covey"
, target "_blank"
, class "icon-button"
]
[ i [ class "fa fa-twitter" ] [] ]
]
, div []
[ a [ href "https://www.tumblr.com/widgets/share/tool?posttype=quote&tags=quotes&content=I%20am%20not%20a%20product%20of%20my%20circumstances.%20I%20am%20a%20product%20of%20my%20decisions.&caption=Stephen%20Covey&canonicalUrl=https%3A%2F%2Fwww.tumblr.com%2Fdocs%2Fen%2Fshare_button"
, target "_blank"
, class "icon-button"
]
[ i [ class "fa fa-tumblr" ] [] ]
]
, div []
[ button
[ type_ "button"
, autofocus True
, class "button"
]
[ text "New quote" ]
]
]
]
, footer [ class "attribution" ]
[ text "by "
, a [ href "https://github.com/dwayne/"
, target "_blank"
, class "attribution__link"
]
[ text "dwayne" ]
]
]
]
An Elm file is called a module. The first line names the module, Main
, and makes the main
function available for use by the outside world.
The import lines import various functions from the Html and Html.Attributes modules for use in your Main
module.
The Html
and Html.Attributes
modules exist in the elm/html package. If you look in your elm.json
file you'd see that elm init
has already set you up with the elm/html
package as a direct dependency. This means you won't need to install it.
The main
function contains mostly what we'd find in index.html
except that instead of HTML tags and attributes we have function calls.
In general,
<foo attr1="a" attr2="b">bar</foo>
is translated into:
foo [ attr1 "a", attr2 "b" ] [ text "bar" ]
where foo
and text
are in Html
and attr1
and attr2
are in
Html.Attributes
.
The only minor difference is our use of the Unicode code point \u{2014}
instead of the —
HTML entity.
Compile to JavaScript
elm make src/Main.elm --output=assets/app.js
The Main
module is compiled to JavaScript and saved in assets/app.js
.
Load it
Edit index.html
and replace the body
with the following:
<body>
<div id="app"></div>
<script src="assets/app.js"></script>
<script>
Elm.Main.init({
node: document.getElementById("app")
});
</script>
</body>
View index.html
in a browser and observe that absolutely nothing has changed. That's a good thing. It means you faithfully converted the HTML to Elm.
For more details, go here.
Refactor the Elm code
We'll extract some useful functions and add a new type.
Extract the viewQuote
function
From the main
function, take the "HTML" that comprises the blockquote
and wrap it in a function named viewQuote
that takes two arguments.
viewQuote : String -> String -> Html msg
viewQuote content author =
blockquote [ class "quote-box__blockquote"]
[ p [ class "quote-box__quote-wrapper" ]
[ span [ class "quote-left" ]
[ i [ class "fa fa-quote-left" ] [] ]
, text content
]
, footer [ class "quote-box__author-wrapper" ]
[ text "\u{2014} "
, cite [ class "author" ] [ text author ]
]
]
Then, from main
call viewQuote
.
viewQuote
"I am not a product of my circumstances. I am a product of my decisions."
"Stephen Covey"
Extract the viewIconButton
function
Notice that
a [ href "https://twitter.com/intent/tweet?hashtags=quotes&text=%22I%20am%20not%20a%20product%20of%20my%20circumstances.%20I%20am%20a%20product%20of%20my%20decisions.%22%20%E2%80%94%20Stephen%20Covey"
, target "_blank"
, class "icon-button"
]
[ i [ class "fa fa-twitter" ] [] ]
and this
a [ href "https://www.tumblr.com/widgets/share/tool?posttype=quote&tags=quotes&content=I%20am%20not%20a%20product%20of%20my%20circumstances.%20I%20am%20a%20product%20of%20my%20decisions.&caption=Stephen%20Covey&canonicalUrl=https%3A%2F%2Fwww.tumblr.com%2Fdocs%2Fen%2Fshare_button"
, target "_blank"
, class "icon-button"
]
[ i [ class "fa fa-tumblr" ] [] ]
are quite similar.
Write a function named viewIconButton
to generalize the pattern you see.
viewIconButton : String -> String -> Html msg
viewIconButton name url =
a [ href url
, target "_blank"
, class "icon-button"
]
[ i [ class ("fa fa-" ++ name) ] [] ]
Go back to main
and replace the links with the relevant function calls.
For Twitter use:
viewIconButton "twitter" "https://twitter.com/intent/tweet?hashtags=quotes&text=%22I%20am%20not%20a%20product%20of%20my%20circumstances.%20I%20am%20a%20product%20of%20my%20decisions.%22%20%E2%80%94%20Stephen%20Covey"
And, for Tumblr use:
viewIconButton "tumblr" "https://www.tumblr.com/widgets/share/tool?posttype=quote&tags=quotes&content=I%20am%20not%20a%20product%20of%20my%20circumstances.%20I%20am%20a%20product%20of%20my%20decisions.&caption=Stephen%20Covey&canonicalUrl=https%3A%2F%2Fwww.tumblr.com%2Fdocs%2Fen%2Fshare_button"
I hope you see that the Twitter and Tumblr URLs will change based on the quotation's content and author. Hence, you'll need a way to generate the URLs given that information.
Extract functions to generate the Twitter and Tumblr URLs
Install elm/url. It provides the functions we need to build the URLs.
$ elm install elm/url
From the Url.Builder module import crossOrigin and string.
N.B. The string function ensures that the query parameter's value is percent-encoded.
import Url.Builder exposing (crossOrigin, string)
To generate the Twitter URL write the twitterUrl
function:
twitterUrl : String -> String -> String
twitterUrl content author =
let
tweet = "\"" ++ content ++ "\" \u{2014} " ++ author
in
crossOrigin "https://twitter.com"
[ "intent", "tweet" ]
[ string "hashtags" "quotes"
, string "text" tweet
]
And, to generate the Tumblr URL write the tumblrUrl
function:
tumblrUrl : String -> String -> String
tumblrUrl content author =
crossOrigin "https://www.tumblr.com"
[ "widgets", "share", "tool" ]
[ string "posttype" "quote"
, string "tags" "quotes"
, string "content" content
, string "caption" author
, string "canonicalUrl" "https://www.tumblr.com/docs/en/share_button"
]
Then, update the arguments to the viewIconButton
function calls.
For Twitter use:
viewIconButton "twitter" (twitterUrl "I am not a product of my circumstances. I am a product of my decisions." "Stephen Covey")
For Tumblr use:
viewIconButton "tumblr" (tumblrUrl "I am not a product of my circumstances. I am a product of my decisions." "Stephen Covey")
Extract the viewQuoteBox
function
viewQuoteBox : String -> String -> Html msg
viewQuoteBox content author =
div [ class "quote-box" ]
[ viewQuote content author
, div [ class "quote-box__actions" ]
[ div []
[ viewIconButton "twitter" (twitterUrl content author) ]
, div []
[ viewIconButton "tumblr" (tumblrUrl content author) ]
, -- ...
]
]
Use it in main
.
main : Html msg
main =
div [ class "background" ]
[ div []
[ viewQuoteBox
"I am not a product of my circumstances. I am a product of my decisions."
"Stephen Covey"
, -- ...
]
]
Add the Quote
record
type alias Quote =
{ content : String
, author : String
}
defaultQuote : Quote
defaultQuote =
{ content = "I am not a product of my circumstances. I am a product of my decisions."
, author = "Stephen Covey"
}
You'll need to update main
, viewQuoteBox
, viewQuote
, twitterUrl
and tumblrUrl
to all work with the Quote
record.
main =
div [ class "background" ]
[ div []
[ viewQuoteBox defaultQuote
, -- ...
]
]
viewQuoteBox : Quote -> Html msg
viewQuoteBox quote =
div [ class "quote-box" ]
[ viewQuote quote
, div [ class "quote-box__actions" ]
[ div []
[ viewIconButton "twitter" (twitterUrl quote) ]
, div []
[ viewIconButton "tumblr" (tumblrUrl quote) ]
, div []
[ button
[ type_ "button"
, autofocus True
, class "button"
]
[ text "New quote" ]
]
]
]
viewQuote : Quote -> Html msg
viewQuote { content, author } =
-- ...
twitterUrl : Quote -> Html msg
twitterUrl { content, author } =
-- ...
tumblrUrl : Quote -> Html msg
tumblrUrl { content, author } =
-- ...
Here's the final version of the code after all the refactoring is completed:
module Main exposing (main)
import Html exposing (Html, a, blockquote, button, cite, div, footer, i, p, span, text)
import Html.Attributes exposing (autofocus, class, href, target, type_)
import Url.Builder exposing (crossOrigin, string)
type alias Quote =
{ content : String
, author : String
}
defaultQuote : Quote
defaultQuote =
{ content = "I am not a product of my circumstances. I am a product of my decisions."
, author = "Stephen Covey"
}
main : Html msg
main =
div [ class "background" ]
[ div []
[ viewQuoteBox defaultQuote
, footer [ class "attribution" ]
[ text "by "
, a [ href "https://github.com/dwayne/"
, target "_blank"
, class "attribution__link"
]
[ text "dwayne" ]
]
]
]
viewQuoteBox : Quote -> Html msg
viewQuoteBox quote =
div [ class "quote-box" ]
[ viewQuote quote
, div [ class "quote-box__actions" ]
[ div []
[ viewIconButton "twitter" (twitterUrl quote) ]
, div []
[ viewIconButton "tumblr" (tumblrUrl quote) ]
, div []
[ button
[ type_ "button"
, autofocus True
, class "button"
]
[ text "New quote" ]
]
]
]
viewQuote : Quote -> Html msg
viewQuote { content, author } =
blockquote [ class "quote-box__blockquote"]
[ p [ class "quote-box__quote-wrapper" ]
[ span [ class "quote-left" ]
[ i [ class "fa fa-quote-left" ] [] ]
, text content
]
, footer [ class "quote-box__author-wrapper" ]
[ text "\u{2014} "
, cite [ class "author" ] [ text author ]
]
]
twitterUrl : Quote -> String
twitterUrl { content, author } =
let
tweet = "\"" ++ content ++ "\" \u{2014} " ++ author
in
crossOrigin "https://twitter.com"
[ "intent", "tweet" ]
[ string "hashtags" "quotes"
, string "text" tweet
]
tumblrUrl : Quote -> String
tumblrUrl { content, author } =
crossOrigin "https://www.tumblr.com"
[ "widgets", "share", "tool" ]
[ string "posttype" "quote"
, string "tags" "quotes"
, string "content" content
, string "caption" author
, string "canonicalUrl" "https://www.tumblr.com/docs/en/share_button"
]
viewIconButton : String -> String -> Html msg
viewIconButton name url =
a [ href url
, target "_blank"
, class "icon-button"
]
[ i [ class ("fa fa-" ++ name) ] [] ]
For more details, go here.
Tomorrow we'll make the "New quote" button work such that when it is clicked a new random quotation will be displayed and the color of certain elements will change. Then, we'll arrange to have quotations fetched from a remote source when the app initially loads. Finally, we'll make sure the URL is easily configurable by passing it in via a flag.
Top comments (0)