/
core.clj
412 lines (333 loc) · 13.5 KB
/
core.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
(ns helix.core
(:require [helix.impl.analyzer :as hana]
[helix.impl.props :as impl.props]
[clojure.string :as string]))
(defmacro $
"Create a new React element from a valid React type.
Will try to statically convert props to a JS object.
To pass in dynamic props, use the special `&` or `:&` key in the props map
to have the map merged in.
Simple example:
($ my-component
\"child1\"
($ \"span\"
{:style {:color \"green\"}}
\"child2\" ))
Dynamic exmaple:
(let [dynamic-props {:foo \"bar\"}]
($ my-component
{:static \"prop\"
& dynamic-props}))
"
[type & args]
(when (and (symbol? (first args))
(= (hana/inferred-type &env (first args))
'cljs.core/IMap))
(hana/warn hana/warning-inferred-map-props
&env
{:form (cons type args)
:props-form (first args)}))
(let [inferred (hana/inferred-type &env type)
native? (or (keyword? type)
(string? type)
(= inferred 'string)
(= inferred 'cljs.core/Keyword)
(:native (meta type)))
type (if (keyword? type)
(name type)
type)]
(cond
(map? (first args))
`^js/React.Element (.createElement
(get-react)
~type
~(if native?
`(impl.props/dom-props ~(first args))
`(impl.props/props ~(first args)))
~@(rest args))
:else `^js/React.Element (.createElement (get-react) ~type nil ~@args))))
(defmacro <>
"Creates a new React Fragment Element"
[& children]
`^js/React.Element ($ Fragment ~@children))
(defmacro provider
"Creates a Provider for a React Context value.
Example:
(def my-context (react/createContext))
(provider {:context my-context :value my-value} child1 child2 ...childN)"
[{:keys [context value] :as props} & children]
`^js/React.Element ($ (.-Provider ~context)
;; use contains to guard against `nil`
~@(when (contains? props :value)
`({:value ~value}))
~@children))
(defmacro suspense
"Creates a React Suspense boundary."
[{:keys [fallback]} & children]
`^js/React.Element ($ Suspense
~@(when fallback
`({:fallback ~fallback}))
~@children))
(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 [(extract-cljs-props props#) maybe-ref#]]
~@body)))
(def meta->form
{:memo (fn [form deps]
`(helix.hooks/use-memo
~(if (coll? deps)
deps
:auto-deps)
~form))
:callback (fn [form deps]
`(helix.hooks/use-callback
~(if (coll? deps)
deps
:auto-deps)
~form))})
(defmacro fnc
"Creates a new anonymous function React component. Used like:
(fnc ?optional-component-name
[props ?forwarded-ref]
{,,,opts-map}
,,,body)
Returns a function that can be used just like a component defined with
`defnc`, i.e. accepts a JS object as props and the body receives them as a
map, can be used with `$` macro, forwardRef, etc.
`opts-map` is optional and can be used to pass some configuration options.
Current options:
- ':wrap' - ordered sequence of higher-order components to wrap the component in
- ':helix/features' - a map of feature flags to enable.
Some feature flags only pertain to named components, i.e. Fast Refresh and
factory functions, and thus can not be used with `fnc`."
[& body]
(let [[display-name props-bindings body] (if (symbol? (first body))
[(first body) (second body)
(rest (rest body))]
[nil (first body) (rest body)])
opts-map? (map? (first body))
opts (if opts-map?
(first body)
{})
feature-flags (:helix/features opts)
;; feature flags
flag-check-invalid-hooks-usage? (:check-invalid-hooks-usage feature-flags true)
flag-metadata-optimizations (:metadata-optimizations feature-flags)
body (cond-> body
opts-map? (rest)
flag-metadata-optimizations (hana/map-forms-with-meta meta->form))
hooks (hana/find-hooks body)]
(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))))
`(-> ~(fnc* nil props-bindings
body)
~@(-> opts :wrap))))
(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.
'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)
{})
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 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 (signature!))))
(def ~(vary-meta
component-var-name
merge
{:helix/component? true}
fn-meta)
~@(when-not (nil? docstring)
(list docstring))
(-> ~(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
(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
(register! ~component-var-name ~fully-qualified-name)))
~display-name)))
(defmacro defnc-
"Same as defnc, yielding a non-public def"
[display-name & rest]
(list* `defnc (vary-meta display-name assoc :private true) rest))
;;
;; Custom hooks
;;
(defmacro defhook
"Defines a new custom hook function.
Checks for invalid usage of other hooks in the body, and other helix
features."
[sym & body]
(let [[docstring params body] (if (string? (first body))
[(first body) (second body) (drop 2 body)]
[nil (first body) (rest body)])
[opts body] (if (map? (first body))
[(first body) (rest body)]
[nil body])
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)]
(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))))
(when-not (string/starts-with? (str sym) "use-")
(hana/warn hana/warning-invalid-hook-name &env {:form &form}))
`(defn ~(vary-meta sym merge {:helix/hook? true})
;; use ~@ here so that we don't emit `nil`
~@(when-not (nil? docstring) (list docstring))
~params
~@body)))
;;
;; Class components
;;
(defn- static? [form]
(boolean (:static (meta form))))
(defn- method? [form]
(and (list? form)
(simple-symbol? (first form))
(vector? (second form))))
(defn- ->method [[sym-name bindings & form]]
{:assert [(simple-symbol? sym-name)]}
(list (str sym-name)
`(fn ~sym-name ~bindings
~@form)))
(defn- ->value [[sym-name value]]
{:assert [(simple-symbol? sym-name)]}
(list (str sym-name) value))
(defmacro defcomponent
"Defines a React class component.
Like `class display-name extends React.Component { ... }` in JS.
Methods are defined using (method-name [this ,,,] ,,,) syntax.
Properties elide the arguments vector (property-name expr)
Static properties and methods can be added by annotating the method or
property with metadata containing the :static keyword.
Some assumptions:
- To use setState, you must store the state as a JS obj
- The render method receives three arguments: this, a CLJS map of props,
and the state object.
- displayName by default is the symbol passed in, but can be customized
by manually adding it as a static property
Example:
(defcomponent foo
(constructor
[this]
(set! (.-state this) #js {:counter 0})))"
{:style/indent [1 :form [1]]}
[display-name & spec]
{:assert [(simple-symbol? display-name)
(seq (filter #(= 'render (first %)) spec))]}
(let [[docstring spec] (if (string? (first spec))
[(first spec) (rest spec)]
[nil spec])
{statics true spec false} (group-by static? spec)
js-spec `(cljs.core/js-obj ~@(->> spec
(map ->method)
(apply concat)))
js-statics `(cljs.core/js-obj
~@(->> statics
(map #(if (method? %)
(->method %)
(->value %)))
(apply concat
(list "displayName"
;; fully qualified name
(str *ns* "/" display-name)))))]
;; TODO handle render specially
`(def ~display-name
~@(when docstring [docstring])
(create-component ~js-spec ~js-statics))))
(comment
(macroexpand
'(defcomponent asdf
(foo [] "bar")
^:static (greeting "asdf")
(bar [this] asdf)
^:static (baz [] 123)))
;; => (helix.core/create-component
;; (cljs.core/js-obj
;; "foo"
;; (clojure.core/fn foo [] "bar")
;; "bar"
;; (clojure.core/fn bar [this] asdf))
;; (cljs.core/js-obj "greeting" "asdf" "baz" (clojure.core/fn baz [] 123)))
)