Last active
May 24, 2023 12:21
-
-
Save dvingo/97bae8b33c08b257153946bb82f38a86 to your computer and use it in GitHub Desktop.
helix defnc with malli instrumentation support
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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