mirror of
https://github.com/bigskysoftware/hypermedia-systems.git
synced 2025-12-04 00:05:18 -05:00
Merge branch 'main' of github.com:bigskysoftware/building-hypermedia-systems
This commit is contained in:
commit
3756062ba9
@ -63,7 +63,7 @@ Unfortunately, today, you probably associate the term "`REST`" with JSON APIs, s
|
||||
used in industry. This is a misapplied use of the term REST because JSON is not a _natural_ hypermedia due to the absence of
|
||||
hypermedia controls. The exchange of hypermedia is an explicit requirement for a system to be considered "`RESTful.`"
|
||||
It is a long story how we got here, using the term REST so incorrectly, and we will go into the details later in this book.
|
||||
But, for now, if you currently think "`REST == JSON`", please try to set that understanding aside while reading this book,
|
||||
But, for now, if you think REST implies JSON, please try to set that understanding aside while reading this book,
|
||||
and come to the concept with fresh eyes.
|
||||
|
||||
It is important to understand that, in his dissertation, Fielding was describing The World Wide Web as it existed in the
|
||||
|
||||
@ -275,7 +275,7 @@ and are often perfectly happy with the results.
|
||||
|
||||
These two tags give a tremendous amount of expressive power to HTML.
|
||||
|
||||
=== So What _Isn't_ Hypermedia?
|
||||
=== So What Isn't Hypermedia?
|
||||
|
||||
So links and forms are the two main hypermedia-based mechanisms for interacting with a server available in HTML.
|
||||
|
||||
@ -619,8 +619,8 @@ This is exactly what the web was designed to do!
|
||||
|
||||
By adopting the hypermedia approach for these applications, you will save yourself a huge amount of client-side complexity
|
||||
that comes with adopting the Single Page Application approach: there is no need for client-side routing, for managing
|
||||
a client-side model, for hand-wiring in JavaScript logic, and so forth. The back button will "`just work`". Deep linking
|
||||
will "`just work`". You will be able to focus your efforts on your server, where your application is actually adding value.
|
||||
a client-side model, for hand-wiring in JavaScript logic, and so forth. The back button will "`just work.`" Deep linking
|
||||
will "`just work.`" You will be able to focus your efforts on your server, where your application is actually adding value.
|
||||
|
||||
And, by layering htmx or another hypermedia-oriented library on top of this approach, you can address many of the usability
|
||||
issues that come with vanilla HTML and take advantage of finer-grained hypermedia transfers. This opens up a whole slew of new
|
||||
|
||||
@ -605,7 +605,7 @@ to the system itself.
|
||||
==== Hypermedia As The Engine of Application State (HATEOAS)
|
||||
|
||||
The final sub-constraint on the Uniform Interface is that, in a RESTful system, hypermedia should be "`the engine of
|
||||
application state`". This is sometimes abbreviated as "`HATEOAS`", although Fielding prefers to use the terminology
|
||||
application state.`" This is sometimes abbreviated as "`HATEOAS`", although Fielding prefers to use the terminology
|
||||
"`the hypermedia constraint`" when discussing it.
|
||||
|
||||
This constraint is closely related to the previous self-describing message constraint. Let us consider again the two different
|
||||
|
||||
@ -455,7 +455,7 @@ Take the following code, which uses radio buttons and CSS hacks to create someth
|
||||
<2> Hide the radio buttons. We can actuate them by clicking the associated labels.
|
||||
<3> A tabpanel will be visible when the associated tab is selected.
|
||||
|
||||
This code will "`work`".
|
||||
This code will "`work.`"
|
||||
Clicking on the tabs will change the content displayed, and without a single line of JavaScript.
|
||||
Unfortunately, tabs have requirements beyond clicking to change content.
|
||||
This implementation has many missing features that will lead to user confusion and frustration, as well as some undesirable behaviors.
|
||||
@ -504,7 +504,7 @@ From the link:https://www.w3.org/WAI/ARIA/apg/patterns/tabs/[ARIA Authoring Prac
|
||||
}
|
||||
----
|
||||
|
||||
`vh` is short for "`visually hidden`". This class uses multiple methods and workarounds to make sure no browser removes the element's function.
|
||||
`vh` is short for "`visually hidden.`"This class uses multiple methods and workarounds to make sure no browser removes the element's function.
|
||||
****
|
||||
|
||||
It turns out that fulfilling all of these requirements takes a lot of code.
|
||||
|
||||
@ -285,7 +285,7 @@ template to inject the content of `index.html` within its HTML.
|
||||
|
||||
Next we have our first bit of actual HTML, rather than just Jinja directives. We have a simple HTML form that allows
|
||||
you to search contacts by issuing a `GET` request to the `/contacts` path. The form itself contains a label and
|
||||
an input with the name "`q`". This input's value will be submitted with the `GET` request to the `/contacts` path,
|
||||
an input with the name "`q.`" This input's value will be submitted with the `GET` request to the `/contacts` path,
|
||||
as a query string (since this is a `GET` request.)
|
||||
|
||||
Note that the value of this input is set to the Jinja expression `{{ request.args.get('q') or '' }}`. This expression
|
||||
|
||||
@ -570,7 +570,7 @@ are pressed together:
|
||||
<1> `keyup` now has a filter, so the control key and L must be pressed.
|
||||
|
||||
The trigger filter in this case is `ctrlKey && key == 'l'`. This can be read as "`A key up event, where the ctrlKey property
|
||||
is true and the key property is equal to l`". Note that the properties `ctrlKey` and `key` are resolved against the event
|
||||
is true and the key property is equal to l.`" Note that the properties `ctrlKey` and `key` are resolved against the event
|
||||
rather than the global name space, so you can easily filter on the properties of a given event. You can use any expression
|
||||
you like for a filter, however: calling a global JavaScript function, for example, is perfectly acceptable.
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@ Believe it or not, that's it! This simple script tag will make htmx's functiona
|
||||
|
||||
== AJAX-ifying Our Application
|
||||
|
||||
To get our feet wet with htmx, the first feature we are going to take advantage of, is what is known as "`boosting`". This is
|
||||
To get our feet wet with htmx, the first feature we are going to take advantage of, is what is known as "`boosting.`" This is
|
||||
a bit of a "`cheater`" feature of htmx in that we don't need to do much beyond adding a single attribute, `hx-boost`, to the
|
||||
application. This `hx-boost` attribute is unlike most other attributes in htmx: whereas other htmx attributes tend to be
|
||||
very focused on one aspect of improving HTML (e.g. `hx-trigger` focuses on the events that trigger a request, `hx-swap` focuses on how responses
|
||||
@ -221,7 +221,7 @@ into this category. We will note when a feature is progressive enhancement frie
|
||||
Ultimately, it is up to you, the developer, to decide if the trade-offs of progressive enhancement (a more basic UX,
|
||||
limited improvements over plain HTML) are worth the benefits for your application users.
|
||||
|
||||
=== Adding `hx-boost` to Contact.app
|
||||
=== Adding "`hx-boost`" to Contact.app
|
||||
|
||||
For the contact app we are building, we want this htmx "`boost`" behavior... well, everywhere.
|
||||
|
||||
@ -261,7 +261,7 @@ All this with one htmx attribute.
|
||||
|
||||
`hx-boost` is more "`magic`" than other attributes in htmx, which generally are lower level and require a bit more explicit
|
||||
annotation work, in order to specify exactly what you want htmx to do. In general, this is the design philosophy of htmx:
|
||||
prefer explicit to implicit and obvious to "`magic`". However, the `hx-boost` attribute is too useful to allow dogma to
|
||||
prefer explicit to implicit and obvious to "`magic.`" However, the `hx-boost` attribute is too useful to allow dogma to
|
||||
override practicality, and so it is included as a feature in the library.
|
||||
|
||||
== A Second Step: Deleting Contacts With HTTP DELETE
|
||||
@ -689,10 +689,10 @@ So, with these two attributes in place, whenever someone changes the value of th
|
||||
_default_ trigger for inputs in htmx) an HTTP `GET` request will be issued to the given URL and, if there are any errors, they
|
||||
will be loaded into the error span.
|
||||
|
||||
=== Validating Emails Server Side
|
||||
=== Validating Emails Server-Side
|
||||
|
||||
Next, let's look at the server-side implementation. We are going to add another end point, similar to our edit
|
||||
end point in some ways: it is going to look up the contact based on the ID encoded in the URL. In this case, however,
|
||||
Next, let's look at the server-side implementation. We are going to add another endpoint, similar to our edit
|
||||
endpoint in some ways: it is going to look up the contact based on the ID encoded in the URL. In this case, however,
|
||||
we only want to update the email of the contact, and we obviously don't want to save it! Instead, we will call the
|
||||
`validate()` method on it.
|
||||
|
||||
|
||||
@ -267,7 +267,7 @@ has the id `search`.
|
||||
Let's add some conditional logic to our controller to look for that header and, if the value is `search`, we render
|
||||
only the rows rather than the whole `index.html` template:
|
||||
|
||||
.Updating our server side search
|
||||
.Updating our server-side search
|
||||
[source,python]
|
||||
----
|
||||
@app.route("/contacts")
|
||||
@ -418,13 +418,13 @@ https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching[MDN article on the top
|
||||
documentation] discusses this issue as well.
|
||||
****
|
||||
|
||||
=== Updating The Navigation Bar With `hx-push-url`
|
||||
=== Updating the Navigation Bar With "`hx-push-url`"
|
||||
|
||||
One shortcoming of our current Active Search implementation, when compared with the normal form submission, is that when
|
||||
you submit the form version it updates the navigation bar of the browser to include the search term. So, for example, if
|
||||
you search for "`joe`" in the search box, you will end up with a url that looks like this in your browser's nav bar:
|
||||
|
||||
.The Updated Location After A Form Search
|
||||
.The updated location after a form search
|
||||
----
|
||||
https://example.com/contacts?q=joe
|
||||
----
|
||||
@ -590,7 +590,7 @@ image::screenshot_total_contacts.png[(22 total Contacts)]
|
||||
Beautiful.
|
||||
|
||||
Of course, as you probably suspected, all is not perfect. Unfortunately, upon shipping this feature to production, we
|
||||
start getting some complaints from the users that the application "`feels slow`". Like all good developers faced with
|
||||
start getting some complaints from the users that the application "`feels slow.`" Like all good developers faced with
|
||||
a performance issue, rather than guessing what the issue might be, we try to get a performance profile of the application
|
||||
to see what exactly is causing the problem.
|
||||
|
||||
@ -842,7 +842,7 @@ To do this, we'll need to do a couple of things:
|
||||
* We'll need to update this link to target the row that it is in.
|
||||
* We'll need to change the swap to `outerHTML`, since we want to replace (really, remove) the entire row.
|
||||
* We'll need to update the server side to render empty content when the `DELETE` is issued from a "`Delete`" link rather
|
||||
than from the "`Delete Contact`" button on the contact edit page.
|
||||
than from the "`Delete Contact`" button on the contact edit page.
|
||||
|
||||
First things first, update the target of our "`Delete`" link to be the row that the link is in, rather than the entire
|
||||
body. We can once again take advantage of the relative positional `closest` feature to target the closest `tr`, like
|
||||
|
||||
@ -62,7 +62,7 @@ we want for this archiving feature.
|
||||
=== UI Requirements
|
||||
|
||||
Before we dive into the implementation, let's discuss in broad terms what our new UI should look like: we want a button
|
||||
in the application labeled "`Download Contact Archive`". When a user clicks on that button, we want to replace that
|
||||
in the application labeled "`Download Contact Archive.`" When a user clicks on that button, we want to replace that
|
||||
button with a UI that shows the progress of the archiving process, ideally with a progress bar. As the archive job makes
|
||||
progress, we want to move the progress bar along towards completion. Then, when the archive job is done, we want to
|
||||
show a link to the user to download the contact archive file.
|
||||
@ -113,7 +113,7 @@ wrap the entire UI in a `div` tag, and then use that `div` as the target for all
|
||||
|
||||
Here is the start of the template content for our new archive user interface:
|
||||
|
||||
.Our Initial Archive UI Template
|
||||
.Our initial archive UI template
|
||||
[source, html]
|
||||
----
|
||||
<div id="archive-ui"
|
||||
@ -121,13 +121,13 @@ Here is the start of the template content for our new archive user interface:
|
||||
hx-swap="outerHTML"> <2>
|
||||
</div>
|
||||
----
|
||||
<1> This div will be the target for all elements within it
|
||||
<2> Replace the entire div every time using `outerHTML`
|
||||
<1> This div will be the target for all elements within it.
|
||||
<2> Replace the entire div every time using `outerHTML`.
|
||||
|
||||
Next, lets add the "`Download Contact Archive`" button to the `div` that will kick off the archive-then-download
|
||||
process. We'll use a `POST` to the path `/contacts/archive` to trigger the start of the archiving process:
|
||||
|
||||
.Adding The Archive Button
|
||||
.Adding the archive button
|
||||
[source, html]
|
||||
----
|
||||
<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
|
||||
@ -136,11 +136,11 @@ process. We'll use a `POST` to the path `/contacts/archive` to trigger the star
|
||||
</button>
|
||||
</div>
|
||||
----
|
||||
<1> This button will issue a `POST` to `/contacts/archive`
|
||||
<1> This button will issue a `POST` to `/contacts/archive`.
|
||||
|
||||
Finally, let's include this new template in our main `index.html` template, above the contacts table:
|
||||
|
||||
.Our Initial Archive UI Template
|
||||
.Our initial archive UI template
|
||||
[source, html]
|
||||
----
|
||||
{% block content %}
|
||||
@ -149,7 +149,7 @@ Finally, let's include this new template in our main `index.html` template, abov
|
||||
|
||||
<form action="/contacts" method="get" class="tool-bar">
|
||||
----
|
||||
<1> This template will now be included in the main template
|
||||
<1> This template will now be included in the main template.
|
||||
|
||||
With that done, we now have a button showing up in our web application to get the download going. Since the enclosing
|
||||
`div` has an `hx-target="this"` on it, the button will inherit that target and replace that enclosing `div` with whatever HTML
|
||||
@ -177,7 +177,7 @@ status of the archive process.
|
||||
|
||||
Here is what the code looks like:
|
||||
|
||||
.Server Side Code To Start The Archive Process
|
||||
.Server-side code to start the archive process
|
||||
[source, python]
|
||||
----
|
||||
@app.route("/contacts/archive", methods=["POST"]) <1>
|
||||
@ -202,7 +202,7 @@ We want to render the "`Download Contact Archive`" button if the archiver has th
|
||||
some sort of message indicating that progress is happening if the status is `Running`. Let's update our template code
|
||||
to do just that:
|
||||
|
||||
.Adding Conditional Rendering
|
||||
.Adding conditional rendering
|
||||
[source, html]
|
||||
----
|
||||
<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
|
||||
@ -238,7 +238,7 @@ it can conditionally render the correct UI.
|
||||
|
||||
That's an easy fix: we just need to pass the archiver through when we render the `index.html` template as well:
|
||||
|
||||
.Including The Archiver When We Render index.html
|
||||
.Including the archiver when we render index.html
|
||||
[source, python]
|
||||
----
|
||||
@app.route("/contacts")
|
||||
@ -275,17 +275,17 @@ on the new state of the server.
|
||||
Polling has a bit of a bad rap, and it isn't the sexiest technique in the world: today
|
||||
developers might look at a more advanced technique like WebSockets or Server Sent Events (SSE) to address this situation.
|
||||
|
||||
But, say what one will, polling _works_ and it is drop-dead simple. You need to be careful to make sure you don't overwhelm
|
||||
you system with polling requests, but, with a bit of care, you can create a reliable, passively updated component in
|
||||
But, say what one will, polling _works_ and it is drop-dead simple. You need to be careful not to overwhelm
|
||||
your system with polling requests, but, with a bit of care, you can create a reliable, passively updated component in
|
||||
your UI using it.
|
||||
****
|
||||
|
||||
htmx offers two types of polling. The first is "`fixed rate polling`", which uses a special `hx-trigger` syntax to indicate
|
||||
Htmx offers two types of polling. The first is "`fixed rate polling`", which uses a special `hx-trigger` syntax to indicate
|
||||
that something should be polled on a fixed interval.
|
||||
|
||||
Here is an example:
|
||||
|
||||
.Fixed Interval Polling
|
||||
.Fixed interval polling
|
||||
[source, html]
|
||||
----
|
||||
<div hx-get="/messages" hx-trigger="every 3s"> <1>
|
||||
@ -314,7 +314,7 @@ how close the archive process is to completion.
|
||||
|
||||
Here is the snippet of HTML we will use:
|
||||
|
||||
.A CSS-based Progress Bar
|
||||
.A CSS-based progress bar
|
||||
[source, html]
|
||||
----
|
||||
<div class="progress" >
|
||||
@ -330,7 +330,7 @@ indicator the appropriate width within the parent div.
|
||||
|
||||
As we have mentioned before, this is not a book on CSS, but, for completeness, here is the CSS for this progress bar:
|
||||
|
||||
.The CSS For Our Progress Bar
|
||||
.The CSS for our progress bar
|
||||
[source, css]
|
||||
----
|
||||
.progress {
|
||||
@ -364,7 +364,7 @@ image::screenshot_progress_bar.png[A blue progress bar that's a little under hal
|
||||
Let's add the code for our progress bar into our `archive_ui.html` template for the case when the archiver is
|
||||
running, and let's update the copy to say "`Creating Archive...`":
|
||||
|
||||
.Adding The Progress Bar
|
||||
.Adding the progress bar
|
||||
[source, html]
|
||||
----
|
||||
<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
|
||||
@ -393,7 +393,7 @@ wrapping around the "`Creating Archive...`" text and the progress bar.
|
||||
|
||||
Let's make it poll by issuing an HTTP `GET` to the same path that the `POST` was issued too: `/contacts/archive`.
|
||||
|
||||
.Implementing Load Polling
|
||||
.Implementing load polling
|
||||
[source, html]
|
||||
----
|
||||
<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
|
||||
@ -411,7 +411,7 @@ Let's make it poll by issuing an HTTP `GET` to the same path that the `POST` was
|
||||
{% endif %}
|
||||
</div>
|
||||
----
|
||||
<1> Issue a `GET` to `/contacts/archive` 500 milliseconds after the content loads
|
||||
<1> Issue a `GET` to `/contacts/archive` 500 milliseconds after the content loads.
|
||||
|
||||
Again, it is important to realize that, when this `GET` is issued to `/contacts/archive`, it is going to replace
|
||||
the `div` with the id `archive-ui`, not just itself. The `hx-target` attribute on the `div` with the id `archive-ui` is
|
||||
@ -421,7 +421,7 @@ _inherited_ by all child elements within that `div`, so the children will all ta
|
||||
Now we need to handle the `GET` to `/contacts/archive` on the server. Thankfully, this is quite easy: all we
|
||||
want to do is re-render `archive_ui.html` with the archiver:
|
||||
|
||||
.Handling Progress Updates
|
||||
.Handling progress updates
|
||||
[source, python]
|
||||
----
|
||||
@app.route("/contacts/archive", methods=["GET"]) <1>
|
||||
@ -501,7 +501,7 @@ bar appears. When the progress bar reaches 100%, it disappears and a link to do
|
||||
can then click on that link and download their archive. A nice, polished user experience when compared with the common
|
||||
click-and-wait experience of many websites.
|
||||
|
||||
== Smoothing Things Out: Animations in htmx
|
||||
== Smoothing Things Out: Animations in Htmx
|
||||
|
||||
As nice as this UI is, there is one minor annoyance with it: as the progress bar updates it "`jumps`" from one position
|
||||
to the next. This looks jerky and is reminiscent of the feel of a full page refresh in web 1.0 style applications. It
|
||||
@ -523,10 +523,10 @@ with their SPA counterparts: it is hard to use CSS transitions without using som
|
||||
|
||||
This is unfortunate, but htmx rectifies this situation with its swapping model. Let's look at how.
|
||||
|
||||
=== The "`Settling`" Step in htmx
|
||||
=== The "`Settling`" Step in Htmx
|
||||
|
||||
When we discussed the htmx swap model in Chapter 5, we focused on the classes that htmx adds and removes, but we skipped
|
||||
over the idea of "`settling`". What is "`settling`" in htmx terms? Settling is the following process: when htmx is
|
||||
over the idea of "`settling.`" What is "`settling`" in htmx terms? Settling is the following process: when htmx is
|
||||
about to replace a chunk of content, it looks through the new content and finds all elements with an `id` on it. It then
|
||||
looks in the _existing_ content for elements with the same `id`. If there is one, it does the following shuffle:
|
||||
|
||||
@ -542,14 +542,14 @@ So, in our case, all we need to do is to add a stable ID to our `progress-bar` e
|
||||
on every update, the progress bar should smoothly move across the screen as it is updating, using the CSS transition
|
||||
defined in our style sheet:
|
||||
|
||||
.Smoothing Things Out
|
||||
.Smoothing things out
|
||||
[source, html]
|
||||
----
|
||||
<div class="progress" >
|
||||
<div id="archive-progress" class="progress-bar" style="width:{{ archiver.progress() * 100 }}%"></div> <1>
|
||||
</div>
|
||||
----
|
||||
<1> The progress bar div now has a stable id across requests
|
||||
<1> The progress bar div now has a stable id across requests.
|
||||
|
||||
So, despite all the complicated mechanics going on behind the scenes in htmx, all we have to do, as an htmx user,
|
||||
is add a stable `id` attribute to the element we want to animate.
|
||||
@ -566,7 +566,7 @@ can be removed or cleaned up.
|
||||
|
||||
We'll add it after the download link, like so:
|
||||
|
||||
.Clearing The Download
|
||||
.Clearing the download
|
||||
[source, html]
|
||||
----
|
||||
<a hx-boost="false" href="/contacts/archive/file" _="on load click() me">Archive Ready! Click here to download. ↓</a>
|
||||
@ -582,7 +582,7 @@ Since this button is picking up the same `hx-target` and `hx-swap` configuration
|
||||
|
||||
Here is the server-side code:
|
||||
|
||||
.Resetting The Download
|
||||
.Resetting the download
|
||||
[source, python]
|
||||
----
|
||||
@app.route("/contacts/archive", methods=["DELETE"])
|
||||
@ -614,7 +614,7 @@ All we need to do to implement the auto-download feature is the following: when
|
||||
|
||||
The +_hyperscript+ code reads almost the same as the previous sentence (which is why we love hyperscript):
|
||||
|
||||
.Auto-Downloading
|
||||
.Auto-downloading
|
||||
[source, html]
|
||||
----
|
||||
<a hx-boost="false" href="/contacts/archive/file"
|
||||
|
||||
@ -505,7 +505,7 @@ If you want to update other content in htmx, you have a few options:
|
||||
|
||||
=== Expanding Your Selection
|
||||
|
||||
The first option, and the simplest, is to "`expand the target`". That is, rather than simply replacing a small part
|
||||
The first option, and the simplest, is to "`expand the target.`" That is, rather than simply replacing a small part
|
||||
of the screen, expand the target of your htmx-driven request until it is large enough to enclose all the elements that
|
||||
need to updated on a screen. This has the tremendous advantage of being simple and reliable. The downside is that
|
||||
it may not provide the user experience that you want, and it may not play well with a particular server-side template
|
||||
|
||||
@ -387,7 +387,7 @@ reasonable system for structuring our JavaScript.
|
||||
=== RSJS
|
||||
|
||||
RSJS (the "`Reasonable System for JavaScript Structure`", 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
|
||||
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.
|
||||
|
||||
We won't reproduce all the RSJS guidelines here, but here are the ones most relevant for our counter widget:
|
||||
@ -1005,9 +1005,8 @@ Let's go through each component of this script:
|
||||
|
||||
* `increment` This is a "`command`" in +_hyperscript+ that "`increments`" things, similar to the `++` operator in JavaScript.
|
||||
* the "`the`" doesn't have any semantic meaning +_hyperscript+, but can used to make scripts more readable.
|
||||
* `textContent of` - This one form of _property access_ in +_hyperscript+. You are probably familiar with the JavaScript
|
||||
syntax `a.b`, meaning "Get the property `b` on object `a`". +_hyperscript+ supports this syntax, but _also_ supports
|
||||
the forms `b of a` and `a's b`. Which one you use should depend on which one is most readable.
|
||||
* `textContent of` - This one form of _property access_ in +_hyperscript+. You are probably familiar with the JavaScript syntax `a.b`, meaning "Get the property `b` on object `a.`" +_hyperscript+ supports this syntax, but _also_ supports
|
||||
the forms `b of a` and `a's b`. Which one you use should depend on which one is most readable.
|
||||
* `the previous` The `previous` expression in +_hyperscript+ finds the previous element in the DOM that matches some condition.
|
||||
* `<output />` This is a _query literal_, which is a CSS selector wrapped between `<` and `/>`.
|
||||
|
||||
@ -1149,7 +1148,7 @@ some sort of functionality, such as showing modal dialogs.
|
||||
|
||||
Components have become very popular in the web development works, with libraries like https://datatables.net/[DataTables]
|
||||
providing rich user experiences with very little JavaScript code on the part of a user. Unfortunately, if these libraries
|
||||
aren't integrated well into a website, they can begin to make an application feel "`patched together`". Furthermore, some
|
||||
aren't integrated well into a website, they can begin to make an application feel "`patched together.`" Furthermore, some
|
||||
libraries go beyond simple DOM manipulation, and require that you integrate with a server endpoint, almost invariably
|
||||
with a JSON data API. This means you are no longer building a Hypermedia-Driven Application, simply because a particular
|
||||
widget demands something different. A shame!
|
||||
@ -1271,7 +1270,7 @@ Here is what our code looks like:
|
||||
<2> We pass the button in to the function, so an event can be triggered on it.
|
||||
<3> We pass through the SweetAlert2 configuration information.
|
||||
|
||||
As you can see, this event-based code is much cleaner and certainly more "`HTML-ish`". The key to this cleaner
|
||||
As you can see, this event-based code is much cleaner and certainly more "`HTML-ish.`" The key to this cleaner
|
||||
implementation is that our new `sweetConfirm()` function fires an event that htmx is able to listen for.
|
||||
|
||||
This is why a rich event model is important to look for when choosing a library to work with, both with htmx and with
|
||||
|
||||
@ -21,7 +21,7 @@ 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!
|
||||
|
||||
|
||||
== 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.
|
||||
@ -74,7 +74,7 @@ There are two approaches employing hypermedia to build & ship native mobile apps
|
||||
- Hyperview, a new hypermedia format 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.
|
||||
@ -212,7 +212,7 @@ HXML responses remain pure, with UI and interactions represented in declarative
|
||||
****
|
||||
|
||||
|
||||
=== Which Hypermedia architecture should you use?
|
||||
=== Which Hypermedia Architecture Should You Use?
|
||||
|
||||
We've discussed two approaches for creating mobile apps using Hypermedia architecture:
|
||||
|
||||
@ -225,10 +225,10 @@ Both approaches solve the fundamental issues with traditional, SPA-like mobile a
|
||||
|
||||
- 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
|
||||
- The app always runs the latest version, there's no API churn to worry about.
|
||||
|
||||
So which approach should you use for a Hypermedia-driven mobile app?
|
||||
Based on my experience building both types of apps, we strongly believe the Hyperview approach results in a better user experience.
|
||||
Based on our experience building both types of apps, we strongly 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.
|
||||
@ -1028,7 +1028,7 @@ This UI allows sharing URLs and messages from one app to another app.
|
||||
Hyperview has a `share` action to support this interaction.
|
||||
It involves a custom namespace, and share-specific attributes.
|
||||
|
||||
.System Share action
|
||||
.System share action
|
||||
[source,xml]
|
||||
----
|
||||
<behavior
|
||||
@ -1110,7 +1110,7 @@ In the next chapter, we will create a custom behavior action to enhance our mobi
|
||||
|
||||
We've already seen the simplest type of trigger, a `press` on an element. Hyperview supports many other common triggers used in mobile apps.
|
||||
|
||||
===== longPress
|
||||
===== Long-press
|
||||
Closely related to a press is a long-press.
|
||||
A behavior with `trigger="longPress"` will trigger when the user presses and holds on the element.
|
||||
"`Long-press`" interactions are often used for shortcuts and power features.
|
||||
@ -1135,7 +1135,7 @@ This is a contrived example for the sake of brevity.
|
||||
A better UX would be for the long-press to bring up a contextual menu of shortcuts and advanced options.
|
||||
This could be achieved by using `action="alert"` and opening a system dialog box with the shortcuts.
|
||||
|
||||
===== load
|
||||
===== Load
|
||||
Sometimes we want an action to trigger as soon as the screen loads.
|
||||
`trigger="load"` does exactly this.
|
||||
One use case is to quickly load a shell of the screen, and then fill in the main content on the screen with a second update action.
|
||||
@ -1163,13 +1163,13 @@ As soon as this screen loads, the behavior with `trigger="`load`"` fires off the
|
||||
It requests content from the `/content` path and replaces the container view with the response.
|
||||
|
||||
|
||||
===== visible
|
||||
===== Visible
|
||||
Unlike `load`, the `visible` trigger will only execute the behavior when the element with the behavior is scrolled into the viewport on the mobile device.
|
||||
The `visible` action is commonly used to implement an infinite-scroll interaction on a `<list>` of `<item>` elements.
|
||||
The last item in the list includes a behavior with `trigger="visible"`.
|
||||
The `append` action will fetch the next page of items and append them to the list.
|
||||
|
||||
===== refresh
|
||||
===== Refresh
|
||||
This trigger captures a "`pull to refresh`" action on `<list>` and `<view>` items.
|
||||
This interaction is associated with fetching up-to-date content from the backend.
|
||||
Thus, it's typically paired with an update or reload action to show the latest data on the screen.
|
||||
@ -1184,12 +1184,12 @@ Thus, it's typically paired with an update or reload action to show the latest d
|
||||
</view>
|
||||
</body>
|
||||
----
|
||||
<1> When the view is pulled down to refresh, reload the screen
|
||||
<1> When the view is pulled down to refresh, reload the screen.
|
||||
|
||||
Note that adding a behavior with `trigger="refresh"` to a `<view>` or `<list>` will add the pull-to-refresh interaction to the element, including showing a spinner as the element is pulled down.
|
||||
|
||||
|
||||
===== `focus`, `blur`, and `change`
|
||||
===== Focus, blur, and change
|
||||
These triggers are related to interactions with input elements.
|
||||
Thus, they will only trigger behaviors attached to elements like `<text-field>`.
|
||||
`focus` and `blur` will trigger when the user focuses and blurs the input element, respectively.
|
||||
@ -1242,13 +1242,9 @@ These concepts can be mixed together too.
|
||||
It's not unusual for a production Hyperview app to contain several behaviors, some triggering together and others triggering on different interactions.
|
||||
Using multiple behaviors with custom actions keeps HXML declarative, without sacrificing functionality.
|
||||
|
||||
=== Quick Sketch: Elements of Hyperview
|
||||
We've covered a lot of new material, so here is a quick summary of the key aspects of Hyperview:
|
||||
|
||||
== Summary
|
||||
|
||||
- Mobile app platforms push developers towards a thick-client architecture. But apps that use a thick client suffer from the same problems as SPAs on the web.
|
||||
- Using the hypermedia architecture for mobile apps solves the problems with thick-client apps.
|
||||
- HTML web views are one way to implement the hypermedia architecture for mobile apps. But HTML is not designed for mobile UIs, so this approach does not deliver a great user experience.
|
||||
- Hyperview is an alternative approach to build mobile apps using the hypermedia architecture. Hyperview introduces a new format called HXML. It also provides an open-source mobile thin-client to render HXML.
|
||||
- HXML looks similar to HTML, but it uses elements that correspond to mobile UIs, like `<screen>`, `<header>`, `<list>` and more.
|
||||
- HXML also includes input elements that implement common patterns in mobile apps, such as `<switch>`, `<select-single>`, and `<select-multiple>`.
|
||||
- New UI components can be added to HXML using namespaced elements. The Hyperview client can be easily extended to render these new elements.
|
||||
@ -1257,4 +1253,14 @@ Using multiple behaviors with custom actions keeps HXML declarative, without sac
|
||||
- Updates to screens in Hyperview are defined using behaviors with update actions, such as `replace` and `append`.
|
||||
- System interactions in Hyperview are defined using behaviors with system actions, such as `alert` and `share`.
|
||||
- New actions can be added to HXML using namespaced attributes. The Hyperview client can be easily extended to interpret the new actions.
|
||||
- The extensibility of HXML and the Hyperview client make it easy for developers to define custom elements and behaviors. Developers can evolve Hyperview to suit their apps' requirements, while fully embracing the hypermedia architecture.
|
||||
|
||||
|
||||
== Summary
|
||||
|
||||
Mobile app platforms push developers towards a thick-client architecture. But apps that use a thick client suffer from the same problems as SPAs on the web. Using the hypermedia architecture for mobile apps can solve these problems.
|
||||
|
||||
One way to implement the hypermedia architecture for mobile apps is with HTML web views. But HTML is not designed for mobile UIs, so this approach does not deliver a great user experience.
|
||||
|
||||
Hyperview offers an alternative approach using the hypermedia architecture, based on a new format called HXML. It also provides an open-source mobile thin-client to render HXML. Again, HXML looks similar to HTML, but it uses elements and patterns that correspond to mobile UIs.
|
||||
|
||||
The extensibility of HXML and the Hyperview client make it easy to define custom elements and behaviors. Developers can evolve Hyperview to suit their apps' requirements, while fully embracing the hypermedia architecture.
|
||||
|
||||
@ -65,7 +65,7 @@ Select an option based on which developer SDK you have installed.
|
||||
(The screenshots in this chapter will be taken from the iOS simulator.)
|
||||
With any luck, you will see the Expo mobile app installed in the simulator.
|
||||
The mobile app will automatically launch and show a screen saying "`Network request failed.`"
|
||||
That's because by default, this app is configured to make a request to `http://0.0.0.0:8085/index.xml`, but our backend is listening on port 5000.
|
||||
That's because by default, this app is configured to make a request to \http://0.0.0.0:8085/index.xml, but our backend is listening on port 5000.
|
||||
To fix this, we can make a simple configuration change in the `demo/src/constants.js` file:
|
||||
|
||||
[source,js]
|
||||
@ -77,7 +77,7 @@ export const ENTRY_POINT_URL = 'http://0.0.0.0:5000/'; <2>
|
||||
<2> Setting the URL to point to our contacts app
|
||||
|
||||
We're not up and running yet.
|
||||
With our Hyperview client now pointing to the right endpoint, we see a different error, a "`ParseError`".
|
||||
With our Hyperview client now pointing to the right endpoint, we see a different error, a "`ParseError.`"
|
||||
That's because the backend is responding to requests with HTML content, but the Hyperview client expects an XML response (specifically, HXML).
|
||||
So it's time to turn our attention to our Flask backend.
|
||||
We will go through the Flask views, and replace the HTML templates with HXML templates.
|
||||
@ -100,7 +100,7 @@ Every other action the user can take will be declared in the HXML of that first
|
||||
This minimal configuration is one elegant aspect of the Hypermedia-driven architecture!
|
||||
|
||||
Of course, you may want to write more on-device code to support more features in your mobile app.
|
||||
We will demonstrate how to do that later in this chapter, in the section called "`Extending the Client`".
|
||||
We will demonstrate how to do that later in this chapter, in the section called "`Extending the Client.`"
|
||||
****
|
||||
|
||||
|
||||
|
||||
@ -294,7 +294,7 @@ export default class HyperviewScreen extends PureComponent {
|
||||
// ... omitted for brevity
|
||||
}
|
||||
----
|
||||
<1> Import the show-toast action
|
||||
<1> Import the show-toast action.
|
||||
<2> Pass the action to the `Hyperview` component, as a prop called `behaviors`.
|
||||
|
||||
All that's left to do is trigger the `show-toast` action from our HXML.
|
||||
@ -369,7 +369,7 @@ By using a custom action, the toast UI remains visible even while the screens ch
|
||||
image::screenshot_hyperview_toast.png["Small gray box shows at top of screen: 'Deleted Contact!'"]
|
||||
|
||||
|
||||
== Swipe gesture on Contacts
|
||||
== Swipe Gesture on Contacts
|
||||
To add communication capabilities and the toast 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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user