om-event-bus

0.2.0


Simple custom events for Om.

dependencies

org.clojure/clojure
1.6.0
org.clojure/clojurescript
0.0-2505
org.clojure/core.async
0.1.346.0-17112a-alpha
om
0.8.0-beta5



(this space intentionally left almost blank)
 

Introduction

Use this library whenever you need Om components to send events either down to components nested within them or up to their parents.

Let's say you have three om components nested inside each other:

Three nested components

When a component triggers an event, it can send it in two possible directions:

  • it can bubble it to its parents all the way to the top, or
  • it can trickle an event to its children.

In om-event-bus each direction is handled by a separate event bus. Components connect to an event bus to handle events passing through it.

Bubbling vs. trickling

(ns om-event-bus.core
  (:require [om.core :as om :include-macros true]
            [cljs.core.async :as async]
            [om-event-bus.impl :as impl]
            [om-event-bus.descriptor :as d])
  (:require-macros [cljs.core.async.macros :as async]
                   [om-event-bus.impl :as impl]))
(declare init-event-bus! shutdown-event-bus! trace root* default-protocols)
(def ^:dynamic ^:private *parent* nil)

Replacements for om.core/root

First, we need a replacement for om.core/root that will inject event buses into every component created by our app.

There are three versions of the replacement. Let's start with one that adds most functionality.

The root<> function adds support both for bubbling (child to parents) and trickling (parent to children) events.

Note

The arity 4 version lets you specify a channel if you also want to handle events outside of component hierarchy. If you pass a channel to receive events through, you MUST consume events.

See this example (demo).

(defn root<>
  ([f value options & [out-event-ch]]
    (root* f
           value
           options
           out-event-ch
           {::bubbling (impl/event-bus (impl/bubbling-router))
            ::trickling (impl/event-bus (impl/trickling-router))}
           default-protocols)))

The other two versions each create a single bus for one direction only, either bubbling or trickling.

Use root> instead of om.core/root to add support for sending events from child components to parent components only (bubbling).

Note

Similarly too root<>, the arity 4 version lets you specify a channel if you also want to handle events outside of component hierarchy and if you pass the channel you MUST consume events.

(defn root>
  ([f value options]
    (root> f value options nil))
  ([f value options out-event-ch]
    (root* f
           value
           options
           out-event-ch
           {::bubbling (impl/event-bus (impl/bubbling-router))}
           default-protocols)))

Use root< instead of om.core/root to add support for sending events from parent components to child components only (trickling).

(defn root<
  ([f value options]
    (root* f
           value
           options
           nil
           {::trickling (impl/event-bus (impl/trickling-router))}
           default-protocols)))

Use these functions anywhere you would normally use om.core/root. Example:

(event-bus/root<>
             (fn [app owner]
               (reify om/IRender
                 (render [_]
                   (dom/h1 nil (:text app)))))
             app-state
             {:target (. js/document (getElementById \"app\"))})

(declare trigger*)

Triggering events.

To trigger an event simply call one of the functions below, passing owner and any data as event (a map is probably the best choice but is not required).

There are two functions to either bubble or trickle events.

The bubble function sends an event from owner up to its parents.

(defn bubble
  [owner event]
  (trigger* owner ::bubbling event))
(defn trickle
  [owner event]
  (trigger* owner ::trickling event))

Let's also define a function that sends in the default direction (depends on one's taste but I've chosen sending from children to parents as the default.)

The trigger function simply bubbles an event.

(defn trigger
  [owner event]
  (bubble owner event))

Here's an example usage:

(defn child-view
             [app owner]
             (reify
               om/IRender
               (render [_]
                 (dom/div nil (dom/button
                                #js {:onClick #(event-bus/bubble owner \"Hi there!\")}
                                \"Click me!\")))))

Handling events.

Reify IGotEvent in components that are interested in both bubbling and trickling events.

Example:

(defn parent-view
      [app owner]
      (reify
        event-bus/IGotEvent
        (got-event [_ event]
          ... do something about the event ...)
        om/IRender
        ...
  

(defprotocol IGotEvent
  (got-event [_ event]))

The IGotBubblingEvent, when reified in a component, will handle bubbling events.

(defprotocol IGotBubblingEvent
  (got-bubbling-event [_ event]))

This protocol, IGotTricklingEvent should be reified to handle trickling events.

(defprotocol IGotTricklingEvent
  (got-trickling-event [_ event]))

There's one more protocol useful when you want to define a function transforming events that pass a particular component (via xforms) or to configure the event bus.

Use IInitEventBus to override event bus options. Return an option hash to be merged into default-config.

(defprotocol IInitEventBus
  (init-event-bus [_]))

Here are the configuration options you can use along with their default values:

  • :xform - this optional xform will be applied to all passing through the component, e.g.
     (map (fn [event]
       (merge event {:extra-info "abc"})))
    
  • :buf-or-n - buffer for internally created channels
(def default-config {:xform    nil
                     :buf-or-n 1})

This is everything you should need to use the library but you are welcome to the internals as well. :)

Looking for feedback

Please make sure to send your critique to gyamtso@gmail.com or tweet me @martinbilski.

(declare get-config debug? build-buses find-handler compose-handlers)

Internals

Implementation of om/root replacements.

Here's what the root* function does:

  1. It intercepts calls to build (via :instrument) to pass on event buses from parent components to their children (via local state).

  2. It creates a custom descriptor to add functionality on top of the existing React.js lifecycle methods to set up and tear down the event bus for a component and to bind *parent* to be used in the :instrument function.

  3. It passes the custom descriptor to om.core/build*.

(defn root*
  [f value options out-event-ch event-buses protocols]
  (let [descriptor (d/make-descriptor {:componentWillMount
                                       (fn [this super]
                                         (when (debug? this)
                                           (println (om/id this) "will-mount"))
                                         (init-event-bus! this protocols)
                                         (super))
                                       :componentWillUnmount
                                       (fn [this super]
                                         (when (debug? this)
                                           (println (om/id this) "will-unmount"))
                                         (shutdown-event-bus! this)
                                         (super))
                                       :render
                                       (fn [this super]
                                         (when (debug? this)
                                           (println (om/id this) "render"))
                                         (binding [*parent* this]
                                           (super)))})]
    (when out-event-ch
      (if-let [bubbling-bus (::bubbling event-buses)]
        (impl/tap bubbling-bus out-event-ch)
        (throw (js/Error. "Bubbling event bus not available. Make sure to use root> or root<>."))))
    (om/root f value
             (merge options {:instrument (fn [f x m]
                                           (let [parent-buses (or
                                                                (and *parent* (om/get-state *parent* ::event-buses))
                                                                event-buses)]
                                             (om/build* f x (-> m
                                                                (update-in [:init-state] merge {::event-buses parent-buses})
                                                                (merge {:descriptor descriptor})))))}))))

As you've learned above there are three protocols corresponding to trickling and bubbling events and to "all" events regardless of their direciton. Using root* you can pass your own custom interfaces and event buses.

(def default-protocols
  {::all #(when (satisfies? IGotEvent %)
           got-event)
   ::bubbling #(when (satisfies? IGotBubblingEvent %)
                got-bubbling-event)
   ::trickling #(when (satisfies? IGotTricklingEvent %)
                 got-trickling-event)})

Event bus setup details.

The init-event-bus! function adds support for triggering events and, if the component reified any of the supported protocols, the code to handle events.

This function is called from when a component mounts (see root*). It sets ::event-buses in the components local state to a map containing one or more event buses.

(defn- init-event-bus!
  [owner protocols]
  (let [{:keys [xform buf-or-n debug] :as config} (get-config owner)]
    (when debug
      (println (om/id owner) "init-event-bus!" (when xform "+xform")))
    (impl/with-options {:buf-or-n buf-or-n
                        :debug    debug}
                       (om/set-state! owner
                                      ::event-buses
                                      (build-buses owner xform protocols)))))

What the build-buses function does does is it takes the event bus from its parent component and extends it, either by forking it to handle events or by creating a 'leg' of the bus with minimal overhead if the component reifies none of the compatible event-handling protocols.

To create a handler it composes potential handlers for supported protocols. Both catch-all-handler and the result of the application of find-handler can return nil but compose-handlers will take care of that.

(defn build-buses
  [owner xform protocols]
  (let [catch-all-handler (find-handler owner ::all protocols)
        buses (into {}
                (for [[k bus] (om/get-state owner ::event-buses)]
                  (let [handler (compose-handlers catch-all-handler (find-handler owner k protocols))]
                    [k (if handler
                         (do
                           (when (debug? owner)
                             (println (om/id owner) "adding fork"))
                           (impl/add-fork bus handler xform))
                         (do
                           (when (debug? owner)
                             (println (om/id owner) "adding leg"))
                           (impl/add-leg bus xform)))])))]
    buses))

The find-handler function looks up a protocol builder function in protocol using bus-key as, well, the key (for instance, ::bubbling) and, if one is found, binds it to a component resulting in an event handling function. If the protocol isn't implemented by the component, the function returns nil.

(defn- find-handler
  [owner bus-key protocols]
  (let [component (om/children owner)]
    (when-let [handler-fn ((bus-key protocols) component)]
      (if-not (debug? owner)
        (partial handler-fn component)
        (fn [event]
          (case (and (map? event)
                     (:event event))
            :om-event-bus.impl/alive (println (om/id owner) "Event-handling go loop is running.")
            :om-event-bus.impl/dead (println (om/id owner) "Event-handling go loop has just died.")
            (do
              (println (map? event) (:event event))
              (println (om/id owner) "received" event)
              (handler-fn component event))))))))

To handle possible non-existent handlers (a.k.a. nils) this function returns either:

  • an event handler calling one of more event-handing functions in handlers if any of the handlers is not nil, or
  • nil (if all handlers are nil).
(defn- compose-handlers
  [& handlers]
  (when-let [funs (not-empty (remove nil? handlers))]
    (fn [event]
      (doseq [f funs]
        (f event)))))

Event bus shutdown.

When a component unmounts, event bus needs to shut down by closing all channels, terminating go loops etc.

This function shuts down event bus for the component. It simply calls shutdown on the event bus set up in init-event-bus! above.

(defn- shutdown-event-bus!
  [owner]
  (when (debug? owner)
    (println (om/id owner) "shutdown-event-bus!"))
  (doseq [[_ bus] (om/get-state owner ::event-buses)]
    (impl/shutdown bus))
  (om/set-state! owner ::event-buses nil))                  ; TODO: Set each to nil-event-bus reporting meaningful errors.

Event triggering details.

The trigger* function triggers an event for a specific component (owner) and a bus identified by event-bus-key (e.g. ::bubbling).

It simply looks up the event bus in ::event-buses local state and uses it to trigger an event.

(defn trigger*
  [owner event-bus-key event]
  (let [event-bus (event-bus-key (om/get-state owner ::event-buses))]
    (impl/trigger event-bus event))
  nil)  ; Avoid the following React.js warning: "Returning `false` from an event handler is deprecated

Helpers.

This function, get-config returns the component's event bus config. See IInitEventBus.

(defn get-config
  [owner]
  (let [c (om/children owner)]
    (merge default-config
           (when (satisfies? IInitEventBus c)
             (init-event-bus c)))))

The debug returns true if event bus debugging is turned on for the component.

(defn debug?
  [owner]
  (some? (:debug (get-config owner))))
 
(ns om-event-bus.descriptor
  (:require [om.core :as om :include-macros true]))
(declare around-method)

This namespace contains functions that let you extend React.js component descriptors by overriding lifecycle methods with support for calling the original function.

Given pure methods and a map of overrides, the extend-pure-methods function extends pure methods with new lifecycle methods.

Example:


  (extend-pure-methods
    {:render (fn [this super]
                ;; Do something.
                (super))}) ;; Call the original method.
  

You can also specify a map of pure methods as the first argument.

(defn- extend-pure-methods
  ([new-methods]
    (extend-pure-methods om/pure-methods new-methods))
  ([methods new-methods]
    (loop [methods' methods [[new-method-name new-method-fn] & new-methods'] (seq new-methods)]
      (if new-method-name
        (recur (around-method new-method-name methods' new-method-fn) new-methods')
        methods'))))

This function does the actual job of overriding a pure method by wrapping it in f.

(defn- around-method
  [method methods f]
  (let [prev-method (method methods)]
    (-> methods
        (assoc method #(this-as this
                                (do
                                  (f this (fn []
                                            (.call prev-method this)))))))))

Creates a custom descriptor with support for an event bus.

(defn- make-descriptor
  [new-methods]
  (let [methods (extend-pure-methods new-methods)
        descriptor (om/specify-state-methods! (clj->js methods))]
    descriptor))
 
(ns om-event-bus.impl
  #+clj
  (:require [clojure.core.async :as async])
  #+cljs
  (:require [cljs.core.async :as async])
  #+cljs
  (:require-macros [cljs.core.async.macros :as async]
                   [om-event-bus.impl :refer [options]]))

Event bus implementation

This namespace contains the actual implementation of event buses based core.async.

This is a portable .cljx file and can be used in both Clojure and ClojureScript.

(def ^:dynamic *options* {:buf-or-n 1
                          :debug false})
(defmacro with-options
  [opts & body]
  `(binding [*options* (merge *options* ~opts)]
     ~@body))
(defmacro options
  []
  `*options*)
(defprotocol ITriggerEvent
  (trigger [_ event]))
(defprotocol IEventBus
  (tap [this ch])
  (sink [this mid-ch bus])
  (add-leg [this]
           [this xform])
  (add-fork [this handler]
            [this handler xform])
  (shutdown [this]))
(defprotocol IEventRouter
  (leg [_ parent mid-ch child])
  (fork [_ parent child ch]))
(declare bubbling-router extend-event-bus event-bus* dbg-handle-events! handle-events! maybe-apply-xform)
(defn event-bus
  ([]
    (event-bus (bubbling-router)))
  ([router]
    (event-bus* router (async/mult (async/chan (:buf-or-n (options)))) true)))
(defn- pass-event-bus
  [router mult]
  (event-bus* router mult false))
(defn- event-bus*
  [router parent-mult close]
  (let [mult parent-mult
        bus (reify
              IEventBus
              (tap [_ ch]
                (async/tap mult ch))
              (sink [_ mid-ch bus]
                (tap bus mid-ch)
                (async/pipe mid-ch (async/muxch* mult) false))
              (add-fork [this handler]
                (extend-event-bus this router handler))
              (add-fork [this handler xform]
                (extend-event-bus this router handler xform))
              (add-leg [_]
                (pass-event-bus router mult))
              (add-leg [this xform]
                (extend-event-bus this router nil xform))
              (shutdown [_]
                (when close
                  (async/untap-all mult)
                  (async/close! (async/muxch* mult))))
              ITriggerEvent
              (trigger [_ event]
                (async/put! (async/muxch* mult) event)))]
    bus))
(defn- extend-event-bus
  ([parent-bus router handler]
    (extend-event-bus parent-bus router handler nil))
  ([parent-bus router handler xform]
    (let [event-feed (when handler (async/chan (:buf-or-n (options))))
          child-mult (async/mult (async/chan (:buf-or-n (options))))
          mid-ch (apply async/chan (:buf-or-n (options)) (if xform [xform] []))
          child-bus (reify
                       IEventBus
                       (tap [_ ch]
                         (async/tap child-mult ch))
                       (sink [_ mid-ch bus]
                         (tap bus mid-ch)
                         (async/pipe mid-ch (async/muxch* child-mult) false))
                       (add-fork [this handler]
                         (extend-event-bus this router handler))
                       (add-fork [this handler xform]
                         (extend-event-bus this router handler xform))
                       (add-leg [_]
                         (pass-event-bus router child-mult))
                       (add-leg [this xform]
                         (extend-event-bus this router nil xform))
                       (shutdown [_]
                         (async/untap-all child-mult)
                         (async/close! (async/muxch* child-mult))
                         (when event-feed (async/close! event-feed))
                         (async/close! mid-ch))
                       ITriggerEvent
                       (trigger [_ event]
                         (async/put! mid-ch event)))]
      (leg router parent-bus mid-ch child-bus)
      (when event-feed
        (fork router parent-bus child-bus event-feed)
        (if (:debug (options))
          (dbg-handle-events! event-feed handler)
          (handle-events! event-feed handler)))
      child-bus)))
(defn trickling-router
  []
  (reify
    IEventRouter
    (leg [_ parent mid-ch child]
      (sink child mid-ch parent))
    (fork [_ parent _ ch]
      (tap parent ch))))
(defn bubbling-router
  []
  (reify
    IEventRouter
    (leg [_ parent mid-ch child]
      (sink parent mid-ch child))
    (fork [_ _ child ch]
      (tap child ch))))
(defn- handle-events!
  [ch f]
  (async/go-loop []
    (let [event (async/<! ch)]
      (when event
        (f event)
        (recur)))))
(defn- dbg-handle-events!
  [ch f]
  (async/go-loop []
    (let [t (async/timeout 5000)
          [event ch] (async/alts! [ch t])]
      ; Use events below instead of println so the component using may identify itself when showing the event to
      ; make the message understandable (printing just a bunch of "I'm alive" messages without saying _who_ is
      ; alive doesn't add much value).
      (if (= ch t)
        (do (f {:event ::alive})
            (recur))
        (if event
          (do (f event)
              (recur))
          (f {:event ::dead}))))))