loading...
Cover image for Let's build a Random Quote Machine in Elm - Part 2

Let's build a Random Quote Machine in Elm - Part 2

dwayne profile image Dwayne Crooks ・7 min read

Build a Random Quote Machine in Elm (3 Part Series)

1) Let's build a Random Quote Machine in Elm - Part 1 2) Let's build a Random Quote Machine in Elm - Part 2 3) Let's build a Random Quote Machine in Elm - Part 3

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 &mdash; 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.

Build a Random Quote Machine in Elm (3 Part Series)

1) Let's build a Random Quote Machine in Elm - Part 1 2) Let's build a Random Quote Machine in Elm - Part 2 3) Let's build a Random Quote Machine in Elm - Part 3

Posted on Jul 30 '19 by:

dwayne profile

Dwayne Crooks

@dwayne

A full stack web developer who has an interest in programming language theory, interpreters, compilers and type theory. I enjoy programming with Elm and Haskell in my free time.

Discussion

markdown guide