Skip to content

Instantly share code, notes, and snippets.

@dvingo
Last active May 24, 2023 12:21
Show Gist options
  • Save dvingo/97bae8b33c08b257153946bb82f38a86 to your computer and use it in GitHub Desktop.
Save dvingo/97bae8b33c08b257153946bb82f38a86 to your computer and use it in GitHub Desktop.
helix defnc with malli instrumentation support
(ns space.matterandvoid.helix
#?(:cljs (:require-macros space.matterandvoid.helix))
#?(:clj
(:require
[helix.core :as h]
[helix.impl.analyzer :as hana]
[helix.impl.props :as impl.props]
[clojure.string :as string]))
#?(:cljs (:require [helix.core :as h])))
#?(:clj
(defn- valid-malli-map-props-shape? [schema]
;; optionally can add helper for bad schemas here
true))
#?(:clj
(defn- fnc*
([display-name props-bindings body]
;; maybe-ref for react/forwardRef support
`(fn ^js/React.Element ~@(when (some? display-name) [display-name])
[props# maybe-ref#]
(let [~props-bindings [(h/extract-cljs-props props#) maybe-ref#]]
~@body)))
([display-name malli-props-schema props-bindings body]
(assert display-name "Must pass display name")
(when-not (valid-malli-map-props-shape? malli-props-schema)
(throw (ex-info (str "Invalid shape for malli schema for component: " *ns* "/" display-name)
{:component (symbol (str *ns*) (str display-name))
:props malli-props-schema})))
(let [delegate-fn-var-name (symbol (str display-name "-impl"))]
`(do
(def ~(vary-meta
delegate-fn-var-name
assoc
:malli/schema [:=> [:cat malli-props-schema :js/object] :react/element])
(fn ^js/React.Element ~@(when (some? display-name) [display-name])
[props# maybe-ref#]
(let [~props-bindings [props# maybe-ref#]]
~@body)))
(fn ^js/React.Element ~@(when (some? display-name) [display-name])
[props# maybe-ref#]
(~delegate-fn-var-name (h/extract-cljs-props props#) maybe-ref#)))))))
#?(:clj
(defmacro defnc*
"Creates a new functional React component. Used like:
(defnc component-name
\"Optional docstring\"
[props ?ref]
{,,,opts-map}
,,,body)
\"component-name\" will now be a React function component that returns a React
Element.
Your component should adhere to the following:
First parameter is 'props', a map of properties passed to the component.
Second parameter is optional and is used with `React.forwardRef`.
'opts-map' is optional and can be used to pass some configuration options to the
macro.
Current options:
- ':wrap' - ordered sequence of higher-order components to wrap the component in
- ':helix/features' - a map of feature flags to enable. See \"Experimental\" docs.
Either in the function metadata or in the opts map you can optionally provide:
:helix/schema which is used to describe the props hashmap.
An inner function is emitted that can be instrumented by malli.
'body' should return a React Element."
[display-name & form-body]
(let [[docstring form-body] (if (string? (first form-body))
[(first form-body) (rest form-body)]
[nil form-body])
[fn-meta form-body] (if (map? (first form-body))
[(first form-body) (rest form-body)]
[nil form-body])
props-bindings (first form-body)
body (rest form-body)
opts-map? (map? (first body))
opts (if opts-map? (first body) {})
malli-props-schema (or (:helix/schema fn-meta) (:helix/schema opts))
sig-sym (gensym "sig")
fully-qualified-name (str *ns* "/" display-name)
feature-flags (:helix/features opts)
;; feature flags
flag-fast-refresh? (:fast-refresh feature-flags)
flag-check-invalid-hooks-usage? (:check-invalid-hooks-usage feature-flags true)
flag-define-factory? (:define-factory feature-flags)
flag-metadata-optimizations (:metadata-optimizations feature-flags)
body (cond-> body
opts-map? (rest)
flag-metadata-optimizations (hana/map-forms-with-meta h/meta->form))
hooks (hana/find-hooks body)
component-var-name (if flag-define-factory?
(with-meta (symbol (str display-name "-type"))
{:private true})
display-name)
component-fn-name (symbol (str display-name "-render"))]
(when flag-check-invalid-hooks-usage?
(when-some [invalid-hooks (->> (map hana/invalid-hooks-usage body)
(flatten)
(filter (comp not nil?))
(seq))]
(doseq [invalid-hook invalid-hooks]
(hana/warn hana/warning-invalid-hooks-usage
&env
invalid-hook))))
`(do ~(when flag-fast-refresh?
`(if ~(with-meta 'goog/DEBUG {:tag 'boolean})
(def ~sig-sym (h/signature!))))
(def ~(vary-meta
component-var-name
merge
{:helix/component? true}
fn-meta)
~@(when-not (nil? docstring)
(list docstring))
(->
~(if malli-props-schema
(fnc* component-fn-name malli-props-schema props-bindings
(cons (when flag-fast-refresh?
`(if ^boolean goog/DEBUG
(when ~sig-sym
(~sig-sym))))
body))
(fnc* component-fn-name props-bindings
(cons (when flag-fast-refresh?
`(if ^boolean goog/DEBUG
(when ~sig-sym
(~sig-sym))))
body)))
(cond->
(true? ^boolean goog/DEBUG)
(doto (-> (.-displayName) (set! ~fully-qualified-name))))
~@(-> opts :wrap)))
~(when flag-define-factory?
`(def ~display-name
(h/cljs-factory ~component-var-name)))
~(when flag-fast-refresh?
`(when (with-meta 'goog/DEBUG {:tag 'boolean})
(when ~sig-sym
(~sig-sym ~component-var-name ~(string/join hooks)
nil ;; forceReset
nil)) ;; getCustomHooks
(h/register! ~component-var-name ~fully-qualified-name)))
~display-name))))
(defmacro defnc
"Args: component name
optional docstring
optional metadata hashmap
vector of parameters 1 or 2 arity - props and optional react ref from forward ref
function body"
[display-name & form-body]
(let [[docstring form-body] (if (string? (first form-body))
[(first form-body) (rest form-body)]
[nil form-body])
[fn-meta form-body] (if (map? (first form-body))
[(first form-body) (rest form-body)]
[nil form-body])
params (first form-body)
body (rest form-body)
opts-map? (map? (first body))
opts-map (if opts-map? (first body) {})
opts-map (cond-> opts-map
(:wrap fn-meta)
(update :wrap (fn [w] (into (:wrap fn-meta) (or w [])))))
default-opts {:helix/features {:fast-refresh false
:check-invalid-hooks-usage true
:metadata-optimizations true}}]
`(defnc* ~display-name
~@(when docstring [docstring])
~@(when fn-meta [fn-meta])
~params
~(merge default-opts opts-map)
~@body)))
;;;;;;;;;
;; example usage:
;; setup your project as described here:
;; https://github.com/metosin/malli/blob/master/docs/clojurescript-function-instrumentation.md
;; also assumes you have these schemas in your malli registry:
;; :react/element (m/-simple-schema {:type :react/element :pred react/isValidElement})
;; :js/object (m/-simple-schema {:type :js/object :pred object?})
(defnc navbar
{:helix/schema
[:map
[:current-route [:maybe :reitit/route]]
[:items [:vector [:map
[:href :string]
[:name :qualified-keyword]
[:label :string]]]]]}
[{:keys [items current-route]}]
(let [selected-route (when current-route (first (filter (comp #(= (-> current-route :data :name) %) :name) items)))]
(d/nav {:css (:container styles)}
($ ant.menu
{:mode "horizontal"
:selectedKeys (when selected-route #js[(str (:name selected-route))])
:items (->js
(for [item items :let [{:keys [href label]} item]]
{:key (str (:name item))
:label (d/a {:css (:link styles) :href href} label)}))}))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment