Learning to code takes practice and building projects. So, here is how to build a simple “Random Quote Machine” with Clojurescript and Reagent.
Hopefully, you’ll get a sense on how Clojurescript and Reagent work.
Here is the live demo: Breaking Bad Quotes.
And here is the Github Repo, so you can see the whole code: Github.
The app displays a quote from the “Breaking Bad” TV series with its author. You can click on a button to get a new quote. Or you can click on a button to tweet the quote.
Caveat:
I consider myself a beginner programmer so this might not be the “ideal way” to do it.
The Stack
We will use Reagent, a simple Clojurescript wrapper for React, shadow-cljs and Wing CSS plus Foundation Icons for styling.
The app will get its input from the public Breaking Bad Quotes API.
Requirements
You should have installed:
- Java (I use v1.8.0)
- Leiningen
- npm or yarn
- a code editor that works with Clojure (I use VS Code with Calva and parinfer plugins)
Setup
Go to the terminal and run the leiningen shadow-cljs template with +reagent.
We will call this project breaking-bad-quotes
.
$ lein new shadow-cljs breaking-bad-quotes +reagent && cd breaking-bad-quotes
Get npm or yarn to install all dependencies. Install shadow-cljs.
$ yarn install
$ yarn global add shadow-cljs
shadow-cljs works out of the box. Run $ shadow-cljs watch app
, navigate to your browser and open http://localhost:8700
.
You can also connect your editor with the repl. Calva does that automatically, so that’s neat.
Let’s add some simple styling.
For simplicity’s sake, just import Wingcss and Foundation Icons from their respective CDNs and add some adjustments.
In breaking-bad-quotes/public/css/style.css:
@import url("https://unpkg.com/wingcss");
@import url("https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/foundation-icons.min.css");
button {
margin: 1rem 0.5rem 1rem 0.5rem;
}
#author {
font-style: italic;
}
a, a:visited, a:hover, a:active {
color: inherit;
text-decoration: none;
}
Let’s Code the App
First Steps
Go to breaking-bad-quotes/src/breaking-bad-quotes/core.cljs. You’ll see something like this:
(ns breaking-bad-quotes.core
(:require [reagent.core :as reagent :refer [atom]]))
;; define your app data so that it doesn't get over-written on reload
(defonce app-state (atom {:text "Hello world!"}))
(defn hello-world []
[:div
[:h1 (:text @app-state)]
[:h3 "Edit this and watch it change!"]])
(defn start []
(reagent/render-component [hello-world]
(. js/document (getElementById "app"))))
(defn ^:export init []
;; init is called ONCE when the page loads
;; this is called in the index.html and must be exported
;; so it is available even in :advanced release builds
(start))
(defn stop []
;; stop is called before any code is reloaded
;; this is controlled by :before-load in the config
(js/console.log "stop"))
Let’s delete some of the boilerplate stuff and change it to our needs.
We will use a Reagent Form-2 component. The local binding with let
sets up a Reagent atom that will hold our local state.
In Clojurescript, data structures are immutable per default. But we need our quotes to change when the user clicks a button, so we use the Reagent implementation of atom.
An atom is like a container that holds a mutable value. When you want to take a peek into it, you have to de-reference it with @
.
Ultimately, we want to fetch the data from a public API. For now, we will hard-code it.
(ns breaking-bad-quotes.core
(:require [reagent.core :as reagent :refer [atom]]))
(defn quote []
(let [data (atom "quote app")]
(fn []
[:p @data])))
(defn start []
(reagent/render-component [quote]
(. js/document (getElementById "app"))))
...
When you save the file, shadow-cljs will automatically recompile it and you can see the changes in the browser immediately.
Sketch Out The UI
Change the component to:
(defn quote []
(let [data (atom "quote app")]
(fn []
[:div.cards>div.card
[:h2.card-header.text-center "Breaking Bad Quotes"]
[:div.card-body.text-center
[:p#quote @data]
[:p#author @data]]
[:div.card-footer.center.text-center
[:button#twitter.outline>a#tweet
{:href "#"
:target "_blank"}
[:i.fi-social-twitter " Tweet"]]
[:button#new-quote.outline
[:i.fi-shuffle " New Quote"]]]])))
That sets up the different HTML elements in Hiccup style. Classes and ID Tags are mostly for the sake of styling the app with Wing CSS and Foundation Icons.
Read this wiki entry for more information.
For example, [:button#twitter.outline>a#tweet ...
creates an outer HTML element with a button with the ID “twitter” and the class “outline”. Inside this element, there is a nested HTML link (“a”) with the id “tweet”.
This element has attributes like an href and a target.
(It helps to check these elements in the browser with the developer console.)
Notice that we de-ref the data atom in the p-tags. If you don’t de-ref it with @
, the browser won’t display the string “quote app”.
Now, the app looks like it should but the logic doesn’t work yet. We will come to that soon.
Fetching the Data
Let’s see how the Breaking Bad Quotes API works.
How do we get the data?
Let’s write a function that creates an HTTP GET request to the API. We’ll use the cljs-ajax library for that.
Go to breaking-bad-quotes/shadow-cljs.edn to manage the dependencies.
...
:dependencies [[binaryage/devtools "0.9.7"]
[reagent "0.8.0-alpha2"]
[cljs-ajax "0.7.4"]]
...
We need to restart shadow-cljs from the terminal when we install new packages.
Now, we will require the GET function in our core.cljs:
(ns breaking-bad-quotes.core
(:require [reagent.core :as r :refer [atom]]
[ajax.core :refer [GET]]))
...
Let’s write the function that will get us the data. Put it above the component:
(ns breaking-bad-quotes.core
(:require [reagent.core :as r :refer [atom]]
[ajax.core :refer [GET]]))
(defn fetch-link! [data]
(GET "https://breaking-bad-quotes.herokuapp.com/v1/quotes"
{:handler #(reset! data %)
:error-handler (fn [{:keys [status status-text]}]
(js/console.log status status-text))}))
(defn quote []
...
The fetch-link! function will take one argument (data) which will be a Reagent atom. It will call GET
with the url of the API.
If successful, the :handler
gets the de-serialized response and uses an anonymous function to store that response in the data atom.
:error-handler
is the function for errors and just logs the status and the status text to the browser console.
Now, let’s use it in our Reagent component.
(defn quote []
(let [data (atom nil)]
(fetch-link! data)
(fn []
[:div.cards>div.card
[:h2.card-header.text-center "Breaking Bad Quotes"]
[:div.card-body.text-center
[:p#quote @data]
[:p#author @data]]
[:div.card-footer.center.text-center
[:button#twitter.outline>a#tweet
{:href "#"
:target "_blank"}
[:i.fi-social-twitter " Tweet"]]
[:button#new-quote.outline
[:i.fi-shuffle " New Quote"]]]])))
The component called quote first sets up a lexical binding to a Reagent atom and gives it the name “data”. Then it calls the function fetch-link!
and stores the HTTP response in the data atom.
Then it sets up the UI for the app with HTML tags (div, p, h2, etc.). The p tags should display the quote and the author from the data atom.
But something breaks! The error messages in the developer console are not really helpful though.
We do a call to fetch-link!
to make an HTTP request to the API and store the response in the data atom. Later we try to de-reference it in the p-tags.
Our HTTP-response will be something like this:
[
{
"quote": "I am not in danger, Skyler. I AM the danger!",
"author": "Walter White"
}
]
JSON - not Clojurescript and the program doesn’t know how to deref this response. This breaks the render method.
We need to pull the values out by destructuring the data atom.
There is a neat cheat sheet on github that will help with this: Clojure Destructuring Tutorial and Cheat Sheet.
The stuff we want is the first item in the array/vector we get. Then we have something like a Clojurescript map but with strings for keys (instead of keywords). We want to parse out the value of “author” and the value of “quote” and render them in the p-tags.
(let [{:strs [quote author]} (first @data)])
...
(defn quote []
(let [data (atom nil)]
(fetch-link! data)
(fn []
(let [{:strs [quote author]} (first @data)]
[:div.cards>div.card
[:h2.card-header.text-center "Breaking Bad Quotes"]
[:div.card-body.text-center
[:p#quote quote]
[:p#author author]]
[:div.card-footer.center.text-center
[:button#twitter.outline>a#tweet
{:href "#"
:target "_blank"}
[:i.fi-social-twitter " Tweet"]]
[:button#new-quote.outline
[:i.fi-shuffle " New Quote"]]]]))))
Yay, that works! If you manually refresh the browser page, you will get a new quote from the API and Reagent will render it nicely.
Get a New Quote
We already have a function that gets us a quote: fetch-link!
And we have a button that the user can click. Let’s combine those with a click event handler.
This is really easy to do. We just use the Hiccup templating language that Reagent uses to register the on-click-handler to the button use our fetch-link!
function:
[:button#new-quote.outline
{:on-click #(fetch-link! data)}
This will fire off a new call to the API and store the response in the data atom. Reagent will then re-render the whole component for us.
...
(defn quote []
(let [data (atom nil)]
(fetch-link! data)
(fn []
(let [{:strs [quote author]} (first @data)]
[:div.cards>div.card
[:h2.card-header.text-center "Breaking Bad Quotes"]
[:div.card-body.text-center
[:p#quote quote]
[:p#author author]]
[:div.card-footer.center.text-center
[:button#twitter.outline>a#tweet
{:href "#"
:target "_blank"}
[:i.fi-social-twitter " Tweet"]]
[:button#new-quote.outline
{:on-click #(fetch-link! data)}
[:i.fi-shuffle " New Quote"]]]]))))
...
Tweet, Tweet, Tweet!
We will use the Twitter Web Intents to pop-up a new window with a pre-filled tweet.
So, the url we need is something like this:https://twitter.com/intent/tweet?hashtags=breakingbad&text="
Then we add the quote and the author to the end. We already have author and quote in a let form.
Let’s just make a new lexical binding for tweet-intent
with the url and slap quote and author at the end with the Clojurescript str
function.
(let [tweet-intent (str "https://twitter.com/intent/tweet?hashtags=breakingbad&text=" quote " ~ " author)])
...
(defn quote []
(let [data (atom nil)]
(fetch-link! data)
(fn []
(let [{:strs [quote author]} (first @data)
tweet-intent (str "https://twitter.com/intent/tweet?hashtags=breakingbad&text=" quote " ~ " author)]
[:div.cards>div.card
[:h2.card-header.text-center "Breaking Bad Quotes"]
[:div.card-body.text-center
[:p#quote quote]
[:p#author author]]
[:div.card-footer.center.text-center
[:button#twitter.outline>a#tweet
{:href tweet-intent
:target "_blank"}
[:i.fi-social-twitter " Tweet"]]
[:button#new-quote.outline
{:on-click #(fetch-link! data)}
[:i.fi-shuffle " New Quote"]]]]))))
...
Tadaa!
Done.
The API Is Slow…
If you don’t use the app and come back to it after a while, you’ll see that the app takes some time until it displays the quote and the author.
The reason behind this is that the API is hosted on a free Heroku account and goes idle if nobody uses it.
We will display a simple “Loading…"-message.
Our data
atom that holds the request response is originally initialized to nil
. When we call fetch-links!
and get a response, this will be reset to the response.
nil
is a falsy value so we can use a simple or
form to set up an alternative message as long as we don’t have a response from the API server.
...
[:p#quote (or quote "Loading... please wait")]
...
If quote
is nil
(for example, when we don’t have a response because the API server is slow), then we show “Loading… please wait”), otherwise the quote
from the data
atom.
Lessons Learned
What I have written out here is an abbreviated version of my own code. During development, my code was uglier and contained more errors.
Some Notes:
- try to get a sketch of the app as soon as possible: mockup the app with Hiccup templating and hard-coded data
- start with a knowledge of how your data is structured: I knew I would get a JSON object from the API with the keys “author” and “quote” and their values as strings
- incremental design: at first I was not sure how to destructure the JSON object, so I just tried to display the complete JSON and improved the design from there
- an eye on UI: I knew I wanted to use Wing CSS so I made sure to include the correct classes into Hiccup/HTML right from the start instead of re-arranging the stuff later
- sometimes things are easier than they look: it took me many experiments to get the Twitter web intent right because I tried some arcane stuff instead of just constructing a string out of the url and the (already destructured) dataset - it helps to take a step back and just sleep on it
What I like about Clojure/Clojurescript is how concise it is. And using Reagent is quite a joy and even easy for beginners. You should try it out.
Next Up
How to deploy the app to firebase. That’s probably overkill for such a simple program but it’s still useful to learn how to do it.