-
-
Notifications
You must be signed in to change notification settings - Fork 115
/
Copy pathdefcard_api.cljs
471 lines (357 loc) · 15 KB
/
defcard_api.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
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
(ns devdemos.defcard-api
(:require
[devcards.core]
[om.core :as om :include-macros true]
[om.dom :as dom :include-macros true]
[reagent.core :as reagent]
[clojure.string :as string]
[sablono.core :as sab :include-macros true]
[cljs.test :as t :include-macros true :refer-macros [testing is]])
(:require-macros
;; Notice that I am not including the 'devcards.core namespace
;; but only the macros. This helps ensure that devcards will only
;; be created when the :devcards is set to true in the build config.
[devcards.core :as dc :refer [defcard defcard-doc noframe-doc deftest dom-node]]))
(defcard-doc
"#It all starts with `defcard`
Once you have Devcards setup and have required the devcards macros as below
```clojure
(:require-macros
[devcards.core :as dc :refer [defcard]])
```
You can then use the `defcard` macro. `defcard` is a multipurpose
macro which is designed to take what you are working on elevate
live into the browser. It can handle many types of data but
primarily takes any type of ReactElement.
So this would be the \"Hello World\" of Devcards,"
'(defcard (sab/html [:h3 "Hello world"]))
"You can see this devcard rendered below:")
(defcard (sab/html [:h3 "Hello World!"]))
(defcard
"# These cards are hot ... loaded
One thing that isn't easy to see from reading this page is that when
you define a Devcard in your code and save it, a card instantly
appears in the Devcards interface. It shows up on the page in the
order of its definition, and when you comment out or delete the
card from your code it dissapears from the interface.
## `defcard` takes 5 arguments
* **name** an optional symbol name for the card to be used as a heading and to
locate it in the Devcards interface
* **documentation** an optional string literal of markdown documentation
* **main object** a required object for the card to display
* **initial data** an optional Atom, RAtom or Clojure data structure (normally
a Map), used to initialize the a state that the devcard will pass back to
your code examples. More on this later ...
* **devcard options** an optional map of options for the underlying devcard
```
(defcard hello-world ;; optional symbol name
\"**Optional Mardown documentation**\" ;; optional literal string doc
{:object \"of focus\"} ;; required object of focus
{} ;; optional intial data
{} ;; optional devcard config
)
```
We are going to explore these arguments and examples of how they work
below.
This is pretty *meta* as I am using Devcards to document Devcards.
So please take this page as an example of **interactive literate
programming**. Where you create a story about your code, supported
by live examples of how it works.
## What's in a name?
The first optional arg to `defcard` is the **name**. This is a
symbol and it is used to provide a distinct key for the card
you are creating.
For cards that aren't stateful like documentation and such the name
really isn't necessary but when you create cards that are displaying
stateful running widgets then this key will help ensure that the
underlying state is mapped back to the correct card.
The name will be used to create a header on the card. The header can
be clicked to display and work on the card by itself.
For instance here is a card with a name:
```
(defcard first-example)
```
You can see this card with its header just below. If you click on
the `first-example` card header, you will be presented with the card
by itself, so that you can work on the card in isolation.
")
(defcard first-example
(sab/html [:div])
{}
{:heading true})
(defcard-doc
"## Name absentia
In the absense of a name, the heading of the card will not be displayed.
Devcards generate's a card name in the order that it shows up on
the page. You can see this autogenerated name by setting the
`:heading` option to `true`.
```
(defcard {} ; main obj
{} ; initial data
{:heading true}) ; devcard options: forces header to show
```
Which is displayed as so:")
(defcard {} {} {:heading true})
(defcard
"The generated card name above is *card-4*. This makes sense
because it's the fouth card on the page that has no name.
The generated card name will work in many cases but not for all.
It's best to have a name for cards with state.")
(defcard
"## Optional markdown doc
You can also add an optional markdown documentation to your card like this:
```
(defcard example-2 \"## Example: This is optional markdown\")
```
")
(defcard example-2 "## Example: This is optional markdown")
(defcard
"Since the name `example-2` is optional you can write docs just like this:
```
(defcard
\"## Example: writing markdown docs is intended to be easy.
You should be able to add docs to your code examples easily.\")
```")
(defcard-doc
"# The object of our attention
The main object that we are displaying comes after the optional
**name** and **documentation** and it will be displayed in the
**body** of the card.
As mentioned before this object can be many things but perhaps most
importantly it can be a ReactElement.
For example this is valid:"
(dc/mkdn-pprint-code
'(defcard react-example (sab/html [:h3 "Example: Rendering a ReactElement"])))
"Above we simply passed a ReactElement created by `sablono` to `defcard`
and it gets rendered as the following card:")
(defcard react-example (sab/html [:h3 "Example: Rendering a ReactElement"]))
(defcard
"## A string is interpreted as markdown
In the example below we are not using a string literal so the first
arg is really the main object and because it is of type `string` it
will be interpreted as markdown.
```
(defcard (str \"This is the main object and it **will** be interpreted as markdown\"))
```
")
(defcard (str "This is the main object and **will** be interpreted as markdown"))
(defcard
"## Many types are displayed as edn
As we are programming we often want to see the result of an
evaluation, for this reason `defcard` will display many types of
data as edn.
This is a growing list of items but right now look at the following examples:")
(defcard
"**Map**s are displayed as edn:
```
(defcard {:this \"is a map\"})
```"
{:this "is a map"})
(defcard
"**Vector**s are displayed as edn:
```
(defcard [\"This\" \"is\" \"a\" \"vector\"])
```"
(string/split "This is a vector" #"\s" ))
(defcard
"**Set**s are displayed as edn
```
(defcard #{1 2 3})
```"
#{1 2 3})
(defcard
"**List**s are displayed as edn
```
(defcard (list 1 2 3))
```"
(list 1 2 3))
(defcard
"## Atoms are displayed as observed edn
When you pass an atom to `defcard` as the main object its contents
will be rendered as edn. And when the atom changes so will the
displayed edn.
```
(defonce observed-atom
(let [a (atom 0)]
(js/setInterval (fn [] (swap! observed-atom inc)) 1000)
a))
(defcard atom-observing-card observed-atom)
```
This will produce the timer card that you can see below:")
(defonce observed-atom
(let [a (atom {:time 0})]
(js/setInterval (fn [] (swap! observed-atom update-in [:time] inc)) 1000)
a))
(defcard atom-observing-card observed-atom {} {:history false})
(defcard-doc
"## A function as a main object
The main point of devcards is to get your code out of the source
file and up and running in front of you as soon as possible. To this
end devcards tries to provide several generic ways for you to run
your code in the devcards interface. The main way is to pass a
function to the `defcard` macro as the main object.
Instead of a ReactElement you can provide a function the takes two
parameters and returns a ReactElement like so:"
(dc/mkdn-pprint-code
'(defcard (fn [data-atom owner]
(sab/html [:div [:h2 "Example: fn that returns React"]
(prn-str data-atom)]))))
"In this example the `data-atom` is a ClojureScript Atom and
the`owner` is the enclosing cards ReactElement.")
(defcard
(fn [data-atom owner]
(sab/html [:div [:h3 "Example: fn that returns React"]
(prn-str data-atom)])))
(defcard-doc
"If `data-atom` in the above example changes then the card will be re-rendered.
Let's make a quick example counter:"
(dc/mkdn-pprint-code
'(defcard
(fn [data-atom owner]
(sab/html [:div [:h3 "Example Counter: " (:count @data-atom)]
[:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]])))))
(defcard
(fn [data-atom owner]
(sab/html [:div [:h3 "Example Counter: " (:count @data-atom)]
[:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]])))
(defcard-doc
"## Initial state
The counter example above was very interesting but what if you want
to introduce some initial state?
Well the next option after the main object is the **initial-data**
parameter. You can use it like so:"
(dc/mkdn-pprint-code
'(defcard
(fn [data-atom owner]
(sab/html [:div [:h3 "Example Counter w/Initial Data: " (:count @data-atom)]
[:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]]))
{:count 50})))
(defcard
(fn [data-atom owner]
(sab/html [:div [:h3 "Example Counter w/Initial Data: " (:count @data-atom)]
[:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]]))
{:count 50})
(defcard-doc
"## Initial state can be an Atom
You can also pass an Atom as the initial state. This is a very
important feature of devcards as it allows you to share state
between cards.
The following examples share state:"
(dc/mkdn-pprint-code
'(defonce first-example-state (atom {:count 55})))
(dc/mkdn-pprint-code
'(defcard example-counter
(fn [data-atom owner]
(sab/html [:h3 "Example Counter w/Shared Initial Atom: " (:count @data-atom)]))
first-example-state))
(dc/mkdn-pprint-code
'(defcard example-incrementer
(fn [data-atom owner]
(sab/html [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "increment"]))
first-example-state))
(dc/mkdn-pprint-code
'(defcard example-decrementer
(fn [data-atom owner]
(sab/html [:button {:onClick (fn [] (swap! data-atom update-in [:count] dec))} "decrement"]))
first-example-state))
"As you can see, we created three cards that all share the same state.
If you try these example cards below you will see that they are all wired together:")
(defonce first-example-state (atom {:count 55}))
(defcard example-counter
(fn [data-atom owner]
(sab/html [:h3 "Example Counter w/Shared Initial Atom: " (:count @data-atom)]))
first-example-state)
(defcard example-incrementer
(fn [data-atom owner]
(sab/html [:button {:onClick (fn [] (swap! data-atom update-in [:count] inc)) } "increment"]))
first-example-state)
(defcard example-decrementer
(fn [data-atom owner]
(sab/html [:button {:onClick (fn [] (swap! data-atom update-in [:count] dec)) } "decrement"]))
first-example-state)
(defcard
"# Reseting the state of a card
The **initial state** is just the initial state of the card. What if
you want to reset the card and start from the initial state or some
new initial state?
There is a simple trick: you just change the name of the card. I
often add and remove a `*` at the end of a card name to bump the
state out of the card in read in a new initial state.
I am debating adding knobs for these things to the heading panel of
the card. A knob to reset the state, a knob to turn on history, a
knob to display the data in the atom. Let me know if you think this
is a good idea.")
(defcard
"# Devcard options
The last argument to `defcard` is an optional map of options.
Here are the available options with their defaults:
```
{
:frame true ;; whether to enclose the card in a padded frame
:heading true ;; whether to add a heading panel to the card
:padding true ;; whether to have padding around the body of the card
:hidden false ;; whether to diplay the card or not
:inspect-data false ;; whether to display the data in the card atom
:watch-atom true ;; whether to watch the atom and render on change
:history false ;; whether to record a change history of the atom
:classname \"\" ;; provide card with a custom classname
:projection identity ;; provide a projection function for card state
}
```
Most of these are fairly straight forward. Whats important to know
is that you can change any of these live and the card will respond
with the new behavior.
Here are some cards that exercise these options:")
(defcard no-framed
(str "## This is a devcard
And it doesn't have a frame")
{}
{:frame false})
(defcard no-heading
(str "# this card is hiding its heading")
{}
{:heading false})
(defcard no-padding
(str " this card is has no padding on its body")
{}
{:padding false})
(defcard custom-classname
(str " this card has a custom class `.red-box`")
{}
{:classname "red-box"})
(defcard inspect-data
(fn [data-atom owner]
(sab/html [:div [:h3 "Inspecting data on this Counter: " (:count @data-atom)]
[:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]]))
{:count 50}
{:inspect-data true})
(defcard inspect-data-and-record-history
(fn [data-atom owner]
(sab/html [:div [:h3 "Inspecting data and recording history this Counter: " (:count @data-atom)]
[:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]]))
{:count 50}
{:inspect-data true :history true})
(defcard project-data
(fn [data-atom _]
(sab/html [:div [:h3 "Inspecting data but only some of the data: Counter" (:count @data-atom)]
[:button {:onClick (fn [] (swap! data-atom update-in [:count] inc))} "inc"]
[:div "Random other state that is not important: " (:whatever @data-atom)]]))
{:count 50
:whatever "this state is present but not shown in `inspect-data` part."}
{:inspect-data true
:projection (fn [state] (select-keys state [:count]))})
(defcard-doc
"## Accessing the DOM with `dom-node`
While Devcards was written in and are very easy to use in
conjunction with React. You may want to write something that writes
directly to the DOM.
The helper macro `dom-node` takes a function that accepts a DOM
node and ClojureScript Atom and returns a ReactElement."
(dc/mkdn-pprint-code
'(defcard example-dom-node
(dom-node (fn [data-atom node]
(set! (.-innerHTML node) "<h2>Example Dom Node</h2>"))))))
(defcard example-dom-node
(dom-node
(fn [data-atom node]
(set! (.-innerHTML node) "<h2>Example Dom Node</h2>"))))