|
| 1 | +# Creating a re-com Component |
| 2 | + |
| 3 | +This guide distils the common patterns found in the `re-com` source tree and explains how to create your own component. It assumes you are familiar with ClojureScript and Reagent. |
| 4 | + |
| 5 | +## 1. Namespace setup |
| 6 | + |
| 7 | +Create a new namespace under `src/re_com`. Components generally require several macros and utilities: |
| 8 | + |
| 9 | +```clojure |
| 10 | +(ns re-com.my-component |
| 11 | + (:require-macros [re-com.core :refer [handler-fn at reflect-current-component]] |
| 12 | + [re-com.validate :refer [validate-args-macro]]) |
| 13 | + (:require [re-com.debug :refer [->attr]] |
| 14 | + [re-com.theme :as theme] |
| 15 | + [re-com.util :refer [deref-or-value]] |
| 16 | + [re-com.config :refer [include-args-desc?]])) |
| 17 | +``` |
| 18 | + |
| 19 | +* `handler-fn` ensures event handlers do not accidentally return `false` which React interprets specially. See the commentary in `core.clj` lines 3‑32 for details. |
| 20 | +* `at` and `reflect-current-component` supply debugging metadata such as source coordinates and the current component name. |
| 21 | +* `validate-args-macro` performs argument validation when `goog.DEBUG` is enabled. |
| 22 | + |
| 23 | +## 2. Describe component parts |
| 24 | + |
| 25 | +Most components document their internal structure with a vector of maps known as `parts-desc` and a derived set `parts`. These are only included when `include-args-desc?` is true so that production builds remain lean. |
| 26 | + |
| 27 | +An example from `buttons.cljs` lines 20‑28 illustrates the idea: |
| 28 | + |
| 29 | +```clojure |
| 30 | +(def button-parts-desc |
| 31 | + (when include-args-desc? |
| 32 | + [{:name :wrapper :level 0 :class "rc-button-wrapper" :impl "[button]" :notes "Outer wrapper of the button, tooltip (if any), everything."} |
| 33 | + {:name :tooltip :level 1 :class "rc-button-tooltip" :impl "[popover-tooltip]" :notes "Tooltip, if enabled."} |
| 34 | + {:type :legacy :level 1 :class "rc-button" :impl "[:button]" :notes "The actual button."}])) |
| 35 | + |
| 36 | +(def button-parts |
| 37 | + (when include-args-desc? |
| 38 | + (-> (map :name button-parts-desc) set))) |
| 39 | +``` |
| 40 | + |
| 41 | +Consumers can customise a component via the optional `:parts` argument by providing classes, styles or attrs keyed by these part names. |
| 42 | + |
| 43 | +## 3. Document arguments |
| 44 | + |
| 45 | +Each component declares an `args-desc` describing supported parameters, default values and validation functions. Again from `buttons.cljs` lines 30‑42: |
| 46 | + |
| 47 | +```clojure |
| 48 | +(def button-args-desc |
| 49 | + (when include-args-desc? |
| 50 | + [{:name :label :required true :type "string | hiccup" :validate-fn string-or-hiccup? :description "label for the button"} |
| 51 | + {:name :on-click :required false :type "-> nil" :validate-fn fn? :description "called when the button is clicked"} |
| 52 | + {:name :tooltip :required false :type "string | hiccup" :validate-fn string-or-hiccup? :description "what to show in the tooltip"} |
| 53 | + ...])) |
| 54 | +``` |
| 55 | + |
| 56 | +The maps typically contain keys such as `:name`, `:required`, `:default`, `:type`, `:validate-fn` and `:description`. Validation helpers live in `re-com.validate` and include sets of allowed keywords, CSS style checks and so on. |
| 57 | + |
| 58 | +## 4. Implement the component |
| 59 | + |
| 60 | +A component function is usually form‑2 or form‑3. It begins with `validate-args-macro` to check its arguments and then produces hiccup markup. The button component demonstrates the pattern in lines 44‑91: |
| 61 | + |
| 62 | +```clojure |
| 63 | +(defn button [] |
| 64 | + (let [showing? (reagent/atom false)] |
| 65 | + (fn [& {:keys [label on-click tooltip tooltip-position disabled? class style attr parts src debug-as] |
| 66 | + :or {class "btn-default"} |
| 67 | + :as args}] |
| 68 | + (or |
| 69 | + (validate-args-macro button-args-desc args) |
| 70 | + (do |
| 71 | + (when-not tooltip (reset! showing? false)) |
| 72 | + (let [disabled? (deref-or-value disabled?) |
| 73 | + the-button [:button |
| 74 | + (merge |
| 75 | + {:class (str "rc-button btn " class) |
| 76 | + :style (merge (flex-child-style "none") style) |
| 77 | + :disabled disabled? |
| 78 | + :on-click (handler-fn |
| 79 | + (when (and on-click (not disabled?)) |
| 80 | + (on-click event)))} |
| 81 | + (when tooltip |
| 82 | + {:on-mouse-over (handler-fn (reset! showing? true)) |
| 83 | + :on-mouse-out (handler-fn (reset! showing? false))}) |
| 84 | + attr) |
| 85 | + label]] |
| 86 | + [box |
| 87 | + :src src |
| 88 | + :debug-as (or debug-as (reflect-current-component)) |
| 89 | + :class (str "rc-button-wrapper display-inline-flex " (get-in parts [:wrapper :class])) |
| 90 | + :style (get-in parts [:wrapper :style]) |
| 91 | + :attr (get-in parts [:wrapper :attr]) |
| 92 | + :align :start |
| 93 | + :child (if tooltip |
| 94 | + [popover-tooltip |
| 95 | + :src (at) |
| 96 | + :label tooltip |
| 97 | + :position (or tooltip-position :below-center) |
| 98 | + :showing? showing? |
| 99 | + :anchor the-button |
| 100 | + :class (str "rc-button-tooltip " (get-in parts [:tooltip :class])) |
| 101 | + :style (get-in parts [:tooltip :style]) |
| 102 | + :attr (get-in parts [:tooltip :attr])] |
| 103 | + the-button)])))))) |
| 104 | +``` |
| 105 | + |
| 106 | +Key takeaways: |
| 107 | + |
| 108 | +* `validate-args-macro` returns a diagnostic component if validation fails. |
| 109 | +* `handler-fn` wraps event handlers to avoid accidentally returning `false`. |
| 110 | +* `deref-or-value` handles parameters that might be plain values or atoms. |
| 111 | +* `->attr` injects debugging attributes (`data-rc` and source coordinates) into the root element. |
| 112 | + |
| 113 | +## 5. Exposing your component |
| 114 | + |
| 115 | +To make the component part of the public API, add a simple `def` in `re_com/core.cljs` that re-exports it. For example, lines 64‑71 show how various button helpers are exposed: |
| 116 | + |
| 117 | +```clojure |
| 118 | +(def button buttons/button) |
| 119 | +(def md-circle-icon-button buttons/md-circle-icon-button) |
| 120 | +(def md-icon-button buttons/md-icon-button) |
| 121 | +(def info-button buttons/info-button) |
| 122 | +(def row-button buttons/row-button) |
| 123 | +(def hyperlink buttons/hyperlink) |
| 124 | +``` |
| 125 | + |
| 126 | +Consumers then `require` `re-com.core` and call your function by its public name. |
| 127 | + |
| 128 | +## 6. Validation internals |
| 129 | + |
| 130 | +The validation system is implemented in `re_com.validate`. The `validate-args` function (lines 138‑170) performs several checks: |
| 131 | + |
| 132 | +1. Unknown or misspelled argument names. |
| 133 | +2. Missing required arguments. |
| 134 | +3. Custom validation functions per argument. |
| 135 | + |
| 136 | +If any problems are detected a special component renders in place of your component and details are logged to the console. |
| 137 | + |
| 138 | +```clojure |
| 139 | +(defn validate-args [arg-defs passed-args] |
| 140 | + (if-not debug? |
| 141 | + nil |
| 142 | + (let [{:keys [parts-validate-fn]} arg-defs |
| 143 | + passed-arg-keys (set (remove #{:theme :re-com :part} (set (keys passed-args)))) |
| 144 | + problems (as-> [] pvec |
| 145 | + (if-not parts-validate-fn |
| 146 | + pvec |
| 147 | + (parts-validate-fn passed-args pvec)) |
| 148 | + (arg-names-known? (:arg-names arg-defs) passed-arg-keys pvec) |
| 149 | + (required-args? (:required-args arg-defs) passed-arg-keys pvec) |
| 150 | + (validate-fns? (:validated-args arg-defs) passed-args pvec) |
| 151 | + (remove nil? pvec))] |
| 152 | + (when-not (empty? problems) |
| 153 | + [debug/validate-args-error |
| 154 | + :problems problems |
| 155 | + :args passed-args |
| 156 | + :component (debug/short-component-name (component/component-name (reagent/current-component)))])))) |
| 157 | +``` |
| 158 | + |
| 159 | +## 7. Putting it all together |
| 160 | + |
| 161 | +1. **Create** a namespace and require the helper macros and functions. |
| 162 | +2. **Define** `parts-desc`, `parts` and `args-desc` to describe your API. |
| 163 | +3. **Write** the component using Reagent hiccup, starting with `(validate-args-macro ...)`. |
| 164 | +4. **Use** `handler-fn`, `deref-or-value`, `->attr` and other utilities as needed. |
| 165 | +5. **Expose** the component via `re_com.core` for library consumers. |
| 166 | + |
| 167 | +Following these conventions keeps new components consistent with the rest of `re-com` and ensures a good debugging experience. |
0 commit comments