DEV Community

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" ]
            ]
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

is translated into:

foo [ attr1 "a", attr2 "b" ] [ text "bar" ]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 ]
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

Then, from main call viewQuote.

viewQuote
  "I am not a product of my circumstances. I am a product of my decisions."
  "Stephen Covey"
Enter fullscreen mode Exit fullscreen mode

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" ] [] ]
Enter fullscreen mode Exit fullscreen mode

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" ] [] ]
Enter fullscreen mode Exit fullscreen mode

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) ] [] ]
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
      ]
Enter fullscreen mode Exit fullscreen mode

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"
    ]
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

For Tumblr use:

viewIconButton "tumblr" (tumblrUrl "I am not a product of my circumstances. I am a product of my decisions." "Stephen Covey")
Enter fullscreen mode Exit fullscreen mode

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) ]
        , -- ...
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

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"
        , -- ...
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

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"
  }
Enter fullscreen mode Exit fullscreen mode

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 } =
  -- ...
Enter fullscreen mode Exit fullscreen mode

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) ] [] ]
Enter fullscreen mode Exit fullscreen mode

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.

Discussion

pic
Editor guide