-
Notifications
You must be signed in to change notification settings - Fork 18
/
ch09-client-side-scripting.typ
1643 lines (1337 loc) · 73.3 KB
/
ch09-client-side-scripting.typ
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
#import "lib/definitions.typ": *
#import "lib/snippets.typ": fielding-rest-thesis
== Client Side Scripting
#blockquote(
attribution: fielding-rest-thesis,
)[
REST allows client functionality to be extended by downloading and executing
code in the form of applets or scripts. This simplifies clients by reducing the
number of features required to be pre-implemented.
]
Thus far we have (mostly) avoided writing any JavaScript (or \_hyperscript) in
Contact.app, mainly because the functionality we implemented has not required
it. In this chapter we are going to look at scripting and, in particular,
hypermedia-friendly scripting within the context of a Hypermedia-Driven
Application.
=== Is Scripting Allowed? <_is_scripting_allowed>
A common criticism of the web is that it’s being misused. There is a narrative
that WWW was created as a delivery system for "documents", and only came to be
used for "applications" by way of an accident or bizarre circumstances.
However, the concept of hypermedia challenges the split of document and
application. Hypermedia systems like HyperCard, which preceded the web, featured
rich capabilities for active and interactive experiences, including scripting.
HTML, as specified and implemented, does lack affordances needed to build highly
interactive applications. This doesn’t mean, however, that hypermedia’s _purpose_ is "documents"
over "applications."
Rather, while the theoretical foundation is there, the implementation is
underdeveloped. With JavaScript being the only extension point and hypermedia
controls not being well integrated to JavaScript (why can’t one click a link
without halting the program?), developers have not internalized hypermedia and
have instead used the web as a dumb pipe for apps that imitate "native" ones.
A goal of this book is to show that it is possible to build sophisticated web
applications using the original technology of the web, hypermedia, without the
application developer needing to reach for the abstractions provided by the
large, popular JavaScript frameworks.
Htmx itself is, of course, written in JavaScript, and one of its advantages is
that hypermedia interactions that go through htmx expose a rich interface to
JavaScript code with configuration, events, and htmx’s own extension support.
Htmx expands the expressiveness of HTML enough that it removes the need for
scripting in many situations. This makes htmx attractive to people who don’t
want to write JavaScript, and there are many of those sorts of developers, wary
of the complexity of Single Page Application frameworks.
However, dunking on JavaScript is not the aim of the htmx project. The goal of
htmx is not less JavaScript, but less code, more readable and
hypermedia-friendly code.
Scripting has been a massive force multiplier for the web. Using scripting, web
application developers are not only able to enhance their HTML websites, but
also create full-fledged client-side applications that can often compete with
native, thick client applications.
This JavaScript-centric approach to building web applications is a testament to
the power of the web and to the sophistication of web browsers in particular. It
has its place in web development: there are situations where the hypermedia
approach simply can’t provide the level of interaction that an SPA can.
However, in addition to this more JavaScript-centric style, we want to develop a
style of scripting more compatible and consistent with Hypermedia-Driven
Applications.
=== Scripting for Hypermedia
#index[scripting][hypermedia friendly]
Borrowing from Roy Fielding’s notion of "constraints" defining REST, we offer
two constraints of hypermedia-friendly scripting. You are scripting in an
HDA-compatible manner if the following two constraints are adhered to:
- The main data format exchanged between server and client must be hypermedia, the
same as it would be without scripting.
- Client-side state, outside the DOM itself, is kept to a minimum.
The goal of these constraints is to confine scripting to where it shines best
and where nothing else comes close: _interaction design_. Business logic and
presentation logic are the responsibility of the server, where we can pick
whichever languages or tools are appropriate for our business domain.
#block(breakable: false,
sidebar[The Server][Keeping business logic and presentation logic both "on the server" does not mean
these two "concerns" are mixed or coupled. They can be modularized on the
server. In fact, they _should_ be modularized on the server, along with all the
other concerns of our application.
Note also that, especially in web development parlance, the humble
"server" is usually a whole fleet of racks, virtual machines, containers and
more. Even a worldwide network of datacenters is reduced to "the server" when
discussing the server-side of a Hypermedia-Driven Application.])
Satisfying these two constraints sometimes requires us to diverge from what is
typically considered best practice for JavaScript. Keep in mind that the
cultural wisdom of JavaScript was largely developed in JavaScript-centric SPA
applications.
The Hypermedia-Driven Application cannot as comfortably fall back on this
tradition. This chapter is our contribution to the development of a new style
and best practices for what we are calling Hypermedia-Driven Applications.
Unfortunately, simply listing "best practices" is rarely convincing or edifying.
To be honest, it’s boring.
Instead, we will demonstrate these best practices by implementing client-side
features in Contact.app. To cover different aspects of hypermedia-friendly
scripting, we will implement three different features:
- An overflow menu to hold the _Edit_, _View_ and
_Delete_ actions, to clean up visual clutter in our list of contacts.
- An improved interface for bulk deletion.
- A keyboard shortcut for focusing the search box.
The important takeaway in the implementation of each of these features is that,
while they are implemented entirely on the client-side using scripting, they _don’t exchange information with the server_ via
a non-hypermedia format, such as JSON, and that they don’t store a significant
amount of state outside of the DOM itself.
=== Scripting Tools for the Web <_scripting_tools_for_the_web>
The primary scripting language for the web is, of course, JavaScript, which is
ubiquitous in web development today.
A bit of interesting internet lore, however, is that JavaScript was not always
the only built-in option. As the quote from Roy Fielding at the start of this
chapter hints, "applets" written in other languages such as Java were considered
to be part of the scripting infrastructure of the web. In addition, there was a
time period when Internet Explorer supported VBScript, a scripting language
based on Visual Basic.
Today, we have a variety of _transcompilers_ (often shortened to
_transpilers_) that convert many languages to JavaScript, such as TypeScript,
Dart, Kotlin, ClojureScript, F\# and more. There is also the WebAssembly (WASM)
bytecode format, which is supported as a compilation target for C, Rust, and the
WASM-first language AssemblyScript.
However, most of these options are not geared towards a hypermedia-friendly
style of scripting. Compile-to-JS languages are often paired with SPA-oriented
libraries (Dart and AngularDart, ClojureScript and Reagent, F\# and Elm), and
WASM is currently mainly geared toward linking to C/C++ libraries from
JavaScript.
We will instead focus on three client-side scripting technologies that
_are_ hypermedia-friendly:
- VanillaJS, that is, using JavaScript without depending on any framework.
- Alpine.js, a JavaScript library for adding behavior directly in HTML.
- \_hyperscript, a non-JavaScript scripting language created alongside htmx. Like
AlpineJS, \_hyperscript is usually embedded in HTML.
Let’s take a quick look at each of these scripting options, so we know what we
are dealing with.
Note that, as with CSS, we are going to show you just enough of each of these
options to give a flavor of how they work and, we hope, spark your interest in
looking into any of them more extensively.
=== Vanilla #indexed[JavaScript]
#blockquote(attribution: [Merb (Ruby web framework), motto])[
No code is faster than no code.
]
Vanilla JavaScript is simply using plain JavaScript in your application, without
any intermediate layers. The term "Vanilla" entered frontend web dev parlance as
it became assumed that any sufficiently "advanced" web app would use some
library with a name ending in ".js". As JavaScript matured as a scripting
language, however, standardized across browsers and provided more and more
functionality, these frameworks and libraries became less important.
Somewhat ironically though, as JavaScript became more powerful and removed the
need for the first generation of JavaScript libraries such as jQuery, it also
enabled people to build complex SPA libraries. These SPA libraries are often
even more elaborate than the original first generation of JavaScript libraries.
A quote from the website #link("http://vanilla-js.com"), which is well worth
visiting even though it’s slightly out of date, captures the situation well:
#blockquote(
attribution: [http:\/\/vanilla-js.com],
)[
VanillaJS is the lowest-overhead, most comprehensive framework I’ve ever used.
]
With JavaScript having matured as a scripting language, this is certainly the
case for many applications. It is especially true in the case of HDAs, since, by
using hypermedia, your application will not need many of the features typically
provided by more elaborate Single Page Application JavaScript frameworks:
- Client-side routing
- An abstraction over DOM manipulation (i.e., templates that automatically update
when referenced variables change)
- Server side rendering #footnote[Rendering here refers to HTML generation. Framework support for server-side
rendering is not needed in a HDA because generating HTML on the server is the
default.]
- Attaching dynamic behavior to server-rendered tags on load (i.e.,
"hydration")
- Network requests
Without all this complexity being handled in JavaScript, your framework needs
are dramatically reduced.
One of the best things about VanillaJS is how you install it: you don’t have to!
You can just start writing JavaScript in your web application, and it will
simply work.
That’s the good news. The bad news is that, despite improvements over the last
decade, JavaScript has some significant limitations as a scripting language that
can make it less than ideal as a stand-alone scripting technology for
Hypermedia-Driven Applications:
- Being as established as it is, it has accreted a lot of features and warts.
- It has a complicated and confusing set of features for working with asynchronous
code.
- Working with events is surprisingly difficult.
- DOM APIs (a large portion of which were originally designed for Java, yes _Java_)
are verbose and don’t have a habit of making common functionality easy to use.
None of these limitations are deal-breakers, of course. Many of them are
gradually being fixed and many people prefer the "close to the metal" (for lack
of a better term) nature of vanilla JavaScript over more elaborate client-side
scripting approaches.
==== A Simple Counter <_a_simple_counter>
To dive into vanilla JavaScript as a front end scripting option, let’s create a
simple counter widget.
Counter widgets are a common "Hello World" example for JavaScript frameworks, so
looking at how it can be done in vanilla JavaScript (as well as the other
options we are going to look at) will be instructive.
Our counter widget will be very simple: it will have a number, shown as text,
and a button that increments the number.
One problem with tackling this problem in vanilla JavaScript is that it lacks
one thing that most JavaScript frameworks provide: a default code and
architectural style.
With vanilla JavaScript, there are no rules!
This isn’t all bad. It presents a great opportunity to take a small journey
through various styles that people have developed for writing their JavaScript.
===== An inline implementation <_an_inline_implementation>
To begin, let’s start with the simplest thing imaginable: all of our JavaScript
will be written inline, directly in the HTML. When the button is clicked, we
will look up the `output` element holding the number, and increment the number
contained within it.
#figure(caption: [Counter in vanilla JavaScript, inline version])[
```html
<section class="counter">
<output id="my-output">0</output> <1>
<button
onclick=" <2>
document.querySelector('#my-output') <3>
.textContent++ <4>
"
>Increment</button>
</section>
``` ]
1. Our output element has an ID to help us find it.
2. We use the `onclick` attribute to add an event listener.
3. Find the output via a querySelector() call.
4. JavaScript allows us use the `++` operator on strings.
Not too bad.
It’s not the most beautiful code, and can be irritating especially if you aren’t
used to the DOM APIs.
It’s a little annoying that we needed to add an `id` to the `output`
element. The `document.querySelector()` function is a bit verbose compared with,
say, the `$` function, as provided by jQuery.
But it works. It’s also easy enough to understand, and crucially it doesn’t
require any other JavaScript libraries.
So that’s the simple, inline approach with VanillaJS.
===== Separating our scripting out <_separating_our_scripting_out>
While the inline implementation is simple in some sense, a more standard way to
write this would be to move the code into a separate JavaScript file. This
JavaScript file would then either be linked to via a
`<script src>` tag or placed into an inline `<script>` tag by a build process.
Here we see the HTML and JavaScript _separated out_ from one another, in
different files. The HTML is now "cleaner" in that there is no JavaScript in it.
The JavaScript is a bit more complex than in our inline version: we need to look
up the button using a query selector and add an _event listener_ to handle the
click event and increment the counter.
#figure(caption: [Counter HTML])[
```html
<section class="counter">
<output id="my-output">0</output>
<button class="increment-btn">Increment</button>
</section>
``` ]
#figure(caption: [Counter JavaScript])[
```js
const counterOutput = document.querySelector("#my-output"), <1>
incrementBtn = document.querySelector(".counter .increment-btn") <2>
incrementBtn.addEventListener("click", e => { <3>
counterOutput.innerHTML++ <4>
})
``` ]
1. Find the output element.
2. Find the button.
3. We use `addEventListener`, which is preferable to `onclick` for many reasons.
4. The logic stays the same, only the structure around it changes.
#index[Separation of Concerns (SoC)]
In moving the JavaScript out to another file, we are following a software design
principle known as _Separation of Concerns (SoC)._
Separation of Concerns posits that the various "concerns" (or aspects) of a
software project should be divided up into multiple files, so that they don’t "pollute"
one another. JavaScript isn’t markup, so it shouldn’t be in your HTML, it should
be _elsewhere_. Styling information, similarly, isn’t markup, and so it belongs
in a separate file as well (A CSS file, for example.)
For quite some time, this Separation of Concerns was considered the
"orthodox" way to build web applications.
A stated goal of Separation of Concerns is that we should be able to modify and
evolve each concern independently, with confidence that we won’t break any of
the other concerns.
However, let’s look at exactly how this principle has worked out in our simple
counter example. If you look closely at the new HTML, it turns out that we’ve
had to add a class to the button. We added this class so that we could look the
button up in JavaScript and add in an event handler for the "click" event.
Now, in both the HTML and the JavaScript, this class name is just a string and
there isn’t any process to _verify_ that the button has the right classes on it
or its ancestors to ensure that the event handler is actually added to the right
element.
Unfortunately, it has turned out that the careless use of CSS selectors in
JavaScript can cause what is known as _#indexed[jQuery soup]_. jQuery soup is a
situation where:
- The JavaScript that attaches a given behavior to a given element is difficult to
find.
- Code reuse is difficult.
- The code ends up wildly disorganized and "flat", with lots of unrelated event
handlers mixed together.
The name "jQuery soup" comes from the fact that most JavaScript-heavy
applications used to be built in jQuery (many still are), which, perhaps
inadvertently, tended to encourage this style of JavaScript.
So, you can see that the notion of Separation of Concerns doesn’t always work as
well as promised: our concerns end up intertwined or coupled pretty deeply, even
when we separate them into different files.
#asciiart(read("images/diagram/separation-of-concerns.txt"), caption: [What concerns?])
To show that it isn’t just naming between concerns that can get you into
trouble, consider another small change to our HTML that demonstrates the
problems with our separation of concerns: imagine that we decide to change the
number field from an `<output>` tag to an
`<input type="number">`.
This small change to our HTML will break our JavaScript, despite the fact we
have "separated" our concerns.
The fix for this issue is simple enough (we would need to change the
`.textContent` property to `.value` property), but it demonstrates the burden of
synchronizing markup changes and code changes across multiple files. Keeping
everything in sync can become increasingly difficult as your application size
increases.
The fact that small changes to our HTML can break our scripting indicates that
the two are _tightly coupled_, despite being broken up into multiple files. This
tight coupling suggests that separation between HTML and JavaScript (and CSS) is
often an illusory separation of concerns: the concerns are sufficiently related
to one another that they aren’t easily separated.
In Contact.app we are not _concerned_ with "structure," "styling" or "behavior";
we are concerned with collecting contact info and presenting it to users. SoC,
in the way it’s formulated in web development orthodoxy, is not really an
inviolate architectural guideline, but rather a stylistic choice that, as we can
see, can even become a hindrance.
===== Locality of Behavior
#index[Locality of Behavior (LoB)]
It turns out that there is a burgeoning reaction _against_ the Separation of
Concerns design principle. Consider the following web technologies and
techniques:
- JSX
- LitHTML
- CSS-in-JS
- Single-File Components
- Filesystem based routing
Each of these technologies _colocate_ code in various languages that address a
single _feature_ (typically a UI widget).
All of them mix _implementation_ concerns together in order to present a unified
abstraction to the end-user. Separating technical detail concerns just isn’t as
much of an, ahem, concern.
Locality of Behavior (LoB) is an alternative software design principle that we
coined, in opposition to Separation of Concerns. It describes the following
characteristic of a piece of software:
#blockquote(
attribution: [https:\/\/htmx.org/essays/locality-of-behaviour/],
)[
The behavior of a unit of code should be as obvious as possible by looking only
at that unit of code.
]
In simple terms: you should be able to tell what a button does by simply looking
at the code or markup that creates that button. This does not mean you need to
inline the entire implementation, but that you shouldn’t need to hunt for it or
require prior knowledge of the codebase to find it.
We will demonstrate Locality of Behavior in all of our examples, both the
counter demos and the features we add to Contact.app. Locality of behavior is an
explicit design goal of both \_hyperscript and Alpine.js (which we will cover
later) as well as htmx.
All of these tools achieve Locality of Behavior by having you embed attributes
directly within your HTML, as opposed to having code look up elements in a
document through CSS selectors in order to add event listeners onto them.
In a Hypermedia-Driven Application, we feel that the Locality of Behavior design
principle is often more important than the more traditional Separation of
Concerns design principle.
===== What to do with our counter?
#index[Javascript][on\*]
So, should we go back to the `onclick` attribute way of doing things? That
approach certainly wins in Locality of Behavior, and has the additional benefit
that it is baked into HTML.
Unfortunately, however, the `on*` JavaScript attributes also come with some
drawbacks:
- They don’t support custom events.
- There is no good mechanism for associating long-lasting variables with an
element --- all variables are discarded when an event listener completes
executing.
- If you have multiple instances of an element, you will need to repeat the
listener code on each, or use something more clever like event delegation.
- JavaScript code that directly manipulates the DOM gets verbose, and clutters the
markup.
- An element cannot listen for events on another element.
Consider this common situation: you have a popup, and you want it to be
dismissed when a user clicks outside of it. The listener will need to be on the
body element in this situation, far away from the actual popup markup. This
means that the body element would need to have listeners attached to it that
deal with many unrelated components. Some of these components may not even be on
the page when it was first rendered, if they are added dynamically after the
initial HTML page is rendered.
So vanilla JavaScript and Locality of Behavior don’t seem to mesh
_quite_ as well as we would like them to.
The situation is not hopeless, however: it’s important to understand that LoB
does not require behavior to be _implemented_ at a use site, but merely _invoked_ there.
That is, we don’t need to write all our code on a given element, we just need to
make it clear that a given element is _invoking_ some code, which can be located
elsewhere.
Keeping this in mind, it _is_ possible to improve LoB while writing JavaScript
in a separate file, provided we have a reasonable system for structuring our
JavaScript.
==== RSJS
#index[RSJS] (the "Reasonable System for JavaScript Structure,"
#link("https://ricostacruz.com/rsjs/")) is a set of guidelines for JavaScript
architecture targeted at "a typical non-SPA website." RSJS provides a solution
to the lack of a standard code style for vanilla JavaScript that we mentioned
earlier.
Here are the RSJS guidelines most relevant for our counter widget:
- "Use `data-` attributes" in HTML: invoking behavior via adding data attributes
makes it obvious there is JavaScript happening, as opposed to using random
classes or IDs that may be mistakenly removed or changed.
- "One component per file": the name of the file should match the data attribute
so that it can be found easily, a win for LoB.
To follow the RSJS guidelines, let’s restructure our current HTML and JavaScript
files. First, we will use _data attributes_, that is, HTML attributes that begin
with `data-`, a standard feature of HTML, to indicate that our HTML is a counter
component. We will then update our JavaScript to use an attribute selector that
looks for the
`data-counter` attribute as the root element in our counter component and wires
in the appropriate event handlers and logic. Additionally, let’s rework the code
to use `querySelectorAll()` and add the counter functionality to _all_ counter
components found on the page. (You never know how many counters you might want!)
Here is what our code looks like now:
#figure(caption: [Counter in vanilla JavaScript, with RSJS])[
```html
<section class="counter" data-counter> <1>
<output id="my-output" data-counter-output>0</output> <2>
<button class="increment-btn" data-counter-increment>Increment</button>
</section>
``` ]
1. Invoke a JavaScript behavior with a data attribute.
2. Mark relevant descendant elements.
#figure[
```js
// counter.js <1>
document.querySelectorAll("[data-counter]") <1>
.forEach(el => {
const
output = el.querySelector("[data-counter-output]"),
increment = el.querySelector("[data-counter-increment]"); <3>
increment.addEventListener("click", e => output.textContent++); <4>
});
```]
1. File should have the same name as the data attribute, so that we can locate it
easily.
2. Get all elements that invoke this behavior.
3. Get any child elements we need.
4. Register event handlers.
Using RSJS solves, or at least alleviates, many of the problems we pointed out
with our first, unstructured example of VanillaJS being split out to a separate
file:
- The JS that attaches behavior to a given element is _clear_
(though only through naming conventions).
- Reuse is _easy_ --- you can create another counter component on the page and it
will just work.
- The code is _well-organized_ --- one behavior per file.
All in all, RSJS is a good way to structure your vanilla JavaScript in a
Hypermedia-Driven Application. So long as the JavaScript isn’t communicating
with a server via a plain data JSON API, or holding a bunch of internal state
outside of the DOM, this is perfectly compatible with the HDA approach.
Let’s implement a feature in Contact.app using the RSJS/vanilla JavaScript
approach.
==== VanillaJS in Action: An Overflow Menu <_vanillajs_in_action_an_overflow_menu>
Our homepage has "Edit", "View" and "Delete" links for every contact in our
table. This uses a lot of space and creates visual clutter. Let’s fix that by
placing these actions inside a drop-down menu with a button to open it.
If you’re less familiar with JavaScript and the code here starts to feel too
complicated, don’t worry; the Alpine.js and \_hyperscript examples --- which
we’ll look at next --- are easier to follow.
Let’s begin by sketching the markup we want for our dropdown menu. First, we
need an element, we’ll use a `<div>`, to enclose the entire widget and mark it
as a menu component. Within this div, we will have a standard `<button>` that
will function as the mechanism that shows and hides our menu items. Finally,
we’ll have another `<div>` that holds the menu items that we are going to show.
These menu items will be simple anchor tags, as they are in the current contacts
table.
Here is what our updated, RSJS-structured HTML looks like:
#figure[
```html
<div data-overflow-menu> <1>
<button type="button" aria-haspopup="menu"
aria-controls="contact-menu-{{ contact.id }}"
>Options</button> <2>
<div role="menu" hidden id="contact-menu-{{ contact.id }}"> <3>
<a role="menuitem"
href="/contacts/{{ contact.id }}/edit">Edit</a> <4>
<a role="menuitem" href="/contacts/{{ contact.id }}">View</a>
<!-- ... -->
</div>
</div>
```]
1. Mark the root element of the menu component
2. This button will open and close our menu
3. A container for our menu items
4. Menu items
The roles and ARIA attributes are based on the Menu and Menu Button patterns
from the ARIA Authoring Practices Guide.
#sidebar[What is #indexed[ARIA]?][
As we web developers create more interactive, app-like websites, HTML’s
repertoire of elements won’t have all we need. As we have seen, using CSS and
JavaScript, we can endow existing elements with extended behavior and
appearances, rivaling those of native controls.
However, there was one thing web apps couldn’t replicate. While these widgets
may _look_ similar enough to the real deal, assistive technology (e.g., screen
readers) could only deal with the underlying HTML elements.
Even if you take the time to get all the keyboard interactions right, some users
often are unable to work with these custom elements easily.
ARIA was created by W3C’s Web Accessibility Initiative (WAI) in 2008 to address
this problem. At a surface level, it is a set of attributes you can add to HTML
to make it meaningful to assistive software such as a screen reader.
ARIA has two main components that interact with one another:
The first is the `role` attribute. This attribute has a predefined set of
possible values: `menu, dialog, radiogroup` etc. The `role` attribute
_does not add any behavior_ to HTML elements. Rather, it is a promise you make
to the user. When you annotate an element as
`role='menu'`, you are saying: _I will make this element work like a menu._
If you add a `role` to an element but you _don’t_ uphold the promise, the
experience for many users will be _worse_ than if the element had no `role` at
all. Thus, it is written:
#blockquote(attribution: [W3C, Read Me First | APG,
https:\/\/www.w3.org/WAI/ARIA/apg/practices/read-me-first/])[
No ARIA is better than Bad ARIA.
]
The second component of ARIA is the _states and properties_, all sharing the `aria-` prefix: `aria-expanded, aria-controls, aria-label`
etc. These attributes can specify various things such as the state of a widget,
the relationships between components, or additional semantics. Once again, these
attributes are _promises_, not implementations.
Rather than learn all the roles and attributes and try to combine them into a
usable widget, the best course of action for most developers is to rely on the
ARIA Authoring Practices Guide (APG), a web resource with practical information
aimed directly at web developers.
If you’re new to ARIA, check out the following W3C resources:
- ARIA: Read Me First:
#link("https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/")
- ARIA UI patterns: #link("https://www.w3.org/WAI/ARIA/apg/patterns/")
- ARIA Good Practices:
#link("https://www.w3.org/WAI/ARIA/apg/practices/")
Always remember to #strong[test] your website for accessibility to ensure all
users can interact with it easily and effectively.
]
On the JS side of our implementation, we’ll begin with the RSJS boilerplate:
query for all elements with some data attribute, iterate over them, get any
relevant descendants.
Note that, below, we’ve modified the RSJS boilerplate a bit to integrate with
htmx; we load the overflow menu when htmx loads new content.
#figure[
```js
function overflowMenu(tree = document) {
tree.querySelectorAll("[data-overflow-menu]").forEach(menuRoot => { <1>
const
button = menuRoot.querySelector("[aria-haspopup]"), <2>
menu = menuRoot.querySelector("[role=menu]"), <3>
items = [...menu.querySelectorAll("[role=menuitem]")];
});
}
addEventListener("htmx:load", e => overflowMenu(e.target)); <4>
```]
1. With RSJS, you’ll be writing `document.querySelectorAll(…).forEach` a lot.
2. To keep the HTML clean, we use ARIA attributes rather than custom data
attributes here.
3. Use the spread operator to convert a `NodeList` into a normal `Array`.
4. Initialize all overflow menus when the page is loaded or content is inserted by
htmx.
Conventionally, we would keep track of whether the menu is open using a
JavaScript variable or a property in a JavaScript state object. This approach is
common in large, JavaScript-heavy web applications.
However, this approach has some drawback:
- We would need to keep the DOM in sync with the state (harder without a
framework).
- We would lose the ability to serialize the HTML (as this open state isn’t stored
in the DOM, but rather in JavaScript).
Instead of taking this approach, we will use the DOM to store our state. We’ll
lean on the `hidden` attribute on the menu element to tell us it’s closed. If
the HTML of the page is snapshotted and restored, the menu can be restored as
well by simply re-running the JS.
#figure[
```js
items = [...menu.querySelectorAll("[role=menuitem]")]; <1>
const isOpen = () => !menu.hidden; <2>
```]
1. We get the list of menu items at the start. This implementation
will not support dynamically adding or removing menu items.
2. The `hidden` attribute is helpfully reflected as a `hidden`
_property_, so we don’t need to use `getAttribute`.
We’ll also make the menu items non-tabbable, so we can manage their focus
ourselves.
#figure[
```js
items.forEach(item => item.setAttribute("tabindex", "-1"));
```]
Now let’s implement toggling the menu in JavaScript:
#figure[
```js
function toggleMenu(open = !isOpen()) { <1>
if (open) {
menu.hidden = false;
button.setAttribute("aria-expanded", "true");
items[0].focus(); <2>
} else {
menu.hidden = true;
button.setAttribute("aria-expanded", "false");
}
}
toggleMenu(isOpen()); <3>
button.addEventListener("click", () => toggleMenu()); <4>
menuRoot.addEventListener("blur", e => toggleMenu(false)); <5>
```]
1. Optional parameter to specify desired state. This allows us to use one function
to open, close, or toggle the menu.
2. Focus first item of menu when opened.
3. Call `toggleMenu` with current state, to initialize element attributes.
4. Toggle menu when button is clicked.
5. Close menu when focus moves away.
Let’s also make the menu close when we click outside it, a nice behavior that
mimics how native drop-down menus work. This will require an event listener on
the whole window.
Note that we need to be careful with this kind of listener: you may find that
listeners accumulate as components add listeners and fail to remove them when
the component is removed from the DOM. This, unfortunately, leads to difficult
to track down memory leaks.
There is not an easy way in JavaScript to execute logic when an element is
removed. The best option is what is known as the `MutationObserver`
API. A `MutationObserver` is very useful, but the API is quite heavy and a bit
arcane, so we won’t be using it for our example.
Instead, we will use a simple pattern to avoid leaking event listeners: when our
event listener runs, we will check if the attaching component is still in the
DOM, and, if the element is no longer in the DOM, we will remove the listener
and exit.
This is a somewhat hacky, manual form of _garbage collection_. As is (usually)
the case with other garbage collection algorithms, our strategy removes
listeners in a nondeterministic amount of time after they are no longer needed.
Fortunately for us, With a frequent event like "the user clicks anywhere in the
page" driving the collection, it should work well enough for our system.
#figure[
```js
window.addEventListener("click", function clickAway(event) {
if (!menuRoot.isConnected)
window.removeEventListener("click", clickAway); <1>
if (!menuRoot.contains(event.target)) toggleMenu(false); <2>
});
```]
1. This line is the garbage collection.
2. If the click is outside the menu, close the menu.
Now, let’s move on to the keyboard interactions for our dropdown menu. The
keyboard handlers turn out to all be pretty similar to one another and not
particularly intricate, so let’s knock them all out in one go:
#figure[
```js
const currentIndex = () => { <1>
const idx = items.indexOf(document.activeElement);
if (idx === -1) return 0;
return idx;
}
menu.addEventListener("keydown", e => {
if (e.key === "ArrowUp") {
items[currentIndex() - 1]?.focus(); <2>
} else if (e.key === "ArrowDown") {
items[currentIndex() + 1]?.focus(); <3>
} else if (e.key === "Space") {
items[currentIndex()].click(); <4>
} else if (e.key === "Home") {
items[0].focus(); <5>
} else if (e.key === "End") {
items[items.length - 1].focus(); <6>
} else if (e.key === "Escape") {
toggleMenu(false); <7>
button.focus(); <8>
}
});
```]
1. Helper: Get the index in the items array of the currently focused menu item (0
if none).
2. Move focus to the previous menu item when the up arrow key is pressed.
3. Move focus to the next menu item when the down arrow key is pressed.
4. Activate the currently focused element when the space key is pressed.
5. Move focus to the first menu item when Home is pressed.
6. Move focus to the last menu item when End is pressed.
7. Close menu when Escape is pressed.
8. Return focus to menu button when closing menu.
That should cover all our bases, and we’ll admit that’s a lot of code. But, in
fairness, it’s code that encodes a lot of behavior.
Now, our drop-down menu isn’t perfect, and it doesn’t handle a lot of things.
For example, we don’t support submenus, or menu items being added or removed
dynamically to the menu. If we needed more menu features like this, it might
make more sense to use an off-the-shelf library, such as GitHub’s
#link(
"https://github.com/github/details-menu-element",
)[`details-menu-element`].
But, for our relatively simple use case, vanilla JavaScript does a fine job, and
we got to explore ARIA and RSJS while implementing it.
=== Alpine.js
OK, so that’s an in-depth look at how to structure plain VanillaJS-style
JavaScript. Let’s turn our attention to an actual JavaScript framework that
enables a different approach for adding dynamic behavior to your application, #link("https://alpinejs.dev")[#indexed[Alpine.js]].
Alpine is a relatively new JavaScript library that allows developers to embed
JavaScript code directly in HTML, akin to the `on*` attributes available in
plain HTML and JavaScript. However, Alpine takes this concept of embedded
scripting much further than `on*` attributes.
Alpine bills itself as a modern replacement for jQuery, the widely used, older
JavaScript library. As you will see, it definitely lives up to this promise.
#index[Alpine.js][installing]
Installing Alpine is very easy: it is a single file and is dependency-free, so
you can simply include it via a CDN:
#figure(caption: [Installing Alpine])[ ```html
<script src="https://unpkg.com/alpinejs"></script>
``` ]
You can also install it via a package manager such as NPM, or vendor it from
your own server.
#index[Alpine.js][x-data]
Alpine provides a set of HTML attributes, all of which begin with the
`x-` prefix, the main one of which is `x-data`. The content of `x-data`
is a JavaScript expression which evaluates to an object. The properties of this
object can, then, be accessed within the element that the
`x-data` attribute is located.
To get a flavor of AlpineJS, let’s look at how to implement our counter example
using it.
For the counter, the only state we need to keep track of is the current number,
so let’s declare a JavaScript object with one property, `count`, in an `x-data` attribute
on the div for our counter:
#figure(caption: [Counter with Alpine, line 1])[ ```html
<div class="counter" x-data="{ count: 0 }">
``` ]
#index[Alpine.js][x-text]
This defines our state, that is, the data we are going to be using to drive
dynamic updates to the DOM. With the state declared like this, we can now use it _within_ the
div element it is declared on. Let’s add an `output` element with an `x-text` attribute.
Next, we will _bind_ the `x-text` attribute to the `count`
attribute we declared in the `x-data` attribute on the parent `div`
element. This will have the effect of setting the text of the `output`
element to whatever the value of `count` is: if `count` is updated, so will the
text of the `output`. This is "reactive" programming, in that the DOM will "react"
to changes to the backing data.
#figure(caption: [Counter with Alpine, lines 1-2])[ ```html
<div x-data="{ count: 0 }">
<output x-text="count"></output> <1>
``` ]
1. The `x-text` attribute.
Next, we need to update the count, using a button. Alpine allows you to attach
event listeners with the `x-on` attribute.
To specify the event to listen for, you add a colon and then the event name
after the `x-on` attribute name. Then, the value of the attribute is the
JavaScript you wish to execute. This is similar to the plain
`on*` attributes we discussed earlier, but it turns out to be much more
flexible.
We want to listen for a `click` event, and we want to increment `count`
when a click occurs, so here is what the Alpine code will look like:
#figure(caption: [Counter with Alpine, the full thing])[```html
<div x-data="{ count: 0 }">
<output x-text="count"></output>
<button x-on:click="count++">Increment</button> <1>
</div>
```]
1. With `x-on`, we specify the event in the attribute _name_.
And that’s all it takes. A simple component like a counter should be simple to
code, and Alpine delivers.
==== "x-on:click" vs. "onclick"
#index[Alpine.js][x-on:click]
As we said, the Alpine `x-on:click` attribute (or its shorthand, the
`@click` attribute) is similar to the built-in `onclick` attribute. However, it
has additional features that make it significantly more useful:
- You can listen for events from other elements. For example, the
`.outside` modifier lets you listen to any click event that is
_not_ within the element.
- You can use other modifiers to:
- throttle or debounce event listeners
- ignore events that are bubbled up from descendant elements
- attach passive listeners
- You can listen to custom events. For example, if you wanted to listen for the `htmx:after-request` event
you could write
`x-on:htmx:after-request="doSomething()"`.
==== Reactivity and Templating
We hope you’ll agree that the AlpineJS version of the counter widget is better,
in general, than the VanillaJS implementation, which was either somewhat hacky
or spread out over multiple files.
A big part of the power of AlpineJS is that it supports a notion of
"reactive" variables, allowing you to bind the count of the `div`
element to a variable that both the `output` and the `button` can reference, and
properly updating all the dependencies when a mutation occurs. Alpine allows for
much more elaborate data bindings than we have demonstrated here, and it is an
excellent general purpose client-side scripting library.
==== Alpine.js in Action: A Bulk Action Toolbar <_alpine_js_in_action_a_bulk_action_toolbar>
Let’s implement a feature in Contact.app with Alpine. As it stands currently,
Contact.app has a "Delete Selected Contacts" button at the very bottom of the
page. This button has a long name, is not easy to find and takes up a lot of
room. If we wanted to add additional "bulk" actions, this wouldn’t scale well
visually.
In this section, we’ll replace this single button with a toolbar. Furthermore,
the toolbar will only appear when the user starts selecting contacts. Finally,
it will show how many contacts are selected and let you select all contacts in
one go.
The first thing we will need to add is an `x-data` attribute, to hold the state
that we will use to determine if the toolbar is visible or not. We will need to
place this on an ancestor element of both the toolbar that we are going to add, as
well as the checkboxes, which will be updating the state when they are checked
and unchecked. The best option given our current HTML is to place the attribute
on the `form` element that surrounds the contacts table. We will declare a
property,
`selected`, which will be an array that holds the selected contact ids, based on
the checkboxes that are selected.
Here is what our form tag will look like:
#figure[```html
<form x-data="{ selected: [] }"> <1>
```]
1. This form wraps around the contacts table.
#index[Alpine.js][x-if]
Next, at the top of the contacts table, we are going to add a `template`
tag. A template tag is _not_ rendered by a browser, by default, so you might be
surprised that we are using it. However, by adding an Alpine `x-if` attribute,
we can tell Alpine: if a condition is true, show the HTML within this template.
Recall that we want to show the toolbar if and only if one or more contacts are
selected. But we know that we will have the ids of the selected contacts in the `selected` property.
Therefore, we can check the _length_ of that array to see if there are any
selected contacts, quite easily:
#figure[```html
<template x-if="selected.length > 0"> <1>
<div class="box info tool-bar">
<slot x-text="selected.length"></slot>
contacts selected
<button type="button" class="bad bg color border">Delete</button> <2>
<hr aria-orientation="vertical">
<button type="button">Cancel</button> <2>
</div>
</template>
```]
1. Show this HTML if there are 1 or more selected contacts.