-
Notifications
You must be signed in to change notification settings - Fork 8
/
ch05-reactive.html
362 lines (288 loc) · 18.5 KB
/
ch05-reactive.html
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
<section data-type="chapter" id="chapter05">
<h1>Reactive Programming</h1>
<p>
Reactive programming is a way of programming that lets you specify how data flows through user interface components; once this is set up, the DOM updating is handled for you automagically. (As noted in <a href="#chapter02" data-type="xref">#chapter02</a>, this is what all the Cool Kids<sup>™</sup> are using.)
<em>Insert further verbiage about the wonderfulness of reactive programming</em>
</p>
<p>
In this chapter, you will write études that use different ClojureScript libraries that interface with <a href="http://facebook.github.io/react/">Facebook®’s React JavaScript library</a>. <a href="http://blog.peeja.com/blog/2014/10/01/react-four-ways-how-to-use-react-in-clojurescript/">This blog post</a> gives you a comparison of the libraries. The two we will use are <a href="https://github.com/levand/quiescent">Quiescent</a> and <a href="http://reagent-project.github.io/">Reagent</a>
</p>
<p>
These études will implement the same web page: a page that displays an image and lets you adjust its width, height, and (via CSS) its border width and style. In both libraries, you will build <em>components</em>, which are functions that, as the Quiescent documentation puts it, tell “how a particular piece of data should be rendered to the DOM.” Since they are functions, they can use all of ClojureScript’s computational power.
</p>
<figure id="reactive_example_figure">
<img src="images/reactive_example.png" alt="Screenshot showing user interface and image"/>
<figcaption>Screenshot of Image Resize Web Page</figcaption>
</figure>
<p>
The HTML for the page will include a <code><div id="interface"></code>, which is where the components will go.
</p>
<p>
Both versions of this étude will declare an <code>atom</code> (with a slight variation for Reagant) to hold the state of the the application in a map. Let’s do a quick review of atoms by defining an atom with a single value:
</p>
<pre data-type="programlisting">(def quantity (atom 32))
cljs.user=> #<Atom:32></pre>
<p>
To access the data in an atom, you must dereference it with the `@` operator:
</p>
<pre data-type="programlisting">cljs.user=>@quantity
32</pre>
<p>
To update an atom’s data, use the <code><a href="http://clojuredocs.org/clojure.core/swap!">swap!</a></code> function (for individual map values) and <code>reset!</code> (for the entire value of the atom). The <code>swap!</code> function takes as its
arguments:
</p>
<ul>
<li>the atom to be modified</li>
<li>A function to apply to the atom</li>
<li>The arguments to that function (if any)</li>
</ul>
<p>
Thus, in the REPL:
</p>
<pre data-type="programlisting">cljs.user=> (swap! quantity inc)
33
cljs.user=> (swap! quantity * 2)
66
cljs.user=> (reset! quantity 47)
47
cljs.user=> quantity
#<Atom: 47>
cljs.user=> @quantity
47</pre>
<p>
However, in most ClojureScript programs, you do not create an atom for each part of the state you need to save. Instead, you will most often use a map.
</p>
<pre data-type="programlisting">cljs.user=> (def inventory (atom {:quantity 32 :price 3.75}))
#<Atom: {:quantity 32, :price 3.75}>
cljs.user=> (swap! inventory assoc :price 4.22)
{:quantity 32, :price 4.22}
cljs.user=> (swap! inventory assoc :quantity (inc (:quantity @inventory)))
{:quantity 33, :price 4.22}
cljs.user=> @inventory
{:quantity 33, :price 4.22}</pre>
<p>
Back to the program for this étude. The page has to keep track of:
</p>
<ul>
<li>The image’s current width and height</li>
<li>Whether you want the width and height to stay in proportion or not</li>
<li>The border width and style (intially three pixels solid)—the color will be set to red for visibility</li>
<li>The image’s original width and height (needed to do proportional scaling properly)</li>
<li>The image file name</li>
</ul>
<p>That gives us this atom.</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defonce status
(atom {:w 0 :h 0 :proportional true
:border-width 3 :border-style "solid"
:orig-w 0 :orig-h 0 :src "clock.jpg"}))</pre>
<!-- Fill in -->
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<section data-type="sect1" id="ETUDE05-01">
<h1>Étude 5-1: Reactive Programming with Quiescent</h1>
<p>
To use Quiescent, add <code>[quiescent "0.2.0-alpha1"]</code> to your project’s dependencies and add requirements to your namespace:
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(:require [quiescent.core :as q]
[quiescent.dom :as d])</pre>
<p>
As an example, let’s define a simple component that displays an input area and some text that goes with the <code>w</code> field in the atom that was defined previously.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(q/defcomponent Example
:name "Example"
[status]
(d/div {}
"Your input here: "
(d/input {:type "text"
:value (:w status)
:size "5"})
(d/br)
"Your input, squared: "
(d/span {} (* (:w status) (:w status)))))</pre>
<p>
The general format for creating an HTML element inside a component is to give its element name, a map of its attributes (or the empty map <code>{}</code> if there are no attributes, as on the <code>div</code>), and the element content, which may contain other elements. The <code>:name</code> before the parameter list gives the component a name for React to use. The key/value pairs before the parameter list make up the component configuration; this is described in detail <a href="https://github.com/levand/quiescent/blob/release/docs.md">in the Quiescent Documentation</a>. The value of the input field and <code>span</code> are provided by the current value of the <code>:w</code> key in the <code>status</code> atom.
</p>
<p>
The only thing remaining to do is to render the component. In Quiescent, the <code>q/render</code> function renders a component once. If you want continuous rendering, you can use JavaScript’s <code>requestAnimationFrame</code> to repeat the process. Remember, when using React, only the components that have changed get re-rendered, so you don’t need to worry about that using <code>requestAnimationFrame</code> will eat your CPU alive.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defn render
"Render the current state atom, and schedule a render on the next frame"
[]
(q/render (Example @status) (aget (.getElementsByTagName js/document "body") 0))
(.requestAnimationFrame js/window render))
(render)</pre>
<p>
Quiescent’s <code>render</code> function takes two arguments: A call to the component with its argument—in this case, the de-referenced atom, and the DOM node where you want the component rooted. For this example, that’s the first (and, we hope, only) <code><body></code> element.
</p>
<p>
If you compile this code and then load the <em>index.html</em> file, you will see a zero in the input and output area - but you will also find that you cannot type into the field. That is because Quiescent and React always keep the DOM value and the atom value synchronized, and since the value in the atom never changes, neither can the field. To fix that, add this code to the input element (it is in bold):
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(d/input {:type "text"
:value (:w status)
<strong>:onChange update-value</strong>
:size "5"})</pre>
<p>
And then write the <code>update-value</code> function, which takes the value from the event target and puts it into the atom that keeps the page’s state.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defn update-value [evt]
(swap! status assoc :w (.-value (.-target evt))))</pre>
<p>
Voilà—your page now updates properly.
</p>
<section data-type="sect2" id="quiescent-hints">
<h2>Hints</h2>
<ol>
<li><p>You will have to initialize the values for the image’s original width and height. To do this, you add an <code>:onLoad</code> clause to the properties of the image component. Its value is a function that handles the event by setting the width, height, original width, and original height. Use the <code>naturalWidth</code> and <code>naturalHeight</code> properties of the image. Those properties do not work with Internet Explorer 8, but will work in Intenet Explorer 9+.
</p></li>
<li><p>Handling the checkbox also requires some extra care. The value of the <code>checked</code> attribute isn’t the checkbox’s value, so you will have to use <code>:on-mount</code> to initialize the checkbox, and you will have to directly change the checkbox status with code like this:</p>
<pre data-type="programlisting" data-code-language="clojurescript">(set! (.-checked (.getElementById js/document "prop"))</pre>
<p>
Here is an example of <code>:on-mount</code> to initialize the example’s input field to the current minute of the hour. <code>:on-mount</code> is followed by the definition of a function that has the current node as its argument.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(q/defcomponent Example
:name "Example"
<strong>:on-mount (fn [node]
(swap! status assoc :w (.getMinutes (js/Date.))))</strong>
[status]
;; etc.</pre>
</li>
<li><p>If you want to use a list to initialize the drop-down menu, you will need to define a component for menu options and then use <code>apply</code> and <code>map</code> cleverly. This took me a <em>long</em> time to get right, so I’m giving you the code for free with an abbreviated example.</p>
<pre data-type="programlisting" data-code-language="clojurescript">(q/defcomponent Option
[item]
(d/option {:value item} item))
;; then, in the component that builds the form:
(apply d/select {:id "menu" :onChange change-border}
(map Option ["none" "solid" "dotted" "<em>etc.</em>"]))</pre>
</li>
</ol>
</section>
<p>See a suggested solution: <a href="#SOLUTION05-ET01" data-type="xref">#SOLUTION05-ET01</a>.</p>
</section>
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<section data-type="sect1" id="ETUDE05-02">
<h1>Étude 5-2: Reactive Programming with Reagent</h1>
<p>
To use Reagent, add <code>[reagent "0.5.0"]</code> to your project’s dependencies and add this requirement to your namespace:
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(:require [reagent.core :as reagent :refer [atom])</pre>
<p>
Note the <code>:refer [atom]</code> clause; Reagent has its own definition of <code>atom</code> that plays nicely with React; it is defined so that you can use it exactly the way you would use a normal ClojureScript atom.
</p>
<p>
As an example, let’s define a simple component that displays an input area and some text that goes with the <code>w</code> field in the atom that was defined previously.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defn example []
[:div
"Your input here:"
[:input {:type "text"
:value (:w @status)
:size "5"}]
[:br]
"Your input, squared: "
[:span (* (:w @status) (:w @status))]])</pre>
<p>
The general format for creating an HTML element inside a component is to create a vector whose first element is a keyword giving the HTML element name, a map of its attributes (if any), and the element content, which may contain other elements. The value of the input field and <code>span</code> are provided by the current value of the <code>:w</code> key in the <code>status</code> atom. Unlike Quiescent, you must dereference the atom.
</p>
<p>
The only thing remaining to do is to render the component. You don’t have to request animation frames; Reagent handles that for you.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defn run []
(reagent/render [example]
(aget (.getElementsByTagName js/document "body") 0)))
(run)
</pre>
<p>
Reagent’s <code>render</code> function takes two arguments: A call to the component and the DOM node where you want the component rooted. In this case, the first (and, we hope, only) <code><body></code> element.
</p>
<p>
If you compile this code and then load the <em>index.html</em> file, you will see a zero in the input and output area - but you will also find that you cannot type into the field. That is because Reagent and React always keep the DOM value and the atom value synchronized, and since the value in the atom never changes, neither can the field. To fix that, add this code to the input element (it is in bold):
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(d/input {:type "text"
:value (:w status)
<strong>:on-change update-value</strong>
:size "5"})</pre>
<p>
And then write the <code>update-value</code> function, which takes the value from the event target and puts it into the atom that keeps the page’s state.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defn update-value [evt]
(swap! status assoc :w (.-value (.-target evt))))</pre>
<p>
Voilà—your page now updates properly.
</p>
<section data-type="sect2" id="reagent-hints">
<h2>Hints</h2>
<ol>
<li><p>You will have to initialize the values for the image’s original width and height. To do this, you add an <code>:on-load</code> clause to the properties of the image component. Its value is a function that handles the event by setting the width, height, original width, and original height. Use the <code>naturalWidth</code> and <code>naturalHeight</code> properties of the image. Those properties do not work with Internet Explorer 8, but will work in Intenet Explorer 9+.
</p></li>
<li><p>Handling the checkbox also requires some extra care. The value of the <code>checked</code> attribute isn’t the checkbox’s value, so you will have to directly change the checkbox status with code like this:</p>
<pre data-type="programlisting" data-code-language="clojurescript">(set! (.-checked (.getElementById js/document "prop"))</pre>
<p>
Initializing the checkbox takes a bit more work in Reagent. You must define a symbol that adds meta-information to the <code>example</code> component. This information includes a function that does the initialization. Here is an example that initializes the example’s input field to the current minute of the hour. You then render the new component:
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(def init-example
(with-meta example
{:component-will-mount
(fn [this]
(swap! status assoc :w (.getMinutes (js/Date.))))}))</pre>
</li>
<li><p>If you want to use a list to initialize the drop-down menu, you will need to define a component for menu options and then use <code>for</code>. This took me a <em>long</em> time to get right, so I’m giving you the code for free with an abbreviated example. React is not happy if each <code>option</code> does not have a unique key, so this code adds it.</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defn option [item]
[:option {:value item :key item} item])
;; then, in the component that builds the form:
[:select {:id "menu" :on-change change-border}
(for [item ["none" "solid" "dotted" "<em>etc.</em>"]]
(option item))]])</pre>
</li>
</ol>
</section>
<p>See a suggested solution: <a href="#SOLUTION05-ET02" data-type="xref">#SOLUTION05-ET02</a>.</p>
</section>
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - -->
<!--
<section data-type="sect1" id="ETUDE05-02">
<h1>>Étude 5-2: Reactive Programming with Om</h1>
<p>
Om is much closer to React than Quiescent is. To use Om, add <code>[org.omcljs/om "0.8.8"]</code> to your project’s dependencies and add requirements to your namespace:
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(:require [om.core :as om]
[om.dom :as dom])</pre>
<p>
As an example, let’s define a simple component that displays an input area and some text that goes with the <code>w</code> field in the atom that was defined previously. In the following code, <code>data</code> refers to a <em>cursor</em>, which is a path to the part of the application state that a component needs in order to do its work. (In this example, we could have stored the state locally to the component, but, again, the étude you will write will have inter-dependent components.) The <code>owner</code> is the React component that is behind the scenes.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defn example [data owner]
(reify
om/IRender
(render [_]
(dom/div nil
(dom/p nil "Your input here: "
(dom/input #js{:type "text" :size "5" :value (:w @data))
(dom/p nil "Square of number:" (* (:w @data) (:w @data)))))))</pre>
<p>
Unlike Quiescent, which uses an ordinary ClojureScript map for attributes, you must use a JavaScript object for the attributes; hence the leading <code>#js</code>.
</p>
<p>
That is the long form for defining a component. If you are going to simply render a component (rather than initialize it when it mounts, etc.) and you don’t need to access the owner, you can simplify the code with <code>om/component</code>.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defn example [data owner]
(om/component
(dom/div nil
(dom/p nil "Your input here: "
(dom/input #js{:type "text" :size "5" :value (:w @data))
(dom/p nil "Square of number:" (* (:w @data) (:w @data)))))))</pre>
<p>
Using the long form, here is how you would add code to initialize the input field to the minute of the current hour. It uses <code>om/transact!</code> which takes three arguments: the cursor, a key, and a function that returns the new value.
</p>
<pre data-type="programlisting" data-code-language="clojurescript">(defn example [data owner]
(reify
<strong>om/IWillMount
(will-mount [owner]
(om/transact! data :n (fn [_] (.getMinutes (js/Date.)))))</strong>
om/IRender
(render [_]
(dom/div nil
(dom/p nil "Your input here: "
(dom/input
#js{:type "text" :size "5" :value (:n @data)}))
(dom/p nil "Square of number:" (* (:n @data) (:n @data))))))</pre>
</section>
-->
</section>