/
dom.cljs
287 lines (251 loc) · 12.2 KB
/
dom.cljs
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
(ns com.fulcrologic.fulcro.dom
"Client-side DOM macros and functions. For isomorphic (server) support, see also com.fulcrologic.fulcro.dom-server"
(:refer-clojure :exclude [map meta time mask select use set symbol filter])
(:require-macros [com.fulcrologic.fulcro.dom])
(:require
[clojure.spec.alpha :as s]
[clojure.string :as str]
[com.fulcrologic.fulcro.components :as comp]
["react" :as react]
["react-dom" :as react.dom]
[goog.object :as gobj]
[goog.dom :as gdom]
[com.fulcrologic.fulcro.dom.inputs :as inputs]
[com.fulcrologic.fulcro.dom-common :as cdom]
[taoensso.timbre :as log]))
(declare a abbr address altGlyph altGlyphDef altGlyphItem animate animateColor animateMotion animateTransform area
article aside audio b base bdi bdo big blockquote body br button canvas caption circle cite clipPath code
col colgroup color-profile cursor data datalist dd defs del desc details dfn dialog discard div dl dt
ellipse em embed feBlend feColorMatrix feComponentTransfer feComposite feConvolveMatrix feDiffuseLighting
feDisplacementMap feDistantLight feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
feImage feMerge feMergeNode feMorphology feOffset fePointLight feSpecularLighting feSpotLight feTile feTurbulence
fieldset figcaption figure filter font font-face font-face-format font-face-name font-face-src font-face-uri
footer foreignObject form g glyph glyphRef h1 h2 h3 h4 h5 h6 hatch hatchpath head header hkern hr html
i iframe image img input ins kbd keygen label legend li line linearGradient link main map mark marker mask
menu menuitem mesh meshgradient meshpatch meshrow meta metadata meter missing-glyph
mpath nav noscript object ol optgroup option output p param path pattern picture polygon polyline pre progress q radialGradient
rect rp rt ruby s samp script section select set small solidcolor source span stop strong style sub summary
sup svg switch symbol table tbody td text textPath textarea tfoot th thead time title tr track tref tspan
u ul unknown use var video view vkern wbr)
(defn element? "Returns true if the given arg is a react element."
[x]
(react/isValidElement x))
(defn child->typed-child [child]
(cond
(string? child) [:string child]
(number? child) [:number child]
(or (vector? child) (seq? child) (array? child)) [:collection child]
(nil? child) [:nil child]
(element? child) [:element child]))
(defn parse-args
"Runtime parsing of DOM tag arguments. Returns a map with keys :css, :attrs, and :children."
[args]
(letfn [(parse-css [[args result :as pair]]
(let [arg (first args)]
(if (keyword? arg)
[(next args) (assoc result :css arg)]
pair)))
(parse-attrs [[args result :as pair]]
(let [has-arg? (seq args)
arg (first args)]
(cond
(and has-arg? (nil? arg)) [(next args) (assoc result :attrs [:nil nil])]
(and (object? arg) (not (element? arg))) [(next args) (assoc result :attrs [:js-object arg])]
(and (map? arg) (not (element? arg))) [(next args) (assoc result :attrs [:map arg])]
:else pair)))
(parse-children [[args result]]
[nil (cond-> result
(seq args) (assoc :children (mapv child->typed-child args)))])]
(-> [args {}]
(parse-css)
(parse-attrs)
(parse-children)
second)))
(defn render
"Equivalent to React.render"
[component el]
(react.dom/render component el))
(defn render-to-str
"Equivalent to React.renderToString. NOTE: You must make sure js/ReactDOMServer is defined (e.g. require cljsjs.react.dom.server) to use this function."
[c]
(js/ReactDOMServer.renderToString c))
(defn node
"Returns the dom node associated with a component's React ref."
([component]
(react.dom/findDOMNode component))
([component name]
(some-> (.-refs component) (gobj/get name) (react.dom/findDOMNode))))
(def Input
"React component that wraps dom/input to prevent cursor madness."
(inputs/StringBufferedInput ::Input {:string->model identity
:model->string identity}))
(def ui-input
"A wrapped input. Use this when you see the cursor jump around while you're trying to type in an input. Drop-in replacement
for `dom/input`.
NOTE: The onChange and onBlur handlers will receive a string value, not an event. If you want the raw event on changes use onInput."
(let [factory (comp/factory Input {:keyfn :key})]
(fn [props]
(if-let [ref (:ref props)]
(factory (assoc props :ref (fn [r] (ref (some-> r (node))))))
(factory props)))))
(defn create-element
"Create a DOM element for which there exists no corresponding function.
Useful to create DOM elements not included in React.DOM. Equivalent
to calling `js/React.createElement`"
([tag]
(create-element tag nil))
([tag opts]
(react/createElement tag opts))
([tag opts & children]
(apply react/createElement tag opts children)))
(defn convert-props
"Given props, which can be nil, a js-obj or a clj map: returns a js object."
[props]
(cond
(nil? props)
#js {}
(map? props)
(clj->js props)
:else
props))
;; called from macro
;; react v16 is really picky, the old direct .children prop trick no longer works
(defn macro-create-element*
"Used internally by the DOM element generation."
[arr]
{:pre [(array? arr)]}
(.apply react/createElement nil arr))
(defn- update-state
"Updates the state of the wrapped input element."
[component next-props value]
(let [on-change (gobj/getValueByKeys component "state" "cached-props" "onChange")
next-state #js {}
inputRef (gobj/get next-props "inputRef")]
(gobj/extend next-state next-props #js {:onChange on-change})
(gobj/set next-state "value" value)
(when inputRef
(gobj/remove next-state "inputRef")
(gobj/set next-state "ref" inputRef))
(.setState component #js {"cached-props" next-state})))
(defonce form-elements? #{"input" "select" "option" "textarea"})
(defn is-form-element? [element]
(let [tag (.-tagName element)]
(and tag (form-elements? (str/lower-case tag)))))
(defn wrap-form-element [element]
(let [ctor (fn [props]
(this-as this
(set! (.-state this)
(let [state #js {:ref (gobj/get props "inputRef")}]
(->> #js {:onChange (goog/bind (gobj/get this "onChange") this)}
(gobj/extend state props))
(gobj/remove state "inputRef")
#js {"cached-props" state}))
(.apply react/Component this (js-arguments))))]
(set! (.-displayName ctor) (str "wrapped-" element))
(goog.inherits ctor react/Component)
(specify! (.-prototype ctor)
Object
(onChange [this event]
(when-let [handler (gobj/get (.-props this) "onChange")]
(handler event)
(update-state
this (.-props this)
(gobj/getValueByKeys event "target" "value"))))
(UNSAFE_componentWillReceiveProps [this new-props]
(let [state-value (gobj/getValueByKeys this "state" "cached-props" "value")
this-node (react.dom/findDOMNode this)
value-node (if (is-form-element? this-node)
this-node
(gdom/findNode this-node #(is-form-element? %)))
element-value (gobj/get value-node "value")]
(when goog.DEBUG
(when (and state-value element-value (not= (type state-value) (type element-value)))
(log/warn "There is a mismatch for the data type of the value on an input with value " element-value
". This will cause the input to miss refreshes. In general you should force the :value of an input to
be a string since that is how values are stored on most real DOM elements. See https://book.fulcrologic.com/#warn-dom-type-mismatch")))
(if (not= state-value element-value)
(update-state this new-props element-value)
(update-state this new-props (gobj/get new-props "value")))))
(render [this]
(react/createElement element (gobj/getValueByKeys this "state" "cached-props"))))
(let [real-factory (fn [& args] (apply react/createElement ctor args))]
(fn [props & children]
(let [t (gobj/get props "type")]
(if (= t "file")
(apply react/createElement "input" props children)
(if-let [r (gobj/get props "ref")]
(if (string? r)
(apply real-factory props children)
(let [p #js{}]
(gobj/extend p props)
(gobj/set p "inputRef" r)
(gobj/remove p "ref")
(apply real-factory p children)))
(apply real-factory props children))))))))
(def wrapped-input "Low-level form input, with no syntactic sugar. Used internally by DOM macros" (wrap-form-element "input"))
(def wrapped-textarea "Low-level form input, with no syntactic sugar. Used internally by DOM macros" (wrap-form-element "textarea"))
(def wrapped-option "Low-level form input, with no syntactic sugar. Used internally by DOM macros" (wrap-form-element "option"))
(def wrapped-select "Low-level form input, with no syntactic sugar. Used internally by DOM macros" (wrap-form-element "select"))
(defn- arr-append* [arr x]
(.push arr x)
arr)
(defn- arr-append [arr tail]
(reduce arr-append* arr tail))
(defn macro-create-wrapped-form-element
"Used internally by element generation."
[opts]
(let [tag (aget opts 0)
props (aget opts 1)
children (.splice opts 2)]
(case tag
"input" (apply wrapped-input props children)
"textarea" (apply wrapped-textarea props children)
"select" (apply wrapped-select props children)
"option" (apply wrapped-option props children))))
;; fallback if the macro didn't do this
(defn macro-create-element
"Runtime interpretation of props. Used internally by element generation when the macro cannot expand the element at compile time."
([type args] (macro-create-element type args nil))
([type args csskw]
(let [[head & tail] (mapv comp/force-children args)
f (if (form-elements? type)
macro-create-wrapped-form-element
macro-create-element*)]
(cond
(nil? head)
(f (doto #js [type (cdom/add-kwprops-to-props #js {} csskw)]
(arr-append tail)))
(element? head)
(f (doto #js [type (cdom/add-kwprops-to-props #js {} csskw)]
(arr-append args)))
(object? head)
(f (doto #js [type (cdom/add-kwprops-to-props head csskw)]
(arr-append tail)))
(map? head)
(f (doto #js [type (clj->js (cdom/add-kwprops-to-props (cdom/interpret-classes head) csskw))]
(arr-append tail)))
:else
(f (doto #js [type (cdom/add-kwprops-to-props #js {} csskw)]
(arr-append args)))))))
(defn macro-create-unwrapped-element
"Just like macro-create-element, but never wraps form input types."
([type args] (macro-create-element type args nil))
([type args csskw]
(let [[head & tail] (mapv comp/force-children args)]
(cond
(nil? head)
(macro-create-element* (doto #js [type (cdom/add-kwprops-to-props #js {} csskw)]
(arr-append tail)))
(element? head)
(macro-create-element* (doto #js [type (cdom/add-kwprops-to-props #js {} csskw)]
(arr-append args)))
(object? head)
(macro-create-element* (doto #js [type (cdom/add-kwprops-to-props head csskw)]
(arr-append tail)))
(map? head)
(macro-create-element* (doto #js [type (clj->js (cdom/add-kwprops-to-props (cdom/interpret-classes head) csskw))]
(arr-append tail)))
:else
(macro-create-element* (doto #js [type (cdom/add-kwprops-to-props #js {} csskw)]
(arr-append args)))))))
(com.fulcrologic.fulcro.dom/gen-client-dom-fns com.fulcrologic.fulcro.dom/macro-create-element com.fulcrologic.fulcro.dom/macro-create-unwrapped-element)