-
Notifications
You must be signed in to change notification settings - Fork 17
/
ch13-extending-the-hyperview-client.typ
839 lines (722 loc) · 33.5 KB
/
ch13-extending-the-hyperview-client.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
#import "lib/definitions.typ": *
== Extending The Hyperview Client
In the previous chapter, we created a fully-featured native mobile version of
our Contacts app. Aside from customizing the entry point URL, we didn’t need to
touch any code that runs on the mobile device. We defined our mobile app’s UI
and logic completely in the backend code, using Flask and HXML templates. This
is possible because the standard Hyperview client supports all of the basic
features by mobile apps.
But the standard Hyperview client can’t do everything out of the box. As app
developers, we want apps to have unique touches like custom UIs or deep
integration with platform capabilities. To support these needs, the Hyperview
client was designed to be extended with custom behavior actions and UI elements.
In this section, we will enhance our mobile app with examples of both.
Before diving in, let’s take a quick look at the tech stack we’ll be using. The
Hyperview client is written in React Native, a popular cross-platform framework
for creating mobile apps. It uses the same component-based API as React. This
means developers familiar with JavaScript and React can quickly pick up React
Native. React Native has a healthy ecosystem of open-source libraries. We’ll be
leveraging these libraries to create our custom extensions to the Hyperview
client.
=== Adding Phone Calls and Email
#index[Hyperview][phone calls]
Let’s start with the most obvious feature missing from our Contacts app: phone
calls. Mobile devices can make phone calls. The contacts in our app have phone
numbers. Shouldn’t our app support calling those phone numbers? And while we’re
at it, our app should also support e-mailing the contacts.
On the web, calling phone numbers is supported with the `tel:` URI scheme, and
e-mails are supported with the `mailto:` URI scheme:
#figure(caption: [`tel` and `mailto` schemes in HTML])[ ```html
<a href="tel:555-555-5555">Call</a> <1>
<a href="mailto:joe@example.com">Email</a> <2>
``` ]
+ When clicked, prompt the user to call the given phone number
+ When clicked, open an e-mail client with the given address populated in the `to:` field.
The Hyperview client doesn’t support the `tel:` and `mailto:` URI schemes. But
we can add these capabilities to the client with custom behavior actions.
Remember that behaviors are interactions defined in HXML. Behaviors have
triggers ("press", "refresh") and actions ("update", "share"). The values of "action"
are not limited to the set that comes in the Hyperview library. So let’s define
two new actions,
"open-phone" and "open-email".
#figure(
caption: [Phone and Email actions],
)[ ```xml
<view
xmlns:comms="https://hypermedia.systems/hyperview/communications"> <1>
<text>
<behavior action="open-phone"
comms:phone-number="555-555-5555" /> <2>
Call
</text>
<text>
<behavior action="open-email"
comms:email-address="joe@example.com" /> <3>
Email
</text>
</view>
``` ]
+ Define an alias for an XML namespace used by our new attributes.
+ When pressed, prompt the user to call the given phone number.
+ When pressed, open an e-mail client with the given address populated in the `to:` field.
Notice we defined the actual phone number and email address using separate
attributes. In HTML, the scheme and data are crammed into the
`href` attribute. HXML’s `<behavior>` elements give more options for
representing the data. We chose to use attributes, but we could have represented
the phone number or email using child elements. We’re also using a namespace to
avoid potential future conflicts with other client extensions.
So far so good, but how does the Hyperview client know how to interpret
`open-phone` and `open-email`, and how to reference the `phone-number`
and `email-address` attributes? This is where we finally need to write some
JavaScript.
First, we’re going to add a 3rd-party library (`react-native-communications`) to
our demo app. This library provides a simple API that interacts with OS-level
functionality for calls and emails.
```bash
cd hyperview/demo
yarn add react-native-communications <1>
yarn start <2>
```
+ Add dependency on `react-native-communications`
+ Re-start the mobile app
Next, we’ll create a new file, `phone.js`, that will implement the code
associated with the `open-phone` action:
#figure(
caption: [demo/src/phone.js],
)[ ```js
import { phonecall } from 'react-native-communications'; <1>
const namespace = "https://hypermedia.systems/hyperview/communications";
export default {
action: "open-phone", <2>
callback: (behaviorElement) => { <3>
const number = behaviorElement
.getAttributeNS(namespace, "phone-number"); <4>
if (number != null) {
phonecall(number, false); <5>
}
},
};
``` ]
+ Import the function we need from the 3rd party library.
+ The name of the action.
+ The callback that runs when the action triggers.
+ Get the phone number from the `<behavior>` element.
+ Pass the phone number to the function from the 3rd party library.
Custom actions are defined as a JavaScript object with two keys:
`action` and `callback`. This is how the Hyperview client associates a custom
action in the HXML with our custom code. The callback value is a function that
takes a single parameter, `behaviorElement`. This parameter is an XML DOM
representation of the `<behavior>` element that triggered the action. That means
we can call methods on it like
`getAttribute`, or access attributes like `childNodes`. In this case, we use `getAttributeNS` to
read the phone number from the `phone-number`
attribute on the `<behavior>` element. If the phone number is defined on the
element, we can call the `phonecall()` function provided by the
`react-native-communications` library.
There’s one more thing to do before we can use our custom action: register the
action with the Hyperview client. The Hyperview client is represented as a React
Native component called `Hyperview`. This component takes a prop called `behaviors`,
which is an array of custom action objects like our "open-phone" action. Let’s
pass our "open-phone" implementation to the `Hyperview` component in our demo
app.
#figure(caption: [demo/src/HyperviewScreen.js])[ ```js
import React, { PureComponent } from 'react';
import Hyperview from 'hyperview';
import OpenPhone from './phone'; <1>
export default class HyperviewScreen extends PureComponent {
// ... omitted for brevity
behaviors = [OpenPhone]; <2>
render() {
return (
<Hyperview
behaviors={this.behaviors} <3>
entrypointUrl={this.entrypointUrl}
// more props...
/>
);
}
}
``` ]
+ Import the open-phone action.
+ Create an array of custom actions.
+ Pass the custom actions to the `Hyperview` component, as a prop called
`behaviors`.
Under the hood, the `Hyperview` component is responsible for taking HXML and
turning it into mobile UI elements. It also handles triggering behavior actions
based on user interactions.
By passing the "open-phone" action to Hyperview, we can now use it as a value
for the `action` attribute on `<behavior>` elements. In fact, let’s do that now
by updating the `show.xml` template in our Flask app:
#figure(
caption: [Snippet of `hv/show.xml`],
)[ ```xml
{% block content %}
<view style="details">
<text style="contact-name">
{{ contact.first }} {{ contact.last }}
</text>
<view style="contact-section">
<behavior <1>
xmlns:comms="https://hypermedia.systems/hyperview/communications"
trigger="press"
action="open-phone" <2>
comms:phone-number="{{contact.phone}}" <3>
/>
<text style="contact-section-label">Phone</text>
<text style="contact-section-info">{{contact.phone}}</text>
</view>
<view style="contact-section">
<behavior <4>
xmlns:comms="https://hypermedia.systems/hyperview/communications"
trigger="press"
action="open-email"
comms:email-address="{{contact.email}}"
/>
<text style="contact-section-label">Email</text>
<text style="contact-section-info">{{contact.email}}</text>
</view>
</view>
{% endblock %}
``` ]
+ Add a behavior to the phone number section that triggers on "press."
+ Trigger the new "open-phone" action.
+ Set the attribute expected by the "open-phone" action.
+ Same idea, with a different action ("open-email").
#index[Hyperview][email]
We’ll skip over the implementation of the second custom action,
"open-email." As you can guess, this action will open a system-level email
composer to let the user send an email to their contact. The implementation of "open-email"
is almost identical to "open-phone." The
`react-native-communications` library exposes a function called
`email()`, so we just wrap it and pass arguments to it in the same way.
We now have a complete example of extending the client with custom behavior
actions. We chose a new name for our actions ("open-phone" and
"open-email"), and mapped those names to functions. The functions take
`<behavior>` elements and can run any arbitrary React Native code. We wrapped an
existing 3rd party library, and read attributes set on the
`<behavior>` element to pass data to the library. After re-starting our demo
app, our client has new capabilities we can immediately utilize by referencing
the actions from our HXML templates.
=== Adding Messages
#index[Hyperview][messages]
The phone and email actions added in the previous section are examples of "system
actions." System actions trigger some UI or capability provided by the device’s
OS. But custom actions are not limited to interacting with OS-level APIs.
Remember, the callbacks that implement actions can run arbitrary code, including
code that renders our own UI elements. This next custom action example will do
just that: render a custom confirmation message UI element.
If you recall, our Contacts web app shows messages upon successful actions, such
as deleting or creating a contact. These messages are generated in the Flask
backend using the `flash()` function, called from the views. Then the base `layout.html` template
renders the messages into the final web page.
#figure(caption: [Snippet templates/layout.html], ```
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
```)
Our Flask app still includes the calls to `flash()`, but the Hyperview app is
not accessing the flashed message to display to the user. Let’s add that support
now.
We could just show the messages using a similar technique to the web app: loop
through the messages and render some `<text>` elements in
`layout.xml`. This approach has a major downside: the rendered messages would be
tied to a specific screen. If that screen was hidden by a navigation action, the
message would be hidden too. What we really want is for our message UI to
display "above" all of the screens in the navigation stack. That way, the
message would remain visible (fading away after a few seconds), even if the
stack of screens changes below. To display some UI outside of the `<screen>` elements,
we’re going to need to extend the Hyperview client with a new custom action,
`show-message`. This is another opportunity to use an open-source library, `react-native-root-toast`.
Let’s add this library to our demo app.
```bash
cd hyperview/demo
yarn add react-native-root-toast <1>
yarn start <2>
```]
+ Add dependency on `react-native-root-toast`
+ Re-start the mobile app
Now, we can write the code to implement the message UI as a custom action.
#figure(
caption: [demo/src/message.js],
)[ ```js
import Toast from 'react-native-root-toast'; <1>
const namespace = "https://hypermedia.systems/hyperview/message";
export default {
action: "show-message", <2>
callback: (behaviorElement) => { <3>
const text = behaviorElement.getAttributeNS(namespace, "text");
if (text != null) {
Toast.show(text, { <4>
position: Toast.positions.TOP, duration: 2000
});
}
},
};
``` ]
+ Import the `Toast` API.
+ The name of the action.
+ The callback that runs when the action triggers.
+ Pass the message to the toast library.
This code looks very similar to the implementation of `open-phone`. Both
callbacks follow a similar pattern: read namespaced attributes from the
`<behavior>` element, and pass those values to a 3rd party library. For
simplicity, we’re hard-coding options to show the message at the top of the
screen, fading out after 2 seconds. But `react-native-root-toast`
exposes many options for positioning, timing of animations, colors, and more. We
could specify these options using extra attributes on
`behaviorElement` to make the action more configurable. For our purposes, we
will just stick to a bare-bones implementation.
Now we need to register our custom action with the `<Hyperview>`
component, by passing it to the `behaviors` prop.
#figure(caption: [demo/src/HyperviewScreen.js])[ ```js
import React, { PureComponent } from 'react';
import Hyperview from 'hyperview';
import OpenEmail from './email';
import OpenPhone from './phone';
import ShowMessage from './message'; <1>
export default class HyperviewScreen extends PureComponent {
// ... omitted for brevity
behaviors = [OpenEmail, OpenPhone, ShowMessage]; <2>
// ... omitted for brevity
}
``` ]
+ Import the `show-message` action.
+ Pass the action to the `Hyperview` component, as a prop called
`behaviors`.
All that’s left to do is trigger the `show-message` action from our HXML. There
are three user actions that result in showing a message:
+ Creating a new contact
+ Updating an existing contact
+ Deleting a contact
The first two actions are implemented in our app using the same HXML template, `form_fields.xml`.
Upon successfully creating or updating a contact, this template will reload the
screen and trigger an event, using behaviors that trigger on "load". The
deletion action also uses behaviors that trigger on "load", defined in the `deleted.xml` template.
So both `form_fields.xml` and `deleted.xml` need to be modified to also show
messages on load. Since the actual behaviors will be the same in both templates,
let’s create a shared template to reuse the HXML.
#figure(caption: [hv/templates/messages.xml])[ ```xml
{% for message in get_flashed_messages() %}
<behavior <1>
xmlns:message="https://hypermedia.systems/hyperview/message"
trigger="load" <2>
action="show-message" <3>
message:text="{{ message }}" <4>
/>
{% endfor %}
``` ]
+ Define a behavior for each message to display.
+ Trigger this behavior as soon as the element loads.
+ Trigger the new "show-message" action.
+ The "show-message" action will display the flashed message in its UI.
Like in `layout.html` of the web app, we loop through all of the flashed
messages and render some markup for each message. However, in the web app, the
message was directly rendered into the web page. In the Hyperview app, each
message is displayed using a behavior that triggers our custom UI. Now we just
need to include this template in
`form_fields.xml`:
#figure(
caption: [Snippet of hv/templates/form\_fields.xml],
)[ ```xml
<view xmlns="https://hyperview.org/hyperview" style="edit-group">
{% if saved %}
{% include "hv/messages.xml" %} <1>
<behavior trigger="load" once="true" action="dispatch-event"
event-name="contact-updated" />
<behavior trigger="load" once="true" action="reload"
href="/contacts/{{contact.id}}" />
{% endif %}
<!-- omitted for brevity -->
</view>
``` ]
+ Show the messages as soon as the screen loads.
And we can do the same thing in `deleted.xml`:
#figure(
caption: [hv/templates/deleted.xml],
)[ ```xml
<view xmlns="https://hyperview.org/hyperview">
{% include "hv/messages.xml" %} <1>
<behavior trigger="load" action="dispatch-event"
event-name="contact-updated" />
<behavior trigger="load" action="back" />
</view>
``` ]
+ Show the messages as soon as the screen loads.
In both `form_fields.xml` and `deleted.xml`, multiple behaviors get triggered on "load."
In `deleted.xml`, we immediately navigate back to the previous screen. In `form_fields.xml`,
we immediately reload the current screen to show the Contact details. If we
rendered our message UI elements directly in the screen, the user would barely
see them before the screen disappeared or reloaded. By using a custom action,
the message UI remains visible even while the screens change beneath them.
#figure([#image("images/screenshot_hyperview_toast.png")], caption: [
Message shown during back navigation
])
=== Swipe Gesture on Contacts <_swipe_gesture_on_contacts>
To add communication capabilities and the message UI, we extended the client
with custom behavior actions. But the Hyperview client can also be extended with
custom UI components that render on the screen. Custom components are
implemented as React Native components. That means anything that’s possible in
React Native can be done in Hyperview as well! Custom components open up endless
possibilities to build rich mobile apps with the Hypermedia architecture.
To illustrate the possibilities, we will extend the Hyperview client in our
mobile app to add a "swipeable row" component. How does it work? The
"swipeable row" component supports a horizontal swiping gesture. As the user
swipes this component from right to left, the component will slide over,
revealing a series of action buttons. Each action button will be able to trigger
standard Hyperview behaviors when pressed. We will use this custom component in
our Contacts List screen. Each contact item will be a "swipeable row", and the
actions will give quick access to edit and delete actions for the contact.
#figure([#image("images/screenshot_hyperview_swipe.png")], caption: [
Swipeable contact item
])
==== Designing The Component <_designing_the_component>
Rather than implementing the swipe gesture from scratch, we will once again use
an open-source third-party library: `react-native-swipeable`.
```bash
cd hyperview/demo
yarn add react-native-swipeable <1>
yarn start <2>
```]
+ Add dependency on `react-native-swipeable`.
+ Re-start the mobile app.
This library provides a React Native component called `Swipeable`. It can render
any React Native components as its main content (the part that can be swiped).
It also takes an array of React Native components as a prop to render as the
action buttons.
When designing a custom component, we like to define the HXML of the component
before writing the code. This way, we can make sure the markup is expressive but
succinct, and will work with the underlying library.
For the swipeable row, we need a way to represent the entire component, the main
content, and one of many buttons.
```xml
<swipe:row
xmlns:swipe="https://hypermedia.systems/hyperview/swipeable"> <1>
<swipe:main> <2>
<!-- main content shown here -->
</swipe:main>
<swipe:button> <3>
<!-- first button that appears when swiping -->
</swipe:button>
<swipe:button> <4>
<!-- second button that appears when swiping -->
</swipe:button>
</swipe:row>
```]
+ Parent element encapsulating the entire swipeable row, with custom namespace.
+ The main content of the swipeable row, can hold any HXML.
+ The first button that appears when swiping, can hold any HXML.
+ The second button that appears when swiping, can hold any HXML.
This structure clearly separates the main content from the buttons. It also
supports one, two, or more buttons. Buttons appear in the order of definition,
making it easy to swap the order.
This design covers everything we need to implement a swipeable row for our
contacts list. But it’s also generic enough to be reusable. The previous markup
contains nothing specific to the contact name, editing the contact, or deleting
the contact. If later we add another list screen to our app, we can use this
component to make the items in that list swipeable.
==== Implementing The Component <_implementing_the_component>
Now that we know the HXML structure of our custom component, we can write the
code to implement it. What does that code look like? Hyperview components are
written as React Native components. These React Native components are mapped to
a unique XML namespace and tag name. When the Hyperview client encounters that
namespace and tag name in the HXML, it delegates rendering of the HXML element
to the matching React Native component. As part of delegation, the Hyperview
Client passes several props to the React Native component:
- `element`: The XML DOM element that maps to the React Native component.
- `stylesheets`: The styles defined in the `<screen>`.
- `onUpdate`: The function to call when the component triggers a behavior.
- `option`: Miscellaneous settings used by the Hyperview client.
Our swipeable row component is a container with slots to render arbitrary main
content and buttons. That means it needs to delegate back to the Hyperview
client to render those parts of the UI. This is done with a public function
exposed by the Hyperview client,
`Hyperview.renderChildren()`.
Now that we know how custom Hyperview components are implemented, let’s write
the code for our swipeable row.
#figure(
caption: [demo/src/swipeable.js],
)[ ```js
import React, { PureComponent } from 'react';
import Hyperview from 'hyperview';
import Swipeable from 'react-native-swipeable';
const NAMESPACE_URI = 'https://hypermedia.systems/hyperview/swipeable';
export default class SwipeableRow extends PureComponent { <1>
static namespaceURI = NAMESPACE_URI; <2>
static localName = "row"; <3>
getElements = (tagName) => {
return Array.from(this.props.element
.getElementsByTagNameNS(NAMESPACE_URI, tagName));
};
getButtons = () => { <4>
return this.getElements("button").map((buttonElement) => {
return Hyperview.renderChildren(buttonElement,
this.props.stylesheets,
this.props.onUpdate,
this.props.options); <5>
});
};
render() {
const [main] = this.getElements("main");
if (!main) {
return null;
}
return (
<Swipeable rightButtons={this.getButtons()}> <6>
{Hyperview.renderChildren(main,
this.props.stylesheets,
this.props.onUpdate,
this.props.options)} <7>
</Swipeable>
);
}
}
``` ]
+ Class-based React Native component.
+ Map this component to the given HXML namespace.
+ Map this component to the given HXML tag name.
+ Function that returns an array of React Native components for each
`<button>` element.
+ Delegate to the Hyperview client to render each button.
+ Pass the buttons and main content to the third-party library.
+ Delegate to the Hyperview client to render the main content.
The `SwipeableRow` class implements a React Native component. At the top of the
class, we set a static `namespaceURI` property and `localName`
property. These properties map the React Native component to a unique namespace
and tag name pair in the HXML. This is how the Hyperview client knows to
delegate to `SwipeableRow` when encountering custom elements in the HXML. At the
bottom of the class, you’ll see a
`render()` method. `render()` gets called by React Native to return the rendered
component. Since React Native is built on the principle of composition, `render()` typically
returns a composition of other React Native components. In this case, we return
the `Swipeable` component (provided by the `react-native-swipeable` library),
composed with React Native components for the buttons and main content. The
React Native components for the buttons and main content are created using a
similar process:
- Find the specific child elements (`<button>` or `<main>`).
- Turn those elements into React Native components using
`Hyperview.renderChildren()`.
- Set the components as children or props of `Swipeable`.
#asciiart(
read("images/diagram/hyperview-components.txt"), caption: [Rendering delegation between the client and the custom components],
)
This code may be hard to follow if you’ve never worked with React or React
Native. That’s OK. The important takeaway is: we can write code to translate
arbitrary HXML into React Native components. The structure of the HXML (both
attributes and elements) can be used to represent multiple facets of the UI (in
this case, the buttons and main content). Finally, the code can delegate
rendering of child components back to the Hyperview client.
The result: this swipeable row component is completely generic. The actual
structure and styling and interactions of the main content and buttons can be
defined in the HXML. Creating a generic component means we can reuse it across
multiple screens for different purposes. If we add more custom components or new
behavior actions in the future, they will work with our swipeable row
implementation.
The last thing to do is register this new component with the Hyperview client.
The process is similar to registering custom actions. Custom components are
passed as a separate `components` prop to the `Hyperview`
component.
#figure(caption: [demo/src/HyperviewScreen.js])[ ```js
import React, { PureComponent } from 'react';
import Hyperview from 'hyperview';
import OpenEmail from './email';
import OpenPhone from './phone';
import ShowMessage from './message';
import SwipeableRow from './swipeable'; <1>
export default class HyperviewScreen extends PureComponent {
// ... omitted for brevity
behaviors = [OpenEmail, OpenPhone, ShowMessage];
components = [SwipeableRow]; <2>
render() {
return (
<Hyperview
behaviors={this.behaviors}
components={this.components} <3>
entrypointUrl={this.entrypointUrl}
// more props...
/>
);
}
}
``` ]
+ Import the `SwipeableRow` component.
+ Create an array of custom components.
+ Pass the custom component to the `Hyperview` component, as a prop called `components`.
We’re now ready to update our HXML templates to make use of the new swipeable
row component.
===== Using the component <_using_the_component>
Currently, the HXML for a contact item in the list consists of a
`<behavior>` and `<text>` element:
#figure(
caption: [Snippet of `hv/rows.xml`],
)[ ```xml
<item key="{{ contact.id }}" style="contact-item">
<behavior trigger="press" action="push"
href="/contacts/{{ contact.id }}" />
<text style="contact-item-label">
<!-- omitted for brevity -->
</text>
</item>
``` ]
With our swipeable row component, this markup will become the "main" UI. So
let’s start by adding `<row>` and `<main>` as ancestor elements.
#figure(
caption: [Adding swipeable row `hv/rows.xml`],
)[ ```xml
<item key="{{ contact.id }}">
<swipe:row <1>
xmlns:swipe="https://hypermedia.systems/hyperview/swipeable">
<swipe:main> <2>
<view style="contact-item"> <3>
<behavior trigger="press" action="push"
href="/contacts/{{ contact.id }}" />
<text style="contact-item-label">
<!-- omitted for brevity -->
</text>
</view>
</swipe:main>
</swipe:row>
</item>
``` ]
+ Added `<swipe:row>` ancestor element, with namespace alias for `swipe`.
+ Added `<swipe:main>` element to define the main content.
+ Wrapped the existing `<behavior>` and `<text>` elements in a `<view>`.
Previously, the `contact-item` style was set on the `<item>` element. That made
sense when the `<item>` element was the container for the main content of the
list item. Now that the main content is a child of
`<swipe:main>`, we need to introduce a new `<view>` where we apply the styles.
If we reload our backend and mobile app, you won’t experience any changes on the
Contacts List screen yet. Without any action buttons defined, there’s nothing to
reveal when swiping a row. Let’s add two buttons to the swipeable row.
#figure(
caption: [Adding swipeable row `hv/rows.xml`],
)[ ```xml
<item key="{{ contact.id }}">
<swipe:row
xmlns:swipe="https://hypermedia.systems/hyperview/swipeable">
<swipe:main>
<!-- omitted for brevity -->
</swipe:main>
<swipe:button> <1>
<view style="swipe-button">
<text style="button-label">Edit</text>
</view>
</swipe:button>
<swipe:button> <2>
<view style="swipe-button">
<text style="button-label-delete">Delete</text>
</view>
</swipe:button>
</swipe:row>
</item>
``` ]
+ Added `<swipe:button>` for edit action.
+ Added `<swipe:button>` for delete action.
Now if we use our mobile app, we can see the swipeable row in action! As you
swipe the contact item, the "Edit" and "Delete" buttons reveal themselves. But
they don’t do anything yet. We need to add some behaviors to these buttons. The "Edit"
button is straight-forward: pressing it should open the contact details screen
in edit mode.
#figure(
caption: [Snippet of `hv/rows.xml`],
)[ ```xml
<swipe:button>
<view style="swipe-button">
<behavior trigger="press" action="push"
href="/contacts/{{ contact.id }}/edit" /> <1>
<text style="button-label">Edit</text>
</view>
</swipe:button>
``` ]
+ When pressed, push a new screen with the Edit Contact UI.
The "Delete" button is a bit more complicated. There’s no screen to open for
deletion, so what should happen when the user presses this button? Perhaps we
use the same interaction as the "Delete" button on the Edit Contact screen. That
interaction brings up a system dialog, asking the user to confirm the deletion.
If the user confirms, the Hyperview client makes a `POST` request to `/contacts/<contact_id>/delete`,
and appends the response to the screen. The response triggers a few behaviors
immediately to reload the contacts list and show a message. This interaction
will work for our action button as well:
#figure(
caption: [Snippet of `hv/rows.xml`],
)[ ```xml
<swipe:button>
<view style="swipe-button">
<behavior <1>
xmlns:alert="https://hyperview.org/hyperview-alert"
trigger="press"
action="alert"
alert:title="Confirm delete"
alert:message="Are you sure you want to delete
{{ contact.first }}?"
>
<alert:option alert:label="Confirm">
<behavior <2>
trigger="press"
action="append"
target="item-{{ contact.id }}"
href="/contacts/{{ contact.id }}/delete"
verb="post"
/>
</alert:option>
<alert:option alert:label="Cancel" />
</behavior>
<text style="button-label-delete">Delete</text>
</view>
</swipe:button>
``` ]
+ When pressed, open a system dialog box asking the user to confirm the action.
+ If confirmed, make a POST request to the deletion endpoint, and append the
response to the enclosing `<item>`.
Now when we press "Delete," we get the confirmation dialog as expected. After
pressing confirm, the backend response triggers behaviors that show a
confirmation message and reload the list of contacts. The item for the deleted
contact disappears from the list.
#figure([#image("images/screenshot_hyperview_swipe_delete.png")], caption: [
Delete from swipe button
])
Notice that the action buttons are able to support any type of behavior action,
from `push` to `alert`. If we wanted to, we could have the action buttons
trigger our custom actions, like `open-phone` and
`open-email`. Custom components and actions can be mixed freely with the
standard components and actions that come standard with the Hyperview framework.
This makes the extensions to the Hyperview client feel like first-class
features.
In fact, we’ll let you in on a secret. Within the Hyperview client, standard
components and actions are implemented the same way as custom components and
actions! The rendering code does not treat `<view>`
differently from `<swipe:row>`. The behavior code does not treat `alert`
differently from `open-phone`. They are both implemented using the same
techniques described in this section. Standard components and actions are just
the ones that are universally needed by all mobile apps. But they are just the
starting point.
Most mobile apps will require some extensions to the Hyperview client to deliver
a great user experience. Extensions evolve the client from being a generic "Hyperview
client," to being a purpose-built client for your app. And importantly, this
evolution preserves the Hypermedia, server-driven architecture and all of its
benefits.
=== Mobile Hypermedia-Driven Applications <_mobile_hypermedia_driven_applications>
That concludes our build of mobile Contact.app. Step back from the code details
and consider the broader pattern:
- The core logic of the app resides on the server.
- Server-rendered templates power both the web and mobile apps.
- Platform customizations are done through scripting on the web, and client
customization on mobile.
The Hypermedia-Driven Application architecture allowed for significant code
reuse and a manageable tech stack. Ongoing app updates and maintenance for both
web and mobile can be done at the same time.
Yes, there is a story for Hypermedia-Driven Applications on mobile.
=== Hypermedia Notes: Good-Enough UX and Islands of Interactivity <_hypermedia_notes_good_enough_ux_and_islands_of_interactivity>
A problem many SPA and native mobile developers face when coming to the HDA
approach is that they look at their current application and imagine implementing
it exactly using hypermedia. While htmx and Hyperview significantly improve the
user experience available via the hypermedia-driven approach, there are still
times when it won’t be easy to pull of a particular user experience.
As we saw in Chapter Two, Roy Fielding noted this tradeoff with respect to the
web’s RESTful network architecture, where "information is transferred in a
standardized form rather than one which is specific to an application’s needs."
Accepting a slightly less efficient and interactive solution to a particular UX
can save you a tremendous amount of complexity when building your applications.
Do not let the perfect be the enemy of the good. Many advantages are to be
gained by accepting a slightly less sophisticated user experience in some cases,
and tools like htmx and Hyperview make that compromise much more palatable when
they are used properly.