/
components.cljc
1556 lines (1362 loc) · 77.8 KB
/
components.cljc
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
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
(ns com.fulcrologic.fulcro.components
#?(:cljs (:require-macros com.fulcrologic.fulcro.components))
(:require
#?@(:clj
[[cljs.analyzer :as ana]
[cljs.env :as cljs-env]]
:cljs
[[goog.object :as gobj]
["react" :as react]])
[edn-query-language.core :as eql]
[clojure.spec.alpha :as s]
[taoensso.timbre :as log]
[clojure.walk :refer [prewalk]]
[clojure.string :as str]
[com.fulcrologic.fulcro.algorithms.do-not-use :as util]
[com.fulcrologic.fulcro.algorithms.denormalize :as fdn]
[com.fulcrologic.fulcro.algorithms.lookup :as ah]
[com.fulcrologic.fulcro.raw.components :as rc]
[com.fulcrologic.guardrails.core :refer [>def]]
[clojure.set :as set])
#?(:clj
(:import
[clojure.lang Associative IDeref APersistentMap])))
#?(:clj
(defn current-config []
(let [config (some-> cljs-env/*compiler* deref (get-in [:options :external-config :fulcro]))]
config)))
;; Bound during Fulcro-driven renders to communicate critical information to components *on their initial render*.
;; Due to the nature of js and React there is no guarantee that future `render` (or lifecycle calls) will actually be done synchronously,
;; so these are *copied* into the raw react props of the component for future reference (a mounted component won't change
;; depth, will know its parent, and the app is a immutable map with atoms). You must ensure these are bound using
;; `with-parent-context` if you cause an initial mount of a component via things like the child-as-a-function, or HOC patterns.
;; If a raw js library wants a Fulcro component (class), then you may need to use the multiple-roots renderer so that
;; it can register on mount with Fulcro.
(def ^:dynamic *app* nil)
(def ^:dynamic *parent* nil)
(def ^:dynamic *depth* nil)
(def ^:dynamic *shared* nil)
;; Used by default shouldComponentUpdate. If set to `true`, then SCU will return true. This is used by hot code reload
;; to know when it should re-render even if props have not changed so you can see the effects of rendering code changes.
;; Also used when you force a root render.
(def ^:dynamic *blindly-render* false)
(def isoget-in
"
[obj kvs]
[obj kvs default]
Like get-in, but for js objects, and in CLJC. In clj, it is just get-in. In cljs it is
gobj/getValueByKeys."
rc/isoget-in)
(def isoget
"
[obj k]
[obj k default]
Like get, but for js objects, and in CLJC. In clj, it is just `get`. In cljs it is
`gobj/get`."
rc/isoget)
(def register-component!
"
[k component-class]
Add a component to Fulcro's component registry. This is used by defsc to ensure that all Fulcro classes
that have been compiled (transitively required) will be accessible for lookup by fully-qualified symbol/keyword.
Not meant for public use, unless you're creating your own component macro that doesn't directly leverage defsc."
rc/register-component!)
(defn force-children
"Utility function that will force a lazy sequence of children (recursively) into realized
vectors (React cannot deal with lazy seqs in production mode)"
[x]
(cond->> x
(seq? x) (into [] (map force-children))))
(def newer-props
"
[props-a props-b]
Returns whichever of the given Fulcro props were most recently generated according to `denormalization-time`. This
is part of props 'tunnelling', an optimization to get updated props to instances without going through the root."
rc/newer-props)
(defn component-instance?
"[x]
Returns true if the argument is a component. A component is defined as a *mounted component*.
This function returns false for component classes, and also returns false for the output of a Fulcro component factory."
#?(:cljs {:tag boolean})
[x]
(rc/component-instance? x))
(def component?
"[x]
Returns true if the argument is a component instance.
DEPRECATED for terminology clarity. Use `component-instance?` instead."
component-instance?)
(defn component-class?
"Returns true if the argument is a component class."
#?(:cljs {:tag boolean})
[x]
(rc/component-class? x))
(>def ::component-class component-class?)
(def component-name
"[class]
Returns a string version of the given react component's name. Works on component instances and classes."
rc/component-name)
(def class->registry-key
"[class]
Returns the registry key for the given component class."
rc/class->registry-key)
(def registry-key->class
"[classname]
Look up the given component in Fulcro's global component registry. Will only be able to find components that have
been (transitively) required by your application.
`classname` can be a fully-qualified keyword or symbol."
rc/registry-key->class)
(def computed
"
[props computed-map]
Add computed properties to props. This will *replace* any pre-existing computed properties. Computed props are
necessary when a parent component wishes to pass callbacks or other data to children that *have a query*. This
is not necessary for \"stateless\" components, though it will work properly for both.
Computed props are \"remembered\" so that a targeted update (which can only happen on a component with a query
and ident) can use new props from the database without \"losing\" the computed props that were originally passed
from the parent. If you pass things like callbacks through normal props, then targeted updates will seem to \"lose
track of\" them.
"
rc/computed)
(def get-computed
"[this-or-props]
[this-or-props k-or-ks]
Return the computed properties on a component or its props. Note that it requires that the normal properties are not nil."
rc/get-computed)
(defn get-extra-props
"Get any data (as a map) that props extensions have associated with the given Fulcro component. Extra props will
be empty unless you've installed props-middleware (on your app) that sets them."
[this]
(isoget-in this [:props :fulcro$extra_props] {}))
(def props
"[this]
Return a component's props."
rc/props)
(defn children
"[this]
Get the sequence of react children of the given component."
[component]
(let [cs #?(:clj (get-in component [:children])
:cljs (gobj/getValueByKeys component "props" "children"))]
(if (or (coll? cs) #?(:cljs (array? cs))) cs [cs])))
(defn react-type
"Returns the component type, regardless of whether the component has been
mounted"
[x]
(rc/component-type x))
(def get-class
"[instance]
Returns the react type (component class) of the given React element (instance). Is identity if used on a class."
rc/get-class)
(def component-options
"[component & ks]
Returns the map of options that was specified (via `defsc`) for the component class."
rc/component-options)
(defn has-feature?
"Returns true if the component has `option-key` declared in the component options map."
#?(:cljs {:tag boolean}) [component option-key]
(contains? (component-options component) option-key))
(defn has-initial-app-state?
"Returns true if the component has initial app state."
#?(:cljs {:tag boolean}) [component]
(has-feature? component :initial-state))
(defn has-ident?
"Returns true if the component has an ident"
#?(:cljs {:tag boolean}) [component] (has-feature? component :ident))
(defn has-query?
"Returns true if the component has a query"
#?(:cljs {:tag boolean}) [component] (has-feature? component :query))
(defn has-pre-merge?
"Returns true if the component has a pre-merge"
#?(:cljs {:tag boolean}) [component] (has-feature? component :pre-merge))
(defn ident
"Returns the ident that would be generated by the given component instance or class IF it was supplied props"
[this props] (when (has-feature? this :ident) ((component-options this :ident) this props)))
(defn query
"Returns the STATIC query of the fgiven component"
[this] (when (has-feature? this :query) ((component-options this :query) this)))
(defn initial-state
"Returns the initial state of component clz if it was passed the given params"
[clz params] (when (has-feature? clz :initial-state) ((component-options clz :initial-state) params)))
(defn pre-merge [this data] (when (has-feature? this :pre-merge) ((component-options this :pre-merge) data)))
(defn depth [this] (isoget-in this [:props :fulcro$depth]))
(defn get-raw-react-prop
"GET a RAW react prop. Used internally. Safe in CLJC, but equivalent to `(gobj/getValueByKeys this \"props\" (name k)`."
[c k]
(isoget-in c [:props k]))
(defn any->app
"Attempt to coerce `x` to an app. Legal inputs are a fulcro application, a mounted component,
or an atom holding any of the above."
[x]
(rc/any->app x))
(defn raw->newest-props
"Using raw react props/state returns the newest Fulcro props. This is part of \"props tunneling\", where component
local state is leveraged as a communication mechanism of updated props directly to a component that has an ident.
This function will return the correct version of props based on timestamps."
[raw-props raw-state]
#?(:clj raw-props
:cljs (let [next-props (gobj/get raw-props "fulcro$value")
opt-props (gobj/get raw-state "fulcro$value")]
(newer-props next-props opt-props))))
(defn shared
"Return the global shared properties of the root. See :shared and
:shared-fn app options. NOTE: Shared props only update on root render and by explicit calls to
`app/update-shared!`.
This function attempts to rely on the dynamic var *shared* (first), but will make a best-effort of
finding shared props when run within a component's render or lifecycle. Passing your app will
ensure this returns the current shared props."
([] *shared*)
([comp-or-app]
(shared comp-or-app []))
([comp-or-app k-or-ks]
(let [shared (or *shared* (some-> (any->app comp-or-app) :com.fulcrologic.fulcro.application/runtime-atom deref :com.fulcrologic.fulcro.application/shared-props))
ks (cond-> k-or-ks
(not (sequential? k-or-ks)) vector)]
(cond-> shared
(not (empty? ks)) (get-in ks)))))
(letfn
[(wrap-props-state-handler
([handler]
(wrap-props-state-handler handler true))
([handler check-for-fresh-props-in-state?]
#?(:clj (fn [& args] (apply handler args))
:cljs
(fn [raw-props raw-state]
(this-as this
(let [props (if check-for-fresh-props-in-state?
(raw->newest-props raw-props raw-state)
(gobj/get raw-props "fulcro$props"))
state (gobj/get raw-state "fulcro$state")]
(handler this props state)))))))
(static-wrap-props-state-handler
[handler]
#?(:clj (fn [& args] (apply handler args))
:cljs
(fn [raw-props raw-state]
(let [props (raw->newest-props raw-props raw-state)
state (gobj/get raw-state "fulcro$state")]
(handler props state)))))
(should-component-update?
[raw-next-props raw-next-state]
#?(:clj true
:cljs (if *blindly-render*
true
(this-as this
(let [current-props (props this)
next-props (raw->newest-props raw-next-props raw-next-state)
next-state (gobj/get raw-next-state "fulcro$state")
current-state (gobj/getValueByKeys this "state" "fulcro$state")
props-changed? (not= current-props next-props)
state-changed? (not= current-state next-state)
next-children (gobj/get raw-next-props "children")
children-changed? (not= (gobj/getValueByKeys this "props" "children") next-children)]
(or props-changed? state-changed? children-changed?))))))
(component-did-update
[raw-prev-props raw-prev-state snapshot]
#?(:cljs
(this-as this
(let [{:keys [ident componentDidUpdate]} (component-options this)
prev-state (gobj/get raw-prev-state "fulcro$state")
prev-props (raw->newest-props raw-prev-props raw-prev-state)]
(when componentDidUpdate
(componentDidUpdate this prev-props prev-state snapshot))
(when ident
(let [old-ident (ident this prev-props)
next-ident (ident this (props this))
app (any->app this)
drop-component! (ah/app-algorithm app :drop-component!)
index-component! (ah/app-algorithm app :index-component!)]
(when (not= old-ident next-ident)
(drop-component! this old-ident)
(index-component! this))))))))
(component-did-mount
[]
#?(:cljs
(this-as this
(gobj/set this "fulcro$mounted" true)
(let [{:keys [componentDidMount]} (component-options this)
app (any->app this)
index-component! (ah/app-algorithm app :index-component!)]
(index-component! this)
(when componentDidMount
(componentDidMount this))))))
(component-will-unmount []
#?(:cljs
(this-as this
(let [{:keys [componentWillUnmount]} (component-options this)
app (any->app this)
drop-component! (ah/app-algorithm app :drop-component!)]
(when componentWillUnmount
(componentWillUnmount this))
(gobj/set this "fulcro$mounted" false)
(drop-component! this)))))
(wrap-this
[handler]
#?(:clj (fn [& args] (apply handler args))
:cljs
(fn [& args] (this-as this (apply handler this args)))))
(wrap-props-handler
([handler]
(wrap-props-handler handler true))
([handler check-for-fresh-props-in-state?]
#?(:clj #(handler %1)
:cljs
(fn [raw-props]
(this-as this
(let [raw-state (.-state this)
props (if check-for-fresh-props-in-state?
(raw->newest-props raw-props raw-state)
(gobj/get raw-props "fulcro$props"))]
(handler this props)))))))
(wrap-base-render [render]
#?(:clj (fn [& args]
(binding [*parent* (first args)]
(apply render args)))
:cljs
(fn [& args]
(this-as this
(if-let [app (any->app this)]
(binding [*app* app
*depth* (inc (depth this))
*shared* (shared this)
*parent* this]
(apply render this args))
(log/fatal "Cannot find app on component!"))))))]
(defn configure-component!
"Configure the given `cls` (a function) to act as a react component within the Fulcro ecosystem.
cls - A js function (in clj, this is ignored)
fqkw - A keyword that shares the exact fully-qualified name of the component class
options - A component options map (no magic) containing things like `:query` and `:ident`.
NOTE: the `options` map expects proper function signatures for:
`:query` - (fn [this] ...)
`:ident` - (fn [this props] ...)
`:initial-state` - (fn [cls params] ...)
Returns (and registers) a new react class.
"
[cls fqkw options]
#?(:clj
(let [name (str/join "/" [(namespace fqkw) (name fqkw)])
{:keys [render]} options
result {::component-class? true
:fulcro$options (assoc options :render (wrap-base-render render))
:fulcro$registryKey fqkw
:displayName name}]
(register-component! fqkw result)
result)
:cljs
;; This user-supplied versions will expect `this` as first arg
(let [{:keys [getDerivedStateFromProps shouldComponentUpdate getSnapshotBeforeUpdate render
initLocalState componentDidCatch getDerivedStateFromError
componentWillUpdate componentWillMount componentWillReceiveProps
UNSAFE_componentWillMount UNSAFE_componentWillUpdate UNSAFE_componentWillReceiveProps]} options
name (str/join "/" [(namespace fqkw) (name fqkw)])
js-instance-props (clj->js
(-> {:componentDidMount component-did-mount
:componentWillUnmount component-will-unmount
:componentDidUpdate component-did-update
:shouldComponentUpdate (if shouldComponentUpdate
(wrap-props-state-handler shouldComponentUpdate)
should-component-update?)
:fulcro$isComponent true
:type cls
:displayName name}
(cond->
render (assoc :render (wrap-base-render render))
getSnapshotBeforeUpdate (assoc :getSnapshotBeforeUpdate (wrap-props-state-handler getSnapshotBeforeUpdate))
componentDidCatch (assoc :componentDidCatch (wrap-this componentDidCatch))
UNSAFE_componentWillMount (assoc :UNSAFE_componentWillMount (wrap-this UNSAFE_componentWillMount))
UNSAFE_componentWillUpdate (assoc :UNSAFE_componentWillUpdate (wrap-props-state-handler UNSAFE_componentWillUpdate))
UNSAFE_componentWillReceiveProps (assoc :UNSAFE_componentWillReceiveProps (wrap-props-handler UNSAFE_componentWillReceiveProps))
componentWillMount (assoc :componentWillMount (wrap-this componentWillMount))
componentWillUpdate (assoc :componentWillUpdate (wrap-this componentWillUpdate))
componentWillReceiveProps (assoc :componentWillReceiveProps (wrap-props-handler componentWillReceiveProps))
initLocalState (assoc :initLocalState (wrap-this initLocalState)))))
statics (cond-> {:displayName name
:fulcro$class cls
:cljs$lang$type true
:cljs$lang$ctorStr name
:cljs$lang$ctorPrWriter (fn [_ writer _] (cljs.core/-write writer name))}
getDerivedStateFromError (assoc :getDerivedStateFromError (fn [error]
(let [v (getDerivedStateFromError error)]
(if (coll? v)
#js {"fulcro$state" v}
v))))
getDerivedStateFromProps (assoc :getDerivedStateFromProps (static-wrap-props-state-handler getDerivedStateFromProps)))]
(gobj/extend (.-prototype cls) (.-prototype react/Component) js-instance-props
#js {"fulcro$options" options})
(gobj/extend cls (clj->js statics) #js {"fulcro$options" options})
(gobj/set cls "fulcro$registryKey" fqkw) ; done here instead of in extend (clj->js screws it up)
(register-component! fqkw cls)))))
(defn add-hook-options!
"Make a given `cls` (a plain fn) act like a a Fulcro component with the given component options map. Registers the
new component in the component-registry. Component options MUST contain :componentName as be a fully-qualified
keyword to name the component in the registry.
component-options *must* include a unique `:componentName` (keyword) that will be used for registering the given
function as the faux class in the component registry."
[render-fn component-options]
(rc/configure-anonymous-component! render-fn component-options))
(defn use-fulcro
"Allows you to use a plain function as a Fulcro-managed React hooks component.
* `js-props` - The React js props from the parent.
* `faux-class` - A Fulcro faux class, which is a fn that has had `add-options!` called on it.
Returns a cljs vector containing `this` and fulcro `props`. You should *not* use the returned `this` directly,
as it is a placeholder.
Prefer `defsc` or `configure-hooks-component! over using this directly.`
"
[js-props faux-class]
#?(:cljs
(let [app (isoget js-props :fulcro$app)
tunnelled-props-state (react/useState #js {})
js-set-tunnelled-props! (aget tunnelled-props-state 1)
{:keys [ident] :as options} (isoget faux-class :fulcro$options)
faux-component-state (react/useState (fn []
(when-not app
(log/error "Cannot create proper fulcro component, as *app* isn't bound."
"This happens when something renders a Fulcro component outside of Fulcro's render context."
"See `with-parent-context`."
"See https://book.fulcrologic.com/#err-comp-app-not-bound"))
(let [depth (or *depth* (isoget js-props :fulcro$depth))
set-tunnelled-props! (fn [updater] (let [new-props (updater nil)] (js-set-tunnelled-props! new-props)))]
#js {:setState set-tunnelled-props!
:fulcro$isComponent true
:fulcro$class faux-class
:type faux-class
:fulcro$options options
:fulcro$mounted false
:props #js {:fulcro$app app
:fulcro$depth (inc depth)}})))
faux-component (aget faux-component-state 0)
current-state (aget tunnelled-props-state 0 "fulcro$value")
props (isoget js-props :fulcro$value)
children (isoget js-props :children)
current-props (newer-props props current-state)
current-ident (when ident (ident faux-class current-props))
shared-props (when app (shared app))]
(doto (gobj/get faux-component "props")
(gobj/set "fulcro$shared" shared-props)
(gobj/set "fulcro$value" current-props)
(gobj/set "children" children))
(react/useEffect
(fn []
(let [original-ident current-ident
index-component! (ah/app-algorithm app :index-component!)
drop-component! (ah/app-algorithm app :drop-component!)]
(gobj/set faux-component "fulcro$mounted" true)
(index-component! faux-component)
(fn []
(gobj/set faux-component "fulcro$mounted" false)
(drop-component! faux-component original-ident))))
#?(:cljs #js [(second current-ident)]))
[faux-component current-props])))
(defn mounted?
"Returns true if the given component instance is mounted on the DOM."
[this]
#?(:clj false
:cljs (gobj/get this "fulcro$mounted" false)))
(defn set-state!
"Set React component-local state. The `new-state` is actually merged with the existing state (as per React docs),
but is wrapped so that cljs maps are used (instead of js objs). `callback` is an optional callback that will be
called as per the React docs on setState."
([component new-state callback]
#?(:clj
(when-let [state-atom (:state component)]
(swap! state-atom update merge new-state)
(callback))
:cljs
(if (mounted? component)
(.setState ^js component
(fn [prev-state props]
#js {"fulcro$state" (merge (gobj/get prev-state "fulcro$state") new-state)})
callback))))
([component new-state]
(set-state! component new-state nil)))
(defn get-state
"Get a component's local state. May provide a single key or a sequential
collection of keys for indexed access into the component's local state. NOTE: This is Fulcro's wrapped component
local state. The low-level React state is as described in the React docs (e.g. `(.-state this)`)."
([component]
(get-state component []))
([component k-or-ks]
(let [cst #?(:clj (some-> component :state deref)
:cljs (gobj/getValueByKeys component "state" "fulcro$state"))]
(get-in cst (if (sequential? k-or-ks) k-or-ks [k-or-ks])))))
(let [update-fn (fn [component f args]
#?(:cljs (.setState ^js component
(fn [prev-state props]
#js {"fulcro$state" (apply f (gobj/get prev-state "fulcro$state") args)}))))]
(defn update-state!
"Update a component's local state. Similar to Clojure(Script)'s swap!
This function affects a managed cljs map maintained in React state. If you want to affect the low-level
js state itself use React's own `.setState` directly on the component."
([component f]
(update-fn component f []))
([component f & args]
(update-fn component f args))))
(def get-initial-state
"
[cls] [cls params]
Get the declared :initial-state value for a component."
rc/get-initial-state)
(defn computed-initial-state?
"Returns true if the given initial state was returned from a call to get-initial-state. This is used by internal
algorithms when interpreting initial state shorthand in `defsc`."
[s]
(and (map? s) (some-> s meta :computed)))
(def get-ident
"
[x] [class props]
Get the ident for a mounted component OR using a component class.
That arity-2 will return the ident using the supplied props map.
The single-arity version should only be used with a mounted component (e.g. `this` from `render`), and will derive the
props that were sent to it most recently."
rc/get-ident)
(defn tunnel-props!
"CLJS-only. When the `component` is mounted this will tunnel `new-props` to that component through React `setState`. If you're in
an event handler, this means the tunnelling will be synchronous, and can be useful when updating props that could affect DOM
inputs. This is typically used internally (see `transact!!`, and should generally not be used in applications unless it is a very advanced
scenario and you've studied how this works. NOTE: You should `tick!` the application clock and bind *denormalize-time*
when generating `new-props` so they are properly time-stamped by `db->tree`, or manually add time to `new-props`
using `fdn/with-time` directly."
[component new-props]
#?(:cljs
(when (mounted? component)
(.setState ^js component (fn [s] #js {"fulcro$value" new-props})))))
(defn is-factory?
"Returns true if the given argument is a component factory."
[class-or-factory]
(rc/is-factory? class-or-factory))
(def query-id
"[class qualifier]
Returns a string ID for the query of the given class with qualifier."
rc/query-id)
(def denormalize-query rc/denormalize-query)
(def get-query-by-id rc/get-query-by-id)
(defn get-query
"Get the query for the given class or factory. If called without a state map, then you'll get the declared static
query of the class. If a state map is supplied, then the dynamically set queries in that state will result in
the current dynamically-set query according to that state."
([class-or-factory]
(rc/get-query class-or-factory (or rc/*query-state*
(some-> *app* :com.fulcrologic.fulcro.application/state-atom deref) {})))
([class-or-factory state-map]
(rc/get-query class-or-factory state-map)))
(defn make-state-map
"Build a component's initial state using the defsc initial-state-data from
options, the children from options, and the params from the invocation of get-initial-state."
[initial-state children-by-query-key params]
(let [join-keys (set (keys children-by-query-key))
init-keys (set (keys initial-state))
is-child? (fn [k] (contains? join-keys k))
value-of (fn value-of* [[isk isv]]
(let [param-name (fn [v] (and (keyword? v) (= "param" (namespace v)) (keyword (name v))))
substitute (fn [ele] (if-let [k (param-name ele)]
(get params k)
ele))
param-key (param-name isv)
param-exists? (contains? params param-key)
param-value (get params param-key)
child-class (get children-by-query-key isk)]
(cond
; parameterized lookup with no value
(and param-key (not param-exists?)) nil
; to-one join, where initial state is a map to be used as child initial state *parameters* (enforced by defsc macro)
; and which may *contain* parameters
(and (map? isv) (is-child? isk)) [isk (get-initial-state child-class (into {} (keep value-of* isv)))]
; not a join. Map is literal initial value.
(map? isv) [isk (into {} (keep value-of* isv))]
; to-many join. elements MUST be parameters (enforced by defsc macro)
(and (vector? isv) (is-child? isk)) [isk (mapv (fn [m] (get-initial-state child-class (into {} (keep value-of* m)))) isv)]
; to-many join. elements might be parameter maps or already-obtained initial-state
(and (vector? param-value) (is-child? isk)) [isk (mapv (fn [params]
(if (computed-initial-state? params)
params
(get-initial-state child-class params))) param-value)]
; vector of non-children
(vector? isv) [isk (mapv (fn [ele] (substitute ele)) isv)]
; to-one join with parameter. value might be params, or an already-obtained initial-state
(and param-key (is-child? isk) param-exists?) [isk (if (computed-initial-state? param-value)
param-value
(get-initial-state child-class param-value))]
param-key [isk param-value]
:else [isk isv])))]
(into {} (keep value-of initial-state))))
(defn wrapped-render
"Run `real-render`, possibly through :render-middleware configured on your app."
[this real-render]
#?(:clj
(real-render)
:cljs
(let [app (gobj/getValueByKeys this "props" "fulcro$app")
render-middleware (ah/app-algorithm app :render-middleware)]
(if render-middleware
(render-middleware this real-render)
(real-render)))))
(defn configure-hooks-component!
"Configure a function `(f [this fulcro-props] ...)` to work properly as a hook-based react component. This can be
used in leiu of `defsc` to create a component, where `options` is the (non-magic) map of component options
(i.e. :query is a `(fn [this])`, not a vector).
IMPORTANT: Your options must include `:componentName`, a fully-qualified keyword to use in the component registry.
Returns a new function that wraps yours (to properly extract Fulcro props) and installs the proper Fulcro component
options on the low-level function so that it will act properly when used within React as a hook-based component.
(def MyComponent
(configure-hooks-component!
(fn [this props]
(let [[v set-v!] (use-state this 0)
(dom/div ...)))
{:query ... :ident (fn [_ props] ...) :componentName ::MyComponent}))
(def ui-my-component (comp/factory MyComponent {:keyfn :id})
This can be used to easily generate dynamic components at runtime (as can `configure-component!`).
"
[f options]
(let [cls-atom (atom nil)
js-fn (fn [js-props]
(let [[this props] (use-fulcro js-props @cls-atom)]
(wrapped-render this
(fn []
(binding [*app* (or *app* (any->app this))
*depth* (inc (depth this))
*shared* (shared *app*)
*parent* this]
(f this props))))))]
(reset! cls-atom js-fn)
(add-hook-options! js-fn options)))
(defn- create-element
"Create a react element for a Fulcro class. In CLJ this returns the same thing as a mounted instance, whereas in CLJS it is an
element (which has yet to instantiate an instance)."
[class props children]
#?(:clj
(let [init-state (component-options class :initLocalState)
state-atom (atom {})
this {::element? true
:fulcro$isComponent true
:props props
:children children
:state state-atom
:fulcro$class class}
state (when init-state (init-state this))]
(when (map? state)
(reset! state-atom state))
this)
:cljs
(apply react/createElement class props (force-children children))))
(defn factory
"Create a factory constructor from a component class created with
defsc."
([class] (factory class nil))
([class {:keys [keyfn qualifier] :as opts}]
(let [qid (query-id class qualifier)]
(with-meta
(fn element-factory [props & children]
(let [key (:react-key props)
key (cond
key key
keyfn (keyfn props))
ref (:ref props)
ref (cond-> ref (keyword? ref) str)
props-middleware (some-> *app* (ah/app-algorithm :props-middleware))
;; Our data-readers.clj makes #js == identity in CLJ
props #js {:fulcro$value props
:fulcro$queryid qid
:fulcro$app *app*
:fulcro$parent *parent*
:fulcro$depth *depth*}
props (if props-middleware
(props-middleware class props)
props)]
#?(:cljs
(do
(when key
(gobj/set props "key" key))
(when ref
(gobj/set props "ref" ref))
;; dev time warnings/errors
(when goog.DEBUG
(when (nil? *app*)
(log/error "A Fulcro component was rendered outside of a parent context. This probably means you are using a library that has you pass rendering code to it as a lambda. Use `with-parent-context` to fix this. See https://book.fulcrologic.com/#err-comp-rendered-outside-parent-ctx"))
(when (or (map? key) (vector? key))
(log/warn "React key for " (component-name class) " is not a simple scalar value. This could cause spurious component remounts. See https://book.fulcrologic.com/#warn-react-key-not-simple-scalar"))
(when (string? ref)
(log/warn "String ref on " (component-name class) " should be a function. See https://book.fulcrologic.com/#warn-string-ref-not-function"))
(when (or (nil? props) (not (gobj/containsKey props "fulcro$value")))
(log/error "Props middleware seems to have corrupted props for " (component-name class) "See https://book.fulcrologic.com/#err-comp-props-middleware-corrupts"))
(when-not ((fnil map? {}) (gobj/get props "fulcro$value"))
(log/error "Props passed to" (component-name class) "are of the type"
(type->str (type (gobj/get props "fulcro$value")))
"instead of a map. Perhaps you meant to `map` the component over the props? See https://book.fulcrologic.com/#err-comp-props-not-a-map")))))
(create-element class props children)))
{:class class
:queryid qid
:qualifier qualifier}))))
(defn computed-factory
"Similar to factory, but returns a function with the signature
[props computed & children] instead of default [props & children].
This makes easier to send computed."
([class] (computed-factory class {}))
([class options]
(let [real-factory (factory class options)]
(with-meta
(fn
([props] (real-factory props))
([props computed-props]
(real-factory (computed props computed-props)))
([props computed-props & children]
(apply real-factory (computed props computed-props) children)))
(meta real-factory)))))
(defn transact!
"Submit a transaction for processing.
The underlying transaction system is pluggable, but the *default* supported options are:
- `:optimistic?` - boolean. Should the transaction be processed optimistically?
- `:ref` - ident. The ident of the component used to submit this transaction. This is set automatically if you use a component to call this function.
- `:component` - React element. Set automatically if you call this function using a component.
- `:refresh` - Vector containing idents (of components) and keywords (of props). Things that have changed and should be re-rendered
on screen. Only necessary when the underlying rendering algorithm won't auto-detect, such as when UI is derived from the
state of other components or outside of the directly queried props. Interpretation depends on the renderer selected:
The ident-optimized render treats these as \"extras\".
- `:only-refresh` - Vector of idents/keywords. If the underlying rendering configured algorithm supports it: The
components using these are the *only* things that will be refreshed in the UI.
This can be used to avoid the overhead of looking for stale data when you know exactly what
you want to refresh on screen as an extra optimization. Idents are *not* checked against queries.
- `:abort-id` - An ID (you make up) that makes it possible (if the plugins you're using support it) to cancel
the network portion of the transaction (assuming it has not already completed).
- `:compressible?` - boolean. Check compressible-transact! docs.
- `:synchronous?` - boolean. When turned on the transaction will run immediately on the calling thread. If run against
a component then the props will be immediately tunneled back to the calling component, allowing for React (raw) input
event handlers to behave as described in standard React Forms docs (uses setState behind the scenes). Any remote operations
will still be queued as normal. Calling `transact!!` is a shorthand for this option. WARNING: ONLY the given component will
be refreshed in the UI. If you have dependent data elsewhere in the UI you must either use `transact!` or schedule
your own global render using `app/schedule-render!`.
- `:after-render?` - Wait until the next render completes before allowing this transaction to run. This can be used
when calling `transact!` from *within* another mutation to ensure that the effects of the current mutation finish
before this transaction takes control of the CPU. This option defaults to `false`, but `defmutation` causes it to
be set to true for any transactions run within mutation action sections. You can affect the default for this value
in a dynamic scope by binding `rc/*after-render*` to true
- `:parallel?` - Boolean. If true, the mutation(s) in the transaction will NOT go into a network queue, nor
will it block later mutations or queries.
You may add any additional keys to the option map (namespaced is ideal), and any value is legal in the options
map, including functions. The options will appear in the `env` of all mutations run in the transaction as
`:com.fulcrologic.fulcro.algorithms.tx-processing/options`. This is the preferred way of passing things like
lambdas (if you wanted something like a callback) to mutations. Note that mutation symbols are perfectly legal
as mutation *arguments*, so chaining mutations can already be done via the normal transaction mechanism, and
callbacks, while sometimes necessary/useful, should be limited to usages where there is no other clean way
to accomplish the goal.
NOTE: This function calls the application's `tx!` function (which is configurable). Fulcro 2 'follow-on reads' are
supported by the default version and are added to the `:refresh` entries. Your choice of rendering algorithm will
influence their necessity.
Returns the transaction ID of the submitted transaction.
"
([app-or-component tx options] (rc/transact! app-or-component tx options))
([app-or-comp tx] (rc/transact! app-or-comp tx {})))
(defn transact!!
"Shorthand for exactly `(transact! component tx (merge options {:synchronous? true}))`.
Runs a synchronous transaction, which is an optimized mode where the optimistic behaviors of the mutations in the
transaction run on the calling thread, and new props are immediately made available to the calling component via
\"props tunneling\" (a behind-the-scenes mechanism using js/setState).
This mode is meant to be used in form input event handlers, since React is designed to only work properly with
raw DOM inputs via component-local state. This prevents things like the cursor jumping to the end of inputs
unexpectedly.
WARNING: Using an `app` instead of a component in synchronous transactions makes no sense. You must pass a component
that has an ident.
If you're using this, you can also set the compiler option:
```
:compiler-options {:external-config {:fulcro {:wrap-inputs? false}}}
```
to turn off Fulcro DOM's generation of wrapped inputs (which try to solve this problem in a less-effective way).
WARNING: Synchronous rendering does *not* refresh the full UI, only the component.
"
([component tx] (rc/transact!! component tx {}))
([component tx options]
(rc/transact! component tx (merge options {:synchronous? true}))))
(declare normalize-query)
(def link-element "Part of internal implementation of dynamic queries." rc/link-element)
(def normalize-query-elements
"Part of internal implementation of dynamic queries.
Determines if there are query elements in the `query` that need to be normalized. If so, it does so.
Returns the new state map containing potentially-updated normalized queries."
rc/normalize-query-elements)
(def link-query
"Part of dyn query implementation. Find all of the elements (only at the top level) of the given query and replace them
with their query ID."
rc/link-query)
(def normalize-query
"Given a state map and a query, returns a state map with the query normalized into the database. Query fragments
that already appear in the state will not be added. Part of dynamic query implementation."
rc/normalize-query)
(defn set-query*
"Put a query in app state.
NOTE: Indexes must be rebuilt after setting a query, so this function should primarily be used to build
up an initial app state."
[state-map class-or-factory {:keys [query] :as args}]
(rc/set-query* state-map class-or-factory args))
(defn set-query!
"Public API for setting a dynamic query on a component. This function alters the query and rebuilds internal indexes.
* `x` : is anything that any->app accepts.
* `class-or-factory` : A component class or factory for that class (if using query qualifiers)
* `opts` : A map with `query` and optionally `params` (substitutions on queries)
"
[x class-or-factory {:keys [query params] :as opts}]
(rc/set-query! x class-or-factory opts))
(defn refresh-dynamic-queries!
"Refresh the current dynamic queries in app state to reflect any updates to the static queries of the components.
This can be used at development time to update queries that have changed but that hot code reload does not
reflect (because there is a current saved query in state). This is *not* always what you want, since a component
may have a custom query whose prop-level elements are set to a particular thing on purpose.
An component that has `:preserve-dynamic-query? true` in its component options will be ignored by
this function."
([app-ish cls force?] (rc/refresh-dynamic-queries! app-ish cls force?))
([app-ish] (rc/refresh-dynamic-queries! app-ish)))
(defn get-indexes
"Get all of the indexes from a component instance or app. See also `ident->any`, `class->any`, etc."
[x]
(let [app (any->app x)]
(some-> app :com.fulcrologic.fulcro.application/runtime-atom deref :com.fulcrologic.fulcro.application/indexes)))
(defn ident->components
"Return all on-screen component instances that are rendering the data for a given ident. `x` is anything any->app accepts."
[x ident]
(some-> (get-indexes x) :ident->components (get ident)))
(defn ident->any
"Return some (random) on-screen components that uses the given ident. `x` is anything any->app accepts."
[x ident]
(first (ident->components x ident)))
(defn prop->classes
"Get all component classes that query for the given prop.
`x` can be anything `any->app` is ok with.
Returns all classes that query for that prop (or ident)"
[x prop]
(some-> (get-indexes x) :prop->classes (get prop)))
(defn class->all
"Get all of the on-screen component instances from the indexes that have the type of the component class.
`x` can be anything `any->app` is ok with."
[x class]
(let [k (class->registry-key class)]
(some-> (get-indexes x) :class->components (get k))))
(defn class->any
"Get a (random) on-screen component instance from the indexes that has type of the given component class.
`x` can be anything `any->app` is ok with."
[x cls]
(first (class->all x cls)))
(defn component->state-map
"Returns the current value of the state map via a component instance. Note that it is not safe to render
arbitrary data from the state map since Fulcro will have no idea that it should refresh a component that
does so; however, it is sometimes useful to look at the state map for information that doesn't
change over time."
[this] (some-> this any->app :com.fulcrologic.fulcro.application/state-atom deref))
(defn wrap-update-extra-props
"Wrap the props middleware such that `f` is called to get extra props that should be placed
in the extra-props arg of the component.
`handler` - (optional) The next item in the props middleware chain.
`f` - A (fn [cls extra-props] new-extra-props)
`f` will be passed the class being rendered and the current map of extra props. It should augment
those and return a new version."
([f]
(fn [cls raw-props]
#?(:clj (update raw-props :fulcro$extra_props (partial f cls))
:cljs (let [existing (or (gobj/get raw-props "fulcro$extra_props") {})
new (f cls existing)]
(gobj/set raw-props "fulcro$extra_props" new)
raw-props))))
([handler f]
(fn [cls raw-props]
#?(:clj (let [props (update raw-props :fulcro$extra_props (partial f cls))]
(handler cls props))
:cljs (let [existing (or (gobj/get raw-props "fulcro$extra_props") {})
new (f cls existing)]
(gobj/set raw-props "fulcro$extra_props" new)