Reagent Live Markdown Editor
In this post, we'll create a simple live Markdown editor with Reagent.
The Reagent Cookbook recipe can be found here.
Setting up the Project
Let's start by creating a new reagent-figwheel project.
lein new reagent-figwheel markdown-editor
First, open up the index.html
file and add the following scripts below the app div
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/default.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/3.1.1-1/css/united/bootstrap.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/highlight.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/languages/clojure.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
We'll be using Bootstrap for basic styling and Highlight.js for code syntax highlighting.
Creating the Editor
Next, open up the core.cljs
file in the src/cljs
folder and remove the defonce
statement; we won't be needing that.
The first thing we'll do is set up the editor and create an atom for the Markdown content.
(defn editor [content]
[:textarea.form-control
{:value @content
:on-change #(reset! content (-> % .-target .-value))}])
(defn page []
(let [content (reagent/atom nil)]
(fn []
[:div
[:h1 "Live Markdown Editor"]
[:div.container-fluid
[:div.row
[:div.col-sm-6
[:h3 "Editor"]
[editor content]]]]])))
This code should be familiar to anyone that's used Reagent before.
Next, we'll add the preview section to the right of the editor. Just add the following under the first col-sm-6
div and that's all we need for the UI.
[:div.col-sm-6
[:h3 "Preview"]
[preview content]
The preview component needs to display the parsed and compiled Markdown so let's do that next. We'll be using Marked to handle the Markdown.
(defn markdown-component [content]
(fn []
[:div {:dangerouslySetInnerHTML
{:__html (-> content str js/marked)}}]))
(defn preview [content]
(when (not-empty @content)
(markdown-component @content)))
This will display the compiled Markdown but it's still missing syntax highlighting for code blocks. The following code will traverse the code nodes in the preview and apply the necessary syntax highlighting.
(defn highlight-code [html-node]
(let [nodes (.querySelectorAll html-node "pre code")]
(loop [i (.-length nodes)]
(when-not (neg? i)
(when-let [item (.item nodes i)]
(.highlightBlock js/hljs item))
(recur (dec i))))))
However, we can't just call this function inside markdown-component
immediately after the Markdown is compiled. The code syntax cannot be highlighted until the preview component has been mounted on the DOM.
So in order to do this, we need to post-process the HTML after the preview component has been mounted. We can do this by adding metadata to the fn
in markdown-component
using with-meta
.
(defn markdown-component [content]
[(with-meta
(fn []
[:div {:dangerouslySetInnerHTML
{:__html (-> content str js/marked)}}])
{:component-did-mount
(fn [this]
(let [node (reagent/dom-node this)]
(highlight-code node)))})])
With this metadata, component-did-mount
will be called after the HTML has been generated and the node is mounted in the browser DOM.
Lastly, if we want to build the ClojureScript with advanced compilation we'll have to specify some externs. This is because the compiler munges variable names that come from external libraries, making them unavailable inside the ClojureScript.
To overcome this, create a file with the following
var hljs = {};
hljs.highlightBlock = function(){};
marked = function(){};
and specify it inside your :compiler
map in your project.clj
. For example,
:externs ["externs/syntax.js"]
For the full source code visit my GitHub or see the live, styled up demo here.