Reagent deep dive part 4: Application principles



(ns my.reagent-examples
  (:require
    [clojure.string :as string]
    [reagent.core :as reagent]
    [reagent.ratom]))

(enable-console-print!)

(defn a-better-mouse-trap [mouse]
  (let [mice (reagent/atom 1)]
    (fn render-mouse-trap [mouse]
      (into
        [:div
         [:button
          {:on-click
           (fn [e]
             (swap! mice (fn [m]
                           (inc (mod m 4)))))}
          "Catch!"]]
        (repeat @mice mouse)))))
Welcome to the fourth and final leg of the Reagent deep dive tour!

In part 1 we saw how to define components and react to change.
In part 2 we observed the lifecycle of a component.
In part 3 we grappled with the nuances of sequences of components.

Today, in part 4, we will construct a scribble application. The scribble application allows users to create line drawings with their mouse. We will not encounter any new Reagent features; rather I shall be pointing out some principles as we go which will be easier to observe at the application level. I'll be highlighting them as numbered principles, which is to overstate their weight. They are my well intentioned advice and opinions, not official principles. I offer them because I find that when starting something new it can be helpful to have some guiding principles on how to choose a path forward between multiple options. I hope they are helpful to you.

You can edit the examples in this page (thanks to KLIPSE). Simply modify the code in the example boxes to complete the exercises.

Let's get swimming.





Drawing paths

Life is the art of drawing without an eraser. -- John W. Gardner


In order to create a scribble application, we will be handling mouse strokes. Scribbles will be represented as an SVG element consisting of paths. In HTML a path is defined with a d attribute indicating commands to draw:
<svg width="100%" height="200">
  <path stroke="black" d="M 50 100 L 150 100"/>
</svg>

The first command is "Move To" (M) followed by a coordinate pair. In this case we "Move To" (50,100). The next command is "Line To" (L), which draws a line from the current position to a new position. In this case we "Line To" (150,100). L can be followed by one or many points to chain together. You can read more about paths in the SVG reference which also describes how to draw curves. To draw an SVG with a horizontal line in Reagent we write:

Example Y: A horizontal line path


[:svg {:width "100%" :height 200}
 [:path {:stroke "black" :d "M 50 100 L 150 100"}]]

To create a scribble application we will need to display a bunch of paths representing user drawn lines. Let's start by defining the model of what a drawing should be; a drawing shall be a vector of lines, where lines are vectors of coordinates to connect.

Example Z: A basic drawing model, a vector of vectors


(def my-drawing
  (reagent/atom []))

(swap! my-drawing conj [50 100 150 100])

To add a new line to a drawing we conj a new vector of coordinates onto the drawing vector. Seems logical right?

Principle 1: Start with data.

Defining some sample data up front really helps flesh out the key ideas behind an application.





So given this model, how would we render it? Easy! We just convert each vector into a path element and shove them into an SVG element parent.

Example AA: A basic drawing view that renders a basic model of a drawing


(defn scribble1 [drawing]
  (into
    [:svg
     {:width "100%"
      :height 200
      :stroke "black"
      :fill "none"}]
    (for [[x y & more-points] @drawing]
      [:path {:d (str "M " x " " y " L " (string/join " " more-points))}])))

[scribble1 my-drawing]

We have seen these constructs in previous parts of the tour, But there are some slight difference about the examples I'll present in this part of the tour which bear pointing out. These differences exist because we are working toward a more unified goal as opposed to isolated examples.

The first difference to notice is that scribble1 takes a drawing as an argument. Explicitly defining your dependencies is a good thing.

Principle 2: Prefer passing in state over a global reference to it.





Components that are explicit about the data they need and the change they will effect are testable in isolation. Occasionally they also crystalize a reusable abstraction.

Exercise: Refactor scribble1 to allow a parent component to modify the background of the SVG.

It is quite tricky to know all the inputs to a component upfront. If we add them as we go, chances are that we will develop a long argument list of highly specific inputs. Here is a tip to keep components flexible without being overly specific; situationally it can be useful to pass a map of attributes to merge. In the case that we want a background parameter, that can just be specified as an attribute to merge. Background is nested in style inside the attributes, so a plain merge wont do. We need a deep merge. In Clojure merge-with merge does the job.

Example AB: A more flexible drawing view which merges attributes


(defn scribble2 [attrs drawing]
  (into
    [:svg
     (merge-with merge attrs
                 {:width "100%"
                  :height 200
                  :stroke "black"
                  :stroke-width 4
                  :fill "none"
                  :style {:border "1px solid"
                          :box-sizing "border-box"
                          :cursor "crosshair"}})]
    (for [[x y & more-points] @drawing]
      [:path {:d (str "M " x " " y " L " (string/join " " more-points))}])))

(def my-drawing2 (reagent/atom [[50 100 75 150 100 100 125 150 150 100]]))

[scribble2 {:style {:background-color "lightgreen"}} my-drawing2]

Exercise: Get comfortable with the lines abstraction by adding more lines to draw a "Z" character.

Not every component we define needs to follow the passed attributes pattern. Accepting attributes is a pattern that tends to work well situationally, but would be tedious to do everywhere. My rule of thumb is to keep an eye out for excessive customization arguments, they may indicate an opportunity to pass attributes instead.

Principle 3: Keep an eye out for opportunities to pass customization attributes.

We have a model and a view. Now if only we had a way to draw the lines with a mouse!

Handling mouse events

All strange and terrible events are welcome, but comforts we despise. -- Cleopatra





To scribble we need to listen to mousedown to detect starting a path, mousemove to detect continuing a path, and mouseup to detect completing a path. Less obvious is mouseleave which should also complete a path, as it indicates the mouse cursor has left the SVG area and no sensible path can be drawn. All handlers of these events need to know the position of the mouse cursor relative to the SVG image. So our first task is to calculate the position of a mouse event:

Example AC: Detecting coordinates of a mouse click


(defn xy [e]
  (let [rect (.getBoundingClientRect (.-target e))]
    [(- (.-clientX e) (.-left rect))
     (- (.-clientY e) (.-top rect))]))

(def my-drawing3 (reagent/atom []))

[:div
  [scribble2
   {:on-click
    (fn [e]
      (js/alert (pr-str (xy e))))}
   (reagent/atom [])]
 [:h3 "Click the empty SVG!"]]

Clicking now alerts the coordinates of the click event.

The xy function takes an event and calculates the coordinates of that event using getBoundingClintRect on the target of the event. In this case the target of the event is our SVG element.

Notice that because the scribble2 function can take attributes we can easily supply an on-click handler without modifying the component. I like how this allows us to separate our view and control behaviors. It is just regular function and data composition.

Now let's define how the model should handle mouse events. We will use a reagent/atom named pen-down? to record if we are currently drawing a path or not. When a path is started we will insert two coordinate pairs because an SVG path requires a starting location to "Move To" and one or more locations to "Line To" from there. It is fine that they will be the exact same points.

To continue a line we append a coordinate pair to a path.

To Finish a line we reset the pen-down? state to indicate we are no longer drawing a path.

Example AD: Event handling that updates a drawing model


(defn mouse-handlers [drawing]
  (let [pen-down? (reagent/atom false)
        start-path
        (fn start-path [e]
          (when (not= (.-buttons e) 0)
            (reset! pen-down? true)
            (let [[x y] (xy e)]
              (swap! drawing conj [x y x y]))))
        continue-path
        (fn continue-path [e]
          (when @pen-down?
            (let [[x y] (xy e)]
              (swap! drawing (fn [lines]
                               (let [last-line-idx (dec (count lines))]
                                 (update lines last-line-idx conj x y)))))))
        end-path
        (fn end-path [e]
          (when @pen-down?
            (continue-path e)
            (reset! pen-down? false)))]
    {:on-mouse-down start-path
     :on-mouse-over start-path
     :on-mouse-move continue-path
     :on-mouse-up end-path
     :on-mouse-out end-path}))

(def my-drawing3 (reagent/atom []))

[:div
 [scribble2 (mouse-handlers my-drawing3) my-drawing3]
 [:h3 "Scribble on me!"]]

I think it is pretty cool how we can separate the event handler code from the view like this. But what if we had many sub-components that all had tasks to do that modified a shared model? A single attribute input would not suffice because there is no explicit mapping of the handlers to the sub-components requirements. However I am sure that you can also see that there is a logical next step of supplying multiple attribute maps, or a map of named handlers, or a dispatch function to accept and route events. The idea of supplying a dispatch function is the heart of reframe; a popular approach to structuring updates to the model. The point is that complex components may require some structure to route changes to the model, and that there are some good patterns for doing that when you need to.

Regardless of how we structure event handlers, it is often a good idea to have a separate model namespace that only deals with domain transformations and operations. Even if the model is simple (indeed it should be), having colocated logic for data operations is a great boon. We don't have to search through all the view code to reason about the underlying behaviors of the application. The view code just renders the data it is passed.

Principle 4: Separate view, model, and event handling code.





Exercise: Draw a scribble with many overlapping lines. Notice anything amiss?

Things are starting to come together but we have hit a snag! Drawing over the top of an existing line causes an erratic jump, leaving weird dashes in the upper left of the SVG. Why is this happening? The problem is that our xy function is calculating the position relative to the target of the mouse event, but when our mouse is over a path, the path is the target of the event, not the SVG container. Two solutions spring to mind:
  1. We could capture the SVG DOM node and calculate the coordinates relative to it,
  2. We can suppress paths from triggering the event.

Principle 5: Prefer HTMLy solutions over DOMy solutions.

Option (1) is reminiscent of the jQuery approach; find the element you want to deal with and just do what you need to do. There's nothing wrong with that in the small, but it can get complicated as new requirements come to light.

In this case option (2) exists which is to express our intent in HTML, so we don't need to keep a reference to the SVG element for coordinate lookup.

Let's side track just a bit to look at another example which presents a similar choice; a button activated input field that should acquire focus when revealed. This could be useful if we were to add an optional title to our squiggles.

Example AE: An input field that acquires focus when revealed


(defn optional-title []
  (let [show? (reagent/atom false)]
    (fn []
      [:div
        [:button
         {:on-click
          (fn [e]
            (swap! show? not))}
         (if @show? "Hide" "Add a title")]
        (when @show?
          [:input {:auto-focus true}])])))

Exercise: Clicking the button currently drops the cursor into the input box. Remove the auto-focus true attribute from the input. Without auto-focus we have to click into the text field to enter text... annoying!

We could have created our own auto focus behavior by having component-did-mount or ref function call focus on the element directly. But seeing as there is a built in facility in HTML, it is cleaner to avoid calling code where adding an attribute will suffice.

So coming back to the problem of paths triggering events, let's go with option (2) because it feels more HTMLy. There is a style pointer-events "none" which will prevent our paths from raising events.

Example AF: Preventing paths from raising events


(defn paths [drawing]
  (into
    [:g
     {:style {:pointer-events "none"}
      :fill "none"
      :stroke "black"
      :stroke-width 4}]
    (for [[x y & more-points] @drawing]
      [:path {:d (str "M " x " " y "L "(string/join " " more-points))}])))

(defn scribble3 [attrs drawing]
  [:svg
   (merge-with merge attrs
               {:width "100%"
                :height 400
                :style {:border "1px solid"
                        :box-sizing "border-box"
                        :cursor "crosshair"}})
   [paths drawing]])

(def my-drawing4 (reagent/atom []))

[scribble3 (mouse-handlers my-drawing4) my-drawing4]

We have modified the definition of our view slightly. Rather than attaching the pointer-events "none" style to every individual path, I opted to use a g tag to group the paths and apply the style once to the entire group.

Success! We can draw scribbles.





Exercise: Modify example AF to scribble in a different color.
Exercise: Add a "clear" button to example AF that resets my-drawing4 to an empty vector.



Compose, compose, compose

No one is an artist unless he carries his picture in his head before painting it, and is sure of his method and composition.
-- Claude Monet


Yes, you guessed it... in this section we are going to put our scribbles inside the mouse trap we defined in part 2. Clicking on the catch button will create new scribbles.

Example AG: A self contained widget


(defn scribble-widget []
  (let [a-drawing (reagent/atom [])
        handlers (mouse-handlers a-drawing)]
    [scribble3 handlers a-drawing]))

[a-better-mouse-trap [:div [scribble-widget]]]


Reagent components compose well because they are quite literally functions and data. We used a higher order component (a component that takes another component as an argument) for our mouse trap, but hopefully you didn't give it a second thought until I pointed it out. It seems quite natural to pass a function to a function in Clojure. Reagent transfers Clojure's super power of function and data composition to the world of HTML.

Principle 6: Use functions liberally.

The underlying theme of this entire series of posts is composition. Representing components the Reagent way really does put the focus on functions and data, and that in turn makes composition both central and seamless. We didn't have to make all our decisions upfront. It was easy to adapt and reorganize because components are functions, and the contents of components are amenable to splitting and extracting further as a function.




By Claude Monet - wartburg.edu, Public Domain, Link


While you can never have too many functions, you can have too many reagent/atoms.

Principle 7: Avoid fragmenting your model.

We have spent much of this series observing cases where a reagent/atom serves to toggle a show state for a component. I want to make a distinction here between state that serves a localized interface goal such as toggling a text box, versus state that represents a logical model. It's usually a good idea to have one central model. If I find myself having a hard time reasoning about the behavior, it's usually a good sign that I need to take a step back and focus on defining the underlying model more clearly.


Conclusion

The most important thing my Physics professor taught me was not formulas but - how to think. -- Carin Meier






Reagent provides a consistent model that feels true to the spirit of Clojure. The core abstractions of functions and data serve us well in component construction and composition. We were able to leverage strong abstractions for data representation and data transformation.

At heart Reagent is beautifully simple. A component is a function that returns a UI element. Change can be observed. Yet we covered a variety of nuances in the application of this simple idea. Perhaps this is not so shocking in light of just how wide the scope of HTML is. Much of the code presented in this tour felt to me like we were working very close to HTML.

The most important thing Reagent taught me was not how to make web applications, but how to compose HTML.

Thank you for reading these articles. I hope that you enjoyed the somewhat circuitous deep dive tour and are as excited as I am about the capabilities Reagent offers for quickly and concisely expressing web applications.

Have a great day!

16 comments:

  1. Truly amazing writing, congratulations. I will use this as educational material for my friends and colleagues.

    ReplyDelete
    Replies
    1. Thank you ingesol :) Glad to hear it!

      Delete
  2. Thanks Timothy...vey nice series!!!

    ReplyDelete
    Replies
    1. Thanks Yassin - I am glad you enjoyed it :)

      Delete
  3. Really good posts, in which I leaned quite a few nice tricks (like reagent/with-let) which already improved my Clojurescript code. Thank you!

    ReplyDelete
    Replies
    1. Hi Quentin. You are welcome! Thanks for reading, and thanks for the kind words :)

      Delete
  4. great writeup. example code could be a bit less if letfn is used.

    ReplyDelete
    Replies
    1. Thanks Yusup, and good idea to use letfn :)

      Delete
  5. Timothy, the next week I'll start to work on my first SPA in ClojureScript, and your post helped me a lot to understand how Reagent works! Thanks a lot for sharing your knowledge.

    ReplyDelete
    Replies
    1. Awesome! Thank you for the great feedback, and best of luck with your SPA. :)

      Delete
  6. Superb articles, many thanks

    ReplyDelete
  7. I must say, this is a rare example of a really well written technical article. I hope you get a chance to write many more Timothy - you clearly know what you're doing :)

    ReplyDelete
    Replies
    1. Thank you very much Nikaustr! I appreciate the encouragement and it motivates me to write more :)

      Delete