How to Build a Markdown Preview App With ClojureScript
Sophia Brandt
Posted on September 13, 2019
Please note: This is an older post I published a while ago on my blog. If you encounter any problems, hit me up and I'll try to fix them.
This app was written before we had React hooks.
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 (see (A)).
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 (see (B)).
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 (see (C)).
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 (see (F)).
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.
Posted on September 13, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.