-
-
Notifications
You must be signed in to change notification settings - Fork 285
/
JavaScript.jl
1401 lines (1058 loc) Β· 40.2 KB
/
JavaScript.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
### A Pluto.jl notebook ###
# v0.19.9
#> [frontmatter]
#> author_url = "https://github.com/JuliaPluto"
#> image = "https://upload.wikimedia.org/wikipedia/commons/9/99/Unofficial_JavaScript_logo_2.svg"
#> tags = ["javascript", "web", "classic"]
#> author_name = "Pluto.jl"
#> description = "Use HTML, CSS and JavaScript to make your own interactive visualizations!"
#> license = "Unlicense"
using Markdown
using InteractiveUtils
# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).
macro bind(def, element)
quote
local iv = try Base.loaded_modules[Base.PkgId(Base.UUID("6e696c72-6542-2067-7265-42206c756150"), "AbstractPlutoDingetjes")].Bonds.initial_value catch; b -> missing; end
local el = $(esc(element))
global $(esc(def)) = Core.applicable(Base.get, el) ? Base.get(el) : iv(el)
el
end
end
# βββ‘ 571613a1-6b4b-496d-9a68-aac3f6a83a4b
using PlutoUI, HypertextLiteral
# βββ‘ 97914842-76d2-11eb-0c48-a7eedca870fb
md"""
# Using _JavaScript_ inside Pluto
You have already seen that Pluto is designed to be _interactive_. You can make fantastic explorable documents using just the basic inputs provided by PlutoUI, together with the wide range of visualization libraries that Julia offers.
_However_, if you want to take your interactive document one step further, then Pluto offers a great framework for **combining Julia with HTML, CSS and _JavaScript_**.
"""
# βββ‘ 168e13f7-2ff2-4207-be56-e57755041d36
md"""
## Prerequisites
This document assumes that you have used HTML, CSS and JavaScript before in another context. If you know Julia, and you want to add these web languages to your skill set, we encourage you to do so! It will be useful knowledge, also outside of Pluto.
"""
# βββ‘ 28ae1424-67dc-4b76-a172-1185cc76cb59
@htl("""
<article class="learning">
<h4>
Learning HTML and CSS
</h4>
<p>
It is easy to learn HTML and CSS because they are not 'programming languages' like Julia and JavaScript, they are <em>markup languages</em>: there are no loops, functions or arrays, you only <em>declare</em> how your document is structured (HTML) and what that structure looks like on a 2D color display (CSS).
</p>
<p>
As an example, this is what this cell looks like, written in HTML and CSS:
</p>
</article>
<style>
article.learning {
background: #f996a84f;
padding: 1em;
border-radius: 5px;
}
article.learning h4::before {
content: "βοΈ";
}
article.learning p::first-letter {
font-size: 1.5em;
font-family: cursive;
}
</style>
""")
# βββ‘ ea39c63f-7466-4015-a66c-08bd9c716343
md"""
> My personal favourite resource for learning HTML and CSS is the [Mozilla Developer Network (MDN)](https://developer.mozilla.org/en-US/docs/Web/CSS).
>
> _-fons_
"""
# βββ‘ 8b082f9a-073e-4112-9422-4087850fc89e
md"""
#### Learning JavaScript
After learning HTML and CSS, you can already spice up your Pluto notebooks by creating custom layouts, generated dynamically from Julia data. To take things to the next level, you can learn JavaScript. We recommend using an online resource for this.
> My personal favourite is [javascript.info](https://javascript.info/), a high-quality, open source tutorial. I use it too!
>
> _-fons_
It is hard to say whether it is easy to _learn JavaScript using Pluto_. On one hand, we highly recommend the high-quality public learning material that already exists for JavaScript, which is generally written in the context of writing traditional web apps. On the other hand, if you have a specific Pluto-related project in mind, then this could be a great motivator to continue learning!
A third option is to learn JavaScript using [observablehq.com](https://observablehq.com), an online reactive notebook for JavaScript (it's awesome!). Pluto's JavaScript runtime is designed to be very close to the way you write code in observable, so the skills you learn there will be transferable!
If you chose to learn JavaScript using Pluto, let me know how it went, and how we can improve! [fons@plutojl.org](mailto:fons@plutojl.org)
"""
# βββ‘ d70a3a02-ef3a-450f-bf5a-4a0d7f6262e2
TableOfContents()
# βββ‘ 10cf6ed1-8276-4a4a-ad06-097d10335512
md"""
# Essentials
## Using HTML, CSS and JavaScript
To use web languages inside Pluto, we recommend the small package [`HypertextLiteral.jl`](https://github.com/MechanicalRabbit/HypertextLiteral.jl), which provides an `@htl` macro.
You wrap `@htl` around a string expression to mark it as an *HTML literal*, as we did in the example cell from earlier. When a cell outputs an HTML-showable object, it is rendered directly in your browser.
"""
# βββ‘ d967cdf9-3df9-40bb-9b08-09cae95a5ca7
@htl(" <b> Hello! </b> ")
# βββ‘ 858745a9-cd59-43a6-a296-803515518e57
md"""
### CSS and JavaScript
You can use CSS and JavaScript by including it inside HTML, just like you do when writing a web page.
For example, here we use `<script>` to include some JavaScript, and `<style>` to include CSS.
"""
# βββ‘ 21a9e3e6-92f4-475d-9c8e-21e15c09336b
@htl("""
<div class='blue-background'>
Hello!
</div>
<script>
// more about selecting elements later!
currentScript.previousElementSibling.innerText = "Hello from JavaScript!"
</script>
<style>
.blue-background {
padding: .5em;
background: lightblue;
color: black;
}
</style>
""")
# βββ‘ 4a3398be-ee86-45f3-ac8b-f627a38c00b8
md"""
## Interpolation
Julia has a nice feature: _string interpolation_:
"""
# βββ‘ 2d5fd611-284b-4428-b6a5-8909203990b9
who = "π"
# βββ‘ 82de4674-9ecc-46c4-8a57-0b4453c579c3
"Hello $(who)!"
# βββ‘ 70a415be-881a-4c01-9f8c-635b8b89e1ad
md"""
With some (frustrating) exceptions, you can also interpolate into Markdown literals:
"""
# βββ‘ 730a692f-2bf2-4d5b-86da-6ab861e8b8ac
md"""
Hello $(who)!
"""
# βββ‘ a45fdec4-2d4b-429b-b809-4c256b57fffe
md"""
**However**, you cannot interpolate into an `html"` string:
"""
# βββ‘ c68ebd7b-5fb6-4527-ac34-33f9730e4587
html"""
<p>Hello $(who)!</p>
"""
# βββ‘ 8c03139f-a94b-40cc-859f-0d86f1c72143
md"""
π’ Luckily we can perform these kinds of interpolations (and much more) with the `@htl` macro, as we will see next.
### Interpolating into HTML -- HypertextLiteral.jl
"""
# βββ‘ d8dcb044-0ac8-46d1-a043-1073bb6d1ff1
@htl("""
<p> Hello $(who)!</p>
""")
# βββ‘ e7d3db79-8253-4cbd-9832-5afb7dff0abf
cool_features = [
md"Interpolate any **HTML-showable object**, such as plots and images, or another `@htl` literal."
md"Interpolated lists are expanded _(like in this cell!)_."
"Easy syntax for CSS"
]
# βββ‘ bf592202-a9a4-4e9b-8433-fed55e3aa3bc
@htl("""
<p>It has a bunch of very cool features! Including:</p>
<ul>$([
@htl(
"<li>$(item)</li>"
)
for item in cool_features
])</ul>
""")
# βββ‘ 5ac5b984-8c02-4b8d-a342-d0f05f7909ec
md"""
#### Why not just `HTML(...)`?
You might be thinking, why don't we just use the `HTML` function, together with string interpolation? The main problem is correctly handling HTML _escaping rules_. For example:
"""
# βββ‘ ef28eb8d-ec98-43e5-9012-3338c3b84f1b
cool_elements = "<div> and <marquee>"
# βββ‘ 1ba370cc-3631-47ea-9db5-75587e8e4ff3
HTML("""
<h6> My favourite HTML elements are $(cool_elements)!</h6>
""")
# βββ‘ 7fcf2f3f-d902-4338-adf0-8ef181e79420
@htl("""
<h6> My favourite HTML elements are $(cool_elements)!</h6>
""")
# βββ‘ 7afbf8ef-e91c-45b9-bf22-24201cbb4828
md"""
### Interpolating into JS -- _HypertextLiteral.jl_
As we see above, using HypertextLiteral.jl, we can interpolate objects (numbers, string, images) into HTML output, great! Next, we want to **interpolate _data_ into _scripts_**. Although you could use `JSON.jl`, HypertextLiteral.jl actually has this ability built-in!
> When you **interpolate Julia objects into a `<script>` tag** using the `@htl` macro, it will be converted to a JS object _automatically_.
"""
# βββ‘ b226da72-9512-4d14-8582-2f7787c25028
simple_data = (msg="Hello! ", times=3)
# βββ‘ a6fd1f7b-a8fc-420d-a8bb-9f549842ad3e
@htl("""
<script>
// interpolate the data πΈ
const data = $(simple_data)
const span = document.createElement("span")
span.innerText = data.msg.repeat(data.times)
return span
</script>
""")
# βββ‘ 965f3660-6ec4-4a86-a2a2-c167dbe9315f
md"""
**Let's look at a more exciting example:**
"""
# βββ‘ 00d97588-d591-4dad-9f7d-223c237deefd
@bind fantastic_x Slider(0:400)
# βββ‘ 01ce31a9-6856-4ee7-8bce-7ce635167457
my_data = [
(name="Cool", coordinate=[100, 100]),
(name="Awesome", coordinate=[200, 100]),
(name="Fantastic!", coordinate=[fantastic_x, 150]),
]
# βββ‘ 21f57310-9ceb-423c-a9ce-5beb1060a5a3
@htl("""
<script src="https://cdn.jsdelivr.net/npm/d3@6.2.0/dist/d3.min.js"></script>
<script>
// interpolate the data πΈ
const data = $(my_data)
const svg = DOM.svg(600,200)
const s = d3.select(svg)
s.selectAll("text")
.data(data)
.join("text")
.attr("x", d => d.coordinate[0])
.attr("y", d => d.coordinate[1])
.style("fill", "red")
.text(d => d.name)
return svg
</script>
""")
# βββ‘ 0866afc2-fd42-42b7-a572-9d824cf8b83b
md"""
## Custom `@bind` output
"""
# βββ‘ 75e1a973-7ef0-4ac5-b3e2-5edb63577927
md"""
**You can use JavaScript to write input widgets.** The `input` event can be triggered on any object using
```javascript
obj.value = ...
obj.dispatchEvent(new CustomEvent("input"))
```
For example, here is a button widget that will send the number of times it has been clicked as the value:
"""
# βββ‘ e8d8a60e-489b-467a-b49c-1fa844807751
ClickCounter(text="Click") = @htl("""
<span>
<button>$(text)</button>
<script>
// Select elements relative to `currentScript`
const span = currentScript.parentElement
const button = span.querySelector("button")
// we wrapped the button in a `span` to hide its default behaviour from Pluto
let count = 0
button.addEventListener("click", (e) => {
count += 1
// we dispatch the input event on the span, not the button, because
// Pluto's `@bind` mechanism listens for events on the **first element** in the
// HTML output. In our case, that's the span.
span.value = count
span.dispatchEvent(new CustomEvent("input"))
e.preventDefault()
})
// Set the initial value
span.value = count
</script>
</span>
""")
# βββ‘ 9346d8e2-9ba0-4475-a21f-11bdd018bc60
@bind num_clicks ClickCounter()
# βββ‘ 7822fdb7-bee6-40cc-a089-56bb32d77fe6
num_clicks
# βββ‘ 701de4b8-42d3-46a3-a399-d7761dccd83d
md"""
As an exercise to get familiar with these techniques, you can try the following:
- π Add a "reset to zero" button to the widget above.
- π Make the bound value an array that increases size when you click, instead of a single number.
- π Create a "two sliders" widget: combine two sliders (`<input type=range>`) into a single widget, where the bound value is the two-element array with both values.
- π Create a "click to send" widget: combine a text input and a button, and only send the contents of the text field when the button is clicked, not on every keystroke.
Questions? Ask them on our [GitHub Discussions](https://github.com/fonsp/Pluto.jl/discussions)!
"""
# βββ‘ 88120468-a43d-4d58-ac04-9cc7c86ca179
md"""
## Debugging
The HTML, CSS and JavaScript that you write run in the browser, so you should use the [browser's built-in developer tools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools) to debug your code.
"""
# βββ‘ ea4b2da1-4c83-4a1f-8fc3-c71a120e58e1
@htl("""
<script>
console.info("Can you find this message in the console?")
</script>
""")
# βββ‘ 08bdeaff-5bfb-49ab-b4cc-3a3446c63edc
@htl("""
<style>
.cool-class {
font-size: 1.3rem;
color: purple;
background: lightBlue;
padding: 1rem;
border-radius: 1rem;
}
</style>
<div class="cool-class">Can you find out which CSS class this is?</div>
""")
# βββ‘ 9b6b5da9-8372-4ebf-9c66-ae9fcfc45d47
md"""
## Selecting elements
When writing the javascript code for a widget, it is common to **select elements inside the widgets** to manipulate them. In the number-of-clicks example above, we selected the `<span>` and `<button>` elements in our code, to trigger the input event, and attach event listeners, respectively.
There are a numbers of ways to do this, and the recommended strategy is to **create a wrapper `<span>`, and use `currentScript.parentElement` to select it**.
### `currentScript`
When Pluto runs the code inside `<script>` tags, it assigns a reference to that script element to a variable called `currentScript`. You can then use properties like `previousElementSibling` or `parentElement` to "navigate to other elements".
Let's look at the "wrapper span strategy" again.
```htmlmixed
@htl("\""
<!-- the wrapper span -->
<span>
<button id="first">Hello</button>
<button id="second">Julians!</button>
<script>
const wrapper_span = currentScript.parentElement
// we can now use querySelector to select anything we want
const first_button = wrapper_span.querySelector("button#first")
console.log(first_button)
</script>
</span>
"\"")
```
"""
# βββ‘ f18b98f7-1e0f-4273-896f-8a667d15605b
md"""
#### Why not just select on `document.body`?
In the example above, it would have been easier to just select the button directly, using:
```javascript
// β do no use:
const first_button = document.body.querySelector("button#first")
```
However, this becomes a problem when **combining using the widget multiple times in the same notebook**, since all selectors will point to the first instance.
Similarly, try not to search relative to the `<pluto-cell>` or `<pluto-output>` element, because users might want to combine multiple instances of the widget in a single cell.
"""
# βββ‘ d83d57e2-4787-4b8d-8669-64ed73d79e73
md"""
## Script loading
To use external javascript dependencies, you can load them from a CDN, such as:
- [jsdelivr.com](https://www.jsdelivr.com/)
- [esm.sh](https://esm.sh)
Just like when writing a browser app, there are two ways to import JS dependencies: a `<script>` tag, and the more modern ES6 import.
### Loading method 1: ES6 imports
We recommend that you use an [**ES6 import**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) if the library supports it. (If it does not, you might be able to still get it using [esm.sh](https://esm.sh) or [skypack.dev](https://www.skypack.dev)!)
##### Awkward note about syntax
Normally, you can import libraries inside JS using the import syntax:
```javascript
// β do no use:
import confetti from "https://esm.sh/canvas-confetti@1.4.0"
import { html, render, useEffect } from "https://cdn.jsdelivr.net/npm/htm@3.0.4/preact/standalone.mjs"
```
In Pluto, this is [currently not yet supported](https://github.com/fonsp/Pluto.jl/issues/992), and you need to use a different syntax as workaround:
```javascript
// β use:
const { default: confetti } = await import("https://esm.sh/canvas-confetti@1.4.0")
const { html, render, useEffect } = await import("https://cdn.jsdelivr.net/npm/htm@3.0.4/preact/standalone.mjs")
```
"""
# βββ‘ 077c95cf-2a1b-459f-830e-c29c11a2c5cc
md"""
### Loading method 2: script tag
`<script src="...">` tags with a `src` attribute set, like this tag to import the d3.js library:
```html
<script src="https://cdn.jsdelivr.net/npm/d3@6.2.0/dist/d3.min.js"></script>
```
will work as expected. The execution of other script tags within the same cell is delayed until a `src` script finished loading, and Pluto will make sure that every source file is only loaded once.
"""
# βββ‘ 80511436-e41f-4913-8a30-d9e113cfaf71
md"""
### Pinning versions
When using a CDN almost **never** want to use an unpinned import. Always version your CDN imports!
```js
// β do no use:
"https://esm.sh/canvas-confetti"
"https://cdn.jsdelivr.net/npm/htm/preact/standalone.mjs"
// β use:
"https://esm.sh/canvas-confetti@1.4.0"
"https://cdn.jsdelivr.net/npm/htm@3.0.4/preact/standalone.mjs"
```
"""
# βββ‘ 8388a833-d535-4cbd-a27b-de323cea60e8
md"""
# Advanced
"""
# βββ‘ 4cf27df3-6a69-402e-a71c-26538b2a52e7
md"""
## Script output & `observablehq/stdlib`
Pluto's original inspiration was [observablehq.com](https://observablehq.com/), and online reactive notebook for JavaScript. _(It's REALLY good, try it out!)_ We design Pluto's JavaScript runtime to be close to the way you write code in observable.
Read more about the observable runtime in their (interactive) [documentation](https://observablehq.com/@observablehq/observables-not-javascript). The following is also true for JavaScript-inside-scripts in Pluto:
- βοΈ If you return an HTML node, it will be displayed.
- βοΈ The [`observablehq/stdlib`](https://observablehq.com/@observablehq/stdlib) library is pre-imported, you can use `DOM`, `html`, `Promises`, etc.
- βοΈ When a cell re-runs reactively, `this` will be set to the previous output (with caveat, see the later section)
- The variable `invalidation` is a Promise that will get resolved when the cell output is changed or removed. You can use this to remove event listeners, for example.
- You can use top-level `await`, and a returned HTML node will be displayed when ready.
- Code is run in "strict mode", use `let x = 1` instead of `x = 1`.
The following is different in Pluto:
- JavaScript code is not reactive, there are no global variables.
- Cells can contain multiple script tags, and they will run consecutively (also when using `await`)
- We do not (yet) support async generators, i.e. `yield`.
- We do not support the observable keywords `viewof` and `mutable`.
"""
# βββ‘ 5721ad33-a51a-4a91-adb2-0915ea0efa13
md"""
### Example:
(Though using `HypertextLiteral.jl` would make more sense for this purpose.)
"""
# βββ‘ fc8984c8-4668-418a-b258-a1718809470c
# βββ‘ 846354c8-ba3b-4be7-926c-d3c9cc9add5f
films = [
(title="Frances Ha", director="Noah Baumbach", year=2012),
(title="Portrait de la jeune fille en feu", director="CΓ©line Sciamma", year=2019),
(title="De noorderlingen", director="Alex van Warmerdam", year=1992),
];
# βββ‘ c857bb4b-4cf4-426e-b340-592cf7700434
@htl("""
<script>
let data = $(films)
// html`...` is from https://github.com/observablehq/stdlib
// note the escaped dollar signs:
let Film = ({title, director, year}) => html`
<li class="film">
<b>\${title}</b> by <em>\${director}</em> (\${year})
</li>
`
// the returned HTML node is rendered
return html`
<ul>
\${data.map(Film)}
</ul>
`
</script>
""")
# βββ‘ a33c7d7a-8071-448e-abd6-4e38b5444a3a
md"""
## Stateful output with `this`
Just like in observablehq, if a cell _re-runs reactively_, then the javascript variable `this` will take the value of the last thing that was returned by the script. If the cell runs for the first time, then `this == undefined`. In particular, if you return an HTML node, and the cell runs a second time, then you can access the HTML node using `this`. Two reasons for using this feature are:
- Stateful output: you can persist some state in-between re-renders.
- Performance: you can 'recycle' the previous DOM and update it partially (using d3, for example). _When doing so, Pluto guarantees that the DOM node will always be visible, without flicker._
##### 'Re-runs reactively'?
With this, we mean that the Julia cell re-runs not because of user input (Ctrl+S, Shift+Enter or clicking the play button), but because it was triggered by a variable reference.
##### βοΈ Caveat
This feature is **only enabled** for `<script>` tags with the `id` attribute set, e.g. `<script id="first">`. Think of setting the `id` attribute as saying: "I am a Pluto script". There are two reasons for this:
- One Pluto cell can output multiple scripts, Pluto needs to know which output to assign to which script.
- Some existing scripts assume that `this` is set to `window` in toplevel code (like in the browser). By hiding the `this`-feature behind this caveat, we still support libraries that output such scripts.
"""
# βββ‘ 91f3dab8-5521-44a0-9890-8d988a994076
trigger = "edit me!"
# βββ‘ dcaae662-4a4f-4dd3-8763-89ea9eab7d43
let
trigger
html"""
<script id="something">
console.log("'this' is currently:", this)
if(this == null) {
return html`<blockquote>I am running for the first time!</blockqoute>`
} else {
return html`<blockquote><b>I was triggered by reactivity!</b></blockqoute>`
}
</script>
"""
end
# βββ‘ e77cfefc-429d-49db-8135-f4604f6a9f0b
md"""
### Example: d3.js transitions
Type the coordinates of the circles here!
"""
# βββ‘ 2d5689f5-1d63-4b8b-a103-da35933ad26e
@bind positions TextField(default="100, 300")
# βββ‘ 6dd221d1-7fd8-446e-aced-950512ea34bc
dot_positions = try
parse.([Int], split(replace(positions, ',' => ' ')))
catch e
[100, 300]
end
# βββ‘ 0a9d6e2d-3a41-4cd5-9a4e-a9b76ed89fa9
# dot_positions = [100, 300] # edit me!
# βββ‘ 0962d456-1a76-4b0d-85ff-c9e7dc66621d
md"""
Notice that, even though the cell below re-runs, we **smoothly transition** between states. We use `this` to maintain the d3 transition states in-between reactive runs.
"""
# βββ‘ bf9b36e8-14c5-477b-a54b-35ba8e415c77
@htl("""
<script src="https://cdn.jsdelivr.net/npm/d3@6.2.0/dist/d3.min.js"></script>
<script id="hello">
const positions = $(dot_positions)
const svg = this == null ? DOM.svg(600,200) : this
const s = this == null ? d3.select(svg) : this.s
s.selectAll("circle")
.data(positions)
.join("circle")
.transition()
.duration(300)
.attr("cx", d => d)
.attr("cy", 100)
.attr("r", 10)
.attr("fill", "gray")
const output = svg
output.s = s
return output
</script>
""")
# βββ‘ 781adedc-2da7-4394-b323-e508d614afae
md"""
### Example: Preact with persistent state
"""
# βββ‘ de789ad1-8197-48ae-81b2-a21ec2340ae0
md"""
Modify `x`, add and remove elements, and notice that preact maintains its state.
"""
# βββ‘ 85483b28-341e-4ed6-bb1e-66c33613725e
x = ["hello pluton!", 232000,2,2,12 ,12,2,21,1,2, 120000]
# βββ‘ 3266f9e6-42ad-4103-8db3-b87d2c315290
state = Dict(
:x => x
)
# βββ‘ 9e37c18c-3ebb-443a-9663-bb4064391d6e
@htl("""
<script id="asdf">
//await new Promise(r => setTimeout(r, 1000))
const { html, render, Component, useEffect, useLayoutEffect, useState, useRef, useMemo, createContext, useContext, } = await import( "https://cdn.jsdelivr.net/npm/htm@3.0.4/preact/standalone.mjs")
const node = this ?? document.createElement("div")
const new_state = $(state)
if(this == null){
// PREACT APP STARTS HERE
const Item = ({value}) => {
const [loading, set_loading] = useState(true)
useEffect(() => {
set_loading(true)
const handle = setTimeout(() => {
set_loading(false)
}, 1000)
return () => clearTimeout(handle)
}, [value])
return html`<li>\${loading ?
html`<em>Loading...</em>` :
value
}</li>`
}
const App = () => {
const [state, set_state] = useState(new_state)
node.set_app_state = set_state
return html`<h5>Hello world!</h5>
<ul>\${
state.x.map((x,i) => html`<\${Item} value=\${x} key=\${i}/>`)
}</ul>`;
}
// PREACT APP ENDS HERE
render(html`<\${App}/>`, node);
} else {
node.set_app_state(new_state)
}
return node
</script>
""")
# βββ‘ 7d9d6c28-131a-4b2a-84f8-5c085f387e85
md"""
## Embedding Julia data directly into JavaScript!
You can use `Main.PlutoRunner.publish_to_js` to embed data directly into JavaScript, using Pluto's built-in, optimized data transfer. See [the Pull Request](https://github.com/fonsp/Pluto.jl/pull/1124) for more info.
Example usage:
```julia
let
x = rand(UInt8, 10_000)
d = Dict(
"some_raw_data" => x,
"wow" => 1000,
)
@htl(\"\"\"
<script>
const d = $(Main.PlutoRunner.publish_to_js(d))
console.log(d)
</script>
\"\"\")
end
```
In this example, the `const d` is populated from a hook into Pluto's data transfer. For large amounts of typed vector data (e.g. `Vector{UInt8}` or `Vector{Float64}`), this is *much* more efficient than interpolating the data directly with HypertextLiteral using `$(d)`, which would use a JSON-like string serialization.
**Note:** this API is still *experimental*, and might change in the future.
"""
# βββ‘ da7091f5-8ba2-498b-aa8d-bbf3b4505b81
md"""
# Appendix
"""
# βββ‘ 64cbf19c-a4e3-4cdb-b4ec-1fbe24be55ad
details(x, summary="Show more") = @htl("""
<details>
<summary>$(summary)</summary>
$(x)
</details>
""")
# βββ‘ 93abe0dc-f041-475f-9ef7-d8ee4408414b
details(md"""
```htmlmixed
<article class="learning">
<h4>
Learning HTML and CSS
</h4>
<p>
It is easy to learn HTML and CSS because they are not 'programming languages' like Julia and JavaScript, they are <em>markup languages</em>: there are no loops, functions or arrays, you only <em>declare</em> how your document is structured (HTML) and what that structure looks like on a 2D color display (CSS).
</p>
<p>
As an example, this is what this cell looks like, written in HTML and CSS:
</p>
</article>
<style>
article.learning {
background: #fde6ea4c;
padding: 1em;
border-radius: 5px;
}
article.learning h4::before {
content: "βοΈ";
}
article.learning p::first-letter {
font-size: 1.5em;
font-family: cursive;
}
</style>
```
""", "Show with syntax highlighting")
# βββ‘ d12b98df-8c3f-4620-ba3c-2f3dadac521b
details(md"""
```htmlmixed
<script>
// interpolate the data πΈ
const data = $(simple_data)
const span = document.createElement("span")
span.innerText = data.msg.repeat(data.times)
return span
</script>
```
""", "Show with syntax highlighting")
# βββ‘ 94561cb1-2325-49b6-8b22-943923fdd91b
details(md"""
```htmlmixed
<script src="https://cdn.jsdelivr.net/npm/d3@6.2.0/dist/d3.min.js"></script>
<script>
// interpolate the data πΈ
const data = $(my_data)
const svg = DOM.svg(600,200)
const s = d3.select(svg)
s.selectAll("text")
.data(data)
.join("text")
.attr("x", d => d.coordinate[0])
.attr("y", d => d.coordinate[1])
.style("fill", "red")
.text(d => d.name)
return svg
</script>
```
""", "Show with syntax highlighting")
# βββ‘ b0c246ed-b871-461b-9541-280e49b49136
details(md"""
```htmlmixed
<div>
<button>$(text)</button>
<script>
// Select elements relative to `currentScript`
const div = currentScript.parentElement
const button = div.querySelector("button")
// we wrapped the button in a `div` to hide its default behaviour from Pluto
let count = 0
button.addEventListener("click", (e) => {
count += 1
// we dispatch the input event on the div, not the button, because
// Pluto's `@bind` mechanism listens for events on the **first element** in the
// HTML output. In our case, that's the div.
div.value = count
div.dispatchEvent(new CustomEvent("input"))
e.preventDefault()
})
// Set the initial value
div.value = count
</script>
</div>
```
""", "Show with syntax highlighting")
# βββ‘ d121e085-c69b-490f-b315-c11a9abd57a6
details(md"""
```htmlmixed
<script>
let data = $(films)
// html`...` is from https://github.com/observablehq/stdlib
// note the escaped dollar signs:
let Film = ({title, director, year}) => html`
<li class="film">
<b>\${title}</b> by <em>\${director}</em> (\${year})
</li>
`
// the returned HTML node is rendered
return html`
<ul>
\${data.map(Film)}
</ul>
`
</script>
```
""", "Show with syntax highlighting")
# βββ‘ d4bdc4fe-2af8-402f-950f-2afaf77c62de
details(md"""
```htmlmixed
<script id="something">
console.log("'this' is currently:", this)
if(this == null) {
return html`<blockquote>I am running for the first time!</blockqoute>`
} else {
return html`<blockquote><b>I was triggered by reactivity!</b></blockqoute>`
}
</script>
```
""", "Show with syntax highlighting")
# βββ‘ e910982c-8508-4729-a75d-8b5b847918b6
details(md"""
```htmlmixed
<script src="https://cdn.jsdelivr.net/npm/d3@6.2.0/dist/d3.min.js"></script>
<script id="hello">
const positions = $(dot_positions)
const svg = this == null ? DOM.svg(600,200) : this
const s = this == null ? d3.select(svg) : this.s
s.selectAll("circle")
.data(positions)
.join("circle")
.transition()
.duration(300)
.attr("cx", d => d)
.attr("cy", 100)
.attr("r", 10)
.attr("fill", "gray")
const output = svg
output.s = s
return output
</script>
```
""", "Show with syntax highlighting")
# βββ‘ 05d28aa2-9622-4e62-ab39-ca4c7dde6eb4
details(md"""
```htmlmixed
<script type="module" id="asdf">
//await new Promise(r => setTimeout(r, 1000))
const { html, render, Component, useEffect, useLayoutEffect, useState, useRef, useMemo, createContext, useContext, } = await import( "https://cdn.jsdelivr.net/npm/htm@3.0.4/preact/standalone.mjs")