DEV Community

Cover image for Let's build a Random Quote Machine in Elm - Part 3
Dwayne Crooks
Dwayne Crooks

Posted on • Edited on

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

Yesterday we ported the HTML to Elm and refactored the code.

Today we're going to make the "New quote" button work, fetch quotations from a remote source when the app initially loads and we'll make the URL for the remote source configurable by setting a flag.

Let's get started.

Make the "New quote" button work

When the "New quote" button is clicked a new quotation will be displayed and the color of certain elements will change.

Prepare for randomness

To work with randomness in our app we'll need to be able to
command Elm's runtime system to generate random values for us.

Browser.element allows us to work with commands.

Let's edit src/Main.elm to use Browser.element.

module Main exposing (main)


import Browser


main : Program () Model msg
main =
  Browser.element
    { init = init
    , view = view
    , update = update
    , subscriptions = always Sub.none
    }


-- MODEL


type alias Model =
  { quote : Quote
  }


init : () -> (Model, Cmd msg)
init _ =
  ( { quote = defaultQuote
    }
  , Cmd.none
  )


-- UPDATE


update : msg -> Model -> (Model, Cmd msg)
update _ model =
  ( model
  , Cmd.none
  )


-- VIEW


view : Model -> Html msg
view { quote } =
  div [ class "background" ]
    [ div []
        [ viewQuoteBox quote
        , -- ...
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

Display a random quote

Install elm/random.

$ elm install elm/random
Enter fullscreen mode Exit fullscreen mode

And then, make the following edits to src/Main.elm:

import Random


main : Program () Model Msg


type alias Model =
   { quote : Quote
   , quotes : List Quote
   }


init : () -> (Model, Cmd msg)
init _ =
  ( { quote = defaultQuote
    , quotes = allQuotes
    }
  , Cmd.none
  )


allQuotes : List Quote
allQuotes =
  [ defaultQuote
  , { content = " Transferring your passion to your job is far easier than finding a job that happens to match your passion."
    , author = "Seth Godin"
    }
  , { content = "Less mental clutter means more mental resources available for deep thinking."
    , author = "Cal Newport"
    }
  , { content = "How much time he saves who does not look to see what his neighbor says or does or thinks."
    , author = "Marcus Aurelius"
    }
  , { content = "You do not rise to the level of your goals. You fall to the level of your systems."
    , author = "James Clear"
    }
  ]


type Msg
  = ClickedNewQuote
  | NewQuote Quote


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    ClickedNewQuote ->
      ( model
      , Random.generate NewQuote (Random.uniform defaultQuote model.quotes)
      )

    NewQuote newQuote ->
      ( { model | quote = newQuote }
      , Cmd.none
      )


view : Model -> Html Msg


viewQuoteBox : Quote -> Html Msg
viewQuoteBox quote =
  div [ class "quote-box" ]
    [ -- ...
    , div [ class "quote-box__actions" ]
        [ -- ...
        , -- ...
        , div []
            [ button
                [ -- ...
                , onClick ClickedNewQuote
                ]
                [ text "New quote" ]
            ]
        ]
    ]
Enter fullscreen mode Exit fullscreen mode

When the "New quote" button is clicked the ClickedNewQuote message is
created by Elm's runtime system and it eventually calls our update function with that message.

The ClickedNewQuote branch of our update function is selected and it sends a command to Elm's runtime that tells the runtime to select a random quotation from our list of quotations and to wrap it in a NewQuote message.

When the NewQuote message is created by the runtime, the runtime eventually calls our update function with that message. The NewQuote branch is selected and our model gets updated with the new quotation.

Change colors randomly

Here are the edits you need to make:

import Html.Attributes exposing (..., style, ...)


type alias Model =
  { -- ...
  , color : Color
  }


type alias Color = String


init : () -> (Model, Cmd msg)
init _ =
  ( { -- ...
    , color = defaultColor
    }
  , Cmd.none
  )


defaultColor : Color
defaultColor =
  "#333"


allColors : List Color
allColors =
  [ "#16a085"
  , "#27ae60"
  , "#2c3e50"
  , "#f39c12"
  , "#e74c3c"
  , "#9b59b6"
  , "#fb6964"
  , "#342224"
  , "#472e32"
  , "#bdbb99"
  , "#77b1a9"
  , "#73a857"
  ]


type Msg
  = ClickedNewQuote
  | NewQuoteAndColor (Quote, Color)


update msg model =
  case msg of
    ClickedNewQuote ->
      ( model
      , Random.generate NewQuoteAndColor <|
          Random.pair
            (Random.uniform defaultQuote model.quotes)
            (Random.uniform defaultColor allColors)
      )

    NewQuoteAndColor (newQuote, newColor) ->
      ( { model | quote = newQuote, color = newColor }
      , Cmd.none
      )


view { quote, color } =
  div
    [ class "background"
    , style "background-color" color
    ]
    [ div []
        [ viewQuoteBox quote color
        , -- ...
        ]
    ]


viewQuoteBox : Quote -> Color -> Html Msg
viewQuoteBox quote color =
  div
    [ class "quote-box"
    , style "color" color
    ]
    [ -- ...
    , div [ class "quote-box__actions" ]
        [ div []
            [ viewIconButton "twitter" (twitterUrl quote) color ]
        , div []
            [ viewIconButton "tumblr" (tumblrUrl quote) color ]
        , div []
            [ button
                [ -- ...
                , style "background-color" color
                , onClick ClickedNewQuote
                ]
                [ text "New quote" ]
            ]
        ]
    ]


viewIconButton : String -> String -> Color -> Html msg
viewIconButton name url color =
  a [ -- ...
    , style "background-color" color
    ]
    [ i [ class ("fa fa-" ++ name) ] [] ]
Enter fullscreen mode Exit fullscreen mode

Now, when the "New quote" button is clicked both a quotation and a color are randomly selected.

Add color transitions

Finally, let's add some color transitions to make the colors change smoothly.

Add two new classes to assets/styles.css:

/* Transitions */

.has-color-transition {
  transition: color 2s;
}

.has-background-color-transition {
  transition: background-color 2s;
}
Enter fullscreen mode Exit fullscreen mode

And then, add the has-color-transition class to the quote box:

viewQuoteBox quote color =
  div
    [ class "quote-box has-color-transition"
    , style "color" color
    ]
    [ -- ...
    ]
Enter fullscreen mode Exit fullscreen mode

And, add the has-background-color-transition class to the background and the buttons:

div
  [ class "background has-background-color-transition"
  , style "background-color" color
  ]
  [ -- ...
  ]


button
  [ -- ...
  , class "button has-background-color-transition"
  , style "background-color" color
  , -- ...
  ]
  [ text "New quote" ]


a [ -- ...
  , class "icon-button has-background-color-transition"
  , style "background-color" color
  ]
  [ i [ class ("fa fa-" ++ name) ] [] ]
Enter fullscreen mode Exit fullscreen mode

For more details, go here.

Get quotations from a remote source

The quotations we'll use will be taken from here.

Decode the quotations

The quotations are returned in the JSON format in the following form:

{
  "quotes": [
    { "content": "Life isn't about getting and having, it's about giving and being.", "author": "Kevin Kruse" },
    { "content": "Whatever the mind of man can conceive and believe, it can achieve.", "author": "Napoleon Hill" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

We'll need to install elm/json to decode the response.

$ elm install elm/json
Enter fullscreen mode Exit fullscreen mode

Let's write the decoders.

import Json.Decode as D


-- DECODERS


quotesDecoder : D.Decoder (List Quote)
quotesDecoder =
  D.field "quotes" (D.list quoteDecoder)


quoteDecoder : D.Decoder Quote
quoteDecoder =
  D.map2 Quote
    (D.field "content" D.string)
    (D.field "author" D.string)
Enter fullscreen mode Exit fullscreen mode

The quoteDecoder can decode JSON in the format:

{
  "content": "...what matters in the long run is sticking with things and working daily to get better at them.",
  "author": "Angela Duckworth"
}
Enter fullscreen mode Exit fullscreen mode

And, the quotesDecoder can decode JSON in the format:

{
  "quotes": [
    {
      "content": "...what matters in the long run is sticking with things and working daily to get better at them.",
      "author": "Angela Duckworth"
    },
    {
      "content": "Don't judge. Teach. It's a learning process.",
      "author": "Carol S. Dweck"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

GET the quotes

Install elm/http.

$ elm install elm/http
Enter fullscreen mode Exit fullscreen mode

And, write the following to fetch the quotes when the app initially loads:

import Http


init : () -> (Model, Cmd Msg)
init _ =
  ( -- ...
  , getQuotes "https://gist.githubusercontent.com/dwayne/ff832ab1d4a0bf81585870369f984ebc/raw/46d874a29e9efe38006ec9865ad67b054ef312a8/quotes.json"
  )


type Msg
  = -- ...
  | GotQuotes (Result Http.Error (List Quote))


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    -- ...

    GotQuotes (Ok remoteQuotes) ->
      ( { model | quotes = remoteQuotes }
      , Cmd.none
      )

    GotQuotes (Err _) ->
      ( model, Cmd.none )


-- COMMANDS


getQuotes : String -> Cmd Msg
getQuotes url =
  Http.get
    { url = url
    , expect = Http.expectJson GotQuotes quotesDecoder
    }
Enter fullscreen mode Exit fullscreen mode

Pass the URL for the remote source via a flag

To make the URL easy to configure we'll pass it into the app via a
flag.

Edit index.html.

Elm.Main.init({
  node: document.getElementById("app"),
  flags: "https://gist.githubusercontent.com/dwayne/ff832ab1d4a0bf81585870369f984ebc/raw/46d874a29e9efe38006ec9865ad67b054ef312a8/quotes.json"
});
Enter fullscreen mode Exit fullscreen mode

Edit src/Main.elm.

main : Program String Model Msg
main =
  -- ...


init : String -> (Model, Cmd Msg)
init url =
  ( -- ...
  , getQuotes url
  )
Enter fullscreen mode Exit fullscreen mode

Select a random quote when the app initially loads

init : String -> (Model, Cmd Msg)
init url =
  ( -- ...
  , Cmd.batch
      [ generateNewQuoteAndColor allQuotes
      , getQuotes url
      ]
  )


update msg model =
  case msg of
    ClickedNewQuote ->
      ( model
      , generateNewQuoteAndColor model.quotes
      )

    -- ...


generateNewQuoteAndColor : List Quote -> Cmd Msg
generateNewQuoteAndColor quotes =
  Random.generate NewQuoteAndColor <|
    Random.pair
      (Random.uniform defaultQuote quotes)
      (Random.uniform defaultColor allColors)
Enter fullscreen mode Exit fullscreen mode

Congratulations! You've reached the end. Review the demo to check that you were able to follow along and recreate the app.

For more details, go here.

Now go and use what you learned in this tutorial to build out your next app. Feel free to reach out to me for advice or help if you get stuck.

Happy coding!

Top comments (0)