mirror of
https://github.com/bigskysoftware/hypermedia-systems.git
synced 2025-12-03 00:03:50 -05:00
860 lines
44 KiB
Plaintext
860 lines
44 KiB
Plaintext
= Hypermedia In Action
|
|
:chapter: 3
|
|
:sectnums:
|
|
:figure-caption: Figure {chapter}.
|
|
:listing-caption: Listing {chapter}.
|
|
:table-caption: Table {chapter}.
|
|
:sectnumoffset: 2
|
|
// line above: :sectnumoffset: 5 (chapter# minus 1)
|
|
:leveloffset: 1
|
|
:sourcedir: ../code/src
|
|
:source-language:
|
|
|
|
= Extending HTML As Hypermedia
|
|
|
|
This chapter covers
|
|
|
|
* Some shortcomings of "plain" HTML
|
|
* How htmx addresses these opportunties to improve the expressiveness of HTML
|
|
* How to issue various HTTP requests with htmx
|
|
* History and back button support in htmx
|
|
|
|
== The Shortcomings of "Plain" HTML
|
|
|
|
In the previous chapter we introduced a simple Web 1.0-style hypermedia application to manage contacts. This application
|
|
supported the normal CRUD operations for contacts, as well as a simple mechanism for searching contacts. Our application
|
|
was built using nothing but forms and anchor tags, the traditional tags used to interact with servers, and it exchanges
|
|
hypermedia (HTML) with the server over HTTP, issuing `GET` and `POST` HTTP requests and receiving back full HTML documents
|
|
in response. It is pretty simple, but it is also definitely a Hypermedia Driven Application.
|
|
|
|
Our application is robust, leverages the web's strengths and is simple to understand. So what's not to like?
|
|
Well, unfortunately, our application isn't completely satisfying from either a user experience perspective, or
|
|
from a technical perspective. It suffers from problems typical of this style of Web 1.0 applications.
|
|
|
|
Two obvious problems that jump out are:
|
|
|
|
* From a user experience perspective: there is a noticeable refresh when you move between pages of the application, of when you create, update or
|
|
delete a contact. This is because every user interaction (link click or form submission) requires a full page
|
|
refresh, with a whole new HTML document to process after each action.
|
|
* From a technical perspective, all the updates are done with the `POST` HTTP action. This is despite the fact that
|
|
more logical actions HTTP request types like `PUT` and `DELETE` exist and would make far more sense for some
|
|
operations. After all, if we wanted to delete a resource, wouldn't it make more sense to use an HTTP `DELETE` request
|
|
to do so? Somewhat ironically, since we are using pure HTML, we are unable to access the full expressive power
|
|
of HTTP, which was designed specifically _for_ HTML!
|
|
|
|
The first point, in particular, is noticeable in Web 1.0 style applications like ours and is what is responsible for giving
|
|
them the reputation for being "clunky" when compared with their more sophisticated JavaScript-based Single Page Application
|
|
cousins.
|
|
|
|
Single Page Applications eliminate this clunkiness by updating a web page directly, by mutating the Document Object Model
|
|
(DOM). The DOM is the internal model that a browser builds up when it processes HTML, forming a tree of "nodes" for
|
|
the tags and other content in the HTML. The DOM provides a programmatic JavaScript API that allows you to update the nodes
|
|
in a page directly, without the use of hypermedia. Using this API, JavaScript code can insert new content, or remove or
|
|
update existing content, entirely outside the normal browser request mechanism.
|
|
|
|
There are a few of different styles of SPA, but, as we discussed in Chapter 1, the most common approach today is to tie
|
|
the DOM to a JavaScript model and then let an SPA framework like react (https://reactjs.org/) or vue (https://vuejs.org/)
|
|
_reactively_ update the DOM when the JavaScript model is updated: you make a change to a JavaScript object and the
|
|
web page magically updates its state to reflect the change in the model.
|
|
|
|
Recall that in this style of application communication with the server is typically done via a JSON Data API,
|
|
with the application sacrificing the advantages of hypermedia in order to provide a better, smoother user experience.
|
|
|
|
Many web developers today would not even consider the hypermedia approach due to the perceived "legacy" feel of these
|
|
Web 1.0 style applications.
|
|
|
|
The second, technical point may strike you as a bit pedantic, and I am the first to admit that conversations around
|
|
REST and which HTTP Action is right for a given operation can become very tedious. But, nonetheless, it has to be
|
|
admitted that, when using plain HTML, it is impossible to use HTTP to its full power and, therefore, it is impossible
|
|
to realize the full vision of the web as a REST-ful system: a complete, stateless, resource-oriented distributed networking
|
|
architecture that is flexible and resilient.
|
|
|
|
=== A Close Look At A Hyperlink
|
|
|
|
As we have been saying, it turns out that you can actually get a lot of interactivity out of the hypermedia model, if
|
|
you adopt a hypermedia-oriented library like htmx. To understand conceptually how htmx allows us to better address the UX
|
|
concerns of Web 1.0 style applications, let's revisit the hyperlink/anchor tag from Chapter 1 and really drill in to
|
|
each facet of it:
|
|
|
|
This simple anchor tag, when interpreted by a browser, creates a hyperlink to the Manning website:
|
|
|
|
[#listing-3-1, reftext={chapter}.{counter:listing}]
|
|
.A Simple Hyperlink, Again
|
|
[source,html]
|
|
----
|
|
<a href="https://www.manning.com/">
|
|
Manning Books
|
|
</a>
|
|
----
|
|
|
|
Breaking down exactly what this link will tell the browser to do, we have the following list:
|
|
|
|
* The browser will render the text "Manning Books" to the screen, likely with a decoration indicating it is clickable
|
|
* Then, when a user clicks on the text...
|
|
* The browser will issue an HTTP `GET` to `https://www.manning.com` and then...
|
|
* The browser will load the HTTP response into the browser window, replacing the current document
|
|
|
|
So we have four aspects of a simple hypermedia link like this, with the last three being the mechanic that distinguishes
|
|
a hyperlink from "normal" text.
|
|
|
|
Let's take a moment and think about how we can generalize this fundamental hypermedia mechanic of HTML. There is no rule saying that
|
|
hypermedia can _only_ work this way, after all!
|
|
|
|
An initial observation is: why are anchor tags so special? Shouldn't other elements (besides forms) be able to
|
|
issue HTTP requests as well? For example, shouldn't `button` elements be able to do so? It seemed silly to have to
|
|
wrap a form tag around a button to make deleting contacts work in our application. Why should only anchor tags and
|
|
forms be able to issue requests?
|
|
|
|
This presents our first opportunity to expand the expressiveness of HTML:
|
|
|
|
[IMPORTANT]
|
|
.Opportunity 1
|
|
====
|
|
HTML could be extended to allow _any_ element to issue a request to the server
|
|
====
|
|
|
|
For our next observation, let's consider the event that triggers the request to the server on our link: a click.
|
|
Well, what's so special about clicking (in the case of anchors) or submitting (in the case of forms)? Those are just two
|
|
of many, many events that are fired by the DOM, after all. Events like mouse down, or key up, or blur are all events
|
|
you might want to use to issue an HTTP request. Why shouldn't these other events be able to trigger requests as well?
|
|
|
|
This gives us our second opportunity to expand the expressiveness of HTML:
|
|
|
|
[IMPORTANT]
|
|
.Opportunity 2
|
|
====
|
|
HTML could be extended to allow _any_ event, not just a click, as in the case of our hyperlinks, to trigger HTTP requests
|
|
====
|
|
|
|
Getting a bit more technical in our thinking leads us to the problem we noted earlier in the chapter: plain HTML only
|
|
give us access to the `GET` and `POST` actions of HTTP? HTTP _stands_ for HyperText Transfer Protocol, and yet the format
|
|
it was explicitly designed for, HTML, only supports two of the five developer-facing request types! You _have_ to use
|
|
JavaScript and issue an AJAX request to get at the other three: `DELETE`, `PUT` and `PATCH`.
|
|
|
|
Let's recall what are all of these different HTTP request types designed to represent?
|
|
|
|
* `GET` corresponds with "getting" a representation for a resource from a URL: it is a pure read, with no mutation of
|
|
the resource
|
|
* `POST` submits an entity (or data) to the given resource, often creating or mutating the resource and causing a state change
|
|
* `PUT` submits an entity (or data) to the given resource for update or replacement, again likely causing a state change
|
|
* `PATCH` is similar to `PUT` but implies a partial update and state change rather than a complete replacement of the entity
|
|
* `DELETE` deletes the given resource
|
|
|
|
These operations correspond closely to the CRUD operations we discussed in Chapter 2, and by only giving us access to two
|
|
of them, HTML is presenting us with a severe and obvious technical limitation.
|
|
|
|
So here is our third opportunity to expand the expressiveness of HTML:
|
|
|
|
[IMPORTANT]
|
|
.Opportunity 3
|
|
====
|
|
HTML could be extended so that it could access these missing three HTTP actions, `PUT`, `PATCH` and `DELETE`.
|
|
====
|
|
|
|
As a final observation, consider that last aspect of a hyperlink: it replaces the _entire_ screen when a user clicks on it. It
|
|
is this technical detail that makes for a poor user experience: it causes flashes of unstyled content, a loss of scroll
|
|
state and so forth. But, again, there is no rule saying that hypermedia exchanges _must_ replace the entire document.
|
|
|
|
This gives us our forth, final and perhaps most important opportunity to generalize HTML:
|
|
|
|
[IMPORTANT]
|
|
.Opportunity 4
|
|
====
|
|
HTML could be extended to allow the responses to requests to replace elements _within_ the current document, rather than
|
|
requiring that they replace the _entire_ document
|
|
====
|
|
|
|
This is actually a very old concept in hypermedia. Ted Nelson, in his 1980 book "Literary Machines" coined the term
|
|
_transclusion_ to capture this idea: the inclusion of content into an existing document via a hypermedia reference.
|
|
If HTML supported this style of "dynamic transclusion", then Hypermedia Driven Applications could function much more like
|
|
a Single Page Application, where only part of the DOM is updated by a given user interaction or network request.
|
|
|
|
If we were to take these four opportunities to generalize HTML, we would be extending HTML far beyond its normal
|
|
capabilities, and we would be doing so _entirely within_ the normal, hypermedia model of the web. We wouldn't be
|
|
changing the fundamentals of the HTML and the HTTP requests that we use to build our application. Rather, these
|
|
four generalizations of existing functionality already found within HTML would simply let us accomplish _more_
|
|
using HTML.
|
|
|
|
== Extending HTML as a Hypermedia with htmx
|
|
|
|
It turns out that there are some JavaScript libraries that extends HTML in exactly this manner. This may seem somewhat ironic,
|
|
given that JavaScript-based SPAs have supplanted HTML-based hypermedia applications, that JavaScript would be used in this
|
|
manner. But JavaScript is simply a language for extending browser functionality on the client side, and there is no rule
|
|
saying it has to be used to write SPAs. In fact, JavaScript is the perfect tool for addressing shortcomings of
|
|
HTML as a hypermedia: it has full access to the DOM and is nearly universally available!
|
|
|
|
One such library is htmx, which will be the focus of the next few chapters. htmx is not the only JavaScript library that
|
|
takes this hypermedia-oriented approach, there are other excellent libraries like Unpoly (https://unpoly.com) and Hotwire
|
|
(https://hotwire.dev). But htmx is perhaps the purest of these libraries in the pursuit of extending HTML as a hypermedia.
|
|
|
|
It focuses on the four opportunities for improving HTML that we discussed above and attempts to incrementally address
|
|
each one, without introducing a significant amount of additional conceptual infrastructure for web developers beyond
|
|
what is necessary to address those opportunities.
|
|
|
|
=== Installing and Using htmx
|
|
|
|
From a practical, getting started perspective, htmx is a simple, dependency-free and stand-alone library that can be
|
|
added to a web application by simply including it via a `script` tag in your `head` element
|
|
|
|
Because of this simple installation model, we can take advantage of tools like public CDNs to install the library.
|
|
Below we are using the popular unpkg Content Delivery Network (CDN) to install version `1.7.0` of the library. We use
|
|
an integrity hash to ensure that the delivered content matches what we expect. This SHA can be found on the htmx
|
|
website. Finally, we mark the script as `crossorigin="anonymous"` so no credentials will be sent to the CDN.
|
|
|
|
[#listing-3-2, reftext={chapter}.{counter:listing}]
|
|
.Installing htmx
|
|
[source,html]
|
|
----
|
|
<head>
|
|
<script src="https://unpkg.com/htmx.org@1.7.0"
|
|
integrity="sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo"
|
|
crossorigin="anonymous"></script>
|
|
|
|
</head>
|
|
----
|
|
|
|
Believe it or not, that's all it takes to install htmx! If you are used to the extensive build systems in today's JavaScript
|
|
world, this may seem impossible or insane, but this is in the spirit of the early web: you could simply include a script tag
|
|
and things would just work. And it still feels like magic, even today!
|
|
|
|
Of course, you may not want to use a CDN, in which case you can download htmx to your local system and adjust the
|
|
script tag to point to wherever you keep your static assets. Or, you may have one of those more sophisticated build system
|
|
that automatically installs dependencies. In this case you can use the Node Package Manager (npm) name for the library:
|
|
`htmx.org` and install it in the usual manner that your build system supports.
|
|
|
|
Once htmx has been installed, you can begin using it immediately.
|
|
|
|
And here we get to the funny part of htmx: unlike the vast majority of JavaScript libraries, htmx does not require you,
|
|
the user, to actually write any JavaScript!
|
|
|
|
Instead, you will use _attributes_ placed directly on elements in your HTML to drive more dynamic behavior. Remember:
|
|
htmx is extending HTML as a hypermedia, and we want that extension to be as natural and consistent as possible with existing
|
|
HTML concepts. Just as an anchor tag uses an `href` attribute to specify the URL to retrieve, and forms use an `action`
|
|
attribute to specify the URL to submit the form to, htmx uses HTML _attributes_ to specify the URL that an HTTP request
|
|
should be issued to.
|
|
|
|
== Triggering HTTP Requests
|
|
|
|
Let's look at the first feature of htmx: the ability for any element in a web page to issue HTTP requests. This is the
|
|
core functionality of htmx, and it consists of five attributes that can be used to issue the five different developer-facing
|
|
types of HTTP requests:
|
|
|
|
* `hx-get` - issues an HTTP `GET` request
|
|
* `hx-post` - issues an HTTP `POST` request
|
|
* `hx-put` - issues an HTTP `PUT` request
|
|
* `hx-patch` - issues an HTTP `PATCH` request
|
|
* `hx-delete` - issues an HTTP `DELETE` request
|
|
|
|
Each of these attributes, when placed on an element, tell the htmx library: "When a user clicks (or whatever) this
|
|
element, issue an HTTP request of the specified type"
|
|
|
|
The values of these attributes are similar to the values of both `href` on anchors and `action` on forms: you specify the
|
|
URL you wish to issue the given HTTP request type to. Typically, this is done via a server-relative path.
|
|
|
|
So, for example, if we wanted a button to issue a `GET` request to `/contacts` then we would write:
|
|
|
|
[#listing-3-2, reftext={chapter}.{counter:listing}]
|
|
.A Simple htmx-Powered Button
|
|
[source,html]
|
|
----
|
|
<button hx-get="/contacts"> <1>
|
|
Get The Contacts
|
|
</button>
|
|
----
|
|
<1> A simple button that issues an HTTP `GET` to `/contacts`
|
|
|
|
htmx will see the `hx-get` attribute on this button, and hook up some JavaScript logic to issue an HTTP
|
|
`GET` AJAX request to the `/contacts` path when the user clicks on it. Very easy to understand and very consistent
|
|
with the rest of HTML.
|
|
|
|
=== It's All Just HTML!
|
|
|
|
Now we get to perhaps the most important thing to understand about htmx: it expects the response to this AJAX request _to be
|
|
HTML_! htmx is an extension of HTML and, just as the response to an anchor tag click or form submission is usually
|
|
expected to be HTML, htmx expects the server to respond with a hypermedia, namely with HTML.
|
|
|
|
This may come as a shock to web developers who are unused to responding to an AJAX request with anything other than JSON,
|
|
which is far and away the most common response format for such requests. But AJAX requests are just HTTP requests and
|
|
there is no rule saying they must be JSON! Recall again that AJAX stands for Asynchronous Javascript & XML, so JSON
|
|
is already a step away from the format originally envisioned for this API: XML. htmx simply goes another direction
|
|
and expects HTML.
|
|
|
|
.htmx vs. "plain" HTML responses
|
|
****
|
|
|
|
So, we have established that htmx expects HTML responses to the HTTP requests it makes. But there is an important
|
|
difference between the HTTP responses to normal anchor and form driven requests and to htmx-powered requests like the one
|
|
made by this button: in the case of htmx triggered requests, responses are often only _partial_ bits of HTML.
|
|
|
|
In htmx-powered interactions we are typically not replacing the entire document. Rather we are using "transclusion" to
|
|
include content _within_ an existing document. Because of this, it is often not necessary or desirable to transfer an entire
|
|
HTML document from the server to the browser. This fact can be used to save bandwidth as well as resource loading time,
|
|
since less overall content is transferred from the server to the client and since it isn't necessary to reprocess a
|
|
`head` tag with style sheets,script tags, and so forth.
|
|
****
|
|
|
|
Let's consider what a simple _partial_ HTML response to the "Get Contacts" button might be when it is clicked.
|
|
|
|
It might look something like this:
|
|
|
|
[#listing-3-3, reftext={chapter}.{counter:listing}]
|
|
.A partial HTML Response to an htmx Request
|
|
[source,html]
|
|
----
|
|
<ul>
|
|
<li><a href="mailto:joe@example.com">Joe</a></li>
|
|
<li><a href="mailto:sarah@example.com">Sarah</a></li>
|
|
<li><a href="mailto:fred@example.com">Fred</a></li>
|
|
</ul>
|
|
----
|
|
|
|
This is just a simple unordered list of contacts with some clickable elements in it. Note that there is no opening
|
|
`html` tag, no `head` tag, and so forth: it is a _raw_ HTML list, without any decoration around it. A response in a
|
|
real application might of course contain far more sophisticated HTML than a simple list, but it wouldn't need to be an
|
|
entire page of HTML.
|
|
|
|
This response is perfect for htmx: it will take the returned content and swap it in to the DOM. This is fast and efficient,
|
|
leveraging the existing HTML parser in the browser. And this demonstrates that htmx is staying within the hypermedia
|
|
paradigm: just like in a "normal" web application, we see hypermedia being transferred to the client in a stateless and
|
|
uniform manner, where the client knows nothing about the internals of the resources being displayed.
|
|
|
|
This button just a more sophisticated component for building a Hypermedia Driven Application!
|
|
|
|
== Targeting Other Elements
|
|
|
|
Now, given that htmx has issued a request and gotten back some HTML as a response, what should be done with it?
|
|
|
|
It turns out that the default htmx behavior is to simply put the returned content inside the element that triggered the
|
|
request. That's obviously _not_ a good thing in this situation: we will end up with a list of contacts awkwardly embedded within
|
|
a button element on the page! That will look pretty silly and is obviously not what we want.
|
|
|
|
Fortunately htmx provides another attribute, `hx-target` which can be used to specify exactly where in the DOM the
|
|
new content should be placed. The value of the `hx-target` attribute is a Cascading Style Sheet (CSS) _selector_ that
|
|
allows you to specify the element to put the new hypermedia content into
|
|
|
|
Let's add a `div` tag that encloses the button with the id `main`. We will then target this `div` with the response:
|
|
|
|
[#listing-3-4, reftext={chapter}.{counter:listing}]
|
|
.A Simple htmx-Powered Button
|
|
[source,html]
|
|
----
|
|
<div id="main"> <1>
|
|
|
|
<button hx-get="/contacts" hx-target="#main"> <2>
|
|
Get The Contacts
|
|
</button>
|
|
|
|
</div>
|
|
----
|
|
<1> A `div` element that wraps the button
|
|
<2> A new `hx-target` attribute that specifies the `div` as the target of the response
|
|
|
|
We have added `hx-target="#main"` to our button, where `#main` is a CSS selector that says "The thing with the ID 'main'".
|
|
Note that by using CSS selectors, htmx is once again building on top of familiar and standard HTML concepts. By doing
|
|
so it keeps the additional conceptual load beyond normal HTML to a minimum.
|
|
|
|
Given this new configuration, what would the HTML on the client look like after a user clicks on this button and a
|
|
response has been received and processed?
|
|
|
|
It would look something like this:
|
|
|
|
[#listing-3-5, reftext={chapter}.{counter:listing}]
|
|
.Our HTML After the htmx Request Finishes
|
|
[source,html]
|
|
----
|
|
<div id="main">
|
|
<ul>
|
|
<li><a href="mailto:joe@example.com">Joe</a></li>
|
|
<li><a href="mailto:sarah@example.com">Sarah</a></li>
|
|
<li><a href="mailto:fred@example.com">Fred</a></li>
|
|
</ul>
|
|
</div>
|
|
----
|
|
|
|
The response HTML has been swapped into the `div`, replacing the button that triggered the request. Transclusion! And
|
|
this has happened "in the background" via AJAX, without a large page refresh. Nonetheless, this is _definitely_ a hypermedia
|
|
interaction. It isn't as coarse-grained as a normal, full web page request coming from an anchor might be, but it certainly
|
|
falls within the same conceptual model!
|
|
|
|
== Swap Styles
|
|
|
|
Now, maybe we don't want to simply load the content from the server response _into_ the div, as child elements. Perhaps,
|
|
for whatever reasons, we wish to _replace_ the entire div with the response. Seems like a reasonable behavior to
|
|
make configurable.
|
|
|
|
As luck would have it, htmx provides another attribute, `hx-swap`, that allows you to specify exactly _how_ the content
|
|
should be swapped into the DOM. (Are you beginning to sense a pattern here?)
|
|
|
|
The `hx-swap` attribute supports the following values:
|
|
|
|
* `innerHTML` - The default, replace the inner html of the target element
|
|
* `outerHTML` - Replace the entire target element with the response
|
|
* `beforebegin` - Insert the response before the target element
|
|
* `afterbegin` - Insert the response before the first child of the target element
|
|
* `beforeend` - Insert the response after the last child of the target element
|
|
* `afterend` - Insert the response after the target element
|
|
* `delete` - Deletes the target element regardless of the response
|
|
* `none` - No swap will be performed
|
|
|
|
The first two values, `innerHTML` and `outerHTML`, are taken from the standard DOM properties that allow you to replace content
|
|
within an element or in place of an entire element respectively.
|
|
|
|
The next four values are taken from the `Element.insertAdjacentHTML()` DOM API, which allow you to place an element or
|
|
elements around a given element in various ways.
|
|
|
|
The last two values, `delete` and `none` are specific to htmx, but should be fairly obvious for you understand.
|
|
|
|
Again, you can see that htmx tries to stay as close as possible to the existing web standards to keep your conceptual
|
|
load to a minimum.
|
|
|
|
Let's consider if, rather than replacing the `innerHTML` content of the main div above, we wished to replace the _entire
|
|
div_ with the HTML response. To do so would require only a small change to our button:
|
|
|
|
[#listing-3-6, reftext={chapter}.{counter:listing}]
|
|
.Replacing the Entire div
|
|
[source,html]
|
|
----
|
|
<div id="main">
|
|
|
|
<button hx-get="/contacts" hx-target="#main" hx-swap="outerHTML"> <1>
|
|
Get The Contacts
|
|
</button>
|
|
|
|
</div>
|
|
----
|
|
<1> The `hx-swap` attribute specifies how to swap new content in
|
|
|
|
Now, when a response is received, the _entire_ div will be replaced with the hypermedia content:
|
|
|
|
[#listing-3-7, reftext={chapter}.{counter:listing}]
|
|
.Our HTML After the htmx Request Finishes
|
|
[source,html]
|
|
----
|
|
<ul>
|
|
<li><a href="mailto:joe@example.com">Joe</a></li>
|
|
<li><a href="mailto:sarah@example.com">Sarah</a></li>
|
|
<li><a href="mailto:fred@example.com">Fred</a></li>
|
|
</ul>
|
|
----
|
|
|
|
You can see that, with this change, the target div has been entirely removed from the DOM, and the list that was returned
|
|
as the response has replaced it.
|
|
|
|
Later in the book we will see additional uses for `hx-swap`, for example when we implement infinite scrolling in our
|
|
contact management application.
|
|
|
|
Note that with the `hx-get`, `hx-post`, `hx-put`, `hx-patch` and `hx-delete` attributes, we have addressed two of the
|
|
opportunities for improvement that we enumerated regarding plain HTML:
|
|
|
|
* Opportunity 1: We can now issue an HTTP request with _any_ element (in this case we are using a button)
|
|
* Opportunity 3: We can issue _any sort_ of HTTP request we want, `PUT`, `PATCH` and `DELETE`, in particular
|
|
|
|
And, with `hx-target` and `hx-swap` we have addressed a third opportunity:
|
|
the requirement that the entire page be replaced.
|
|
|
|
* Opportunity 4: We can now replace any element we want in our page via transclusion, and we can do so in any manner want
|
|
|
|
So, with seven relatively simple additional attributes, we have addressed most of the hypermedia shortcomings we identified
|
|
earlier with HTML. Not bad!
|
|
|
|
There was one remaining shortcoming of HTML that we noted: the fact that only a `click` event (on an anchor) or a `submit` event
|
|
(on a form) can trigger HTTP request. Let's look at how we can address that concern next.
|
|
|
|
== Using Other Events
|
|
|
|
Thus far we have been using a button to issue a request with htmx. You have probably intuitively understood that the
|
|
request will be issued when the button is clicked on since, well, that's what you do with buttons! You click on them!
|
|
|
|
And, yes, by default when an `hx-get` or another request-driving annotation from htmx is placed on a button, the request
|
|
will beissued when the button is clicked.
|
|
|
|
However, htmx generalizes this notion of an event triggering a request by using, you guessed it, another attribute:
|
|
`hx-trigger`. The `hx-trigger` attribute allows you to specify one or more events that will cause the element to
|
|
trigger an HTTP request, overriding the default triggering event.
|
|
|
|
What is the "default triggering event" in htmx? It depends on the element type, but should be fairly intuitive to anyone
|
|
familiar with HTML:
|
|
|
|
* Requests on `input`, `textarea` & `select` elements are triggered by the `change` event
|
|
* Requests on `form` elements are triggered on the `submit` event
|
|
* Requests on all other elements are triggered by the `click` event
|
|
|
|
So, lets consider if we wanted to trigger the request on our button when the mouse entered it. This is certainly
|
|
not a recommended UX pattern, but let's just look at it as an example!
|
|
|
|
To do this, we would add the following attribute to our button:
|
|
|
|
[#listing-3-8, reftext={chapter}.{counter:listing}]
|
|
.A Terrible Idea, But It Demonstrates The Concept!
|
|
[source,html]
|
|
----
|
|
<div id="main">
|
|
|
|
<button hx-get="/contacts" hx-target="#main" hx-swap="outerHTML" hx-trigger="mouseenter"> <1>
|
|
Get The Contacts
|
|
</button>
|
|
|
|
</div>
|
|
----
|
|
<1> Issue a request... on the `mouseenter` event?
|
|
|
|
Now, whenever the mouse enters this button, a request will be triggered. Hey, we didn't say this was a _good_ idea!
|
|
|
|
Let's try something a bit more realistic: let's add support for a keyboard shortcut for loading the contacts, `Ctrl-L`
|
|
(for "Load"). To do this we will need to take advantage of some additional syntax that the `hx-trigger` attribute
|
|
supports: event filters and additional arguments.
|
|
|
|
Event filters are a mechanism for determining if a given event should trigger a request or not. They are applied to an
|
|
event by adding square brackets after it: `someEvent[someFilter]`. The filter itself is a JavaScript expression that
|
|
will be evaluated when the given event occurs. If the result is truthy, in the JavaScript sense, it will trigger the
|
|
request. If not, it will not.
|
|
|
|
In the case of keyboard shortcuts, we want to catch the `keyup` event in addition to the keyup event:
|
|
|
|
[#listing-3-9, reftext={chapter}.{counter:listing}]
|
|
.A Start
|
|
[source,html]
|
|
----
|
|
<div id="main">
|
|
|
|
<button hx-get="/contacts" hx-target="#main" hx-swap="outerHTML" hx-trigger="click, keyup"> <1>
|
|
Get The Contacts
|
|
</button>
|
|
|
|
</div>
|
|
----
|
|
<1> A trigger with two events
|
|
|
|
Note that we have a comma separated list of events that can trigger this element, allowing us to respond to more than
|
|
one potential triggering event.
|
|
|
|
There are two problems with this:
|
|
|
|
* It will trigger requests on _any_ keyup event
|
|
* It will trigger requests only when a keyup occurs _within_ this button (an unlikely occurrence!)
|
|
|
|
To fix the first issue, lets use a trigger filter:
|
|
|
|
[#listing-3-10, reftext={chapter}.{counter:listing}]
|
|
.Better!
|
|
[source,html]
|
|
----
|
|
<div id="main">
|
|
|
|
<button hx-get="/contacts" hx-target="#main" hx-swap="outerHTML" hx-trigger="click, keyup[ctrlKey && key == 'l']"> <1>
|
|
Get The Contacts
|
|
</button>
|
|
|
|
</div>
|
|
----
|
|
<1> A trigger with an added filter, specifying that 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
|
|
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.
|
|
|
|
OK, so this filter limits the keyups that will trigger the request to only `Ctrl-L` presses. However, we still have
|
|
the problem that, as it stands, only `keyup` events _within_ the button will trigger the request. If you are familiar
|
|
with the JavaScript event bubbling model: events typically "bubble" up to parent elements so an event like a keyup
|
|
will be triggered first on the focused element, then on it's parent, and so on, until it reaches the top level `document`
|
|
that is the root of all other elements.
|
|
|
|
In this case, this is obviously not what we want! People typically aren't typing characters _within_ the button, they
|
|
click on buttons! Here we want to listen to the `keyup` events on the entire page, or, equivalently, on the `body`
|
|
element.
|
|
|
|
To fix this, we need to take advantage of another feature that the `hx-trigger` attribute supports:
|
|
the ability to listen to _other elements_ for events using the `from:` modifier. The `from:`modifier, as with many other
|
|
attributes and modifiers in htmx, uses a CSS selector to select the element to listen on.
|
|
|
|
We can use it like this:
|
|
|
|
[#listing-3-11, reftext={chapter}.{counter:listing}]
|
|
.Better!
|
|
[source,html]
|
|
----
|
|
<div id="main">
|
|
|
|
<button hx-get="/contacts" hx-target="#main" hx-swap="outerHTML" hx-trigger="click, keyup[ctrlKey && key == 'L'] from:body"><1>
|
|
Get The Contacts
|
|
</button>
|
|
|
|
</div>
|
|
----
|
|
<1> Listen to the event on the `body` tag
|
|
|
|
Now, in addition to clicks, our button is listening for `keyup` events on the body of the page, and should issue a
|
|
request both when it is clicked on, and also whenever someone hits `Ctrl-L` within the body of the page!
|
|
|
|
A nice little keyboard shortcut! Perfect!
|
|
|
|
The `hx-trigger` attribute is more elaborate than the other htmx attributes we have looked at so far, but that is because
|
|
events, in general, are used more elaborately in modern user interfaces. The default options often suffice, however, and you
|
|
shouldn't need to reach for complicated trigger features too often when using htmx.
|
|
|
|
That being said, even in the more elaborate situations like the example above, where we have a keyboard shortcut, the
|
|
overall feel of htmx is _declarative_ rather than _imperative_ and follows along closely with the standard feel and
|
|
philosophy of HTML.
|
|
|
|
And hey, check it out! With this final attribute, `hx-trigger`, we have addressed the final opportunity for improvement
|
|
of HTMl that we enumerated at the start of this chapter:
|
|
|
|
* Opportunity 2: We can use _any_ event to trigger an HTTP request
|
|
|
|
That's a grand total of eight, count 'em, _eight_ attributes that all fall squarely within the same conceptual model as
|
|
normal HTML and that, by extending HTML as a hypermedia, open up whole new world of user interaction possibilities
|
|
within HTML!
|
|
|
|
Here is a table summarizing the opportunities to improve HTML and which htmx attributes address them:
|
|
|
|
.Opportunities For Improving HTML
|
|
|===
|
|
|Opportunity To Improve HTML | htmx attributes
|
|
|
|
|Any element should be able to make HTTP requests
|
|
| `hx-get`, `hx-post`, `hx-put`, `hx-patch`, `hx-delete`
|
|
|
|
|Any event should be able to trigger an HTTP request
|
|
| `hx-trigger`
|
|
|
|
|Any HTTP Action should be available
|
|
|`hx-put`, `hx-patch`, `hx-delete`
|
|
|
|
|Any place on the page should be replaceable (transclusion)
|
|
|`hx-target`, `hx-swap`
|
|
|
|
|===
|
|
|
|
== Passing Request Parameters
|
|
|
|
So far we have been just looking at situation where a button makes a simple `GET` request. This is conceptually very
|
|
close to what an anchor tag might do. But there is the other primary element in traditional hypermedia-based applications:
|
|
forms. Forms are used to pass additional information beyond just a URL up to the server in a request. This information
|
|
is typically entered into elements within the form via the various types of input tags in HTML.
|
|
|
|
htmx allows you include this additional information in a natural way that mirrors how HTML itself works.
|
|
|
|
=== Enclosing Forms
|
|
|
|
The simplest way to pass additional input values up with a request in htmx is to enclose the input within a form tag.
|
|
|
|
Let's take our original button for retrieving contacts and repurpose it for searching contacts:
|
|
|
|
[#listing-3-12, reftext={chapter}.{counter:listing}]
|
|
.A Simple htmx-Powered Button
|
|
[source,html]
|
|
----
|
|
<div id="main">
|
|
|
|
<form> <1>
|
|
<label for="search">Search Contacts:</label>
|
|
<input id="search" name="q" type="search" placeholder="Search Contacts"> <2>
|
|
<button hx-post="/contacts" hx-target="#main"> <3>
|
|
Search The Contacts
|
|
</button>
|
|
</form>
|
|
|
|
</div>
|
|
----
|
|
<1> The form tag encloses the button, thereby including all values within it in the button request
|
|
<2> A new input that users will be able to enter search text into
|
|
<3> Our button has been converted to an `hx-post`
|
|
|
|
Here we have added a form tag surrounding the button along with a search input that can be used to enter a term to
|
|
search the contacts with.
|
|
|
|
Now, when a user clicks on the button, the value of the input with the id `search` will be included in the request. This
|
|
is by virtue of the fact that there is a form tag enclosing both the button and the input: when an htmx-driven request
|
|
is triggered, htmx will look up the DOM hierarchy for an enclosing form, and, if one is found, it will include all
|
|
values from within that form. (This is sometimes referred to as "serializing" the form.)
|
|
|
|
You might have noticed that the button was switched from a `GET` request to a `POST` request. This is because, by default,
|
|
htmx does _not_ include the closest enclosing form for `GET` requests. This is to avoid serializing forms in situations
|
|
where the data is not needed and to keep URLs clean when dealing with history entries, which we discuss in the next
|
|
section.
|
|
|
|
=== Including inputs
|
|
|
|
While enclosing all the inputs you want included in a request is the most common approach for including values from inputs
|
|
in htmx requests, it isn't always possible or desirable: form tags can have layout consequences and simply cannot be
|
|
placed in some spots in HTML documents. A good example of the latter situation is in table row (`tr`) elements: the
|
|
`form` tag is not a valid child or parent of table rows, so you can't place a form within or around an entire
|
|
row of data in a table.
|
|
|
|
To address this issue, htmx provides another mechanism for including value in requests: the `hx-include` attribute. The
|
|
`hx-include` attribute allows you to select input values that you wish to include in a request via CSS selectors.
|
|
|
|
Here is the above example reworked to include the input, dropping the form:
|
|
|
|
[#listing-3-13, reftext={chapter}.{counter:listing}]
|
|
.A Simple htmx-Powered Button
|
|
[source,html]
|
|
----
|
|
<div id="main">
|
|
|
|
<label for="search">Search Contacts:</label>
|
|
<input id="search" name="q" type="search" placeholder="Search Contacts">
|
|
<button hx-post="/contacts" hx-target="#main" hx-include="#search"><1>
|
|
Search The Contacts
|
|
</button>
|
|
|
|
</div>
|
|
----
|
|
<1> `hx-include` can be used to include values directly in a request
|
|
|
|
The `hx-include` attribute takes a CSS selector value and allows you to specify exactly which values to send along
|
|
with the request. This can be useful if it is difficult to colocate an element issuing a request with all the inputs
|
|
that need to be submitted with it. It is also useful when you do, in fact, want to submit values with a `GET` request
|
|
and overcome the default behavior of htmx with respect to `GET` requests.
|
|
|
|
=== Inline Values
|
|
|
|
A final way to include values in htmx-driven requests is to use the `hx-vals` attribute, which allows you to include
|
|
"static" values in the request. This can be useful if you have additional information that you want to include in
|
|
requests, but you don't want to have this information embedded in, for example, hidden inputs (which would be the
|
|
standard mechanism for including additional, hidden information in HTML.)
|
|
|
|
Here is an example of `hx-vals`:
|
|
|
|
[#listing-3-13, reftext={chapter}.{counter:listing}]
|
|
.A Simple htmx-Powered Button
|
|
[source,html]
|
|
----
|
|
<button hx-get="/contacts" hx-vals='{"state":"MT"}'> <1>
|
|
Get The Contacts In Montana
|
|
</button>
|
|
----
|
|
<1> `hx-vals`, a JSON value to include in the request
|
|
|
|
The parameter `state` the value `MT` will be included in the `GET` request, resulting in a path and parameters that
|
|
looks like this: `/contacts?state=MT`. One thing to note is that we switched the `hx-vals` attribute to use single quotes
|
|
around its value. This is because JSON strictly requires double quotes and, therefore, to avoid escaping we needed to
|
|
use the single-quote form for the attribute value.
|
|
|
|
This approach is useful when you have fixed data that you want to include in a request and you don't want to rely on
|
|
something like a hidden input. You can also prefix `hx-vals` with a `js:` and pass values evaluated at the time of the
|
|
request, which can be useful for including things like a dynamically maintained variable, or value from a third party
|
|
javascript library.
|
|
|
|
For example, if the `state` variable were maintained dynamically, via some JavaScript, and there existed a JavaScript
|
|
function, `getCurrentState()`, that returned the currently selected state, it could be included dynamically in htmx
|
|
requests like so:
|
|
|
|
[#listing-3-13, reftext={chapter}.{counter:listing}]
|
|
.A Dynamic Value
|
|
[source,html]
|
|
----
|
|
<button hx-get="/contacts" hx-vals='js:{"state":getCurrentState()}'> <1>
|
|
Get The Contacts In The Selected State
|
|
</button>
|
|
----
|
|
<1> With the `js:` prefix, this expression will evaluate at submit time
|
|
|
|
These three mechanisms, using `form` tags, using the `hx-include` attribute and using the `hx-vals` attribute, allow you
|
|
to include values in your hypermedia requests with htmx in a manner that should feel very familiar and in keeping with
|
|
the spirit of HTML, while also giving you the flexibility to achieve what you want.
|
|
|
|
== History Support
|
|
|
|
A final piece of functionality to discuss to close out our overview of htmx is browser history. When you use normal
|
|
HTML links and forms, your browser will keep track of all the pages that you have visited. You can use the back button
|
|
to navigate back to a previous page and, once you have done this, you can use a forward button to go forward to the
|
|
original page you were on.
|
|
|
|
This notion of history was one of the killer features of the early web. Unfortunately it turns out that history becomes
|
|
tricky when you move to the Single Page Application paradigm. An AJAX request does not, by itself, register a web
|
|
page in your browsers history and this is a good thing! An AJAX request may have nothing to do with the state of the
|
|
web page (perhaps it is just recording some activity in the browser), so it wouldn't be appropriate to create a new
|
|
history entry for the interaction.
|
|
|
|
However, there are likely to be a lot of AJAX driven interactions in a Single Page Application where it _is_ appropriate
|
|
to create a history entry. And JavaScript does provide an API for working with the history cache. Unfortunately the
|
|
API is very difficult to work with and is often simply ignored by developers. If you have ever used a Single Page
|
|
Application and accidentally clicked the back button, only to lose your entire application state and have to start over,
|
|
you have seen this problem in action.
|
|
|
|
In htmx, as in Single Page Application frameworks, you often need to explicitly work with the history API. Fortunately, htmx
|
|
makes it much easier to do so than most other libraries.
|
|
|
|
Consider the button we have been discussing again:
|
|
|
|
[#listing-3-14, reftext={chapter}.{counter:listing}]
|
|
.Our trusty button
|
|
[source,html]
|
|
----
|
|
<button hx-get="/contacts" hx-target="#main">
|
|
Get The Contacts
|
|
</button>
|
|
----
|
|
|
|
As it stands, if you click this button it will retrieve the content from `/contacts` and load it into the element with the
|
|
id `main`, but it will _not_ create a new history entry. If we wanted it to create a history entry we would add another
|
|
attribute to the button, `hx-push-url`:
|
|
|
|
[#listing-3-14, reftext={chapter}.{counter:listing}]
|
|
.Our trusty button, now with history!
|
|
[source,html]
|
|
----
|
|
<button hx-get="/contacts" hx-target="#main" hx-push-url="true"><1>
|
|
Get The Contacts
|
|
</button>
|
|
----
|
|
<1> `hx-push-url` will create an entry in history when the button is clicked
|
|
|
|
Now, when the button is clicked, the `/contacts` path will be put into the browser's navigation bar and a history entry
|
|
will be created for it. Furthermore, if the user clicks the back button, the original content for the page will be
|
|
restored, along with th original URL.
|
|
|
|
`hx-push-url` might sound a little obscure, but this is based on the JavaScript API, `history.pushState()`. This notion
|
|
of "pushing" derives from the fact that history entries are modeled as a stack, and so you are "pushing" new entries
|
|
onto the top of the stack of history entries.
|
|
|
|
With this (relatively) simple mechanism, htmx allows you to integrate with the back button in a way that mimics the
|
|
"normal" behavior of HTML. Not bad if you look at what other javascript librarires require of you!
|
|
|
|
.Drawbacks To The htmx Approach
|
|
****
|
|
htmx is a very pure extension to HTML, aiming to incrementally improve the language as a hypermedia in a manner that is
|
|
conceptually coherent with the underlying markup language. This approach, like any technical choice, is not without
|
|
tradeoffs: by staying so close to HTML, htmx does not give developers a lot of infrastructure that many might feel
|
|
should be there "by default".
|
|
|
|
A good example is the concept of modal dialogs. Many web applications today make heavy use of modal dialogs, effectively
|
|
in-page pop-ups that sit "on top" of the existing page. (Of course, in reality, this is an optical illusion and it is
|
|
all just a web page: the web has no notion of"modals" in this regard.)
|
|
|
|
A web developer might expect htmx to provide some sort of modal dialog component out of the box, since it is, after all,
|
|
a front-end library, and many front end libraries offer support for this pattern.
|
|
|
|
htmx, however, has no notion of modals. That's not to say you can't use modals with htmx, and we will look at how you
|
|
can do so later. But htmx, like HTML itself, won't give you an API specifically for creating modals. You
|
|
would need to use a 3rd party library or roll your own modal implementation and then integrate htmx into it if you want
|
|
to use modals within an htmx-based application.
|
|
|
|
This is the design tradeoff that htmx makes: it retains conceptual purity as an extension of HTML, and, in exchange,
|
|
lacks some of the "batteries included" features found in other front end libraries.
|
|
|
|
As an aside, it's worth nothing that htmx _can_ be used to effectively implement a slightly different UX pattern, inline
|
|
editing, which is often a good alternative to modals, and, in our opinion, is more consistent with the stateless nature
|
|
of the web. We will look at this approach later in the book, and you can see an example of it here: https://htmx.org/examples/click-to-edit/
|
|
****
|
|
|
|
== Summary
|
|
|
|
* HTML presents a few opportunties for improvement as a hypermedia:
|
|
** It doesn't give you access to non-`GET` or `POST` requests
|
|
** It requires that you update the entire page
|
|
** It only offers limited interactivity with the user, via links and forms
|
|
* htmx addresses each of these opportunities, increasing the expressiveness of HTML as a hypermedia
|
|
* The `hx-get`, `hx-post`, etc. attributes can be used to issue requests with any element in the dom
|
|
* The `hx-swap` attribute can be used to control exactly how HTML responses to htmx requests should be swapped
|
|
into the DOM
|
|
* The `hx-trigger` attribute can be used to control the event that triggers a request
|
|
* Event filters can be used in `hx-trigger` to narrow down the exact situation that you want to issue a request for
|
|
* htmx offers three mechanisms for including additional input information with requests:
|
|
** Enclosing elements within a `form` tag
|
|
** Using the `hx-include` attribute to select inputs to include in the request
|
|
** `hx-vals` for embedding values directly via JSON or, dynamically, resolving values via JavaScript
|
|
* htmx also provides integration with the browser history and back button, using the `hx-push-url` attribute
|