June 23, 2015

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]
   {:value @content
    :on-change #(reset! content (-> % .-target .-value))}])

(defn page []
  (let [content (reagent/atom nil)]
    (fn []
       [:h1 "Live Markdown Editor"]
          [: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.

 [: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]
     (fn []
       [:div {:dangerouslySetInnerHTML
              {:__html (-> content str js/marked)}}])
      (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.

Tags: clojure reagent clojurescript