How to Build a Markdown Preview App With Reagent

01/25/20192 Min Read — In ClojureScript

Why ClojureScript?

With React, you build small components and combine them. You design from data and then flow it through functions (and React classes).

You start with the programming logic. You then add your UI with HTML-like syntax (JSX).

I enjoy the data-centric approach.

It also powers the core of Clojure and ClojureScript.

I hate Javascript's verbose syntax. Don't get me started on the object model and the pitfalls of this. Code breaks because you forgot to make sure to bind your functions correctly.

Thinking in ClojureScript frees you from some of the disaster of mutability. You know that all values are immutable per default.
So what exactly has to change in the program? And that thing has to be an atom.

With Javascript/React I sometimes confuse what exactly can change.
Which functions should be pure? Use a Stateless Functional Component.
Which components change state? Use a React class.

ClojureScript and Reagent, the React wrapper for ClojureScript, differentiate between mutable state and immutable data.

Their concise language and markup syntax are easier to read. Less noise lowers the barrier to understanding the code.

Build Something

The Markdown preview app has a text area where you can type in text and a live preview that shows how this text converts to HTML.

Let's define the state:

(ns mdpreview.state
(:require [reagent.core :refer [atom]]))
(def inital-value ; (B)
"## Welcome to Markdown Preview!
Type in some [Markdown text](https://daringfireball.net/projects/markdown/), e.g. in *italic*.
#### About this site
> Markdown Preview was built with Clojurescript and Reagent.
Documentation and more info for this site is available on **[Github](https://github.com/sophiabrandt/markdown-preview)**.
")
(defonce app-state (atom {:value inital-value})) ; (A)

The app-state, a Reagent atom (A), is a hash-map with the key :value and a value of string (B).

Now the UI:

(ns mdpreview.views
(:require [mdpreview.state :refer [app-state]] ; (A)
[mdpreview.events :refer [update-preview, clear-textarea]]
["react-markdown" :as ReactMarkdown]))
(defn header
[]
[:div
[:h1 "Markdown Preview"]])
(defn textarea
[]
(let [text (:value @app-state)] ; (B)
[:div
[:textarea
{:placeholder text
:value text
:on-focus #(clear-textarea %) ; (C)
:on-change #(update-preview %)}]]))
(defn preview
[]
[:div
[:> ReactMarkdown {:source (:value @app-state)}]]) ; (F)
(defn app []
[:div
[header]
[textarea]
[preview]])
(ns mdpreview.events
(:require [mdpreview.state :refer [app-state]]))
(defn clear-textarea [event] ; (D)
(.preventDefault event)
(reset! app-state nil))
(defn update-preview [event] ; (E)
(.preventDefault event)
(swap! app-state assoc :value (.. event -target -value)))

The view has four areas:

  • a simple H1 tag with the title (header)
  • a component with the text area which also contains the event handlers (textarea)
  • a component that converts everything from the text area to HTML (preview)
  • the final component combines the sub-components (app)

views.cljs imports app-state from state.cljs. We stored the Event handler functions in a separate file.

In the text area we set up a let binding to text where we dereference our app-state. Dereferencing (the @-symbol) means that we get the value of the app-state atom. Reagent will always re-render a component when any part of that atom is updated.

We use text as the placeholder and the value for this input field. When the user triggers the Synthetic events onFocus or onChange, the functions from the events.cljs-file change the content.
on-focus (in Hiccup we use kebap-case instead of camelCase) wipes the text area (and the state) clean with a reset! (see (D)).
on-Change takes the event target value and updates the state. Whenever we type into the text area, we update the value of the app-state atom with swap! (see (E)).

The preview component then takes the app-state and takes advantage of the (Javascript) "react-markdown" library. React Markdown creates a pure React Component. We use reagent/adapt-react-class (the [:>] syntax) to employ the React component with Reagent.

ns mdpreview.core
(:require [reagent.core :as r]
[mdpreview.views :as views]))
(defn ^:dev/after-load start
[]
(r/render [views/app]
(.getElementById js/document "app")))
(defn ^:export main
[]
(start))

Finally, core.cljs renders the app and uses shadow-cljs to compile the ClojureScript code.

And that's the whole app.

The code is available on Github. I've deployed the live demo to firebase.