Components are registered via baredom.utils.component/register!, which creates the native ES class, wires lifecycle callbacks, and installs property accessors from a declarative options map. Do not create element-class functions manually — the factory handles all boilerplate.
(ns baredom.components.x-foo.x-foo
(:require [baredom.utils.component :as component]
[baredom.utils.dom :as du]
[baredom.components.x-foo.model :as model]))
;; Named lifecycle functions — always separate defn-, never inline
(defn- connected! [^js el] ...)
(defn- disconnected! [^js el] ...)
(defn- attribute-changed! [^js el _name old-val new-val] ...)
;; Property accessors — use du/install-properties! driven by model/property-api
(defn- install-property-accessors! [^js proto]
(du/install-properties! proto model/property-api))
(defn init! []
(component/register! model/tag-name
{:observed-attributes model/observed-attributes
:connected-fn connected!
:attribute-changed-fn attribute-changed!
;; Optional:
:disconnected-fn disconnected!
:form-associated? true
:setup-prototype-fn install-property-accessors!}))
Components install JS property accessors at one of three tiers. Pick the simplest tier the component qualifies for. Mixing tiers within one install-property-accessors! is fine; document and justify any drop to a lower tier inline.
One line:
(defn- install-property-accessors! [^js proto]
(du/install-properties! proto model/property-api))
Requires model/property-api to follow the canonical schema:
(def property-api
{:size {:type 'string :reflects-attribute attr-size :default "md"}
:open {:type 'boolean :reflects-attribute attr-open}
:delay {:type 'number :reflects-attribute attr-delay :default 400}})
Use Tier 0 when every property is a simple bool/string/number reflector with no custom logic. Methods (el.show()) and computed read-only properties don't fit property-api — if a component has those, drop to Tier 1.
Reference: x_icon (primary Golden Sample), x_button, x_i18n.
Use when most properties are simple but at least one needs a method or a custom parser:
(defn- install-property-accessors! [^js proto]
(du/install-properties! proto model/property-api) ;; data-reflective props
(aset proto "show" (fn [] (this-as ^js this (do-show! this))))
(aset proto "hide" (fn [] (this-as ^js this (do-hide! this)))))
Or keep it explicit when only a couple of helpers are needed:
(defn- install-property-accessors! [^js proto]
(du/define-bool-prop! proto "open" model/attr-open)
(du/define-string-prop! proto "label" model/attr-label)
(du/define-number-prop! proto "duration" model/attr-duration model/default-duration)
(custom-method! proto))
Reference: x_dropdown, x_combobox.
.defineProperty (last resort)Use only when helpers can't express the semantics:
el.src = "" should remove the attribute (e.g., x_image).el.open = true triggers an animation (e.g., x_drawer, x_modal).x_toaster's position keeps "" because (if v ...) is truthy on empty string.naturalWidth on x_image).:writable or :configurable flags.Document the reason inline so future readers understand why the tier dropped. Reference: x_image, x_currency_field, x_text_area.
defn- functions (connected!, disconnected!, attribute-changed!) — never inline anonymous functions for lifecycle.setup-prototype-fn should be a named defn- function (e.g. install-property-accessors!).this-as inside property getter/setter bodies — never reference this directly.when-not .get js/customElements).Pick one of two patterns depending on whether the component needs to cache shadow children for re-decoration.
du/initialized? flag (preferred when applicable)Golden sample: x_icon. init-dom! builds the shadow tree, calls (du/mark-initialized! el k-initialized?), and returns nil. ensure-shadow! calls init-dom! only when (du/initialized? el k-initialized?) is false. No refs map — the host element, shadow root, and any selectors are looked up freshly when needed.
(def ^:private k-initialized? "__xFooInitialized")
(defn- init-dom! [^js el]
(let [root (.attachShadow el #js {:mode "open"})
style (.createElement js/document "style")]
;; ... build tree ...
(du/mark-initialized! el k-initialized?)))
(defn- ensure-shadow! [^js el]
(when-not (du/initialized? el k-initialized?)
(init-dom! el)))
Use this when apply-model! writes directly to the host element (CSS variables on .-style, attributes via du/) and shadow children aren't re-touched after creation.
When apply-model! reads back specific shadow children to mutate, store a refs map. Use CLJS keywords for refs map keys with :keys destructure:
(def ^:private k-refs "__xFooRefs")
(defn- init-dom! [^js el]
(let [root (.attachShadow el #js {:mode "open"})
...
refs {:root root :container container :input-el input-el}]
;; ... assemble ...
(du/setv! el k-refs refs)
refs))
(defn- ensure-refs! [^js el]
(or (du/getv el k-refs) (init-dom! el)))
(defn- apply-model! [^js el m]
(let [{:keys [container input-el]} (ensure-refs! el)]
...))
References: x-avatar, x-badge, x-text-area.
Refs-key convention — Prefer CLJS keyword keys ({:foo el}) with :keys [foo] destructuring over string-keyed #js {} JS objects accessed via gobj/get. The CLJS form is destructure-friendly and Closure-Advanced-safe. Only drop to the #js {} + gobj/get form when the refs object crosses a register-prototype boundary (rare).
When in doubt: if your component would (du/getv el k-refs) only for an init flag (never reading the values back), use du/initialized? — that's the sentinel-refs anti-pattern. Audited and swept out in PRs #216–#218.
element-class functions with js* — use component/register! insteadjs/Reflect.construct with atom-based constructor wiringjs/Object.create / js/Object.setPrototypeOf prototype compositionCan you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |