" (.. el -innerHTML))))))
167 |
--------------------------------------------------------------------------------
/src/dumdom/element.cljc:
--------------------------------------------------------------------------------
1 | (ns dumdom.element
2 | (:require [clojure.set :as set]
3 | [clojure.string :as str]))
4 |
5 | (defn- event-entry [attrs k]
6 | [(.toLowerCase (.substring (name k) 2)) (attrs k)])
7 |
8 | (defn- camelCase [s]
9 | (let [[f & rest] (str/split s #"-")]
10 | (str f (str/join "" (map str/capitalize rest)))))
11 |
12 | (defn- camel-key [k]
13 | (keyword (camelCase (name k))))
14 |
15 | (def ^:private skip-pixelize-attrs
16 | (->>
17 | [:animation-iteration-count
18 | :box-flex
19 | :box-flex-group
20 | :box-ordinal-group
21 | :column-count
22 | :fill-opacity
23 | :flex
24 | :flex-grow
25 | :flex-positive
26 | :flex-shrink
27 | :flex-negative
28 | :flex-order
29 | :font-weight
30 | :line-clamp
31 | :line-height
32 | :opacity
33 | :order
34 | :orphans
35 | :stop-opacity
36 | :stroke-dashoffset
37 | :stroke-opacity
38 | :stroke-width
39 | :tab-size
40 | :widows
41 | :z-index
42 | :zoom]
43 | (mapcat (fn [k] [k (camel-key k)]))
44 | set))
45 |
46 | (defn- normalize-styles [styles]
47 | (reduce (fn [m [attr v]]
48 | (if (number? v)
49 | (if (skip-pixelize-attrs attr)
50 | (update m attr str)
51 | (update m attr str "px"))
52 | m))
53 | styles
54 | styles))
55 |
56 | (def ^:private attr-mappings
57 | {:acceptCharset :accept-charset
58 | :accessKey :accesskey
59 | :autoCapitalize :autocapitalize
60 | :autoComplete :autocomplete
61 | :autoFocus :autofocus
62 | :autoPlay :autoplay
63 | :bgColor :bgcolor
64 | :className :class
65 | :codeBase :codebase
66 | :colSpan :colspan
67 | :contentEditable :contenteditable
68 | :contextMenu :contextmenu
69 | :crossOrigin :crossorigin
70 | :dateTime :datetime
71 | :dirName :dirname
72 | :dropZone :dropzone
73 | :encType :enctype
74 | :htmlFor :for
75 | :formAction :formaction
76 | :hrefLang :hreflang
77 | :httpEquiv :http-equiv
78 | :isMap :ismap
79 | :itemProp :itemprop
80 | :keyType :keytype
81 | :maxLength :maxlength
82 | :minLength :minlength
83 | :noValidate :novalidate
84 | :placeHolder :placeholder
85 | :preLoad :preload
86 | :radioGroup :radiogroup
87 | :readOnly :readonly
88 | :rowSpan :rowspan
89 | :spellCheck :spellcheck
90 | :srcDoc :srcdoc
91 | :srcLang :srclang
92 | :srcSet :srcset
93 | :tabIndex :tabindex
94 | :useMap :usemap
95 | :accentHeight :accent-height
96 | :alignmentBaseline :alignment-baseline
97 | :arabicForm :arabic-form
98 | :baselineShift :baseline-shift
99 | :capHeight :cap-height
100 | :clipPath :clip-path
101 | :clipRule :clip-rule
102 | :colorInterpolation :color-interpolation
103 | :colorInterpolationFilters :color-interpolation-filters
104 | :colorProfile :color-profile
105 | :colorRendering :color-rendering
106 | :dominantBaseline :dominant-baseline
107 | :enableBackground :enable-background
108 | :fillOpacity :fill-opacity
109 | :fillRule :fill-rule
110 | :floodColor :flood-color
111 | :floodOpacity :flood-opacity
112 | :fontFamily :font-family
113 | :fontSize :font-size
114 | :fontSizeAdjust :font-size-adjust
115 | :fontStretch :font-stretch
116 | :fontStyle :font-style
117 | :fontVariant :font-variant
118 | :fontWeight :font-weight
119 | :glyphName :glyph-name
120 | :glyphOrientationHorizontal :glyph-orientation-horizontal
121 | :glyphOrientationVertical :glyph-orientation-vertical
122 | :horizAdvX :horiz-adv-x
123 | :horizOriginX :horiz-origin-x
124 | :imageRendering :image-rendering
125 | :letterSpacing :letter-spacing
126 | :lightingColor :lighting-color
127 | :markerEnd :marker-end
128 | :markerMid :marker-mid
129 | :markerStart :marker-start
130 | :overlinePosition :overline-position
131 | :overlineThickness :overline-thickness
132 | :panose1 :panose-1
133 | :paintOrder :paint-order
134 | :pointerEvents :pointer-events
135 | :renderingIntent :rendering-intent
136 | :shapeRendering :shape-rendering
137 | :stopColor :stop-color
138 | :stopOpacity :stop-opacity
139 | :strikethroughPosition :strikethrough-position
140 | :strikethroughThickness :strikethrough-thickness
141 | :strokeDasharray :stroke-dasharray
142 | :strokeDashoffset :stroke-dashoffset
143 | :strokeLinecap :stroke-linecap
144 | :strokeLinejoin :stroke-linejoin
145 | :strokeMiterlimit :stroke-miterlimit
146 | :strokeOpacity :stroke-opacity
147 | :strokeWidth :stroke-width
148 | :textAnchor :text-anchor
149 | :textDecoration :text-decoration
150 | :textRendering :text-rendering
151 | :underlinePosition :underline-position
152 | :underlineThickness :underline-thickness
153 | :unicodeBidi :unicode-bidi
154 | :unicodeRange :unicode-range
155 | :unitsPerEm :units-per-em
156 | :vAlphabetic :v-alphabetic
157 | :vHanging :v-hanging
158 | :vIdeographic :v-ideographic
159 | :vMathematical :v-mathematical
160 | :vectorEffect :vector-effect
161 | :vertAdvY :vert-adv-y
162 | :vertOriginX :vert-origin-x
163 | :vertOriginY :vert-origin-y
164 | :wordSpacing :word-spacing
165 | :writingMode :writing-mode
166 | :xHeight :x-height
167 | :xlinkActuate :xlink:actuate
168 | :xlinkArcrole :xlink:arcrole
169 | :xlinkHref :xlink:href
170 | :xlinkRole :xlink:role
171 | :xlinkShow :xlink:show
172 | :xlinkTitle :xlink:title
173 | :xlinkType :xlink:type
174 | :xmlBase :xml:base
175 | :xmlLang :xml:lang
176 | :xmlSpace :xml:space
177 | :mountedStyle :mounted-style
178 | :leavingStyle :leaving-style
179 | :disappearingStyle :disappearing-style})
180 |
181 | (defn data-attr? [[k v]]
182 | (re-find #"^data-" (name k)))
183 |
184 | (defn- prep-attrs [attrs k]
185 | (let [event-keys (filter #(and (str/starts-with? (name %) "on") (ifn? (attrs %))) (keys attrs))
186 | dataset (->> attrs
187 | (filter data-attr?)
188 | (map (fn [[k v]] [(str/replace (name k) #"^data-" "") v]))
189 | (into {}))
190 | attrs (->> attrs
191 | (remove data-attr?)
192 | (map (fn [[k v]] [(camel-key k) v]))
193 | (remove (fn [[k v]] (nil? v)))
194 | (into {}))
195 | attrs (set/rename-keys attrs attr-mappings)
196 | el-key (or (:key attrs)
197 | (when (contains? attrs :dangerouslySetInnerHTML)
198 | (hash [(:dangerouslySetInnerHTML attrs) k])))]
199 | (cond-> {:attrs (apply dissoc attrs :style :mounted-style :leaving-style :disappearing-style
200 | :component :value :key :ref :dangerouslySetInnerHTML event-keys)
201 | :props (cond-> {}
202 | (:value attrs) (assoc :value (:value attrs))
203 |
204 | (contains? (:dangerouslySetInnerHTML attrs) :__html)
205 | (assoc :innerHTML (-> attrs :dangerouslySetInnerHTML :__html)))
206 | :style (merge (normalize-styles (:style attrs))
207 | (when-let [enter (:mounted-style attrs)]
208 | {:delayed (normalize-styles enter)})
209 | (when-let [remove (:leaving-style attrs)]
210 | {:remove (normalize-styles remove)})
211 | (when-let [destroy (:disappearing-style attrs)]
212 | {:destroy (normalize-styles destroy)}))
213 | :on (->> event-keys
214 | (mapv #(event-entry attrs %))
215 | (into {}))
216 | :hook (merge
217 | {}
218 | (when-let [callback (:ref attrs)]
219 | {:insert #(callback (.-elm %))
220 | :destroy #(callback nil)}))
221 | :dataset dataset}
222 | el-key (assoc :key el-key))))
223 |
224 | (declare create)
225 |
226 | (defn hiccup? [sexp]
227 | (and (vector? sexp)
228 | (not (map-entry? sexp))
229 | (or (keyword? (first sexp)) (fn? (first sexp)))))
230 |
231 | (defn parse-hiccup-symbol [sym attrs]
232 | (let [[_ id] (re-find #"#([^\.#]+)" sym)
233 | [el & classes] (-> (str/replace sym #"#([^#\.]+)" "")
234 | (str/split #"\."))]
235 | [el
236 | (cond-> attrs
237 | id (assoc :id id)
238 | (seq classes) (update :className #(str/join " " (if % (conj classes %) classes))))]))
239 |
240 | (defn explode-styles [s]
241 | (->> (str/split s #";")
242 | (map #(let [[k v] (map str/trim (str/split % #":"))]
243 | [k v]))
244 | (into {})))
245 |
246 | (defn prep-hiccup-attrs [attrs]
247 | (cond-> attrs
248 | (string? (:style attrs)) (update :style explode-styles)))
249 |
250 | (defn flatten-seqs [xs]
251 | (loop [res []
252 | [x & xs] xs]
253 | (cond
254 | (and (nil? xs) (nil? x)) (seq res)
255 | (seq? x) (recur (into res (flatten-seqs x)) xs)
256 | :default (recur (conj res x) xs))))
257 |
258 | (defn add-namespace [vnode]
259 | (cond-> vnode
260 | (not= "foreignObject" (:sel vnode))
261 | (assoc-in [:data :ns] "http://www.w3.org/2000/svg")
262 |
263 | (:children vnode)
264 | (update :children #(map add-namespace %))))
265 |
266 | (defn svg? [sel]
267 | (and (= "s" (nth sel 0))
268 | (= "v" (nth sel 1))
269 | (= "g" (nth sel 2))
270 | (or (= 3 (count sel))
271 | (= "." (nth sel 3))
272 | (= "#" (nth sel 3)))))
273 |
274 | (defn primitive? [x]
275 | (or (string? x) (number? x)))
276 |
277 | (defn convert-primitive-children [children]
278 | (for [c children]
279 | (if (primitive? c)
280 | {:text c}
281 | c)))
282 |
283 | ;; This is a port of Snabbdom's `h` function, but without the varargs support.
284 | (defn create-vdom-node [sel attrs children]
285 | (let [cmap? (map? children)]
286 | (cond-> {:sel sel
287 | :data (dissoc attrs :key)}
288 | (primitive? children)
289 | (assoc :text children)
290 |
291 | cmap?
292 | (assoc :children [children])
293 |
294 | (and (seq? children) (not cmap?))
295 | (assoc :children children)
296 |
297 | :always (update :children convert-primitive-children)
298 |
299 | (svg? sel)
300 | add-namespace
301 |
302 | (:key attrs)
303 | (assoc :key (:key attrs)))))
304 |
305 | (defn inflate-hiccup [sexp]
306 | (cond
307 | (nil? sexp) (create-vdom-node "!" {} "nil")
308 |
309 | (not (hiccup? sexp)) sexp
310 |
311 | :default
312 | (let [tag-name (first sexp)
313 | args (rest sexp)
314 | args (if (map? (first args)) args (concat [{}] args))]
315 | (if (fn? tag-name)
316 | (apply tag-name (rest sexp))
317 | (let [[element attrs] (parse-hiccup-symbol (name tag-name) (first args))]
318 | (apply create element (prep-hiccup-attrs attrs) (flatten-seqs (rest args))))))))
319 |
320 | (defn create [tag-name attrs & children]
321 | (fn [path k]
322 | (let [fullpath (conj path k)]
323 | (create-vdom-node
324 | tag-name
325 | (-> (prep-attrs attrs k)
326 | (assoc-in [:hook :update]
327 | (fn [old-vnode new-vnode]
328 | (doseq [node (filter #(some-> % .-willEnter) (.-children new-vnode))]
329 | ((.-willEnter node)))
330 | (doseq [node (filter #(some-> % .-willAppear) (.-children new-vnode))]
331 | ((.-willAppear node))))))
332 | (->> children
333 | (mapcat #(if (seq? %) % [%]))
334 | (map inflate-hiccup)
335 | (map-indexed #(do
336 | (if (fn? %2)
337 | (%2 fullpath %1)
338 | %2))))))))
339 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
4 |
5 | 1. DEFINITIONS
6 |
7 | "Contribution" means:
8 |
9 | a) in the case of the initial Contributor, the initial code and
10 | documentation distributed under this Agreement, and
11 |
12 | b) in the case of each subsequent Contributor:
13 |
14 | i) changes to the Program, and
15 |
16 | ii) additions to the Program;
17 |
18 | where such changes and/or additions to the Program originate from and are
19 | distributed by that particular Contributor. A Contribution 'originates' from
20 | a Contributor if it was added to the Program by such Contributor itself or
21 | anyone acting on such Contributor's behalf. Contributions do not include
22 | additions to the Program which: (i) are separate modules of software
23 | distributed in conjunction with the Program under their own license
24 | agreement, and (ii) are not derivative works of the Program.
25 |
26 | "Contributor" means any person or entity that distributes the Program.
27 |
28 | "Licensed Patents" mean patent claims licensable by a Contributor which are
29 | necessarily infringed by the use or sale of its Contribution alone or when
30 | combined with the Program.
31 |
32 | "Program" means the Contributions distributed in accordance with this
33 | Agreement.
34 |
35 | "Recipient" means anyone who receives the Program under this Agreement,
36 | including all Contributors.
37 |
38 | 2. GRANT OF RIGHTS
39 |
40 | a) Subject to the terms of this Agreement, each Contributor hereby grants
41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
42 | reproduce, prepare derivative works of, publicly display, publicly perform,
43 | distribute and sublicense the Contribution of such Contributor, if any, and
44 | such derivative works, in source code and object code form.
45 |
46 | b) Subject to the terms of this Agreement, each Contributor hereby grants
47 | Recipient a non-exclusive, worldwide, royalty-free patent license under
48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
49 | transfer the Contribution of such Contributor, if any, in source code and
50 | object code form. This patent license shall apply to the combination of the
51 | Contribution and the Program if, at the time the Contribution is added by the
52 | Contributor, such addition of the Contribution causes such combination to be
53 | covered by the Licensed Patents. The patent license shall not apply to any
54 | other combinations which include the Contribution. No hardware per se is
55 | licensed hereunder.
56 |
57 | c) Recipient understands that although each Contributor grants the licenses
58 | to its Contributions set forth herein, no assurances are provided by any
59 | Contributor that the Program does not infringe the patent or other
60 | intellectual property rights of any other entity. Each Contributor disclaims
61 | any liability to Recipient for claims brought by any other entity based on
62 | infringement of intellectual property rights or otherwise. As a condition to
63 | exercising the rights and licenses granted hereunder, each Recipient hereby
64 | assumes sole responsibility to secure any other intellectual property rights
65 | needed, if any. For example, if a third party patent license is required to
66 | allow Recipient to distribute the Program, it is Recipient's responsibility
67 | to acquire that license before distributing the Program.
68 |
69 | d) Each Contributor represents that to its knowledge it has sufficient
70 | copyright rights in its Contribution, if any, to grant the copyright license
71 | set forth in this Agreement.
72 |
73 | 3. REQUIREMENTS
74 |
75 | A Contributor may choose to distribute the Program in object code form under
76 | its own license agreement, provided that:
77 |
78 | a) it complies with the terms and conditions of this Agreement; and
79 |
80 | b) its license agreement:
81 |
82 | i) effectively disclaims on behalf of all Contributors all warranties and
83 | conditions, express and implied, including warranties or conditions of title
84 | and non-infringement, and implied warranties or conditions of merchantability
85 | and fitness for a particular purpose;
86 |
87 | ii) effectively excludes on behalf of all Contributors all liability for
88 | damages, including direct, indirect, special, incidental and consequential
89 | damages, such as lost profits;
90 |
91 | iii) states that any provisions which differ from this Agreement are offered
92 | by that Contributor alone and not by any other party; and
93 |
94 | iv) states that source code for the Program is available from such
95 | Contributor, and informs licensees how to obtain it in a reasonable manner on
96 | or through a medium customarily used for software exchange.
97 |
98 | When the Program is made available in source code form:
99 |
100 | a) it must be made available under this Agreement; and
101 |
102 | b) a copy of this Agreement must be included with each copy of the Program.
103 |
104 | Contributors may not remove or alter any copyright notices contained within
105 | the Program.
106 |
107 | Each Contributor must identify itself as the originator of its Contribution,
108 | if any, in a manner that reasonably allows subsequent Recipients to identify
109 | the originator of the Contribution.
110 |
111 | 4. COMMERCIAL DISTRIBUTION
112 |
113 | Commercial distributors of software may accept certain responsibilities with
114 | respect to end users, business partners and the like. While this license is
115 | intended to facilitate the commercial use of the Program, the Contributor who
116 | includes the Program in a commercial product offering should do so in a
117 | manner which does not create potential liability for other Contributors.
118 | Therefore, if a Contributor includes the Program in a commercial product
119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend
120 | and indemnify every other Contributor ("Indemnified Contributor") against any
121 | losses, damages and costs (collectively "Losses") arising from claims,
122 | lawsuits and other legal actions brought by a third party against the
123 | Indemnified Contributor to the extent caused by the acts or omissions of such
124 | Commercial Contributor in connection with its distribution of the Program in
125 | a commercial product offering. The obligations in this section do not apply
126 | to any claims or Losses relating to any actual or alleged intellectual
127 | property infringement. In order to qualify, an Indemnified Contributor must:
128 | a) promptly notify the Commercial Contributor in writing of such claim, and
129 | b) allow the Commercial Contributor tocontrol, and cooperate with the
130 | Commercial Contributor in, the defense and any related settlement
131 | negotiations. The Indemnified Contributor may participate in any such claim
132 | at its own expense.
133 |
134 | For example, a Contributor might include the Program in a commercial product
135 | offering, Product X. That Contributor is then a Commercial Contributor. If
136 | that Commercial Contributor then makes performance claims, or offers
137 | warranties related to Product X, those performance claims and warranties are
138 | such Commercial Contributor's responsibility alone. Under this section, the
139 | Commercial Contributor would have to defend claims against the other
140 | Contributors related to those performance claims and warranties, and if a
141 | court requires any other Contributor to pay any damages as a result, the
142 | Commercial Contributor must pay those damages.
143 |
144 | 5. NO WARRANTY
145 |
146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
151 | appropriateness of using and distributing the Program and assumes all risks
152 | associated with its exercise of rights under this Agreement , including but
153 | not limited to the risks and costs of program errors, compliance with
154 | applicable laws, damage to or loss of data, programs or equipment, and
155 | unavailability or interruption of operations.
156 |
157 | 6. DISCLAIMER OF LIABILITY
158 |
159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
166 | OF SUCH DAMAGES.
167 |
168 | 7. GENERAL
169 |
170 | If any provision of this Agreement is invalid or unenforceable under
171 | applicable law, it shall not affect the validity or enforceability of the
172 | remainder of the terms of this Agreement, and without further action by the
173 | parties hereto, such provision shall be reformed to the minimum extent
174 | necessary to make such provision valid and enforceable.
175 |
176 | If Recipient institutes patent litigation against any entity (including a
177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
178 | (excluding combinations of the Program with other software or hardware)
179 | infringes such Recipient's patent(s), then such Recipient's rights granted
180 | under Section 2(b) shall terminate as of the date such litigation is filed.
181 |
182 | All Recipient's rights under this Agreement shall terminate if it fails to
183 | comply with any of the material terms or conditions of this Agreement and
184 | does not cure such failure in a reasonable period of time after becoming
185 | aware of such noncompliance. If all Recipient's rights under this Agreement
186 | terminate, Recipient agrees to cease use and distribution of the Program as
187 | soon as reasonably practicable. However, Recipient's obligations under this
188 | Agreement and any licenses granted by Recipient relating to the Program shall
189 | continue and survive.
190 |
191 | Everyone is permitted to copy and distribute copies of this Agreement, but in
192 | order to avoid inconsistency the Agreement is copyrighted and may only be
193 | modified in the following manner. The Agreement Steward reserves the right to
194 | publish new versions (including revisions) of this Agreement from time to
195 | time. No one other than the Agreement Steward has the right to modify this
196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
197 | Eclipse Foundation may assign the responsibility to serve as the Agreement
198 | Steward to a suitable separate entity. Each new version of the Agreement will
199 | be given a distinguishing version number. The Program (including
200 | Contributions) may always be distributed subject to the version of the
201 | Agreement under which it was received. In addition, after a new version of
202 | the Agreement is published, Contributor may elect to distribute the Program
203 | (including its Contributions) under the new version. Except as expressly
204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
205 | licenses to the intellectual property of any Contributor under this
206 | Agreement, whether expressly, by implication, estoppel or otherwise. All
207 | rights in the Program not expressly granted under this Agreement are
208 | reserved.
209 |
210 | This Agreement is governed by the laws of the State of New York and the
211 | intellectual property laws of the United States of America. No party to this
212 | Agreement will bring a legal action under this Agreement more than one year
213 | after the cause of action arose. Each party waives its rights to a jury trial
214 | in any resulting litigation.
215 |
--------------------------------------------------------------------------------
/src/snabbdom/snabbdom.min.js:
--------------------------------------------------------------------------------
1 | "use strict";var snabbdom={};function createElement(e,t){return document.createElement(e,t)}function createElementNS(e,t,n){return document.createElementNS(e,t,n)}function createTextNode(e){return document.createTextNode(e)}function createComment(e){return document.createComment(e)}function insertBefore(e,t,n){e.insertBefore(t,n)}function removeChild(e,t){t.parentNode&&t.parentNode.removeChild(t)}function appendChild(e,t){e.appendChild(t)}function parentNode(e){return e.parentNode}function nextSibling(e){return e.nextSibling}function tagName(e){return e.tagName}function setTextContent(e,t){e.textContent=t}function getTextContent(e){return e.textContent}function isElement(e){return 1===e.nodeType}function isText(e){return 3===e.nodeType}function isComment(e){return 8===e.nodeType}const htmlDomApi={createElement:createElement,createElementNS:createElementNS,createTextNode:createTextNode,createComment:createComment,insertBefore:insertBefore,removeChild:removeChild,appendChild:appendChild,parentNode:parentNode,nextSibling:nextSibling,tagName:tagName,setTextContent:setTextContent,getTextContent:getTextContent,isElement:isElement,isText:isText,isComment:isComment};function vnode(e,t,n,o,a){return{sel:e,data:t,children:n,text:o,elm:a,key:void 0===t?void 0:t.key}}const array=Array.isArray;function primitive(e){return"string"==typeof e||"number"==typeof e}function isUndef(e){return void 0===e}function isDef(e){return void 0!==e}const emptyNode=vnode("",{},[],void 0,void 0);function sameVnode(e,t){var n=e.key===t.key,o=(null===(o=e.data)||void 0===o?void 0:o.is)===(null===(o=t.data)||void 0===o?void 0:o.is);return e.sel===t.sel&&n&&o}function isVnode(e){return void 0!==e.sel}function createKeyToOldIdx(t,n,o){const a={};for(let e=n;e<=o;++e){var r=null===(r=t[e])||void 0===r?void 0:r.key;void 0!==r&&(a[r]=e)}return a}const hooks=["create","update","remove","destroy","pre","post"];function init$1(e,t){let n,o;const v={create:[],update:[],remove:[],destroy:[],pre:[],post:[]},p=void 0!==t?t:htmlDomApi;for(n=0;ni?y(e,null==n[s+1]?null:n[s+1].elm,n,r,s,o):g(e,t,a,i))}(i,l,d,e):isDef(d)?(isDef(t.text)&&p.setTextContent(i,""),y(i,null,d,0,d.length-1,e)):isDef(l)?g(i,l,0,l.length-1):isDef(t.text)&&p.setTextContent(i,""):t.text!==n.text&&(isDef(l)&&g(i,l,0,l.length-1),p.setTextContent(i,n.text)),null!==(i=null==r?void 0:r.postpatch)&&void 0!==i&&i.call(r,t,n)}}return function(e,t){let n,o,a;const r=[];for(n=0;n rendered
25 | k (assoc :key (cond
26 | (or (string? k)
27 | (number? k)) k
28 | (keyword? k) (str k)
29 | :default (hash k))))))
30 |
31 | (defn setup-animation-hooks [rendered animation {:keys [will-enter will-appear]}]
32 | (when will-appear
33 | (swap! animation assoc :will-appear will-appear))
34 | (cond-> rendered
35 | will-enter (assoc :willEnter #(swap! animation assoc :will-enter will-enter))
36 | will-appear (assoc :willAppear #(swap! animation dissoc :will-appear))))
37 |
38 | (defn- setup-mount-hook [rendered {:keys [on-mount on-render will-appear did-appear will-enter did-enter]} data args animation]
39 | (cond-> rendered
40 | (or on-mount on-render will-enter will-appear)
41 | (update-in
42 | [:data :hook :insert]
43 | (fn [insert-hook]
44 | (fn [vnode]
45 | (when insert-hook (insert-hook vnode))
46 | (when on-mount (apply on-mount (.-elm vnode) data args))
47 | (when on-render (apply on-render (.-elm vnode) data nil args))
48 | (let [{:keys [will-enter will-appear]} @animation]
49 | (when-let [callback (or will-enter will-appear)]
50 | (swap! animation assoc :ready? false)
51 | (apply callback
52 | (.-elm vnode)
53 | (fn []
54 | (swap! animation assoc :ready? true)
55 | (when-let [completion (if (= callback will-enter)
56 | did-enter
57 | did-appear)]
58 | (apply completion (.-elm vnode) data args)))
59 | data
60 | args))))))))
61 |
62 | (defn- setup-update-hook [rendered {:keys [on-update on-render]} data old-data args]
63 | (cond-> rendered
64 | (or on-update on-render)
65 | (assoc-in
66 | [:data :hook :update]
67 | (fn [old-vnode vnode]
68 | (when on-update (apply on-update (.-elm vnode) data old-data args))
69 | (when on-render (apply on-render (.-elm vnode) data old-data args))))))
70 |
71 | (defn- setup-unmount-hook [rendered component data args animation on-destroy]
72 | (cond-> rendered
73 | :always
74 | (assoc-in
75 | [:data :hook :destroy]
76 | (fn [vnode]
77 | (when-let [on-unmount (:on-unmount component)]
78 | (apply on-unmount (.-elm vnode) data args))
79 | (on-destroy)))
80 |
81 | (:will-leave component)
82 | (assoc-in
83 | [:data :hook :remove]
84 | (fn [vnode snabbdom-callback]
85 | (let [callback (fn []
86 | (when-let [did-leave (:did-leave component)]
87 | (apply did-leave (.-elm vnode) data args))
88 | (snabbdom-callback))]
89 | (if (:ready? @animation)
90 | (apply (:will-leave component) (.-elm vnode) callback data args)
91 | (add-watch animation :leave
92 | (fn [k r o n]
93 | (when (:ready? n)
94 | (remove-watch animation :leave)
95 | (apply (:will-leave component) (.-elm vnode) callback data args))))))))))
96 |
97 | (defn component
98 | "Returns a component function that uses the provided function for rendering. The
99 | resulting component will only call through to its rendering function when
100 | called with data that is different from the data that produced the currently
101 | rendered version of the component.
102 |
103 | The rendering function can be called with any number of arguments, but only
104 | the first one will influence rendering decisions. You should call the
105 | component with a single immutable value, followed by any number of other
106 | arguments, as desired. These additional constant arguments are suitable for
107 | passing messaging channels, configuration maps, and other utilities that are
108 | constant for the lifetime of the rendered element.
109 |
110 | The optional opts argument is a map with additional properties:
111 |
112 | :on-mount - A function invoked once, immediately after initial rendering. It
113 | is passed the rendered DOM node, and all arguments passed to the render
114 | function.
115 |
116 | :on-update - A function invoked immediately after an updated is flushed to the
117 | DOM, but not on the initial render. It is passed the underlying DOM node, the
118 | value, and any constant arguments passed to the render function.
119 |
120 | :on-render - A function invoked immediately after the DOM is updated, both on
121 | the initial render and subsequent updates. It is passed the underlying DOM
122 | node, the value, the old value, and any constant arguments passed to the
123 | render function.
124 |
125 | :on-unmount - A function invoked immediately before the component is unmounted
126 | from the DOM. It is passed the underlying DOM node, the most recent value and
127 | the most recent constant args passed to the render fn.
128 |
129 | :will-appear - A function invoked when this component is added to a mounting
130 | container component. Invoked at the same time as :on-mount. It is passed the
131 | underlying DOM node, a callback function, the most recent value and the most
132 | recent constant args passed to the render fn. The callback should be called to
133 | indicate that the element is done \"appearing\".
134 |
135 | :did-appear - A function invoked immediately after the callback passed
136 | to :will-appear is called. It is passed the underlying DOM node, the most
137 | recent value, and the most recent constant args passed to the render fn.
138 |
139 | :will-enter - A function invoked when this component is added to an already
140 | mounted container component. Invoked at the same time as :on.mount. It is
141 | passed the underlying DOM node, a callback function, the value and any
142 | constant args passed to the render fn. The callback function should be called
143 | to indicate that the element is done entering.
144 |
145 | :did-enter - A function invoked after the callback passed to :will-enter is
146 | called. It is passed the underlying DOM node, the value and any constant args
147 | passed to the render fn.
148 |
149 | :will-leave - A function invoked when this component is removed from its
150 | containing component. Is passed the underlying DOM node, a callback function,
151 | the most recent value and the most recent constant args passed to the render
152 | fn. The DOM node will not be removed until the callback is called.
153 |
154 | :did-leave - A function invoked after the callback passed to :will-leave is
155 | called (at the same time as :on-unmount). Is passed the underlying DOM node,
156 | the most recent value and the most recent constant args passed to the render
157 | fn."
158 | ([render] (component render {}))
159 | ([render opt]
160 | (when *render-eagerly?*
161 | (reset! eager-render-required? true))
162 | (let [instances (atom {})]
163 | (fn [data & args]
164 | (let [comp-fn
165 | (fn [path k]
166 | (let [key (when-let [keyfn (:keyfn opt)] (keyfn data))
167 | fullpath (conj path (or key k))
168 | instance (@instances fullpath)
169 | animation (atom {:ready? true})]
170 | (if (should-component-update? instance data)
171 | (let [rendered
172 | (some->
173 | (when-let [vdom (apply render data args)]
174 | ((e/inflate-hiccup vdom) fullpath 0))
175 | #?(:cljs (set-key key))
176 | #?(:cljs (setup-animation-hooks animation opt))
177 | #?(:cljs (setup-unmount-hook opt data args animation #(swap! instances dissoc fullpath))))]
178 | (swap! instances assoc fullpath {:vdom rendered :data data})
179 | ;; The insert and update hooks are added after the instance
180 | ;; is cached. When used from the cache, we never want
181 | ;; insert or update hooks to be called. Snabbdom will
182 | ;; occasionally call these even when there are no changes,
183 | ;; because it uses identity to determine if a vdom node
184 | ;; represents a change. Since dumdom always produces a new
185 | ;; JavaScript object, Snabbdom's check will have false
186 | ;; positives.
187 | (some-> rendered
188 | #?(:cljs (setup-mount-hook opt data args animation))
189 | #?(:cljs (setup-update-hook opt data (:data instance) args))))
190 | (:vdom instance))))]
191 | #?(:cljs (set! (.-dumdom comp-fn) true))
192 | comp-fn)))))
193 |
194 | (defn single-child? [x]
195 | (or (fn? x) ;; component
196 | (and (vector? x)
197 | (keyword? (first x))) ;; hiccup
198 | ))
199 |
200 | (defn TransitionGroup [el-fn opt children]
201 | ;; Vectors with a function in the head position are interpreted as hiccup data
202 | ;; - force children to be seqs to avoid them being parsed as hiccup.
203 | (let [children (if (single-child? children)
204 | (list children)
205 | (seq children))]
206 | (if (ifn? (:component opt))
207 | ((:component opt) children)
208 | (apply el-fn (or (:component opt) "span") opt children))))
209 |
210 | (defn- complete-transition [node timeout callback]
211 | (if timeout
212 | #?(:cljs (js/setTimeout callback timeout))
213 | (let [callback-fn (atom nil)
214 | f (fn []
215 | (callback)
216 | (.removeEventListener node "transitionend" @callback-fn))]
217 | (reset! callback-fn f)
218 | (.addEventListener node "transitionend" f))))
219 |
220 | (defn- transition-classes [transitionName transition]
221 | (if (string? transitionName)
222 | [(str transitionName "-" transition) (str transitionName "-" transition "-active")]
223 | (let [k (keyword transition)
224 | k-active (keyword (str transition "Active"))]
225 | [(k transitionName) (get transitionName k-active (str (k transitionName) "-active"))])))
226 |
227 | (defn- animate [transition {:keys [enabled-by-default?]}]
228 | (let [timeout (keyword (str "transition" transition "Timeout"))]
229 | (fn [node callback {:keys [transitionName] :as props}]
230 | (if (get props (keyword (str "transition" transition)) enabled-by-default?)
231 | (let [[init-class active-class] (transition-classes transitionName (.toLowerCase transition))]
232 | (.add (.-classList node) init-class)
233 | (complete-transition node (get props timeout) callback)
234 | #?(:cljs (js/setTimeout #(.add (.-classList node) active-class) 0)))
235 | (callback)))))
236 |
237 | (defn- cleanup-animation [transition]
238 | (fn [node {:keys [transitionName]}]
239 | (.remove (.-classList node) (str transitionName "-" transition))
240 | (.remove (.-classList node) (str transitionName "-" transition "-active"))))
241 |
242 | (def TransitioningElement
243 | (component
244 | (fn [{:keys [child]}]
245 | child)
246 | {:will-appear (animate "Appear" {:enabled-by-default? false})
247 | :did-appear (cleanup-animation "appear")
248 | :will-enter (animate "Enter" {:enabled-by-default? true})
249 | :did-enter (cleanup-animation "enter")
250 | :will-leave (animate "Leave" {:enabled-by-default? true})}))
251 |
252 | (defn CSSTransitionGroup [el-fn opt children]
253 | (let [children (if (single-child? children)
254 | (list children)
255 | (seq children))]
256 | (TransitionGroup el-fn opt (map #(TransitioningElement (assoc opt :child %)) children))))
257 |
258 | (defn component? [x]
259 | (and x (.-dumdom x)))
260 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # dumdom - The dumb DOM component library
2 |
3 | **dumdom** is a component library that renders (and re-renders) immutable data
4 | efficiently. It delivers on the basic value proposition of React and its peers
5 | while eschewing features like component local state and object oriented APIs,
6 | and embracing ClojureScript features like immutable data structures.
7 |
8 | **dumdom** is API compatible with
9 | [Quiescent](https://github.com/levand/quiescent/), and can be used as a drop-in
10 | replacement for it so long as you don't use React features directly. Refer to
11 | [differences from React](#differences-from-react) for things to be aware of.
12 |
13 | **dumdom** is currently a wrapper for
14 | [Snabbdom](https://github.com/snabbdom/snabbdom), but that should be considered
15 | an implementation detail, and may be subject to change. Using snabbdom features
16 | not explicitly exposed by dumdom is **not** recommended.
17 |
18 | **dumdom** aims to be finished, stable, and worthy of your trust. Breaking
19 | changes will never be intentionally introduced to the codebase. For this reason,
20 | dumdom does not adhere to the "semantic" versioning scheme.
21 |
22 | In addition to being API compatible with Quiescent, **dumdom** supports:
23 |
24 | - Rendering to strings (useful for server-side rendering from both the JVM and node.js)
25 | - Efficient "inflation" of server-rendered markup on the client side
26 | - Hiccup syntax for components
27 |
28 | ## Table of contents
29 |
30 | * [Install](#install)
31 | * [Example](#example)
32 | * [Rationale](#rationale)
33 | * [Limitations](#limitations)
34 | * [Differences from Quiescent](#differences-from-quiescent)
35 | * [Using with Devcards](#using-with-devcards)
36 | * [Contribute](#contribute)
37 | * [Documentation](#documentation)
38 | * [Building virtual DOM](#building-virtual-dom)
39 | * [Event listeners](#event-listeners)
40 | * [Creating components](#creating-components)
41 | * [CSS transitions](#css-transitions)
42 | * [Class name transitions](#class-name-transitions)
43 | * [Refs](#refs)
44 | * [Server-rendering](#server-rendering)
45 | * [API Docs](#api-docs)
46 | * [Examples](#examples)
47 | * [Changelog](#changelog)
48 | * [Roadmap](#roadmap)
49 | * [License](#license)
50 |
51 | ## Install
52 |
53 | With tools.deps:
54 |
55 | ```clj
56 | cjohansen/dumdom {:mvn/version "2021.06.21"}
57 | ```
58 |
59 | With Leiningen:
60 |
61 | ```clj
62 | [cjohansen/dumdom "2021.06.21"]
63 | ```
64 |
65 | ## Example
66 |
67 | Using hiccup-style data:
68 |
69 | ```clj
70 | (require '[dumdom.core :as dumdom :refer [defcomponent]])
71 |
72 | (defcomponent heading
73 | :on-render (fn [dom-node val old-val])
74 | [data]
75 | [:h2 {:style {:background "#000"}} (:text data)])
76 |
77 | (defcomponent page [data]
78 | [:div
79 | [heading (:heading data)]
80 | [:p (:body data)]])
81 |
82 | (dumdom/render
83 | [page {:heading {:text "Hello world"}
84 | :body "This is a web page"}]
85 | (js/document.getElementById "app"))
86 | ```
87 |
88 | Using the Quiescent-compatible function API:
89 |
90 | ```clj
91 | (require '[dumdom.core :as dumdom :refer [defcomponent]]
92 | '[dumdom.dom :as d])
93 |
94 | (defcomponent heading
95 | :on-render (fn [dom-node val old-val])
96 | [data]
97 | (d/h2 {:style {:background "#000"}} (:text data)))
98 |
99 | (defcomponent page [data]
100 | (d/div {}
101 | (heading (:heading data))
102 | (d/p {} (:body data))))
103 |
104 | (dumdom/render
105 | (page {:heading {:text "Hello world"}
106 | :body "This is a web page"})
107 | (js/document.getElementById "app"))
108 | ```
109 |
110 | ## Rationale
111 |
112 | Of the many possible options, [Quiescent](https://github.com/levand/quiescent)
113 | is to me the perfect expression of "React in ClojureScript". It's simple,
114 | light-weight, does not allow component-local state, and pitches itself as
115 | strictly a rendering library, not a state management tool or UI framework.
116 |
117 | While Quiescent has been done (as in "complete") for a long time, it is built on
118 | React, which is on a cycle of recurring "deprecations" and API changes, making
119 | it hard to keep Quiescent up to date with relevant security patches etc. At the
120 | same time, React keeps adding features which are of no relevance to the API
121 | Quiescent exposes, thus growing the total bundle size for no advantage to
122 | its users.
123 |
124 | **dumdom** provides the same API as that of Quiescent, but does not depend on
125 | React. It aims to be as stable and complete as Quiescent, but still be able to
126 | ship occasional security patches as they are made to the underlying virtual DOM
127 | library. **dumdom** aims to reduce the amount of churn in your UI stack.
128 |
129 | ## Limitations
130 |
131 | Because **dumdom** is not based on React, you opt out of the "React ecosystem"
132 | entirely by using it. If you depend on a lot of open source/shared React
133 | components, or other React-oriented tooling, **dumdom** might not be the best
134 | fit for you.
135 |
136 | Because **dumdom** does not offer any kind of component local state, it cannot
137 | be used as a wholistic UI framework - it's just a rendering library. It does not
138 | come with any system for routing, dispatching actions, or managing state (either
139 | inside or outside of components), and is generally a batteries-not-included
140 | tool. I consider this a strength, others may see it differently.
141 |
142 | ## Differences from Quiescent
143 |
144 | Dumdom strives to be API compliant with Quiescent to the degree that it should
145 | be a drop-in replacement for Quiescent in any project that does not rely
146 | explicitly on any React APIs or third-party components. It does not necessarily
147 | commit to all the same restrictions that the Quiescent API imposes. The
148 | following is a list of minor details between the two:
149 |
150 | - Quiescent does not allow the use of `:on-render` along with either of
151 | `:on-mount` and `:on-update`. Dumdom acknowledges that some components will
152 | implement `:on-render` *and* `:on-mount` or `:on-update`, and allows this.
153 | - Dumdom doesn't really care about `TransitionGroup`. You are free to use them,
154 | but the animation callbacks will work equally well outside `TransitionGroup`.
155 | This may cause breakage in some cases when porting from Quiescent to Dumdom.
156 | The risk is pretty low, and the upside is significant enough to allow Dumdom
157 | to take this liberty.
158 |
159 | ## Differences from React
160 |
161 | In React, [`onChange` is really
162 | `onInput`](https://github.com/facebook/react/issues/9567). This is not true in
163 | dumdom. When swapping out Quiescent and React for dumdom, you must replace
164 | all occurrences of `onChange` with `onInput` to retain behavior.
165 |
166 | ## Using with Devcards
167 |
168 | [Devcards](https://github.com/bhauman/devcards) is a system for rendering React
169 | components in isolation. Because **dumdom** components are not React components,
170 | they need some wrapping for Devcards to make sense of them.
171 |
172 | You need to add [dumdom-devcards](https://github.com/cjohansen/dumdom-devcards)
173 | as a separate dependency. Then use the `dumdom.devcards` namespace just like you
174 | would `devcards.core`:
175 |
176 | ```clj
177 | (require '[dumdom.devcards :refer-macros [defcard]])
178 |
179 | (defcard my-dumdom-card
180 | (my-dumdom-component {:value 0}))
181 | ```
182 |
183 | ## Contribute
184 |
185 | Feel free to report bugs and, even better, provide bug fixing pull requests!
186 | Make sure to add tests for your fixes, and make sure the existing ones stay
187 | green before submitting fixes.
188 |
189 | ```sh
190 | make test
191 | ```
192 |
193 | You can also run the tests in a browser with figwheel, which might be more
194 | useful during development:
195 |
196 | ```sh
197 | clojure -A:dev:repl
198 | ```
199 |
200 | Then open [http://localhost:9595/figwheel-extra-main/tests](http://localhost:9595/figwheel-extra-main/tests).
201 |
202 | If you're not yet sure how to formulate a test for your feature, fire up
203 | [http://localhost:9595/](http://localhost:9595/) and play around in
204 | [./dev/dumdom/dev.cljs](./dev/dumdom/dev.cljs) until you figure it out. More
205 | visually oriented code can be tested with devcards instead. Add a devcard to
206 | [./devcards/dumdom](./devcards/dumdom), and inspect the results at
207 | [http://localhost:9595/devcards.html](http://localhost:9595/devcards.html)
208 |
209 | If you have ideas for new features, please open an issue to discuss the idea and
210 | the API before implementing it to avoid putting lots of work into a pull request
211 | that might be rejected. I intend to keep **dumdom** a focused package, and don't
212 | want it to accrete a too wide/too losely coherent set of features.
213 |
214 | ### Running from Emacs
215 |
216 | There is a `.dir-locals.el` file in the root of this repo to help you out. Run
217 | `cider-jack-in-cljs`, and you should get a REPL and figwheel running on port 9595:
218 |
219 | - [Dev scratchpad](http://localhost:9595/)
220 | - [Devcards](http://localhost:9595/devcards.html)
221 | - [Tests](http://localhost:9595/figwheel-extra-main/tests)
222 |
223 | ## Documentation
224 |
225 | The vast majority of use-cases are covered by using [hiccup-style markup]() for
226 | DOM elements, defining custom components with [`defcomponent`](#defcomponent),
227 | and rendering the resulting virtual DOM to an element with [`render`](#render):
228 |
229 | ```clj
230 | (require '[dumdom.core :as dumdom :refer [defcomponent]])
231 |
232 | (defcomponent my-component [data]
233 | [:div
234 | [:h1 "Hello world!"]
235 | [:p (:message data)]])
236 |
237 | (dumdom/render
238 | (my-component {:message "Hello, indeed"})
239 | (js/document.getElementById "app"))
240 | ```
241 |
242 | Components defined by `defcomponent` are functions, as demonstrated in the above
243 | example. You can also use them for hiccup markup, e.g.:
244 |
245 | ```clj
246 | (dumdom/render
247 | [my-component {:message "Hello, indeed"}]
248 | (js/document.getElementById "app"))
249 | ```
250 |
251 | The strength of hiccup markup is being able to represent DOM structures as pure
252 | data. Because functions are not data, there is no real benefit to using hiccup
253 | syntax for custom components, so I typically don't, but it doesn't make any
254 | difference either way.
255 |
256 | ### Building virtual DOM
257 |
258 | Virtual DOM elements are built with hiccup markup:
259 |
260 | ```clj
261 | [tagname attr? children...]
262 | ```
263 |
264 | `tagname` is always a keyword, attributes are in an optional map, and there
265 | might be one or more children, or a list of children. Beware that children
266 | should not be provided as a vector, lest it be interpreted as a new hiccup
267 | element.
268 |
269 | **Note:** dumdom currently does not support inlining class names and ids on the
270 | tag name selector (e.g. `:div.someclass#someid`). This might be added in a
271 | future release.
272 |
273 | For API compatibility with Quiescent, elements can also be created with the
274 | functions in `dumdom.dom`:
275 |
276 | ```clj
277 | (dumdom.dom/div {:style {:border "1px solid red"}} "Hello world")
278 | ```
279 |
280 | Note that with these functions, the attribute map is not optional, and must
281 | always be provided, even if empty.
282 |
283 | #### Keys
284 |
285 | You can specify the special attribute `:key` do help dumdom recognize DOM
286 | elements that move. `:key` should be set to a value that is unique among the
287 | element's siblings. For instance, if you are rendering lists of things, setting
288 | a key on each item means dumdom can update the rendered view by simply moving
289 | existing elements around in the DOM. Not setting the key will lead dumdom to
290 | work harder to align the DOM with the virtual representation:
291 |
292 | ```clj
293 | (require '[dumdom.core :as dumdom :refer [defcomponent]])
294 |
295 | (defcomponent list-item [fruit]
296 | [:li {:key fruit} fruit])
297 |
298 | (def el (js/document.getElementById "app"))
299 |
300 | (dumdom/render [:ul (map list-item ["Apples" "Oranges" "Kiwis"])] el)
301 |
302 | ;; This will now result in reordering the DOM elements, instead of recreating them
303 | (dumdom/render [:ul (map list-item ["Oranges" "Apples" "Kiwis"])] el)
304 | ```
305 |
306 | ### Event listeners
307 |
308 | To attach events to your virtual DOM nodes, provide functions to camel-cased
309 | event name keys in the attribute map:
310 |
311 | ```clj
312 | [:a {:href "#"
313 | :onClick (fn [e]
314 | (.preventDefault e)
315 | (prn "You clicked me!"))} "Click me!"]
316 | ```
317 |
318 | ### Creating components
319 |
320 | You create components with `defcomponent` or `component` - the first is
321 | just a convenience macro for `def` + `component`:
322 |
323 | ```clj
324 | (require '[dumdom.core :refer [component defcomponent]])
325 |
326 | (defcomponent my-component
327 | :on-render (fn [e] (js/console.log "Rendered" e))
328 | [data]
329 | [:div "Hello world"])
330 |
331 | ;; ...is the same as:
332 |
333 | (def my-component
334 | (component
335 | (fn [data]
336 | [:div "Hello world"])
337 | {:on-render (fn [e] (js/console.log "Rendered" e))}))
338 | ```
339 |
340 | Refer to the API docs for [`component`](#component) for details on what options
341 | it supports, life-cycle hooks etc, and the API docs for
342 | [`defcomponent`](#defcomponent) for more on how to use it.
343 |
344 | A dumdom component is a function. When you call it with data it returns
345 | something that dumdom knows how to render, e.g.:
346 |
347 | ```clj
348 | (dumdom.core/render (my-component {:id 42}) root-el)
349 | ```
350 |
351 | You can also invoke the component with hiccup markup, although there is no real
352 | benefit to doing so - the result is exactly the same:
353 |
354 | ```clj
355 | (dumdom.core/render [my-component {:id 42}] root-el)
356 | ```
357 |
358 | #### Component arguments
359 |
360 | When you call a dumdom component with data, it will recreate the virtual DOM
361 | node only if the data has changed since it was last called. However, this
362 | decision is based solely on the first argument passed to the component. So while
363 | you can pass any number of arguments to a component beware that only the first
364 | one is used to influence rendering decisions.
365 |
366 | This design is inherited from Quiescent, and the idea is that you can pass along
367 | things like core.async message channels without having them interferring with
368 | the rendering decisions. When passing more than one argument to a dumdom
369 | component, make sure that any except the first one are constant for the lifetime
370 | of the component.
371 |
372 | This only applies to components created with `component`/`defcomponent`, not
373 | virtual DOM functions, which take any number of DOM children.
374 |
375 | ### CSS transitions
376 |
377 | CSS transitions can be defined inline on components to animate the appearing or
378 | disappearing of elements. There are three keys you can use to achieve this
379 | effect:
380 |
381 | - `:mounted-style` - Styles that will apply after the element has been mounted
382 | - `:leaving-style` - Styles that will apply before the element is removed from
383 | its parent - the element will not be removed until all its transitions
384 | complete
385 | - `:disappearing-style` - Styles that will apply before the element is removed
386 | along with its parent element is being removed - the element will not be
387 | removed until all its transitions are complete
388 |
389 | As an example, if you want an element to fade in, set its opacity to 0, and then
390 | its `:mounted-style` opacity to 1. To fade it out as well, set its
391 | `:leaving-styles` opacity to 0 again. Remember to enable transitions for the
392 | relevant CSS property:
393 |
394 | ```clj
395 | [:div {:style {:opacity "0"
396 | :transition "opacity 0.25s"}
397 | :mounted-style {:opacity "1"}
398 | :leaving-style {:opacity "0"}}
399 | "I will fade both in and out"]
400 | ```
401 |
402 | ### Class name transitions
403 |
404 | In order to be API compatible with Quiescent, dumdom supports React's
405 | `CSSTransitionGroup` for doing enter/leave transitions with class names instead
406 | of inline CSS. Given the following CSS:
407 |
408 | ```css
409 | .example-leave {
410 | opacity: 1;
411 | transition: opacity 0.25s;
412 | }
413 |
414 | .example-leave-active {
415 | opacity: 0;
416 | }
417 | ```
418 |
419 | Then we could fade out an element with:
420 |
421 | ```clj
422 | (require '[dumdom.core :refer [CSSTransitionGroup]])
423 |
424 | (CSSTransitionGroup {:transitionName "example"}
425 | [[:div "I will fade out"]])
426 | ```
427 |
428 | Note that `CSSTransitionGroup` takes a vector/seq of children. Refer to the
429 | [API docs for `CSSTransitionGroup`](#css-transition-group) for more details. In
430 | general, using inline CSS transitions will be more straight-forward, and is
431 | recommended.
432 |
433 | ### Refs
434 |
435 | A `:ref` on an element is like an `:on-mount` callback that you can attach from
436 | "the outside":
437 |
438 | ```clj
439 | ;; NB! Just an example, there are better ways to do this with CSS
440 |
441 | (defn square-element [el]
442 | (set! (.. el -style -height) (str (.-offsetWidth el) "px")))
443 |
444 | [:div {:style {:border "1px solid red"}
445 | :ref square-element} "I will be in a square box"]
446 | ```
447 |
448 | The `:ref` function will be called only once, when the element is first mounted.
449 | Use this feature with care - do not use it with functions that behave
450 | differently at different times. Consider this example:
451 |
452 | ```clj
453 | (defcomponent my-component [data]
454 | [:div
455 | [:h1 "Example"]
456 | [:div {:ref (when (:actionable? data)
457 | setup-click-indicator)}
458 | "I might or might not be clickable"]])
459 | ```
460 |
461 | While this looks reasonable, refs are only called when the element mounts. Thus,
462 | if the value of `(:actionable? data)` changes, the changes will not be reflected
463 | on the element. If you need to conditionally make changes to an element this
464 | way, create a custom component and use the `:on-render` hook instead, which is
465 | called every time data changes.
466 |
467 | ### Server rendering
468 |
469 | Dumdom supports rendering your components to strings on the server and then
470 | "inflating" the view client-side. Inflating consists of associating the
471 | resulting DOM elements with their respective virtual DOM nodes, so dumdom can
472 | efficiently update your UI, and adding client-side event handlers so users can
473 | interact with your app.
474 |
475 | Even though it sounds straight-forward, using server rendering requires that
476 | you write your entire UI layer in a way that can be loaded on both the server
477 | and client. This is easier said than done.
478 |
479 | To render your UI to a string on the server:
480 |
481 | ```clj
482 | (require '[dumdom.string :as dumdom])
483 |
484 | (defn body []
485 | (str "