Skip to content

Container Component Macro Ideas

Josh Freckleton edited this page Mar 9, 2017 · 2 revisions

This is a macro inspired by the discussion under Testing Components 2c and the linked blog posts/talks etc.

With one def form it creates two functions, one is the container and sets up the appropriate subscriptions, the other is the actual component.

While I am currently using this macro in one of my projects, I think this usage pattern is sub-optimal since it assumes a 1:1 correspondence between containers and components. Based on the talk by the facebook architect, this may not be an ideal assumption to make. However, I think it covers probably 40% of the form-2 components that I create. The implementation is also fairly... decoupled. Possibly overly so.

(defmulti valid-spec?
  "Validate whether this subscription spec is valid."
  class)

(defmethod valid-spec? :default [spec] false)

(defmethod valid-spec? clojure.lang.Symbol [spec] true)

(defmethod valid-spec?
  clojure.lang.PersistentVector
  [spec]
  (= 2 (count spec)))

(defmethod valid-spec?
  clojure.lang.PersistentUnrolledVector
  [spec]
  (= 2 (count spec)))

(defprotocol SubscriptionSpec
  (subscription-symbol [spec] "Extract the subscription binding symbol from this spec.")
  (subscription-key [spec] "Extract the subscription key from this spec."))

(extend-protocol SubscriptionSpec
  clojure.lang.Symbol
  (subscription-symbol [spec] spec)
  (subscription-key [spec] [(keyword spec)])

  clojure.lang.PersistentVector
  (subscription-symbol [spec] (first spec))
  (subscription-key [spec] (second spec)))

(defn subscription-binding [subscription-spec]
  (let [generate-spec (fn [spec]
                        `[~(subscription-symbol spec)
                          (re-frame.core/subscribe
                           ~(subscription-key spec))])]
    (if (valid-spec? subscription-spec)
      (generate-spec subscription-spec)
      (throw (ex-info "Invalid subscription spec. Must be a symbol or a two-element vector."
                      {:spec subscription-spec})))))

(defn container-name [component-name]
  (symbol (format "%s-container"
                  (name component-name))))

(defmacro defcomponent-2 [name subscriptions & body]
  `(do
     (defn ~name ~subscriptions
       ~@body)

     (defn ~(container-name name) []
       (let [~@(mapcat subscription-binding subscriptions)]
         (fn []
           [~name ~@(map (fn [sub] `(deref ~(subscription-symbol sub))) subscriptions)])))))