interstar
interstar

Reputation: 27186

Attaching event handlers to html in ClojureScript / Reagent

I'm working on a simple "wiki-like" hypertext view in ClojureScript / Reagent.

I have some text. I now want to turn things that match the LinkPattern into clickable links.

In "traditional" wiki programming, I'd just use regexes to substitute the pattern for a URL.

Eg. turn

this is SomeText that's a link

into

this is <a href="/view/SomeText">SomeText</a> that's a link

But in this version,

a) I don't want to make them normal html anchor links, I want to attach some kind of on-click event-handler to the links (which will trigger an Ajax call to the server, not reload the whole page)

b) I'm working in Clojure's Reagent wrapper for React. So I'm assuming I don't want to be simply kludging raw Javascript into the html. I want to be attaching the event-handlers in a "principled" way that's right for both ClojureScript and React / Reagent.

So how should I do that?

FYI, I'm pre-processing the markup into HTML and then using :dangerouslySetInnerHTML to insert it into the page. So whatever I do to add the click handler to the links needs to survive that.

Or do I have to add the event handlers after I've rendered them?

In which case, how should I do that? I know what I'd do in JQuery, use "bind" to attach the event handler to spans of a particular class. But again, I'm confused by the right way to think about this in ClojureScript Reagent. What's the equivalent of jQuery's "bind" in that world?

Upvotes: 0

Views: 2347

Answers (3)

clartaq
clartaq

Reputation: 5372

It seems like there are multiple parts to your question. I've been working on a personal wiki myself for awhile. (Repository here until BitBucket takes it down in a few months since I use Mercurial.)

Event Handlers

Much of my program is written in Clojure, with only a Markdown editor and sizing controls written in ClojureScript. I was surprised to learn that you can add event handlers to hiccup components using a string with the JavaScript to run, at least for the simple cases I've tried.

For example, here's a function from my (server-side) page rendering logic:

(defn sidebar-and-article
  "Return a sidebar and article div with the given content."
  [sidebar article]
  [:div {:class "sidebar-and-article"}
   sidebar
   [:div {:class "vertical-page-divider"}]
   [:div {:class       "vertical-page-splitter"
          :id          "splitter"
          ; Don't forget to translate the hyphen to an underscore. The false
          ; return is required for correct behavior on Safari.
          :onmousedown "cwiki_mde.dragger.onclick_handler(); return false;"}]
   [:article {:class "page-content"}
    article]])

Even though this is part of the server, in Clojure, it is possible to assign an handler for the :onmousedown event. This code draws a vertical splitter between columns on the page. The event handler lets the user drag the splitter to resize the columns, which is then sent back to the server to store in the database of user preferences.

EDIT

Since the OP asked, the method of adding an event handler can call handlers written in ClojureScript. For example, in the snippet above, the handler references, wiki_mde.dragger.onclick_handler, which is just a function in a namespace:

(ns cwiki-mde.dragger
  (:require [ajax.core :refer [ajax-request text-request-format
                               text-response-format]]))
.
.
.
(defn ^{:export true} onclick_handler []
  (reset! sidebar-ele (.getElementById js/document "left-aside"))
  (reset! starting-basis (- (.-offsetWidth @sidebar-ele) twice-padding-width))
  (reset! dragging true)
  (.addEventListener js/window "mousedown" start-tracking)
  (.addEventListener js/window "mousemove" move)
  (.addEventListener js/window "mouseup" stop-tracking))

The OP's comment point out something a little confusing though. The contents of the string with the event handler is actually JavaScript. It just so happens that the ClojureScript compiler has translated the function name to the JavaScript equivalent.

Another surprising, and possibly confusing, thing is that the contents of the string is not just a single function name, but two JavaScript statements. The additional return false; had to be added to get one browser to work. I was surprised that two statements could even be used.

Links

Like you, I originally used regular expressions to find and recognize wiki links. However, they were just regular links with some styling to distinguish links to non-existent pages. The scheme for creating the links is somewhat like that used in MediaWiki that just builds links recognized by the server.

Since then, I've switched to a more advanced (faster) Markdown converter, flexmark-java. I had to write some custom code for the converter to handle the wiki links. This stretched my Java interop skills to their very limit though. Maybe you could use something like this to construct exactly the types of links you want.

Rendering

I don't see any need for using :dangerouslySetInnerHTML. The way I've handled it is to convert the Markdown to HTML while rendering the page and put the HTML into a hiccup :div as others have explained. I do the conversion and page building on the server in response to clicking on one of the links described above.

For example, here is the configuration I use now to generate the article and sidebar content items in the listing above. Most of this is just configuration of fexmark-java.

(def options (-> (MutableDataSet.)
                 (.set Parser/REFERENCES_KEEP KeepType/LAST)
                 (.set HtmlRenderer/INDENT_SIZE (Integer/valueOf 2))
                 (.set HtmlRenderer/PERCENT_ENCODE_URLS true)
                 (.set TablesExtension/COLUMN_SPANS false)
                 (.set TablesExtension/MIN_HEADER_ROWS (Integer/valueOf 1))
                 (.set TablesExtension/MAX_HEADER_ROWS (Integer/valueOf 1))
                 (.set TablesExtension/APPEND_MISSING_COLUMNS true)
                 (.set TablesExtension/DISCARD_EXTRA_COLUMNS true)
                 (.set TablesExtension/WITH_CAPTION false)
                 (.set TablesExtension/HEADER_SEPARATOR_COLUMN_MATCH true)
                 (.set WikiLinkExtension/LINK_FIRST_SYNTAX true)
                 (.set WikiLinkExtension/LINK_ESCAPE_CHARS "")
                 (.set Parser/EXTENSIONS (ArrayList.
                                           [(FootnoteExtension/create)
                                            (StrikethroughExtension/create)
                                            ; Order is important here.
                                            ; Our custom link resolver must
                                            ; preceed the default resolver.
                                            (CWikiLinkResolverExtension/create)
                                            (WikiLinkExtension/create)
                                            (CWikiLinkAttributeExtension/create)
                                            (TablesExtension/create)]))))

(def parser (.build ^Parser$Builder (Parser/builder options)))
(def renderer (.build ^HtmlRenderer$Builder (HtmlRenderer/builder options)))

(defn- convert-markdown-to-html
  "Convert the markdown formatted input string to html
  and return it."
  [mkdn]
  (->> mkdn
       (.parse parser)
       (.render renderer)))

Hope this is helpful.

Upvotes: 3

Thomas Heller
Thomas Heller

Reputation: 4356

You could turn your markup into hiccup (or direct React Elements) instead of HTML. That would let React take care of the rendering aspects and you can attach your event handlers where you need them without having to worry about the DOM.

So instead of turning

this is SomeText that's a link

into

this is <a href="/view/SomeText">SomeText</a> that's a link

you turn it into

[:div "this is " [:a {:href "/view/SomeText" :onClick (fn ...)} "SomeText"] "that's a link"]

or :<> instead of :div if you don't want the containing element.

Upvotes: 2

Tim X
Tim X

Reputation: 4235

I think you may be on the wrong path wrt using :dangerouslySetInnerHTML. As your using reagent, which is a react wrapper, you rarely want to be injecting HTML into the DOM in this manner.

The basic approach is to use hiccup for all your HTML and to structure your application as a hierarchy of reagent components. In additon to the reagent components you use, you will also have some 'state' stored in a reagent atom (or atoms). Once you have all the bits hooked up, the application is essentially driven by changes to this state (you leave reagent to manage the DOM).

For example (extremely simplified and off the top of my head!)

You might have a state atom with the following structure

(def state (reagent/atom {:articl1 {:title "An Article"
                                    :body "This is the body of the article"
                                    :read false}
                          :article2 {:title "A second article"
                                     :body "Yet another article"
                                      :read false}})

The above state atom may be populated via an AJAX call to get the list of articles. I might then define 3 reagent components -

  • A read toggle component
  • An article component
  • An article list component

There are a number of approaches which can be used, but essent8ially the idea is that the article list component gets the list of articles from the state atom, iterates through the list, calling the article component for each article. The article component would generate the html for each article - possibly calling other components to help with that (for example, the read toggle component, which might present some text with an on-click handler that would toggle the :read attribute in the state atom. For example

(defn read-toggle-component [path]
  [:div {:on-click (fn []
                     (swap! state update-in path not))}
  (if (get-in @state [path])
    "Mark unread"
    "Mark read"])

The above will generate HTML with the content "Mark unread" or "Mark read" depending on the value in the state atom pointed to by path. For example, this might be [:article1 :read] from the above example. If the value is true, "Mark unread" will be rendered and if the value is false "Mark read" will be rendered. The click handler will toggle this value when this is clicked on.

The above component may in turn be called by the article component, which might be something like

(defn article-component [article-id]
  (let [article (get @state article-id)]
    [:article
      [:h2 (:title article)]
      (when (not (:read article))
        [:p (:body article)])
      [read-toogle-component [article-id :read)]]))

The above component will render an article inside an tag. The article will contain a header, might contain the article body (depending on value of :read) and a toggle to toggle the :read attribute between true and false.

Finally, you would have the article-list-component, which might be something like

(defn article-list-component []
  (let [articles (keys @state)]
    (into
     [:section]
     (for [a articles]
        [article-component a]))))

The above will generate a tag and inside that tage, there will be tags for each of the articles.

You would then have something in your clojureScript which would mount the article-list-component. The nice part is you don't need to deal with HTML directly or even interact with the DOM directly. The page will be updated as soon as the reagent atom is modified (weather that be by new articles being added or changes in the :read attribute.

the above is very rough and simplified to give you the idea. It is bound to be full of errors, but I hope wil point you in the right direction.

Things you should get familiar with -

  • Reagent uses hiccup for markup. Get to know hiccup. Hiccup
  • There are 3 types of reagent components. Get to understand what they are and when to use them. See for example Eric Normand's tutorial
  • There are lots of reagent component examples and libraries out there. You can learn a lot by looking at how they work. For exaple enter link description here
  • there is also some good documentation at the Reagent Project site

The last two have been written by the author of re-frame, which is a really powerful, but elegant framework for writing reagent based apps and well worth a look at. Even if you think it is too large/complex for your needs, there are some really good ideas in re-frame which you can adapt to your requirements.

If you haven't already don so, make sure to check out figwheel main. In additon to providing a great development envrionment, it can really heop with learning as it provides an easy way to 'experiment' and get immediate feedback, which is by far the best way to learn clojure/clojurescript/reagent.

Upvotes: 3

Related Questions