dependencies
| (this space intentionally left almost blank) | ||||||||||||
IntroductionUse 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:
When a component triggers an event, it can send it in two possible directions:
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.
| (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
| (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
| (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 | (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: | |||||||||||||
(declare trigger*) | |||||||||||||
Triggering events. | |||||||||||||
To trigger an event simply call one of the functions below, passing | |||||||||||||
There are two functions to either bubble or trickle events. | |||||||||||||
The | (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 | (defn trigger [owner event] (bubble owner event)) | ||||||||||||
Here's an example usage:
| |||||||||||||
Handling events. | |||||||||||||
Reify Example:
| (defprotocol IGotEvent (got-event [_ event])) | ||||||||||||
The | (defprotocol IGotBubblingEvent (got-bubbling-event [_ event])) | ||||||||||||
This protocol, | (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 | (defprotocol IInitEventBus (init-event-bus [_])) | ||||||||||||
Here are the configuration options you can use along with their default values:
| |||||||||||||
(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 feedbackPlease 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
| (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 | |||||||||||||
(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 This function is called from when a component mounts (see | (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 To create a handler it composes potential handlers for supported protocols. Both | (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 | (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:
| (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 | (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 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, | (defn get-config
[owner]
(let [c (om/children owner)]
(merge default-config
(when (satisfies? IInitEventBus c)
(init-event-bus c))))) | ||||||||||||
The | (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 Example:
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})))))) | |||||||||||||