Skip to content

Commit a780f7d

Browse files
committed
Add defc macro to create fn components with Fast Refresh support
1 parent 94b2e5d commit a780f7d

File tree

7 files changed

+118
-21
lines changed

7 files changed

+118
-21
lines changed

.clj-kondo/config.edn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{:lint-as {reagent.core/with-let clojure.core/let
2+
reagent.core/defc clojure.core/defn
23
reagenttest.utils/deftest clojure.test/deftest
34
reagenttest.utils/with-render clojure.core/let}
45
:linters {:unused-binding {:level :off}

demo/reagentdemo/intro.cljs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,44 @@
66
[simpleexample.core :as simple]
77
[todomvc.core :as todo]))
88

9-
(defn simple-component []
9+
(r/defc simple-component []
1010
[:div
1111
[:p "I am a component!"]
1212
[:p.someclass
1313
"I have " [:strong "bold"]
1414
[:span {:style {:color "red"}} " and red "] "text."]])
1515

16-
(defn simple-parent []
16+
(r/defc simple-parent []
1717
[:div
1818
[:p "I include simple-component."]
1919
[simple-component]])
2020

21-
(defn hello-component [name]
21+
(r/defc hello-component [name]
2222
[:p "Hello, " name "!"])
2323

24-
(defn say-hello []
24+
(r/defc say-hello []
2525
[hello-component "world"])
2626

27-
(defn lister [items]
27+
(r/defc lister [items]
2828
[:ul
2929
(for [item items]
3030
^{:key item} [:li "Item " item])])
3131

32-
(defn lister-user []
32+
(r/defc lister-user []
3333
[:div
3434
"Here is a list:"
3535
[lister (range 3)]])
3636

3737
(def click-count (r/atom 0))
3838

39-
(defn counting-component []
39+
(r/defc counting-component []
4040
[:div
4141
"The atom " [:code "click-count"] " has value: "
4242
@click-count ". "
4343
[:input {:type "button" :value "Click me!"
4444
:on-click #(swap! click-count inc)}]])
4545

46-
(defn atom-input [value]
46+
(r/defc atom-input [value]
4747
[:input {:type "text"
4848
:value @value
4949
:on-change #(reset! value (-> % .-target .-value))}])
@@ -70,7 +70,7 @@
7070

7171
(def bmi-data (r/atom (calc-bmi {:height 180 :weight 80})))
7272

73-
(defn slider [param value min max invalidates]
73+
(r/defc slider [param value min max invalidates]
7474
[:input {:type "range" :value value :min min :max max
7575
:style {:width "100%"}
7676
:on-change (fn [e]
@@ -82,7 +82,7 @@
8282
(dissoc invalidates)
8383
calc-bmi)))))}])
8484

85-
(defn bmi-component []
85+
(r/defc bmi-component []
8686
(let [{:keys [weight height bmi]} @bmi-data
8787
[color diagnose] (cond
8888
(< bmi 18.5) ["orange" "underweight"]
@@ -109,7 +109,7 @@
109109
(def ns-src-with-rdom (s/syntaxed "(ns example
110110
(:require [reagent.dom :as rdom]))"))
111111

112-
(defn intro []
112+
(r/defc intro []
113113
(let [github {:href "https://github.com/reagent-project/reagent"}
114114
clojurescript {:href "https://github.com/clojure/clojurescript"}
115115
react {:href "https://reactjs.org/"}
@@ -171,7 +171,7 @@
171171
is a map). See React’s " [:a react-keys "documentation"] "
172172
for more info."]]))
173173

174-
(defn managing-state []
174+
(r/defc managing-state []
175175
[:div.demo-text
176176
[:h2 "Managing state in Reagent"]
177177

@@ -219,7 +219,7 @@
219219
component is updated when your data changes. Reagent assumes by
220220
default that two objects are equal if they are the same object."]])
221221

222-
(defn essential-api []
222+
(r/defc essential-api []
223223
[:div.demo-text
224224
[:h2 "Essential API"]
225225

@@ -235,7 +235,7 @@
235235
ns-src-with-rdom
236236
(s/src-of [:simple-component :render-simple])]}]])
237237

238-
(defn performance []
238+
(r/defc performance []
239239
[:div.demo-text
240240
[:h2 "Performance"]
241241

@@ -280,7 +280,7 @@
280280
into the browser, React automatically attaches event-handlers to
281281
the already present DOM tree."]])
282282

283-
(defn bmi-demo []
283+
(r/defc bmi-demo []
284284
[:div.demo-text
285285
[:h2 "Putting it all together"]
286286

@@ -296,7 +296,7 @@
296296
(s/src-of [:calc-bmi :bmi-data :slider
297297
:bmi-component])]}]])
298298

299-
(defn complete-simple-demo []
299+
(r/defc complete-simple-demo []
300300
[:div.demo-text
301301
[:h2 "Complete demo"]
302302

@@ -308,7 +308,7 @@
308308
:complete true
309309
:src (s/src-of nil "simpleexample/core.cljs")}]])
310310

311-
(defn todomvc-demo []
311+
(r/defc todomvc-demo []
312312
[:div.demo-text
313313
[:h2 "Todomvc"]
314314

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
{:lint-as {reagent.core/with-let clojure.core/let}}
1+
{:lint-as {reagent.core/with-let clojure.core/let
2+
reagent.core/defc clojure.core/defn}}

src/reagent/core.clj

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
(ns reagent.core
2-
(:require [reagent.ratom :as ra]))
2+
(:require [cljs.core :as core]
3+
[reagent.ratom :as ra]))
34

45
(defmacro with-let
56
"Bind variables as with let, except that when used in a component
@@ -24,3 +25,58 @@
2425
[& body]
2526
`(reagent.ratom/make-reaction
2627
(fn [] ~@body)))
28+
29+
(defn- parse-sig
30+
"Parse doc-string, attr-map, and other metadata from the defn like arguments list."
31+
[name fdecl]
32+
(let [;; doc-string
33+
[fdecl m] (if (string? (first fdecl))
34+
[(next fdecl) {:doc (first fdecl)}]
35+
[fdecl {}])
36+
;; attr-map
37+
[fdecl m] (if (map? (first fdecl))
38+
[(next fdecl) (conj m (first fdecl))]
39+
[fdecl m])
40+
;; If single arity, wrap in one item list for next step
41+
fdecl (if (vector? (first fdecl))
42+
(list fdecl)
43+
fdecl)
44+
;; If multi-arity, the last item could be an additional attr-map
45+
[fdecl m] (if (map? (last fdecl))
46+
[(butlast fdecl) (conj m (last fdecl))]
47+
[fdecl m])
48+
m (conj {:arglists (list 'quote (#'cljs.core/sigs fdecl))} m)
49+
;; Merge with the meta from the original sym
50+
m (conj (if (meta name) (meta name) {}) m)]
51+
[(with-meta name m) fdecl]))
52+
53+
(defmacro defc
54+
"Create a Reagent function component
55+
56+
The functions works like other components (defined using regular defn) when
57+
used inside hiccup elements (`[component]`), but it can't be used like a regular
58+
function. The created function is a React JS function component, i.e., it
59+
takes single js-props argument, and the function body is already wrapped to
60+
use Reagent implementation to work with Ratoms etc."
61+
{:arglists '([name doc-string? attr-map? [params*] prepost-map? body]
62+
[name doc-string? attr-map? ([params*] prepost-map? body) + attr-map?])}
63+
[sym & fdecl]
64+
(let [[fname fdecl] (parse-sig sym fdecl)]
65+
;; Consider if :arglists should be replaced with [jsprops] or if that should be
66+
;; included as one item?
67+
`(do
68+
(def ~fname (reagent.impl.component/memo
69+
(fn ~sym [jsprops#]
70+
(let [;; It is important that this fn is using the original name, so
71+
;; multi-arity definitions can call the other arities.
72+
render-fn# (fn ~sym ~@fdecl)
73+
jsprops2# (js/Object.assign (core/js-obj "reagentRender" render-fn#) jsprops#)]
74+
(reagent.impl.component/functional-render reagent.impl.template/*current-default-compiler* jsprops2#)))))
75+
(set! (.-reagent-component ~fname) true)
76+
(set! (.-displayName ~fname) ~(str sym))
77+
(js/Object.defineProperty ~fname "name" (core/js-obj "value" ~(str sym) "writable" false)))))
78+
79+
(comment
80+
(clojure.pprint/pprint (macroexpand-1 '(defc foobar [a b] (+ a b))))
81+
(clojure.pprint/pprint (macroexpand-1 '(defc foobar "docstring" ([a] (foobar a nil)) ([a b] (+ a b)))))
82+
(clojure.pprint/pprint (clojure.walk/macroexpand-all '(defc foobar [a b] (+ a b)))))

src/reagent/impl/component.cljs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,3 +477,8 @@
477477
f (react/memo f functional-render-memo-fn)]
478478
(cache-react-class compiler tag f)
479479
f)))
480+
481+
;; defc impl
482+
483+
(defn memo [f]
484+
(react/memo f functional-render-memo-fn))

src/reagent/impl/template.cljs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@
2828
(or (named? x)
2929
(string? x)))
3030

31-
(defn ^boolean valid-tag? [x]
31+
(defn ^boolean valid-tag? [^clj x]
3232
(or (hiccup-tag? x)
33+
(.-reagent-component x)
3334
(ifn? x)
3435
(instance? NativeWrapper x)))
3536

@@ -170,6 +171,13 @@
170171
(set! (.-key jsprops) key))
171172
(react/createElement c jsprops)))
172173

174+
(defn reag-element-2 [tag v]
175+
(let [jsprops #js {}]
176+
(set! (.-argv jsprops) (subvec v 1))
177+
(when-some [key (util/react-key-from-vec v)]
178+
(set! (.-key jsprops) key))
179+
(react/createElement tag jsprops)))
180+
173181
(defn function-element [tag v first-arg compiler]
174182
(let [jsprops #js {}]
175183
(set! (.-reagentRender jsprops) tag)
@@ -284,14 +292,17 @@
284292
(when (nil? compiler)
285293
(js/console.error "vec-to-elem" (pr-str v)))
286294
(assert (pos? (count v)) (util/hiccup-err v (comp/comp-name) "Hiccup form should not be empty"))
287-
(let [tag (nth v 0 nil)]
295+
(let [^clj tag (nth v 0 nil)]
288296
(assert (valid-tag? tag) (util/hiccup-err v (comp/comp-name) "Invalid Hiccup form"))
289297
(case tag
290298
:> (native-element (->HiccupTag (nth v 1 nil) nil nil nil) v 2 compiler)
291299
:r> (raw-element (nth v 1 nil) v compiler)
292300
:f> (function-element (nth v 1 nil) v 2 compiler)
293301
:<> (fragment-element v compiler)
294302
(cond
303+
(.-reagent-component tag)
304+
(reag-element-2 tag v)
305+
295306
(hiccup-tag? tag)
296307
(hiccup-element v compiler)
297308

test/reagenttest/testreagent.cljs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,9 @@
13751375
(u/act (reset! val 0))
13761376
(is (= 3 @render))))))
13771377

1378+
(r/defc test-1 [x]
1379+
[:span "Hello " x])
1380+
13781381
(deftest ^:dom functional-component-poc-simple
13791382
(let [c (fn [x]
13801383
[:span "Hello " x])]
@@ -1384,6 +1387,11 @@
13841387
{:compiler u/class-compiler}
13851388
(is (= "Hello foo" (.-innerText div)))))
13861389

1390+
(testing "defc"
1391+
(u/with-render [div [test-1 "foo"]]
1392+
{:compiler u/class-compiler}
1393+
(is (= "Hello foo" (.-innerText div)))))
1394+
13871395
(testing "compiler options"
13881396
(u/with-render [div [c "foo"]]
13891397
{:compiler u/fn-compiler}
@@ -1443,6 +1451,21 @@
14431451
(u/act (@set-count! 17))
14441452
(is (= "Counts 6 17" (.-innerText div)))))))
14451453

1454+
(r/defc test-2
1455+
"doc-1"
1456+
([a] (test-2 a "x"))
1457+
([a b]
1458+
(let [[v set-v] (react/useState 1)]
1459+
[:div "Hello " a " " b " " v])))
1460+
1461+
(deftest ^:dom defc-component
1462+
(is (= "doc-1" (:doc (meta #'test-2))))
1463+
1464+
(u/async
1465+
(u/with-render [div [test-2 "foo"]]
1466+
{:compiler u/class-compiler}
1467+
(is (= "Hello foo x 1" (.-innerText div))))))
1468+
14461469
(u/deftest ^:dom test-input-el-ref
14471470
(let [ref-1 (atom nil)
14481471
ref-1-fn #(reset! ref-1 %)

0 commit comments

Comments
 (0)