-
Notifications
You must be signed in to change notification settings - Fork 18
/
ch11-hyperview-a-mobile-hypermedia.typ
1520 lines (1321 loc) · 69.6 KB
/
ch11-hyperview-a-mobile-hypermedia.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": *
== Hyperview: A Mobile Hypermedia
You may be forgiven for thinking the hypermedia architecture is synonymous with
the web, web browsers, and HTML. No doubt, the web is the largest hypermedia
system, and web browsers are the most popular hypermedia client. The dominance
of the web in discussions about hypermedia make it easy to forget that
hypermedia is a general concept, and can be applied to all types of platforms
and applications. In this chapter, we will see the hypermedia architecture
applied to a non-web platform: native mobile applications.
Mobile as a platform has different constraints than the web. It requires
different trade-offs and design decisions. Nonetheless, the concepts of
hypermedia, HATEOAS, and REST can be directly applied to build delightful mobile
applications.
In this chapter we will cover shortcomings with the current state of mobile app
development, and how a hypermedia architecture can address these problems. We
will then look at a path toward hypermedia on mobile: Hyperview, a mobile app
framework that uses the hypermedia architecture. We’ll conclude with an overview
of HXML, the hypermedia format used by Hyperview.
=== The State of Mobile App Development <_the_state_of_mobile_app_development>
Before we can discuss how to apply hypermedia to mobile platforms, we need to
understand how native mobile apps are commonly built. I’m using the word "native"
to refer to code written against an SDK provided by the phone’s operating system
(typically Android or iOS). This code is packaged into an executable binary, and
uploaded & approved through app stores controlled by Google and Apple. When
users install or update an app, they’re downloading this executable and running
the code directly on their device’s OS. In this way, mobile apps have a lot in
common with old-school desktop apps for Mac, Windows, or Linux. There is one
important difference between PC desktop apps of yesteryear and today’s mobile
apps. These days, almost all mobile apps are "networked". By networked, we mean
the app needs to read and write data over the Internet to deliver its core
functionality. In other words, a networked mobile app needs to implement the
client-server architecture.
When implementing the client-server architecture, the developer needs to make a
decision: Should the app be designed as a thin client or thick client? The
current mobile ecosystems strongly push developers towards a thick-client
approach. Why? Remember, Android and iOS require that a native mobile app be
packaged and distributed as an executable binary. There’s no way around it.
Since the developer needs to write code to package into an executable, it seems
logical to implement some of the app’s logic in that code. The code may as well
initiate HTTP calls to the server to retrieve data, and then render that data
using the platform’s UI libraries. Thus, developers are naturally led into a
thick-client pattern that looks something like this:
- The client contains code to make API requests to the server, and code to
translate those responses to UI updates
- The server implements an HTTP API that speaks JSON, and knows little about the
state of the client
Just like with SPAs on the web, this architecture has a big downside: the app’s
logic gets spread across the client and server. Sometimes, this means that logic
gets duplicated (like to validate form data). Other times, the client and server
each implement disjoint parts of the app’s overall logic. To understand what the
app does, a developer needs to trace interactions between two very different
codebases.
There’s another downside that affects mobile apps more than SPAs: API churn.
Remember, the app stores control how your app gets distributed and updated.
Users can even control if and when they get updated versions of your app. As a
mobile developer, you can’t assume that every user will be on the latest version
of your app. Your frontend code gets fragmented across many versions, and now
your backend needs to support all of them.
=== Hypermedia for Mobile Apps
#index[hypermedia][for mobile]
We’ve seen that the hypermedia architecture can address the shortcomings of SPAs
on the web. But can hypermedia work for mobile apps as well? The answer is yes!
Just like on the web, we can use hypermedia formats on mobile and let it serve
as the engine of application state. All of the logic is controlled from the
backend, rather than being spread between two codebases. Hypermedia architecture
also solves the annoying problem of API churn on mobile apps. Since the backend
serves a hypermedia response containing both data and actions, there’s no way
for the data and UI to get out of sync. No more worries about backwards
compatibility or maintaining multiple API versions.
So how can you use hypermedia for your mobile app? There are two approaches
employing hypermedia to build & ship native mobile apps today:
- Web views, which wraps the trusty web platform in a mobile app shell
- Hyperview, a new hypermedia system we designed specifically for mobile apps
==== Web Views <_web_views>
The simplest way to use hypermedia architecture on mobile is by leveraging web
technologies. Both Android and iOS SDKs provide "web views": chromeless web
browsers that can be embedded in native apps. Tools like Apache Cordova make it
easy to take the URL of a website, and spit out native iOS and Android apps
based on web views. If you already have a responsive web app, you can get a "native"
mobile HDA for free. Sounds too good to be true, right?
Of course, there is a fundamental limitation with this approach. The web
platform and mobile platforms have different capabilities and UX conventions.
HTML doesn’t natively support common UI patterns of mobile apps. One of the
biggest differences is around how each platform handles navigation. On the web,
navigation is page-based, with one page replacing another and the browser
providing back/forward buttons to navigate the page history. On mobile,
navigation is more complex, and tuned for the physicality of gesture-based
interactions.
- To drill down, screens slide on top of each other, forming stacks of screens.
- Tab bars at the top or bottom of the app allow switching between various stacks
of screens.
- Modals slide up from the bottom of the app, covering the other stacks and tab
bar.
- Unlike with web pages, all of these screens are still present in memory,
rendered and updating based on app state.
The navigation architecture is a major difference between how mobile and web
apps function. But it’s not the only one. Many other UX patterns are present in
mobile apps, but are not natively supported on the web:
- pull-to-refresh to refresh content in a screen
- horizontal swipe on UI elements to reveal actions
- sectioned lists with sticky headers
While these interactions are not natively supported by web browsers, they can be
simulated with JS libraries. Of course, these libraries will never have the same
feel and performance as native gestures. And using them usually requires
embracing a JS-heavy SPA architecture like React. This puts us back at square 1!
To avoid using the typical thick-client architecture of native mobile apps, we
turned to a web view. The web view allows us to use good-old hypermedia-based
HTML. But to get the desired look & feel of a mobile app, we end up building a
SPA in JS, losing the benefits of Hypermedia in the process.
To build a mobile HDA that acts and feels like a native app, HTML isn’t going to
cut it. We need a format designed to represent the interactions and patterns of
native mobile apps. That’s exactly what Hyperview does.
==== Hyperview
#indexed[Hyperview] is an open-source hypermedia system that provides:
- A hypermedia format for defining mobile apps called HXML
- A hypermedia client for HXML that works on iOS and Android
- Extension points in HXML and the client to customize the framework for a given
app
===== The format
#indexed[HXML] was designed to feel familiar to web developers, used to working
with HTML. Thus the choice of XML for the base format. In addition to familiar
ergonomics, XML is compatible with server-side rendering libraries. For example,
Jinja2 is perfectly suited as a templating library to render HXML. The
familiarity of XML and the ease of integration on the backend make it simple to
adopt in both new and existing codebases. Take a look at a "Hello World" app
written in HXML. The syntax should be familiar to anyone who’s worked with HTML:
#figure(caption: [Hello World])[
```xml
<doc xmlns="https://hyperview.org/hyperview">
<screen>
<styles />
<body>
<header>
<text>My first app</text>
</header>
<view>
<text>Hello World!</text>
</view>
</body>
</screen>
</doc>
``` ]
But HXML is not just a straight port of HTML with differently named tags. In
previous chapters, we’ve seen how htmx enhances HTML with a handful of new
attributes. These additions maintain the declarative nature of HTML, while
giving developers the power to create rich web apps. In HXML, the concepts of
htmx are built into the spec. Specifically, HXML is not limited to "click a
link" and "submit a form" interactions like basic HTML. It supports a range of
triggers and actions for modifying the content on a screen. These interactions
are bundled together in a powerful concept of "behaviors." Developers can even
define new behavior actions to add new capabilities to their app, without the
need for scripting. We will learn more about behaviors later in this chapter.
===== The client
#index[hypermedia][client]
Hyperview provides an open-source HXML client library written in React Native.
With a little bit of configuration and a few steps on the command line, this
library compiles into native app binaries for iOS or Android. Users install the
app on their device via an app store. On launch, the app makes an HTTP request
to the configured URL, and renders the HXML response as the first screen.
It may seem a little strange that developing a HDA using Hyperview requires a
single-purpose client binary. After all, we don’t ask users to first download
and install a binary to view a web app. No, users just enter a URL in the
address bar of a general-purpose web browser. A single HTML client renders apps
from any HTML server (@fig-1clientmanyserver).
#asciiart(
read("images/diagram/one-client-many-servers.txt"), caption: [One HTML client, multiple HTML servers],
)<fig-1clientmanyserver>
It is theoretically possible to build an equivalent general-purpose
"Hyperview browser." This HXML client would render apps from any HXML server,
and users would enter a URL to specify the app they want to use. But iOS and
Android are built around the concept of single-purpose apps. Users expect to
find and install apps from an app store, and launch them from the home screen of
their device. Hyperview embraces this app-centric paradigm of today’s popular
mobile platforms. That means that the HXML client (app binary) renders its UI
from a single pre-configured HXML server (@fig-1client1server).
#asciiart(
read("images/diagram/one-server-one-hxml-client.txt"), caption: [One HXML client, one HXML server],
)<fig-1client1server>
Luckily, developers do not need to write a HXML client from scratch; the
open-source client library does 99% of the work. And as we will see in the next
section, there are major benefits to controlling both the client and server in a
HDA.
===== Extensibility <_extensibility>
To understand the benefits of Hyperview’s architecture, we need to first discuss
the drawbacks of the web architecture. On the web, any web browser can render
HTML from any web server. This level of compatibility can only happen with
well-defined standards such as HTML5. But defining and evolving standards is a
laborious process. For example, the W3C took over 7 years to go from first draft
to recommendation on the HTML5 spec. It’s not surprising, given the level of
thoughtfulness that needs to go into a change that impacts so many people. But
it means that progress happens slowly. As a web developer, you may need to wait
years for browsers to gain widespread support for the feature you need.
So what are the benefits of Hyperview’s architecture? In a Hyperview app, _your_ mobile
app only renders HXML from _your_ server. You don’t need to worry about
compatibility between your server and other mobile apps, or between your mobile
app and other servers. There is no standards body to consult. If you want to add
a blink feature to your mobile app, go ahead and implement a `<blink>` element
in the client, and start returning `<blink>` elements in the HXML responses from
your server. In fact, the Hyperview client library was built with this type of
extensibility in mind. There are extension points for custom UI elements and
custom behavior actions. We expect and encourage developers to use these
extensions to make HXML more expressive and customized to their app’s
functionality.
And by extending the HXML format and client itself, there’s no need for
Hyperview to include a scripting layer in HXML. Features that require
client-side logic get "built-in" to the client binary. HXML responses remain
pure, with UI and interactions represented in declarative XML.
==== Which Hypermedia Architecture Should You Use? <_which_hypermedia_architecture_should_you_use>
We’ve discussed two approaches for creating mobile apps using hypermedia
systems:
- create a backend that returns HTML, and serve it in a mobile app through a web
view
- create a backend that returns HXML, and serve it in a mobile app with the
Hyperview client
I purposefully described the two approaches in a way to highlight their
similarities. After all, they are both based on hypermedia systems, just with
different formats and clients. Both approaches solve the fundamental issues with
traditional, SPA-like mobile app development:
- The backend controls the full state of the app.
- Our app’s logic is all in one place.
- The app always runs the latest version, there’s no API churn to worry about.
So which approach should you use for a mobile HDA? Based on our experience
building both types of apps, we believe the Hyperview approach results in a
better user experience. The web-view will always feel out-of-place on iOS and
Android; there’s just no good way to replicate the patterns of navigation and
interaction that mobile users expect. Hyperview was created specifically to
address the limitations of thick-client and web view approaches. After the
initial investment to learn Hyperview, you’ll get all of the benefits of the
Hypermedia architecture, without the downsides of a degraded user experience.
Of course, if you already have a simple, mobile-friendly web app, then using a
web-view approach is sensible. You will certainly save time from not having to
serve your app as HXML in addition to HTML. But as we will show at the end of
this chapter, it doesn’t take a lot of work to convert an existing
Hypermedia-driven web app into a Hyperview mobile app. But before we get there,
we need to introduce the concepts of elements and behaviors in Hyperview. Then,
we’ll re-build our contacts app in Hyperview.
#sidebar[When Shouldn't You Use Hypermedia to Build a Mobile App?][ Hypermedia is not always the right choice to build a mobile app. Just like on
the web, apps that require highly dynamic UIs (such as a spreadsheet
application) are better implemented with client-side code. Additionally, some
apps need to function while fully offline. Since HDAs require a server to render
UI, offline-first mobile apps are not a good fit for this architecture. However,
just like on the web, developers can use a hybrid approach to build their mobile
app. The highly dynamic screens can be built with complex client-side logic,
while the less dynamic screens can be built with web views or Hyperview. In this
way, developers can spend their _complexity budget_ on the core of the
application, and keep the simple screens simple. ]
=== Introduction to HXML
==== Hello World!
#index[HXML][Hello World!]
HXML was designed to feel natural to web developers coming from HTML. Let’s take
a closer look at the "Hello World" app defined in HXML:
#figure(caption: [Hello World, revisited])[ ```xml
<doc xmlns="https://hyperview.org/hyperview"> <1>
<screen> <2>
<styles />
<body> <3>
<header> <4>
<text>My first app</text>
</header>
<view> <5>
<text>Hello World!</text> <6>
</view>
</body>
</screen>
</doc>
``` ]
1. The root element of the HXML app
2. The element representing a screen of the app
3. The element representing the UI of the screen
4. The element representing the top header of the screen
5. A wrapper element around the content shown on the screen
6. The text content shown on the screen
Nothing too strange here, right? Just like HTML, the syntax defines a tree of
elements using start tags (`<screen>`) and end tags (`</screen>`). Elements can
contain other elements (`<view>`) or text (`Hello World!`). Elements can also be
empty, represented with an empty tag (`<styles />`). However, you’ll notice that
the names of the HXML element are different from those in HTML. Let’s take a
closer look at each of those elements to understand what they do.
#index[HXML][\<doc\>]
`<doc>` is the root of the HXML app. Think of it as equivalent to the
`<html>` element in HTML. Note that the `<doc>` element contains an attribute `xmlns="https://hyperview.org/hyperview"`.
This defines the default namespace for the doc. Namespaces are a feature of XML
that allow one doc to contain elements defined by different developers. To
prevent conflicts when two developers use the same name for their element, each
developer defines a unique namespace. We will talk more about namespaces when we
discuss custom elements & behaviors later in this chapter. For now, it’s enough
to know that elements in a HXML doc without an explicit namespace are considered
to be part of the
`https://hyperview.org/hyperview` namespace.
#index[HXML][\<screen\>]
`<screen>` represents the UI that gets rendered on a single screen of a mobile
app. It’s possible for one `<doc>` to contain multiple `<screen>`
elements, but we won’t get into that now. Typically, a `<screen>`
element will contain elements that define the content and styling of the screen.
#index[HXML][\<styles\>]
`<styles>` defines the styles of the UI on the screen. We won’t get too much
into styling in Hyperview in this chapter. Suffice it to say, unlike HTML,
Hyperview does not use a separate language (CSS) to define styles. Instead,
styling rules such as colors, spacing, layout, and fonts are defined in HXML.
These rules are then explicitly referenced by UI elements, much like using
classes in CSS.
#index[HXML][\<body\>]
`<body>` defines the actual UI of the screen. The body includes all text,
images, buttons, forms, etc that will be shown to the user. This is equivalent
to the `<body>` element in HTML.
#index[HXML][\<header\>]
`<header>` defines the header of the screen. Typically in mobile apps, the
header includes some navigation (like a back button), and the title of the
screen. It’s useful to define the header separately from the rest of the body.
Some mobile OSes will use a different transition for the header than the rest of
the screen content.
#index[HXML][\<view\>]
`<view>` is the basic building block for layouts and structure within the
screen’s body. Think of it like a `<div>` in HTML. Note that unlike in HTML, a `<div>` cannot
directly contain text.
#index[HXML][\<text\>]
`<text>` elements are the only way to render text in the UI. In this example, "Hello
World" is contained within a `<text>` element.
That’s all there is to define a basic "Hello World" app in HXML. Of course, this
isn’t very exciting. Let’s cover some other built-in display elements.
==== UI Elements
===== Lists
#index[HXML][\<list\>]
#index[HXML][\<item\>]
A very common pattern in mobile apps is to scroll through a list of items. The
physical properties of a phone screen (long & vertical) and the intuitive
gesture of swiping a thumb up & down makes this a good choice for many screens.
HXML has dedicated elements for representing lists and items.
#figure(caption: [List element])[ ```xml
<list> <1>
<item key="item1"> <2>
<text>My first item</text> <3>
</item>
<item key="item2">
<text>My second item</text>
</item>
</list>
``` ]
1. Element representing a list
2. Element representing an item in the list, with a unique key
3. The content of the item in the list.
Lists are represented with two new elements. The `<list>` wraps all of the items
in the list. It can be styled like a generic `<view>` (width, height, etc). A `<list>` element
only contains `<item>` elements. Of course, these represent each unique item in
the list. Note that `<item>`
is required to have a `key` attribute, which is unique among all items in the
list.
You might be asking, "Why do we need a custom syntax for lists of items? Can’t
we just use a bunch of `<view>` elements?". Yes, for lists with a small number
of items, using nested `<views>` will work quite well. However, often the number
of items in a list can be long enough to require optimizations to support smooth
scrolling interactions. Consider browsing a feed of posts in a social media app.
As you keep scrolling through the feed, it’s not unusual for the app to show
hundreds if not thousands of posts. At any time, you can flick your finger to
scroll to almost any part of the feed. Mobile devices tend to be
memory-constrained. Keeping the fully-rendered list of items in memory could
consume more resources than available. That’s why both iOS and Android provide
APIs for optimized list UIs. These APIs know which part of the list is currently
on-screen. To save memory, they clear out the non-visible list items, and
recycle the item UI objects to conserve memory. By using explicit `<list>` and `<item>` elements
in HXML, the Hyperview client knows to use these optimized list APIs to make
your app more performant.
#index[HXML][\<section\>]
#index[HXML][\<section-list\>]
#index[HXML][\<section-title\>]
It’s also worth mentioning that HXML supports section lists. Section lists are
useful for building list-based UIs, where the items in the list can be grouped
for the user’s convenience. For example, a UI showing a restaurant menu could
group the offerings by dish type:
#figure(caption: [Section list element])[ ```xml
<section-list> <1>
<section> <2>
<section-title> <3>
<text>Appetizers</text>
</section-title>
<item key="1"> <4>
<text>French Fries</text>
</item>
<item key="2">
<text>Onion Rings</text>
</item>
</section>
<section> <5>
<section-title>
<text>Entrees</text>
</section-title>
<item key="3">
<text>Burger</text>
</item>
</section>
</section-list>
``` ]
1. Element representing a list with sections
2. The first section of appetizer offerings
3. Element for the title of the section, rendering the text "Appetizers"
4. An item representing an appetizer
5. A section for entree offerings
You’ll notice a couple of differences between `<list>` and
`<section-list>`. The section list element only contains `<section>`
elements, representing a group of items. A section can contain a
`<section-title>` element. This is used to render some UI that acts as the
header of the section. This header is "sticky", meaning it stays on screen while
scrolling through items that belong to the corresponding section. Finally, `<item>` elements
act the same as in the regular list, but can only appear within a `<section>`.
===== Images
#index[HXML][\<image\>]
#index[Hyperview][images]
Showing images in Hyperview is pretty similar to HTML, but there are a few
differences.
#figure(caption: [Image element])[ ```xml
<image source="/profiles/1.jpg" style="avatar" />
``` ]
The `source` attribute specifies how to load the image. Like in HTML, the source
can be an absolute or relative URL. Additionally, the source can be an encoded
data URI, for example `data:image/png;base64,iVBORw`. However, the source can
also be a "local" URL, referring to an image that is bundled as an asset in the
mobile app. The local URL is prefixed with `./`:
#figure(caption: [Image element, pointing to local source])[ ```xml
<image source="./logo.png" style="logo" />
``` ]
Using Local URLs is an optimization. Since the images are on the mobile device,
they don’t require a network request and will appear quickly. However, bundling
the image with the mobile app binary increases the binary size. Using local
images is a good trade-off for images that are frequently accessed but rarely
change. Good examples include the app logo, or common button icons.
The other thing to note is the presence of the `style` attribute on the
`<image>` element. In HXML, images are required to have a style that has rules
for the image’s `width` and `height`. This is different from HTML, where `<img>` elements
do not need to explicitly set a width and height. web browsers will re-flow the
content of a web page once the image is fetched and the dimensions are known.
While re-flowing content is a reasonable behavior for web-based documents, users
do not expect mobile apps to re-flow as content loads. To maintain a static
layout, HXML requires the dimensions to be known before the image loads.
==== Inputs
#index[Hyperview][inputs]
There’s a lot to cover about inputs in Hyperview. Since this is meant to be an
introduction and not an exhaustive resource, I’ll highlight just a few types of
inputs. Let’s start with an example of the simplest type of input, a text field.
#figure(caption: [Text field element])[ ```xml
<text-field
name="first_name" <1>
style="input" <2>
value="Adam" <3>
placeholder="First name" <4>
/>
``` ]
1. The name used when serializing data from this input
2. The style class applied to the UI element
3. The current value set in the field
4. A placeholder to display when the value is empty
#index[HXML][\<text-field\>]
This element should feel familiar to anyone who’s created a text field in HTML.
One difference is that most inputs in HTML use the `<input>`
element with a `type` attribute, eg `<input type="text">`. In Hyperview, each
input has a unique name, in this case `<text-field>`. By using different names,
we can use more expressive XML to represent the input.
For example, let’s consider a case where we want to render a UI that lets the
user select one among several options. In HTML, we would use a radio button
input, something like
`<input type="radio" name="choice" value="option1" />`. Each choice is
represented as a unique input element. This never struck me as ideal. Most of
the time, radio buttons are grouped together to affect the same name. The HTML
approach leads to a lot of boilerplate (duplication of
`type="radio"` and `name="choice"` for each choice). Also, unlike radio buttons
on desktop, mobile OSes don’t provide a strong standard UI for selecting one
option. Most mobile apps use richer, custom UIs for these interactions. So in
HXML, we implement this UI using an element called
`<select-single>`:
#figure(caption: [Select-single element])[ ```xml
<select-single name="choice"> <1>
<option value="option1"> <2>
<text>Option 1</text> <3>
</option>
<option value="option2">
<text>Option 2</text>
</option>
</select-single>
``` ]
1. Element representing an input where a single choice is selected. The name of the
selection is defined once here.
2. Element representing one of the choices. The choice value is defined here.
3. The UI of the selection. In this example, we use text, but we can use any UI
elements.
#index[HXML][\<select-single\>]
The `<select-single>` element is the parent of the input for selecting one
choice out of many. This element contains the `name` attribute used when
serializing the selected choice. `<option>` elements within
`<select-single>` represent the available choices. Note that each
`<option>` element has a `value` attribute. When pressed, this will be the
selected value of the input. The `<option>` element can contain any other UI
elements within it. This means that we’re not hampered by rendering the input as
a list of radio buttons with labels. We can render the options as radios, tags,
images, or anything else that would be intuitive for our interface. HXML styling
supports modifiers for pressed and selected states, letting us customize the UI
to highlight the selected option.
Describing all features of inputs in HXML would take an entire chapter. Instead,
I’ll summarize a few other input elements and their features.
#index[HXML][\<select-multiple\>]
#index[HXML][\<switch\>]
#index[HXML][\<date-field\>]- `<select-multiple>` works like `<select-single>`,
but it supports toggling multiple options on & off. This replaces checkbox
inputs in HTML. - The `<switch>` element renders an on/off switch that is common
in mobile UIs - The `<date-field>` element supports entering in specific dates,
and comes with a wide range of customizations for formatting, settings ranges,
etc.
#index[HXML][\<form\>]
#index[HXML][custom elements]
Two more things to mention about inputs. First is the `<form>` element. The `<form>` element
is used to group together inputs for serialization. When a user takes an action
that triggers a backend request, the Hyperview client will serialize all inputs
in the surrounding `<form>`
and include them in the request. This is true for both `GET` and `POST`
requests. We will cover this in more detail when talking about behaviors later
in this chapter. Also later in this chapter, I’ll talk about support for custom
elements in HXML. With custom elements, you can also create your own input
elements. Custom input elements allow you to build incredible powerful
interactions with simple XML syntax that integrates well with the rest of HXML.
==== Styling
#index[HXML][styling]
So far, we haven’t mentioned how to apply styling to all of the HXML elements.
We’ve seen from the Hello World app that each `<screen>` can contain a `<styles>` element.
Let’s re-visit the Hello World app and fill out the `<styles>` element.
#figure(
caption: [UI styling example],
)[ ```xml
<doc xmlns="https://hyperview.org/hyperview">
<screen>
<styles> <1>
<style class="body" flex="1" flexDirection="column" /> <2>
<style class="header"
borderBottomWidth="1" borderBottomColor="#ccc" />
<style class="main" margin="24" />
<style class="h1" fontSize="32" />
<style class="info" color="blue" />
</styles>
<body style="body"> <3>
<header style="header">
<text style="info">My first app</text>
</header>
<view style="main">
<text style="h1 info">Hello World!</text> <4>
</view>
</body>
</screen>
</doc>
``` ]
1. Element encapsulating all of the styling for the screen
2. Example of a definition of a style class for "body"
3. Applying the "body" style class to a UI element
4. Example of applying multiple style classes (h1 and info) to an element
You’ll note that in HXML, styling is part of the XML format, rather than using a
separate language like CSS. However, we can draw some parallels between CSS
rules and the `<style>` element. A CSS rule consists of a selector and
declarations. In the current version of HXML, the only available selector is a
class name, indicated by the `class` attribute. The rest of the attributes on
the `<style>` element are declarations, consisting of properties and property
values.
UI elements within the `<screen>` can reference the `<style>` rules by adding
the class names to their `<style>` property. Note the `<text>`
element around "Hello World!" references two style classes: `h1` and
`info`. The styles from the corresponding classes are merged together in the
order they appear on the element. It’s worth noting that styling properties are
similar to those in CSS (color, margins/padding, borders, etc). Currently, the
only available layout engine is based on flexbox.
Style rules can get quite verbose. For the sake of brevity, we won’t include the `<styles>` element
in the rest of the examples in this chapter unless necessary.
==== Custom elements
#index[HXML][custom elements]
The core UI elements that ship with Hyperview are quite basic. Most mobile apps
require richer elements to deliver a great user experience. Luckily, HXML can
easily accommodate custom elements in its syntax. This is because HXML is really
just XML, aka "Extensible Markup Language". Extensibility is already built into
the format! Developers are free to define new elements and attributes to
represent custom elements.
Let’s see this in action with a concrete example. Assume that we want to add a
map element to our Hello World app. We want the map to display a defined area,
and one or more markers at specific coordinates in that area. Let’s translate
these requirements into XML:
- An `<area>` element will represent the area displayed by the map. To specify the
area, the element will include attributes for `latitude`
and `longitude` for the center of the area, and a `latitude-delta` and
`longitude-delta` indicating the +/- display area around the center.
- A `<marker>` element will represent a marker in the area. The coordinates of the
marker will be defined by `latitude` and
`longitude` attributes on the marker.
Using these custom XML elements, an instance of the map in our app might look
like this:
#figure(
caption: [Custom elements in HXML],
)[ ```xml
<doc xmlns="https://hyperview.org/hyperview">
<screen>
<body>
<view>
<text>Hello World!</text>
<area latitude="37.8270" longitude="122.4230"
latitude-delta="0.1" longitude-delta="0.1"> <1>
<marker latitude="37.8118" longitude="-122.4177" /> <2>
</area>
</view>
</body>
</screen>
</doc>
``` ]
1. Custom element representing the area rendered by the map
2. Custom element representing a marker rendered at specific coordinates on the map
The syntax feels right at home among the core HXML elements. However, there’s a
potential problem. "area" and "marker" are pretty generic names. I could see `<area>` and `<marker>` elements
being used by a customization to render charts & graphs. If our app renders both
maps and charts, the HXML markup would be ambiguous. What should the client
render when it sees `<area>` or `<marker>`?
#index[Hyperview][XML namespaces]
This is where XML namespaces come in. XML namespaces eliminate ambiguity and
collisions between elements and attributes used to represent different things.
Remember that the `<doc>` element declares that
`https://hyperview.org/hyperview` is the default namespace for the entire
document. Since no other elements define namespaces, every element in the
example above is part of the
`https://hyperview.org/hyperview` namespace.
Let’s define a new namespace for our map elements. Since this namespace will not
be the default for the document, we also need to assign the namespace to a
prefix we will add to our elements:
#figure[```xml
<doc xmlns="https://hyperview.org/hyperview"
xmlns:map="https://mycompany.com/hyperview-map">
```]
This new attribute declares that the `map:` prefix is associated with the
namespace "https:\/\/mycompany.com/hyperview-map". This namespace could be
anything, but remember the goal is to use something unique that won’t have
collisions. Using your company/app domain is a good way to guarantee uniqueness.
Now that we have a namespace and prefix, we need to use it for our elements:
#figure(
caption: [Namespacing the custom elements],
)[ ```xml
<doc xmlns="https://hyperview.org/hyperview"
xmlns:map="https://mycompany.com/hyperview-map"> <1>
<screen>
<body>
<view>
<text>Hello World!</text>
<map:area latitude="37.8270" longitude="122.4230"
latitude-delta="0.1" longitude=delta="0.1"> <2>
<map:marker latitude="37.8118" longitude="-122.4177" /> <3>
</map:area> <4>
</view>
</body>
</screen>
</doc>
``` ]
1. Definition of namespace aliased to "map"
2. Adding the namespace to the "area" start tag
3. Adding the namespace to the "marker" self-closing tag
4. Adding the namespace to the "area" end tag
That’s it! If we introduced a custom charting library with "area" and
"marker" elements, we would create a unique namespace for those elements as
well. Within the HXML doc, we could easily disambiguate `<map:area>`
from `<chart:area>`.
At this point you might be wondering, "how does the Hyperview client know to
render a map when my doc includes \<map:area\>?" It’s true, so far we only
defined the custom element format, but we haven’t implemented the element as a
feature in our app. We will get into the details of implementing custom elements
in the next chapter.
==== Behaviors <_behaviors>
As discussed in earlier chapters, HTML supports two basic types of interactions:
- Clicking a hyperlink: the client will make a GET request and render the response
as a new web page.
- Submitting a form: the client will (typically) make a POST request with the
serialized content of the form, and render the response as a new web page.
Clicking hyperlinks and submitting forms is enough to build simple web
applications. But relying on just these two interactions limits our ability to
build richer UIs. What if we want something to happen when the user mouses over
a certain element, or perhaps when they scroll some content into the viewport?
We can’t do that with basic HTML. Additionally, both clicks and form submits
result in loading a full new web page. What if we only want to update a small
part of the current page? This is a very common scenario in rich web
applications, where users expect to fetch and update content without navigating
to a new page.
So with basic HTML, interactions (clicks and submits) are limited and tightly
coupled to a single action (loading a new page). Of course, using JavaScript, we
can extend HTML and add some new syntax to support our desired interactions.
Htmx does exactly that with a new set of attributes:
- Interactions can be added to any element, not just links and forms.
- The interaction can be triggered via a click, submit, mouseover, or any other
JavaScript event.
- The actions resulting from the trigger can modify the current page, not just
request a new page.
By decoupling elements, triggers, and actions, htmx allows us to build rich
Hypermedia-driven applications in a way that feels very compatible with HTML
syntax and server-side web development.
#index[HXML][behaviors]
HXML takes the idea of defining interactions via triggers & actions and builds
them into the spec. We call these interactions "behaviors." We use a special `<behavior>` element
to define them. Here’s an example of a simple behavior that pushes a new mobile
screen onto the navigation stack:
#figure(caption: [Basic behavior])[ ```xml
<text>
<behavior <1>
trigger="press" <2>
action="push" <3>
href="/next-screen" <4>
/>
Press me!
</text>
``` ]
1. The element encapsulating an interaction on the parent `<text>`
element.
2. The trigger that will execute the interaction, in this case pressing the `<text>` element.
3. The action that will execute when triggered, in this case pushing a new screen
onto the current stack.
4. The href to load on the new screen.
Let’s break down what’s happening in this example. First, we have a
`<text>` element with the content "Press me!". We’ve shown `<text>`
elements before in examples of HXML, so this is nothing new. But now, the `<text>` element
contains a new child element, `<behavior>`. This
`<behavior>` element defines an interaction on the parent `<text>`
element. It contains two attributes that are required for any behavior:
- `trigger`: defines the user action that triggers the behavior
- `action`: defines what happens when triggered
In this example, the `trigger` is set to `press`, meaning this interaction will
happen when the user presses the `<text>` element. The
`action` attribute is set to `push`. `push` is an action that will push a new
screen onto the navigation stack. Finally, Hyperview needs to know what content
to load on the newly pushed screen. This is where the
`href` attribute comes in. Notice we don’t need to define the full URL. Much
like in HTML, the `href` can be an absolute or relative URL.
So that’s a first example of behaviors in HXML. You may be thinking this syntax
seems quite verbose. Indeed, pressing elements to navigate to a new screen is
one of the most common interactions in a mobile app. It would be nice to have a
simpler syntax for the common case. Luckily,
`trigger` and `action` attributes have default values of `press` and
`push`, respectively. Therefore, they can be omitted to clean up the syntax:
#figure(caption: [Basic behavior with defaults])[ ```xml
<text>
<behavior href="/next-screen" /> <1>
Press me!
</text>
``` ]
1. When pressed, this behavior will open a new screen with the given URL.
This markup for the `<behavior>` will produce the same interaction as the
earlier example. With the default attributes, the `<behavior>`
element looks similar to an anchor `<a>` in HTML. But the full syntax achieves
our goals of decoupling elements, triggers, and actions:
- Behaviors can be added to any element, they are not limited to links and forms.
- Behaviors can specify an explicit `trigger`, not just clicks or form submits.
- Behaviors can specify an explicit `action`, not just a request for a new page.
- Extra attributes like `href` provide more context for the action.
Additionally, using a dedicated `<behavior>` element means a single element can
define multiple behaviors. This lets us execute several actions from the same
trigger. Or, we can execute different actions for different triggers on the same
element. We will show examples of the power of multiple behaviors at the end of
this chapter. First we need to show the variety of supported actions and
triggers.
===== Actions
#index[HXML][behavior actions]
Behavior actions in Hyperview fall into four general categories:
- Navigation actions, which load new screens and move between them
- Update actions, which modify the HXML of the current screen
- System actions, which interact with OS-level capabilities.
- Custom actions, which can execute any code you add to the client.
====== Navigation actions
#index[HXML][navigation actions]
We’ve already seen the simplest type of action, `push`. We classify
`push` as a "navigation action", since it’s related to navigating screens in the
mobile app. Pushing a screen onto the navigation stack is just one of several
navigation actions supported in Hyperview. Users also need to be able to go back
to previous screens, open and close modals, switch between tabs, or jump to
arbitrary screens. Each of these types of navigation is supported through a
different value for the
`action` attribute:
- `push`: Push a new screen into the current navigation stack. This looks like a
screen sliding in from the right, on top of the current screen.
- `new`: Open a new navigation stack as a modal. This looks like a screen sliding
in from the bottom, on top of the current screen.
- `back`: This is a complement to the `push` action. It pops the current screen
off of the navigation stack (sliding it to the right).
- `close`: This is a complement to the `new` action. It closes the current
navigation stack (sliding it down).
- `reload`: Similar to a browser’s "refresh" button, this will re-request the
content of the current screen.
- `navigate`: This action will attempt to find a screen with the given
`href` already loaded in the app. If the screen exists, the app will jump to
that screen. If it doesn’t exist, it will act the same as
`push`.
`push`, `new`, and `navigate` all load a new screen. Thus, they require an `href` attribute
so that Hyperview knows what content to request for the new screen. `back` and `close` do
not load new screens, and thus do not require the `href` attribute. `reload` is
an interesting case. By default, it will use the URL of the screen when
re-requesting the content for the screen. However, if you want to replace the
screen with a different one, you can provide an `href` attribute with `reload` on
the behavior element.
Let’s look at an example "widgets" app that uses several navigation actions on
one screen:
#figure(caption: [Navigation action examples])[ ```xml
<screen>
<body>
<header>
<text>
<behavior action="back" /> <1>
Back
</text>
<text>
<behavior action="new" href="/widgets/new" /> <2>
New Widget
</text>
</header>
<text>
<behavior action="reload" /> <3>
Check for new widgets
</text>
<list>
<item key="widget1">
<behavior action="push" href="/widgets/1" /> <4>
</item>
</list>
</body>
</screen>
``` ]
1. Takes the user to the previous screen
2. Opens a new modal to add a widget
3. Reloads the content of the screen, showing new widgets from the backend
4. Pushes a new screen with details for a specific widget
Most screens in your app will need a way for the user to backtrack to the
previous screen. This is usually done with a button in the header that uses
either a "back" or "close" action, depending on how the screen was opened. In
this example, we’re assuming the widgets screen was pushed onto the navigation
stack, so the "back" action is appropriate. The header contains a second button
that allows the user to enter data for a new widget. Pressing this button will
open a modal with a "New Widget" screen. Since this "New Widget" screen will
open as a modal, it will need a corresponding "close" action to dismiss itself
and show our
"widgets" screen again. Finally, to see more details about a specific widget,
each `<item>` element contains a behavior with a "push" action. This action will
push a "Widget Detail" screen onto the current navigation stack. Like in the "Widgets"
screen, "Widget Detail" will need a button in the header that uses the "back"
action to let the user backtrack.
On the web, the browser handles basic navigation needs such as going
back/forward, reloading the current page, or jumping to a bookmark. iOS and
Android don’t provide this sort of universal navigation for native mobile apps.
It’s on the app developers to handle this themselves. Navigation actions in HXML
provide an easy but powerful way for developers to build an architecture that
makes sense for their app.
====== Update actions
#index[HXML][update actions]
Behavior actions are not just limited to navigating between screens. They can
also be used to change the content on the current screen. We call these "update
actions". Much like navigation actions, update actions make a request to the
backend. However, the response is not an entire HXML document, but a fragment of
HXML. This fragment is added to the HXML of the current screen, resulting in an
update to the UI. The
`action` attribute of the `<behavior>` determines how the fragment gets
incorporated into the HXML. We also need to introduce a new `target`
attribute on `<behavior>` to define where the fragment gets incorporated in the
existing doc. The `target` attribute is an ID reference to an existing element
on the screen.
Hyperview currently supports these update actions, representing different ways
to incorporate the fragment into the screen:
- `replace`: replaces the entire target element with the fragment
- `replace-inner`: replaces the children of the target element with the fragment
- `append`: adds the fragment after the last child of the target element
- `prepend`: adds the fragment before the first child of the target element.
Let’s look at some examples to make this more concrete. For these examples,
let’s assume our backend accepts `GET` requests to
`/fragment`, and the response is a fragment of HXML that looks like
`<text>My fragment</text>`.
#figure(
caption: [Update action examples],
)[ ```xml
<screen>
<body>
<text>
<behavior action="replace" href="/fragment" target="area1" /> <1>
Replace