|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Hypermedia Systems - Complete Book</title> |
|
<style> |
|
body { |
|
max-width: 900px; |
|
margin: 0 auto; |
|
padding: 20px; |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; |
|
line-height: 1.6; |
|
color: #333; |
|
background-color: #fff; |
|
} |
|
.chapter { |
|
margin-bottom: 60px; |
|
page-break-after: always; |
|
} |
|
.chapter-title { |
|
font-size: 2em; |
|
margin-top: 40px; |
|
margin-bottom: 20px; |
|
padding-bottom: 10px; |
|
border-bottom: 2px solid #333; |
|
color: #000; |
|
} |
|
h1, h2, h3, h4, h5, h6 { |
|
margin-top: 1.5em; |
|
margin-bottom: 0.5em; |
|
color: #000; |
|
} |
|
p { |
|
margin-bottom: 1em; |
|
} |
|
pre { |
|
background: #f5f5f5; |
|
padding: 15px; |
|
overflow-x: auto; |
|
border-radius: 5px; |
|
border: 1px solid #ddd; |
|
} |
|
code { |
|
background: #f5f5f5; |
|
padding: 2px 5px; |
|
border-radius: 3px; |
|
font-family: 'Monaco', 'Menlo', 'Courier New', monospace; |
|
font-size: 0.9em; |
|
} |
|
pre code { |
|
background: transparent; |
|
padding: 0; |
|
} |
|
img { |
|
max-width: 100%; |
|
height: auto; |
|
display: block; |
|
margin: 20px auto; |
|
} |
|
blockquote { |
|
border-left: 4px solid #ddd; |
|
padding-left: 20px; |
|
margin-left: 0; |
|
color: #666; |
|
font-style: italic; |
|
} |
|
table { |
|
border-collapse: collapse; |
|
width: 100%; |
|
margin: 20px 0; |
|
} |
|
th, td { |
|
border: 1px solid #ddd; |
|
padding: 8px; |
|
text-align: left; |
|
} |
|
th { |
|
background-color: #f5f5f5; |
|
font-weight: bold; |
|
} |
|
a { |
|
color: #0066cc; |
|
text-decoration: none; |
|
} |
|
a:hover { |
|
text-decoration: underline; |
|
} |
|
.book-title { |
|
text-align: center; |
|
font-size: 3em; |
|
margin-bottom: 10px; |
|
margin-top: 20px; |
|
} |
|
.book-subtitle { |
|
text-align: center; |
|
color: #666; |
|
margin-bottom: 50px; |
|
} |
|
@media print { |
|
body { |
|
max-width: 100%; |
|
} |
|
.chapter { |
|
page-break-after: always; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<h1 class="book-title">Hypermedia Systems</h1> |
|
<p class="book-subtitle">Downloaded from hypermedia.systems</p> |
|
|
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Copyright & Acknowledgments</h2> |
|
<main> |
|
<p><strong>Hypermedia Systems</strong><br/> |
|
© 2023 Carson Gross, Adam Stepinski, Deniz Akşimşek</p> |
|
<p>Editor: William Talcott</p> |
|
<p>CC BY-NC-SA: This work is licensed under the Creative Commons |
|
Attribution-NonCommercial-ShareAlike 4.0 International License. To view |
|
a copy of this license, visit <a href="http://creativecommons.org/licenses/by-nc-sa/4.0/">http://creativecommons.org/licenses/by-nc-sa/4.0/</a> |
|
or send a letter to Creative Commons, PO Box 1866, Mountain View, CA |
|
94042, USA.</p> |
|
<p>Read online: <a href="https://hypermedia.systems/">https://hypermedia.systems/</a></p> |
|
<p>Thanks to:</p> |
|
<ul> |
|
<li><p>Berkeley Graphics for their permission to use <em>Berkeley |
|
Mono</em> and the design of the first edition’s cover,</p></li> |
|
<li><p>All contributors to the book: (<a href="https://github.com/bigskysoftware/hypermedia-systems-book/graphs/contributors">https://github.com/bigskysoftware/hypermedia-systems-book/graphs/contributors</a>),</p></li> |
|
<li><p>Manning Publications for their support in the initial stages of |
|
writing this book,</p></li> |
|
<li><p>India Hackle for her valuable feedback and editing.</p></li> |
|
</ul> |
|
<p>Typefaces:</p> |
|
<ul> |
|
<li><p>Libertinus Serif (by its authors: <a href="https://github.com/alerque/libertinus/blob/master/CONTRIBUTORS.txt,">https://github.com/alerque/libertinus/blob/master/CONTRIBUTORS.txt,</a> |
|
for body text),</p></li> |
|
<li><p>Libertinus Sans (by the same, for secondary text),</p></li> |
|
<li><p>Berkeley Mono (by Berkeley Graphics, for code and ascii art |
|
diagrams).</p></li> |
|
<li><p>Jaro (by Agyei Archer, Celine Hurka, and Mirko Velimirović, for |
|
headings)</p></li> |
|
</ul> |
|
<p>Written and typeset with Typst (<a href="https://typst.app">https://typst.app</a>).</p> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Dedication</h2> |
|
<main> |
|
<div data-align="horizon"> |
|
<p><em>To my family and the htmx discord.</em> — Carson Gross</p> |
|
<p><em>To my wife Tarunya, for her support through the ups and downs of |
|
this project.</em> — Adam Stepinski</p> |
|
<p><em>Annem Lamia Akşimşek ve babam Özgür Akşimşek’e.</em> — Deniz |
|
Akşimşek</p> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Foreword</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
</details> |
|
<div class="division-content"> |
|
<h0>Foreword</h0> |
|
<p>While there have been many books on the topic of hypermedia, there is |
|
a select number of publications that chronicle important advances in the |
|
field of hypermedia and this book is one of them. Not only does this |
|
book describe the benefits of creating hypermedia-driven applications |
|
(HDAs), it leads the reader through working and practical examples of |
|
how to do just that. And, in doing so, the authors (Gross, Stepinski, |
|
and Akşimşek) call out contributions from important figures in the |
|
history of hypermedia systems. And, as of this writing, that history |
|
spans more than half a century.</p> |
|
<p>In 1974, Ted Nelson’s <em class="test">“Computer Lib/Machine Dreams”</em> marked |
|
the start of the modern hypermedia era with a book that Steven Levy |
|
(author of <em class="test">“Hackers”</em>) described as “the epic of the computer |
|
revolution.” Nelson is credited with coining the terms HyperText, |
|
HyperLink, HyperMedia, and HyperData as well as Intertwingularity; the |
|
notion that all information is connected — both intertwined and |
|
intermingled. Almost half a century ago, he foretold a future where any |
|
person could publish anything anytime without the need for permission |
|
from any central controlling source. And his hyperlinks were the engine |
|
of that future.</p> |
|
<p>It took two decades before Nelson’s idea of intertwingled computing |
|
became widespread. Along the way, Douglas Engelbart created the |
|
<em class="test">oN-Line System</em> or NLS, Wendy Hall built the <em class="test">Microcosm</em>, |
|
and, eventually, in the 1980s, Tim Berners-Lee defined the World Wide |
|
Web (WWW), HTML, and HTTP. It was Berners-Lee’s iteration that has |
|
become the backbone and the standard for the intertwingularity we all |
|
experience today.</p> |
|
<p>By the year 2000, the technical foundations of “the web” were |
|
documented in Roy Fielding’s PhD dissertation <em class="test">“Architectural Styles |
|
and the Design of Network-based Software Architectures”</em>). In that |
|
work, Fielding defined the architectural model of <em class="test">REpresentational |
|
State Transfer</em> or REST. This set of system properties and |
|
implementation constraints have proven — even a quarter-century later — |
|
to be a reliable model for designing and building the intertwingled |
|
machines that today affect billions of people around the globe.</p> |
|
<p>Even though Fielding’s work was important, it wasn’t until Leonard |
|
Richardson and Sam Ruby published <em class="test">“RESTful Web Services”</em> in |
|
2008 that the REST model became well-known to the world of software |
|
architecture and development. Backed by the Ruby programming platform, |
|
the ideas behind Fielding’s REST model became <em class="test">de rigueur</em> for |
|
the creation of web-based services and client applications.</p> |
|
<p>One of the reasons Richardson and Ruby’s work was so important was |
|
that, unlike dissertations and futuristic predictions, the <em class="test">RESTful |
|
Web Services</em> book outlined a practical working framework for |
|
building powerful applications for the Web. It described not only the |
|
power of REST but also provided step-by-step instructions on how to |
|
build them. Richardson and Ruby brought together the hypermedia |
|
scholarship of the previous twenty years all in one place.</p> |
|
<p>And now we can add this book (<em class="test">“Hypermedia Systems”</em>) to that |
|
list of important works. From the book’s introduction through the |
|
step-by-step directions on how to use HTMX for browsers and Hyperview |
|
for mobile devices, the authors describe the benefits of creating |
|
hypermedia-driven applications (HDAs). They also offer dozens of |
|
practical working examples the reader can use right away in building |
|
their own hypermedia solutions.</p> |
|
<p>I’ve been working in the field of hypermedia for close to thirty |
|
years and have seen quite a few books, papers, dissertations, and |
|
programming platforms come and go in that time. Occasionally, one of |
|
these works “nails it” — provides the right mix of theory and practice |
|
delivered in a way that helps readers make a connection between their |
|
own efforts and the activities of the community at large. I am happy to |
|
say that this book is one of those works. The authors have not only |
|
created powerful tooling in HTMX and Hyperview, they have also advanced |
|
the notion of hypermedia systems and hypermedia-driven applications in |
|
ways that a wide audience can understand and apply.</p> |
|
<p>Nelson describes a future where the barriers to publishing and data |
|
sharing are lowered and the creative energies of the world are easily |
|
shared and applied. This is neither a new or unique idea but one that |
|
does need continual renewal and encouragement. Nelson saw his hyperlink |
|
and hypermedia as the driving force for intertwingularity between people |
|
and machines around the world. In this idea alone, hypermedia is a |
|
powerful approach to creating computer systems that enable people to |
|
work together for the common good. As this book’s authors say, |
|
“Hypermedia was a great idea! It still is!”</p> |
|
<p><em class="test">Mike Amundsen, April 2023</em></p> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Introduction</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/introduction/#what-is-a-hypermedia-system">What is a Hypermedia System?</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/introduction/#hypermedia-driven-applications">Hypermedia-Driven Applications</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/introduction/#goals">Goals</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/introduction/#book-layout">Book Layout</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/introduction/#hypermedia--a-new-generation">Hypermedia: A New Generation</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/introduction/#html-note-title">HTML Notes: Hypermedia In Practice</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>This is a book about building applications using hypermedia systems. |
|
<em class="test">Hypermedia systems</em> might seem like a strange phrase: how is |
|
hypermedia a <em class="test">system</em>? Isn’t hypermedia just a way to link |
|
documents together?</p> |
|
<p>Like with HTML, on the World Wide Web?</p> |
|
<p>What do you mean hypermedia <em class="test">systems</em>?</p> |
|
<p>Well, yes, HTML is <em class="test">a</em> hypermedia. But there is more to the |
|
way the web works than just HTML: HTTP, the Hyper Text Transfer |
|
Protocol, is what transfers HTML from servers to clients, and there are |
|
many details and features associated with it: caching, various headers, |
|
response codes, and so forth.</p> |
|
<p>And then, of course, there are <em class="test">hypermedia servers</em>, which |
|
present <em class="test">hypermedia APIs</em> (yes, <em class="test">APIs</em>) to clients over |
|
the network.</p> |
|
<p>And, finally, there is the all-important <em class="test">hypermedia client</em>: |
|
a software client that understands how to render a <em class="test">hypermedia |
|
response</em> intelligibly to a human, so that a human can interact with |
|
the remote system. The most widely known and used hypermedia clients |
|
are, of course, web browsers.</p> |
|
<p>Web browsers are perhaps the most sophisticated pieces of software we |
|
use. They not only understand HTML, CSS and many other file formats, but |
|
they also provide a JavaScript runtime and programming environment that |
|
is so powerful that web developers can create entire applications in it |
|
that are nearly as sophisticated as <em class="test">thick clients</em>, that is, |
|
native applications.</p> |
|
<p>This JavaScript runtime is so powerful, in fact, that today many |
|
developers ignore the <em class="test">hypermedia</em> features of the browser, in |
|
favor of building their web applications entirely in JavaScript. |
|
Applications built in this manner have come to be called Single Page |
|
Applications (SPAs). Rather than navigating between pages, these web |
|
applications use JavaScript for updating the user interface directly. |
|
When they communicate with a server, these applications typically use |
|
JSON API calls via AJAX. And they often update the user interface using |
|
a “reactive” style frontend JavaScript library.</p> |
|
<p>In these applications HTML becomes a (somewhat awkward) graphical |
|
interface description language that is used because, for historical |
|
reasons, that’s what happens to be there, in the browser.</p> |
|
<p>Applications built in this style are not <em class="test">hypermedia-driven</em>: |
|
they do not take advantage of the underlying hypermedia system of the |
|
web.</p> |
|
<p>To explain what a hypermedia-driven application looks like, and to |
|
contrast it with the popular SPA approach of today, we need to first |
|
explore the entire <em class="test">hypermedia system</em> of the web, beyond just |
|
discussing HTML. We need to look at the <em class="test">network architecture</em> of |
|
the web, including how a web server delivers a hypermedia API, and how |
|
to effectively use the hypermedia features available in the hypermedia |
|
<em class="test">client</em> (e.g., the browser).</p> |
|
<p>Each of these are important aspects of building an effective |
|
hypermedia-driven application, and it is the entire <em class="test">hypermedia |
|
system</em> that comes together to make hypermedia such a powerful |
|
architecture.</p> |
|
<h2 id="what-is-a-hypermedia-system">What is a Hypermedia System?</h2> |
|
<p>To understand what a hypermedia system is we’ll first take an |
|
in-depth look at <em class="test">the</em> canonical hypermedia system: the World |
|
Wide Web. Roy Fielding, an engineer who helped create specifications and |
|
build the implementations of many early pieces of the web, gave us the |
|
term REpresentational State Transfer, or REST. In his PhD dissertation |
|
he described REST as a <em class="test">network architecture</em>, and he contrasted |
|
it with earlier approaches to building distributed software.</p> |
|
<p>We define a <em class="test">hypermedia system</em> as a system that adheres to |
|
the RESTful network architecture in Fielding’s <em class="test">original</em> sense |
|
of this term.</p> |
|
<p>Unfortunately, today, you probably associate the term “REST” with |
|
JSON APIs, since that is where the term is typically used in industry. |
|
This is a misapplied use of the term REST because JSON is not a |
|
<em class="test">natural</em> 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 think REST implies JSON, please try to set that |
|
understanding aside while reading this book, and come to the concept |
|
with fresh eyes.</p> |
|
<p>It is important to understand that, in his dissertation, Fielding was |
|
describing The World Wide Web as it existed in the late 1990s. The web, |
|
at that point, was simply web browsers exchanging hypermedia. That |
|
system, with its simple links and forms, was what Fielding was calling |
|
RESTful.</p> |
|
<p>JSON APIs were a decade away from becoming a common tool in web |
|
development: REST was about <em class="test">hypermedia</em> and the 1.0 version of |
|
the web.</p> |
|
<h2 id="hypermedia-driven-applications">Hypermedia-Driven Applications</h2> |
|
<p>In this book we are going to take a look at hypermedia as a |
|
<em class="test">system architecture</em> and then explore some practical, |
|
<em class="test">modern</em> approaches to building web applications using it. We |
|
will call applications built in this style <em class="test">Hypermedia-Driven |
|
Applications</em>, or HDAs, and we contrast them with a popular style in |
|
use today, the Single Page Application.</p> |
|
<p>A Hypermedia-Driven Application is an application built on top of a |
|
hypermedia system that respects and utilizes the hypermedia |
|
functionality of that underlying system.</p> |
|
<h2 id="goals">Goals</h2> |
|
<p>The goal of this book is to give you a strong sense of how the |
|
RESTful, hypermedia system architecture <em class="test">differs</em> from other |
|
client-server systems, and what the strengths (and weaknesses) of the |
|
hypermedia approach are. Further, we hope to convince you that the |
|
hypermedia architecture is <em class="test">relevant</em> to developers building |
|
modern web applications.</p> |
|
<p>We aim to give you the tools to evaluate the requirements for an |
|
application and answer the question:</p> |
|
<p>“Could I build this as a Hypermedia-Driven Application?”</p> |
|
<p>We hope that for many applications the answer to that question will |
|
be “Yes!”</p> |
|
<h2 id="book-layout">Book Layout</h2> |
|
<p>The book is broken into three parts:</p> |
|
<ul> |
|
<li><p>An introduction (or re-introduction) to hypermedia, with a |
|
particular focus on HTML and HTTP. We will finish this review of core |
|
hypermedia concepts by creating a simple “Web 1.0“-style application, |
|
Contact.app, for managing contacts.</p></li> |
|
<li><p>Next we will look at how we can use <a href="https://htmx.org">htmx</a>, a hypermedia-oriented JavaScript |
|
library created by the authors of this book, to improve Contact.app. By |
|
using htmx, we will be able to achieve a level of interactivity in our |
|
application that many developers would expect to require a large, |
|
sophisticated front end library, such as React. Thanks to htmx, we will |
|
be able to do this using hypermedia as our system architecture.</p></li> |
|
<li><p>Finally, we will look at a completely different hypermedia |
|
system, Hyperview. Hyperview is a <em class="test">mobile</em> hypermedia system, |
|
related to, but distinct from the web and created by one of the authors |
|
of this book – Adam Stepinski. It supports <em class="test">mobile specific</em> |
|
features by providing not only a mobile specific hypermedia, but also a |
|
mobile hypermedia client. These novel components, combined with any HTTP |
|
server, make it possible to build mobile Hypermedia-Driven |
|
Applications.</p></li> |
|
</ul> |
|
<p>Note that each section is <em class="test">somewhat</em> independent of the |
|
others. If you already know hypermedia in-depth and how basic Web 1.0 |
|
applications function, you may want to skip ahead to the second section |
|
on htmx and how to build modern web applications using hypermedia. |
|
Similarly, if you are well versed in htmx and want to dive into a novel |
|
<em class="test">mobile</em> hypermedia, you can skip ahead to the Hyperview |
|
section.</p> |
|
<p>That being said, the book is designed to be read in order and both |
|
the htmx and Hyperview sections build on the Web 1.0 application |
|
described at the end of the first section. Furthermore, even if you |
|
<em class="test">are</em> well versed in all the concepts of hypermedia and details |
|
of HTML & HTTP, it is likely worth it to at least skim through the |
|
first few chapters for a refresher.</p> |
|
<h2 id="hypermedia--a-new-generation">Hypermedia: A New Generation</h2> |
|
<p>Hypermedia isn’t a frequent topic of discussion these days. Even many |
|
older programmers who grew up with the web in the late 1990s and early |
|
2000s haven’t thought much about these ideas in years. Many younger web |
|
developers have grown up knowing nothing but Single Page Applications |
|
and the frameworks that are used to build them.</p> |
|
<p>In particular, many young web developers began their careers by |
|
building React.js applications that interact with a Node server using a |
|
JSON API; they may never have learned about hypermedia as a system at |
|
all.</p> |
|
<p>This is a tragedy, and, frankly, a failure on the part of the thought |
|
leaders in the web development community to properly communicate and |
|
advocate for the hypermedia approach.</p> |
|
<p>Hypermedia was a great idea! It still is!</p> |
|
<p>By the end of this book, you will have the tools and the |
|
<em class="test">language</em> to put this great idea to work in your own |
|
applications. And, further, you will be able to bring the ideas and |
|
concepts of hypermedia systems to the broader web development |
|
community.</p> |
|
<p>Hypermedia can compete, hypermedia <em class="test">can win</em>, hypermedia |
|
<em class="test">has won</em> as an architectural choice against the Single Page |
|
Application approach, but <em class="test">only</em> if smart people (like you) learn |
|
about it, build with it and then tell the world about it.</p> |
|
<blockquote> |
|
<p>Remember the message? “The future is not set. There is no fate but |
|
what we make for ourselves.”</p> |
|
</blockquote><p class="quote-attribution"> Kyle Reese, Terminator 2: Judgement Day</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: Hypermedia In Practice</h2> |
|
<p>Clearly, HTML plays a central role in the story we tell here. At the |
|
end of each chapter we will share what we have learned about writing |
|
HTML for hypermedia-driven web applications.</p> |
|
<p>To start, remember that our web applications are not islands. We’re |
|
writing HTML not just for a particular application, but also to play |
|
along with other members of the web. When we write with the hypermedia |
|
<em class="test">system</em> in mind, we’re better able to tap the range of abilities |
|
available to the web.</p> |
|
<p>HTML is hypermedia-friendly when it is written for the full range of |
|
constituents of the hypermedia system. It conveys the state of an |
|
application to people viewing our sites with a browser, as well as to |
|
people listening to screen readers that read sites aloud. It conveys the |
|
aims of our sites to search engines that scrape sites programmatically. |
|
It also conveys its behavior as clearly as possible to other |
|
developers.</p> |
|
<p>No, we can’t fix every problem with good HTML. The mantra that HTML |
|
is “accessible by default” is misleading. We would miss out on important |
|
opportunities if we shunned other technologies like JavaScript. And we |
|
still need to test, a lot, everywhere, to ensure things work as |
|
expected.</p> |
|
<p>But good HTML lets browsers do a <em class="test">lot</em> of work for us.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Hypermedia: A Reintroduction</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_what_is_hypermedia">What Is Hypermedia?</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_a_brief_history_of_hypermedia">A Brief History of |
|
Hypermedia</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_modern_implementation">Modern Implementation</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_the_worlds_most_successful_hypertext_html">The World’s Most |
|
Successful Hypertext: HTML</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_the_essence_of_html_as_a_hypermedia">The Essence of HTML as a |
|
Hypermedia</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_anchor_tags">Anchor tags</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_form_tags">Form tags</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_web_1_0_applications">Web 1.0 applications</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_so_what_isnt_hypermedia">So What Isn’t Hypermedia?</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_single_page_applications">Single Page Applications</a> |
|
</li></ul> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_why_use_hypermedia">Why Use Hypermedia?</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_javascript_fatigue">JavaScript Fatigue</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_a_hypermedia_resurgence">A Hypermedia Resurgence?</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_hypermedia_oriented_javascript_libraries">Hypermedia-Oriented |
|
JavaScript Libraries</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_hypermedia_driven_applications">Hypermedia-Driven |
|
Applications</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_when_should_you_use_hypermedia">When Should You Use |
|
Hypermedia?</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_when_shouldnt_you_use_hypermedia">When Shouldn’t You Use |
|
Hypermedia?</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#_hypermedia_a_sophisticated_modern_system_architecture">Hypermedia: |
|
A Sophisticated, Modern System Architecture</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hypermedia-a-reintroduction/#html-note-title">HTML Notes: <div> Soup</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>Hypermedia is a universal technology today, almost as common as |
|
electricity.</p> |
|
<p>Billions of people use hypermedia-based systems every day, mainly by |
|
interacting with the <em class="test">Hypertext Markup Language (HTML)</em> being |
|
exchanged via the <em class="test">Hypertext Transfer Protocol (HTTP)</em> by using a |
|
web browser connected to the World Wide Web.</p> |
|
<p>People use these systems to get their news, check in on friends, buy |
|
things online, play games, send emails and so forth: the variety and |
|
sheer number of online services being delivered by hypermedia is truly |
|
astonishing.</p> |
|
<p>And yet, despite this ubiquity, the topic of hypermedia itself is a |
|
strangely under-explored concept today, left mainly to specialists. Yes, |
|
you can find a lot of tutorials on how to author HTML, create links and |
|
forms, etc. But it is rare to see a discussion of HTML <em class="test">as a |
|
hypermedia</em> and, more broadly, on how an entire hypermedia |
|
<em class="test">system</em> fits together.</p> |
|
<p>This is in contrast with the early web development era when concepts |
|
like <em class="test">Representational State Transfer (REST)</em> and <em class="test">Hypermedia |
|
As The Engine of Application State (HATEOAS)</em> were discussed |
|
frequently, refined and debated among web developers.</p> |
|
<p>In a sad turn of events, today, the world’s most popular hypermedia, |
|
HTML, is often viewed resentfully: it is an awkward, legacy markup |
|
language that must be grudgingly used to build user interfaces in what |
|
are increasingly entirely JavaScript-based web applications.</p> |
|
<p>HTML happens to be there, in the browser, and so we have to use |
|
it.</p> |
|
<p>This is a shame and we hope to convince you that hypermedia is |
|
<em class="test">not</em> simply a piece of legacy technology that we have to accept |
|
and deal with. Instead, we aim to show you that hypermedia is a |
|
tremendously innovative, simple and <em class="test">flexible</em> way to build |
|
robust applications: <em class="test">Hypermedia-Driven Applications</em>.</p> |
|
<p>We hope that by the end of this book you will feel, as we do, that |
|
the hypermedia approach deserves a seat at the table when you, a web |
|
developer, are considering the architecture of your next application. |
|
Creating a Hypermedia-Driven Application on top of a <em class="test">hypermedia |
|
system</em> like the web is a viable and, indeed, often excellent choice |
|
for <em class="test">modern</em> web applications.</p> |
|
<p>(And, as the section on Hyperview will show, not just web |
|
applications.)</p> |
|
<h2 id="_what_is_hypermedia">What Is Hypermedia?</h2> |
|
<blockquote> |
|
<p>Hypertexts: new forms of writing, appearing on computer screens, that |
|
will branch or perform at the reader’s command. A hypertext is a |
|
non-sequential piece of writing; only the computer display makes it |
|
practical.</p> |
|
</blockquote><p class="quote-attribution"> Ted Nelson, |
|
https://archive.org/details/SelectedPapers1977/page/n7/mode/2up</p> |
|
<p>Let us begin at the beginning: what is hypermedia?</p> |
|
<p>Hypermedia is a media, for example a text, that includes |
|
<em class="test">non-linear branching</em> from one location in the media to another, |
|
via, for example, hyperlinks embedded in the media. The prefix “hyper-” |
|
derives from the Greek prefix “ὑπερ-” which means “beyond” or “over”, |
|
indicating that hypermedia <em class="test">goes beyond</em> normal, passively |
|
consumed media like magazines and newspapers.</p> |
|
<p>Hyperlinks are a canonical example of what is called a <em class="test">hypermedia |
|
control</em>:</p> |
|
<dl> |
|
<dt>Hypermedia Control</dt> |
|
<dd> |
|
<p>A hypermedia control is an element in a hypermedia that describes (or |
|
controls) some sort of interaction, often with a remote server, by |
|
encoding information about that interaction directly and completely |
|
within itself.</p> |
|
</dd> |
|
</dl> |
|
<p>Hypermedia controls are what differentiate hypermedia from other |
|
sorts of media.</p> |
|
<p>You may be more familiar with the term <em class="test">hypertext</em>, from whose |
|
Wikipedia page the above quote is taken. Hypertext is a sub-category of |
|
hypermedia and much of this book is going to discuss how to build modern |
|
applications using hypertexts such as HTML, the Hypertext Markup |
|
Language, or HXML, a hypertext used by the Hyperview mobile hypermedia |
|
system.</p> |
|
<p>Hypertexts like HTML function alongside other technologies crucial |
|
for making an entire hypermedia system work: network protocols like |
|
HTTP, other media types such as images and videos, hypermedia servers |
|
(i.e., servers providing hypermedia APIs), sophisticated hypermedia |
|
clients (e.g., web browsers), and so on.</p> |
|
<p>Because of this, we prefer the broader term <em class="test">hypermedia |
|
systems</em> when describing the underlying architecture of applications |
|
built using hypertext, to emphasize the system architecture over the |
|
particular hypermedia being used.</p> |
|
<p>It is the entire hypermedia <em class="test">system architecture</em> that is |
|
underappreciated and ignored by many modern web developers.</p> |
|
<h2 id="_a_brief_history_of_hypermedia">A Brief History of |
|
Hypermedia</h2> |
|
<p>Where did the idea of hypermedia come from?</p> |
|
<p>While there were many precursors to the modern idea of hypertext and |
|
the more general hypermedia, many people point to the 1945 article |
|
<em class="test">As We May Think</em> written by Vannevar Bush in <em class="test">The |
|
Atlantic</em> as a starting point for looking at what has become modern |
|
hypermedia.</p> |
|
<p>In this article Bush described a device called a Memex, which, using |
|
a complex mechanical system of reels and microfilm, along with an |
|
encoding system, would allow users to jump between related frames of |
|
content. The Memex was never actually implemented, but it was an |
|
inspiration for later work on the idea of hypermedia.</p> |
|
<p>The terms “hypertext” and “hypermedia” were coined in 1963 by Ted |
|
Nelson, who would go on to work on the <em class="test">Hypertext Editing System</em> |
|
at Brown University and who later created the <em class="test">File Retrieval and |
|
Editing System (FRESS)</em>, a shockingly advanced hypermedia system for |
|
its time. (This was perhaps the first digital system to have a notion of |
|
“undo”.)</p> |
|
<p>While Nelson was working on his ideas, Douglas Engelbart was busy at |
|
work at the Stanford Research Institute, explicitly attempting to make |
|
Vannevar Bush’s Memex a reality. In 1968, Englebart gave “The Mother of |
|
All Demos” in San Francisco, California.</p> |
|
<p>Englebart demonstrated an unbelievable amount of technology:</p> |
|
<ul> |
|
<li><p>Remote, collaborative text editing with his peers in Menlo |
|
Park</p></li> |
|
<li><p>Video and audio chat</p></li> |
|
<li><p>An integrated windowing system, with window resizing, |
|
etc</p></li> |
|
<li><p>A recognizable hypertext, whereby clicking on underlined text |
|
navigated to new content.</p></li> |
|
</ul> |
|
<p>Despite receiving a standing ovation from a shocked audience after |
|
his talk, it was decades before the technologies Englebart demonstrated |
|
became mainstream.</p> |
|
<h3 id="_modern_implementation">Modern Implementation</h3> |
|
<p>In 1990, Tim Berners-Lee, working at CERN, published the first |
|
website. He had been working on the idea of hypertext for a decade and |
|
had finally, out of desperation at the fact it was so hard for |
|
researchers to share their research, found the right moment and |
|
institutional support to create the World Wide Web:</p> |
|
<blockquote> |
|
<p>Creating the web was really an act of desperation, because the |
|
situation without it was very difficult when I was working at CERN |
|
later. Most of the technology involved in the web, like the hypertext, |
|
like the Internet, multifont text objects, had all been designed |
|
already. I just had to put them together. It was a step of generalising, |
|
going to a higher level of abstraction, thinking about all the |
|
documentation systems out there as being possibly part of a larger |
|
imaginary documentation system.</p> |
|
</blockquote><p class="quote-attribution"> Tim Berners-Lee, |
|
https://britishheritage.org/tim-berners-lee-the-world-wide-web</p> |
|
<p>By 1994 his creation was taking off so quickly that Berners-Lee |
|
founded the W3C, a working group of companies and researchers tasked |
|
with improving the web. All standards created by the W3C were |
|
royalty-free and could be adopted and implemented by anyone, cementing |
|
the open, collaborative nature of the web.</p> |
|
<p>In 2000, Roy Fielding, then at U.C. Irvine, published a seminal PhD |
|
dissertation on the web: “Architectural Styles and the Design of |
|
Network-based Software Architectures.” Fielding had been working on the |
|
open source Apache HTTP Server and his thesis was a description of what |
|
he felt was a <em class="test">new and distinct networking architecture</em> that had |
|
emerged in the early web. Fielding had worked on the initial HTTP |
|
specifications and, in the paper, defined the web’s hypermedia network |
|
model using the term <em class="test">REpresentational State Transfer |
|
(REST)</em>.</p> |
|
<p>Fielding’s work became a major touchstone for early web developers, |
|
giving them a language to discuss the new technical medium they were |
|
building applications in.</p> |
|
<p>We will discuss Fielding’s key ideas in depth in Chapter 2, and try |
|
to correct the record with respect to REST, HATEOAS and hypermedia.</p> |
|
<h2 id="_the_worlds_most_successful_hypertext_html">The World’s Most |
|
Successful Hypertext: HTML</h2> |
|
<blockquote> |
|
<p>In the beginning was the hyperlink, and the hyperlink was with the |
|
web, and the hyperlink was the web. And it was good.</p> |
|
</blockquote><p class="quote-attribution"> Rescuing REST From the API Winter, |
|
https://intercoolerjs.org/2016/01/18/rescuing-rest.html</p> |
|
<p>The system that Berners-Lee, Fielding and many others had created |
|
revolved around a hypermedia: HTML. HTML started as a read-only |
|
hypermedia, used to publish (at first) academic documents. These |
|
documents were linked together via anchor tags which created |
|
<em class="test">hyperlinks</em> between them, allowing users to quickly navigate |
|
between documents.</p> |
|
<p>When HTML 2.0 was released, it introduced the notion of the |
|
<code>form</code> tag, joining the anchor tag (i.e., hyperlink) as a |
|
second hypermedia control. The introduction of the form tag made |
|
building <em class="test">applications</em> on the web viable by providing a |
|
mechanism for <em class="test">updating</em> resources, rather than just reading |
|
them.</p> |
|
<p>It was at this point that the web transitioned from an interesting |
|
document-oriented system to a compelling <em class="test">application |
|
architecture</em>.</p> |
|
<p>Today HTML is the most widely used hypermedia in existence and this |
|
book naturally assumes that the reader has a reasonable familiarity with |
|
it. You do not need to be an HTML (or CSS) expert to understand the code |
|
in this book, but the better you understand the core tags and concepts |
|
of HTML, the more you will get out of it.</p> |
|
<h3 id="_the_essence_of_html_as_a_hypermedia">The Essence of HTML as a |
|
Hypermedia</h3> |
|
<p>Let us consider these two defining hypermedia elements (that is the |
|
two defining <em class="test">hypermedia controls</em>) of HTML, the anchor tag and |
|
the form tag, in a bit of detail.</p> |
|
<h4 id="_anchor_tags">Anchor tags</h4> |
|
<p>Anchor tags are so familiar as to be boring but, as the original |
|
hypermedia control, it is worth reviewing the mechanics of hyperlinks to |
|
get our minds in the right place for developing a deeper understanding |
|
of hypermedia.</p> |
|
<p>Consider a simple anchor tag, embedded within a larger HTML |
|
document:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb1"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb1-1"><a aria-hidden="true" href="#cb1-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"https://hypermedia.systems/"</span><span class="dt">></span></span> |
|
<span id="cb1-2"><a aria-hidden="true" href="#cb1-2" tabindex="-1"></a> Hypermedia Systems</span> |
|
<span id="cb1-3"><a aria-hidden="true" href="#cb1-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A simple hyperlink</p></figcaption> |
|
</figure> |
|
<p>An anchor tag consists of the tag itself, |
|
<code><a></a></code>, as well as the attributes and content |
|
within the tag. Of particular interest is the <code>href</code> |
|
attribute, which specifies a <em class="test">hypertext reference</em> to another |
|
document or document fragment. It is this attribute that makes the |
|
anchor tag a hypermedia control.</p> |
|
<p>In a typical web browser, this anchor tag would be interpreted to |
|
mean:</p> |
|
<ul> |
|
<li><p>Show the text “Hypermedia Systems” in a manner indicating that it |
|
is clickable</p></li> |
|
<li><p>When the user clicks on that text, issue an HTTP <code>GET</code> |
|
request to the URL <code>https://hypermedia.systems/</code></p></li> |
|
<li><p>Take the HTML content in the body of the HTTP response to this |
|
request and replace the entire screen in the browser as a new document, |
|
updating the navigation bar to this new URL.</p></li> |
|
</ul> |
|
<p>Anchors provide the main mechanism we use to navigate around the web |
|
today, by selecting links to navigate from document to document, or from |
|
resource to resource. <a class="ref" href="#fig-get-in-action">[fig-get-in-action]</a> shows what a user interaction with |
|
an anchor tag/hyperlink looks like in visual form.</p> |
|
<figure id="fig-get-in-action"> |
|
<div> |
|
<div data-align="start"> |
|
<pre><code>┌────────────────────────┐ ┌─HTTP REQUEST────────────────┐ |
|
│ BROWSER X │ │ │ |
|
├────────────────────────┤ │ GET / │ |
|
│ │ │ Host: hypermedia.systems │ |
|
│ lorem ipsum dolor │ └─────────────────────────────┘ |
|
│ │ |
|
│ Hypermedia Systems ────┼───────────┐ |
|
│ ────────────────── │ │ |
|
│ sit amet │ │ |
|
│ │ │ |
|
└────────────────────────┘ │ |
|
┌──────▼──────┐ |
|
│ H T T P │ |
|
│ S E R V E R │ |
|
└──────┬──────┘ |
|
┌────────────────────────┐ │ |
|
│ BROWSER X │ │ |
|
├────────────────────────┤ │ |
|
│ │ │ |
|
│ HYPERMEDIA SYSTEMS ◀───────────┘ |
|
│ │ |
|
│ The revolutionary │ ┌─HTTP RESPONSE───────────────┐ |
|
│ │ │ │ |
|
│ ideas that empowered...│ │ 200 OK │ |
|
│ │ │ ... │ |
|
└────────────────────────┘ │ <h1>Hypermedia Systems</h1> │ |
|
│ ... │ |
|
└─────────────────────────────┘ |
|
</code></pre> |
|
</div> |
|
</div> |
|
<figcaption><p>An HTTP GET In Action</p></figcaption> |
|
</figure> |
|
<p>When the link is clicked the browser (or, as we sometimes refer to |
|
it, the <em class="test">hypermedia client</em>) initiates an HTTP <code>GET</code> |
|
request to the URL encoded in the link’s <code>href</code> |
|
attribute.</p> |
|
<p>Note that the HTTP request includes additional data (i.e., |
|
<em class="test">metadata</em>) on what, exactly, the browser wants from the server, |
|
in the form of headers. We will discuss these headers, and HTTP in more |
|
depth in Chapter 2.</p> |
|
<p>The <em class="test">hypermedia server</em> then responds to this request with a |
|
<em class="test">hypermedia response</em> — the HTML — for the new page. This may |
|
seem like a small and obvious point, but it is an absolutely crucial |
|
aspect of a truly RESTful <em class="test">hypermedia system</em>: the client and |
|
server must communicate via hypermedia!</p> |
|
<h4 id="_form_tags">Form tags</h4> |
|
<p>Anchor tags provide <em class="test">navigation</em> between documents or |
|
resources, but don’t allow you to update those resources. That |
|
functionality falls to the form tag.</p> |
|
<p>Here is a simple example of a form in HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/signup"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"post"</span><span class="dt">></span></span> |
|
<span id="cb3-2"><a aria-hidden="true" href="#cb3-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Enter Email To Sign Up"</span><span class="dt">></span></span> |
|
<span id="cb3-3"><a aria-hidden="true" href="#cb3-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="dt">></span>Sign Up<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb3-4"><a aria-hidden="true" href="#cb3-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A simple form</p></figcaption> |
|
</figure> |
|
<p>Like an anchor tag, a form tag consists of the tag itself, |
|
<code><form></form></code>, combined with the attributes and |
|
content within the tag. Note that the form tag does not have an |
|
<code>href</code> attribute, but rather has an <code>action</code> |
|
attribute that specifies where to issue an HTTP request.</p> |
|
<p>Furthermore, it also has a <code>method</code> attribute, which |
|
specifies exactly which HTTP “method” to use. In this example the form |
|
is asking the browser to issue a <code>POST</code> request.</p> |
|
<p>In contrast with anchor tags, the content and tags <em class="test">within</em> a |
|
form can have an effect on the hypermedia interaction that the form |
|
makes with a server. The <em class="test">values</em> of <code>input</code> tags and |
|
other tags such as <code>select</code> tags will be included with the |
|
HTTP request when the form is submitted, as URL parameters in the case |
|
of a <code>GET</code> and as part of the request body in the case of a |
|
<code>POST</code>. This allows a form to include an arbitrary amount of |
|
information collected from a user in a request, unlike the anchor |
|
tag.</p> |
|
<p>In a typical browser this form tag and its contents would be |
|
interpreted by the browser roughly as follows:</p> |
|
<ul> |
|
<li><p>Show a text input and a “Sign Up” button to the user</p></li> |
|
<li><p>When the user submits the form by clicking the “Sign Up” button |
|
or by hitting the enter key while the input element is focused, issue an |
|
HTTP <code>POST</code> request to the path <code>/signup</code> on the |
|
“current” server</p></li> |
|
<li><p>Take the HTML content in the body of the HTTP response body and |
|
replace the entire screen in the browser as a new document, updating the |
|
navigation bar to this new URL.</p></li> |
|
</ul> |
|
<p>This mechanism allows the user to issue requests to <em class="test">update the |
|
state</em> of resources on the server. Note that despite this new type |
|
of request the communication between client and server is still done |
|
entirely with <em class="test">hypermedia</em>.</p> |
|
<p>It is the form tag that makes Hypermedia-Driven Applications |
|
possible.</p> |
|
<p>If you are an experienced web developer you probably recognize that |
|
we are omitting a few details and complications here. For example, the |
|
response to a form submission often <em class="test">redirects</em> the client to a |
|
different URL.</p> |
|
<p>This is true, and we will get down into the muck with forms in more |
|
detail in later chapters but, for now, this simple example suffices to |
|
demonstrate the core mechanism for updating system state purely within |
|
hypermedia. <a class="ref" href="#fig-post-in-action">[fig-post-in-action]</a> is a diagram of the |
|
interaction.</p> |
|
<figure id="fig-post-in-action"> |
|
<div> |
|
<div data-align="start"> |
|
<pre><code>┌────────────────────────┐ ┌─HTTP REQUEST────────────────┐ |
|
│ BROWSER X │ │ │ |
|
├────────────────────────┤ │ POST / │ |
|
│ │ │ Host: hypermedia.systems │ |
|
│ SIGN UP │ │ ... │ |
|
│ ┌────────────────────┐ │ │ email=joe@example.com │ |
|
│ │ joe@example.com │ │ └─────────────────────────────┘ |
|
│ └────────────────────┘ │ |
|
│ ┌─────────┐ ────┼───────────┐ |
|
│ │ Sign up │ │ │ |
|
│ └─────────┘ │ │ |
|
└────────────────────────┘ │ |
|
┌──────▼──────┐ |
|
│ H T T P │ |
|
│ S E R V E R │ |
|
└──────┬──────┘ |
|
┌────────────────────────┐ │ |
|
│ BROWSER X │ │ |
|
├────────────────────────┤ │ |
|
│ │ │ |
|
│ THANK YOU FOR SIGNING ◀───────────┘ |
|
│ UP │ |
|
│ │ ┌─HTTP RESPONSE───────────────┐ |
|
│ │ │ │ |
|
│ │ │ 201 Created │ |
|
│ │ │ ... │ |
|
└────────────────────────┘ │ <h1>Thank you for signing │ |
|
│ up</h1> │ |
|
└─────────────────────────────┘ |
|
</code></pre> |
|
</div> |
|
</div> |
|
<figcaption><p>An HTTP POST In Action</p></figcaption> |
|
</figure> |
|
<h4 id="_web_1_0_applications">Web 1.0 applications</h4> |
|
<p>As someone interested in web development, the above diagrams and |
|
discussion are probably very familiar to you. You may even find this |
|
content boring. But take a step back and consider the fact that these |
|
two hypermedia controls, anchors and forms, are the <em class="test">only</em> native |
|
ways for a user to interact with a server in plain HTML.</p> |
|
<p>Only two tags!</p> |
|
<p>And yet, armed with only these two tags, the early web was able to |
|
grow exponentially and offer a staggeringly large amount of online, |
|
dynamic functionality to billions of people.</p> |
|
<p>This is strong evidence of the power of hypermedia. Even today, in a |
|
web development world increasingly dominated by large JavaScript-centric |
|
front end frameworks, many people choose to use simple vanilla HTML to |
|
achieve their application goals and are often perfectly happy with the |
|
results.</p> |
|
<p>These two tags give a tremendous amount of expressive power to |
|
HTML.</p> |
|
<h3 id="_so_what_isnt_hypermedia">So What Isn’t Hypermedia?</h3> |
|
<p>So links and forms are the two main hypermedia-based mechanisms for |
|
interacting with a server available in HTML.</p> |
|
<p>Now let’s consider a different approach: let’s interact with a server |
|
by issuing an HTTP request via JavaScript. To do this, we will use the |
|
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API"><code>fetch()</code></a> |
|
API, a popular API for issuing an “Asynchronous JavaScript and XML,” or |
|
AJAX request, available in all modern web browsers:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> onclick</span><span class="op">=</span><span class="st">"fetch('/api/v1/contacts/1') </span><span class="er"><</span><span class="st">1></span></span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a><span class="st"> .then(response => response.json()) </span><span class="er"><</span><span class="st">2></span></span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a><span class="st"> .then(data => updateUI(data)) "</span><span class="dt">></span> <span class="er"><</span>3></span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a> Fetch Contact</span> |
|
<span id="cb5-5"><a aria-hidden="true" href="#cb5-5" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>JavaScript</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Issue the request.</p></li> |
|
<li><p>Convert the response to a JavaScript object.</p></li> |
|
<li><p>Invoke the <code>updateUI()</code> function with the |
|
object.</p></li> |
|
</ol> |
|
<p>This button has an <code>onclick</code> attribute that specifies some |
|
JavaScript to run when the button is clicked.</p> |
|
<p>The JavaScript will issue an AJAX HTTP <code>GET</code> request to |
|
<code>/api/v1/contacts/1</code> using <code>fetch()</code>. An AJAX |
|
request is like a “normal” HTTP request, but it is issued “behind the |
|
scenes” by the browser. The user does not see a request indicator from |
|
the browser as they would with normal links and forms. Additionally, |
|
unlike requests issued by those hypermedia controls, it is up to the |
|
JavaScript code to handle the response from the server.</p> |
|
<p>Despite AJAX having XML as part of its acronym, today the HTTP |
|
response to this request would almost certainly be in the JavaScript |
|
Object Notation (JSON) format rather than XML.</p> |
|
<p>An HTTP response to this request might look something like this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb6"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb6-1"><a aria-hidden="true" href="#cb6-1" tabindex="-1"></a><span class="fu">{</span> <span class="er"><1></span></span> |
|
<span id="cb6-2"><a aria-hidden="true" href="#cb6-2" tabindex="-1"></a> <span class="dt">"id"</span><span class="fu">:</span> <span class="dv">42</span><span class="fu">,</span> <span class="er"><2></span></span> |
|
<span id="cb6-3"><a aria-hidden="true" href="#cb6-3" tabindex="-1"></a> <span class="dt">"email"</span> <span class="fu">:</span> <span class="st">"json-example@example.org"</span> <span class="er"><</span><span class="dv">3</span><span class="er">></span></span> |
|
<span id="cb6-4"><a aria-hidden="true" href="#cb6-4" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div> |
|
<figcaption><p>JSON</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The start of a JSON object.</p></li> |
|
<li><p>A property, in this case with the name <code>id</code> and the |
|
value <code>42</code>.</p></li> |
|
<li><p>Another property, the email of the contact with this id.</p></li> |
|
</ol> |
|
<p>The JavaScript code above converts the JSON text received from the |
|
server into a JavaScript object by calling the <code>json()</code> |
|
method on it. This new JavaScript object is then handed off to the |
|
<code>updateUI()</code> method.</p> |
|
<p>The <code>updateUI()</code> method is responsible for updating the UI |
|
based on the data encoded in the JavaScript Object, perhaps by |
|
displaying the contact in a bit of HTML generated via a client-side |
|
template in the JavaScript application.</p> |
|
<p>The details of exactly what the <code>updateUI()</code> function does |
|
aren’t important for our discussion.</p> |
|
<p>What <em class="test">is</em> important, what is the <em class="test">crucial</em> aspect of |
|
this JSON-based server interaction is that it is <em class="test">not</em> using |
|
hypermedia. The JSON API being used here does not return a hypermedia |
|
response. There are no <em class="test">hyperlinks</em> or other hypermedia-style |
|
controls in it.</p> |
|
<p>This JSON API is, rather, a <em class="test">Data API</em>.</p> |
|
<p>Because the response is in JSON and is <em class="test">not</em> hypermedia, the |
|
JavaScript <code>updateUI()</code> method must understand how to turn |
|
this contact data into HTML.</p> |
|
<p>In particular, the code in <code>updateUI()</code> needs to know |
|
about the <em class="test">internal structure</em> and meaning of the data.</p> |
|
<p>It needs to know:</p> |
|
<ul> |
|
<li><p>Exactly how the fields in the JSON data object are structured and |
|
named.</p></li> |
|
<li><p>How they relate to one another.</p></li> |
|
<li><p>How to update the local data this new data corresponds |
|
with.</p></li> |
|
<li><p>How to render this data to the browser.</p></li> |
|
<li><p>What additional actions/API end points can be called with this |
|
data.</p></li> |
|
</ul> |
|
<p>In short, the logic in <code>updateUI()</code> needs to have intimate |
|
knowledge of the API endpoint at <code>/api/v1/contact/1</code>, |
|
knowledge provided via some side-channel beyond the response itself. As |
|
a result, the <code>updateUI()</code> code and the API have a strong |
|
relationship, known as <em class="test">tight coupling</em>: if the format of the |
|
JSON response changes, then the code for <code>updateUI()</code> will |
|
almost certainly also need to be changed as well.</p> |
|
<h4 id="_single_page_applications">Single Page Applications</h4> |
|
<p>This bit of JavaScript, while very modest, is the organic beginnings |
|
of a much larger conceptual approach to building web applications. This |
|
is the beginning of a <em class="test">Single Page Application (SPA)</em>. The web |
|
application is no longer navigating <em class="test">between</em> pages using |
|
hypermedia controls as was the case with links and forms.</p> |
|
<p>Instead, the application is exchanging <em class="test">plain data</em> with the |
|
server and then updating the content <em class="test">within</em> a single page.</p> |
|
<p>When this strategy or architecture is adopted for an entire |
|
application, everything happens on a “Single Page” and, thus the |
|
application becomes a “Single Page Application.”</p> |
|
<p>The Single Page Application architecture is extremely popular today |
|
and has been the dominant approach to building web applications for the |
|
last decade. This can be observed by the high level of mind-share and |
|
discussion it has received in the industry.</p> |
|
<p>Today the vast majority of Single Page Applications adopt far more |
|
sophisticated frameworks for managing their user interface than this |
|
simple example shows. Popular libraries such as React, Angular, Vue.js, |
|
etc. are now the common — indeed, the standard — way to build web |
|
applications.</p> |
|
<p>With these more complex frameworks developers typically work with an |
|
elaborate client-side model — that is, with JavaScript objects stored |
|
locally in the browser’s memory that represent the “model” or “domain” |
|
of your application. These JavaScript objects are updated via JavaScript |
|
code and the framework then “reacts” to these changes, updating the user |
|
interface.</p> |
|
<p>When the user interface is updated by a user these changes also flow |
|
<em class="test">into</em> the model objects, establishing a “two-way” binding |
|
mechanism: the model can update the UI, and the UI can update the |
|
model.</p> |
|
<p>This is a much more sophisticated approach to a web client than |
|
hypermedia, and it typically does away almost entirely with the |
|
underlying hypermedia infrastructure available in the browser.</p> |
|
<p>HTML is still used to build user interfaces, but the |
|
<em class="test">hypermedia</em> aspect of the two major hypermedia controls, anchors |
|
and forms, are unused. Neither tag interacts with a server via their |
|
native <em class="test">hypermedia</em> mechanism. Rather, they become user interface |
|
elements that drive local interactions with the in-memory domain model |
|
via JavaScript, which is then synchronized with the server using plain |
|
data JSON APIs.</p> |
|
<p>So, as with our simple button above, the Single Page Application |
|
approach foregoes the hypermedia architecture. It leaves aside the |
|
advantages of the existing RESTful architecture of the web and the |
|
built-in functionality found in HTML’s native hypermedia controls in |
|
favor of JavaScript driven behaviors.</p> |
|
<p>SPAs are much more like <em class="test">thick client applications</em>, that is, |
|
like the client-server applications of the 1980s — an architecture |
|
popular <em class="test">before</em> the web came along and that the web was, in many |
|
ways, a reaction to.</p> |
|
<p>This approach isn’t necessarily wrong, of course: there are times |
|
when a thick client approach is the appropriate choice for an |
|
application. But it is worth thinking about <em class="test">why</em> web developers |
|
so frequently make this choice without considering other alternatives, |
|
and if there are reasons <em class="test">not</em> to go down this path.</p> |
|
<h2 id="_why_use_hypermedia">Why Use Hypermedia?</h2> |
|
<blockquote> |
|
<p>The emerging norm for web development is to build a React single-page |
|
application, with server rendering. The two key elements of this |
|
architecture are something like:</p> |
|
<ol> |
|
<li><p>The main UI is built & updated in JavaScript using React or |
|
something similar.</p></li> |
|
<li><p>The backend is an API that that application makes requests |
|
against.</p></li> |
|
</ol> |
|
<p>This idea has really swept the internet. It started with a few major |
|
popular websites and has crept into corners like marketing sites and |
|
blogs.</p> |
|
</blockquote><p class="quote-attribution"> Tom MacWright, <a href="https://macwright.com/2020/05/10/spa-fatigue.html">https://macwright.com/2020/05/10/spa-fatigue.html</a></p> |
|
<p>The JavaScript-based Single Page Application approach has taken the |
|
web development world by storm, and if there was one single reason for |
|
its wild success it was this: The Single Page Application offers a far |
|
more interactive and immersive experience than the old, gronky, Web 1.0 |
|
hypermedia-based applications could. SPAs had the ability to smoothly |
|
update elements inline on a page without a dramatic reload of the entire |
|
document, they had the ability to use CSS transitions to create nice |
|
visual effects, and the ability to hook into arbitrary events like mouse |
|
movements.</p> |
|
<p>All of these abilities give JavaScript-based applications a huge |
|
advantage in building sophisticated user experiences.</p> |
|
<p>Given the popularity, power and success of this modern approach to |
|
building web applications, why on earth would you consider an older, |
|
clunkier and less popular approach like hypermedia?</p> |
|
<h3 id="_javascript_fatigue">JavaScript Fatigue</h3> |
|
<p>We are glad you asked!</p> |
|
<p>It turns out that the hypermedia architecture, even in its original |
|
Web 1.0 form, has a number of advantages when compared with the Single |
|
Page Application + JSON Data API approach. Three of the biggest are:</p> |
|
<ul> |
|
<li><p>It is an extremely <em class="test">simple</em> approach to building web |
|
applications.</p></li> |
|
<li><p>It is extremely tolerant of content and API changes. In fact, it |
|
thrives on them!</p></li> |
|
<li><p>It leverages tried and true features of web browsers, such as |
|
caching.</p></li> |
|
</ul> |
|
<p>The first two advantages, in particular, address major pain points in |
|
modern web development:</p> |
|
<ul> |
|
<li><p>Single Page Application infrastructure has become extremely |
|
complex, often requiring an entire team to manage.</p></li> |
|
<li><p>JSON API churn — constant changes made to JSON APIs to support |
|
application needs — has become a major pain point for many application |
|
teams.</p></li> |
|
</ul> |
|
<p>The combination of these two problems, along with other issues such |
|
as JavaScript library churn, has led to a phenomenon known as |
|
“JavaScript Fatigue.” This refers to a general sense of exhaustion with |
|
all the hoops that are necessary to jump through to get anything done in |
|
modern-day web applications.</p> |
|
<p>We believe that a hypermedia architecture can help cure JavaScript |
|
Fatigue for many developers and teams.</p> |
|
<p>But if hypermedia is so great, and if it addresses so many of the |
|
problems that beset the web development industry, why was it set aside |
|
in the first place? After all, hypermedia was there first. Why didn’t |
|
web developers just stick with it?</p> |
|
<p>There are two major reasons hypermedia hasn’t made a comeback in web |
|
development.</p> |
|
<p>The first is this: the expressiveness of HTML <em class="test">as a |
|
hypermedia</em> hasn’t changed much, if at all, since HTML 2.0, which |
|
was released <em class="test">in the mid 1990s</em>. Many new <em class="test">features</em> have |
|
been added to HTML, of course, but there haven’t been <em class="test">any</em> major |
|
new ways to interact with a server in HTML in almost three decades.</p> |
|
<p>HTML developers still only have anchor tags and forms available as |
|
hypermedia controls, and those hypermedia controls can still only issue |
|
<code>GET</code> and <code>POST</code> requests.</p> |
|
<p>This baffling lack of progress by HTML leads immediately to the |
|
second, and perhaps more practical reason that HTML-as-hypermedia has |
|
fallen on hard times: as the interactivity and expressiveness of HTML |
|
has remained frozen, the demands of web users have continued to |
|
increase, calling for more and more interactive web applications.</p> |
|
<p>JavaScript-based applications coupled to data-oriented JSON APIs have |
|
stepped in as a way to provide these more sophisticated user interfaces. |
|
It was the <em class="test">user experience</em> that you could achieve in |
|
JavaScript, and that you couldn’t achieve in plain HTML, that drove the |
|
web development community to the JavaScript-based Single Page |
|
Application approach. The shift was not driven by any inherent |
|
superiority of the Single Page Application as a system architecture.</p> |
|
<p>It didn’t have to be this way. There is nothing <em class="test">intrinsic</em> to |
|
the idea of hypermedia that prevents it from having a richer, more |
|
expressive interactivity model than vanilla HTML. Rather than moving |
|
away from a hypermedia-based approach, the industry could have demanded |
|
more interactivity from HTML.</p> |
|
<p>Instead, building thick-client style applications within web browsers |
|
became the standard, in an understandable move to a more familiar model |
|
for building rich applications.</p> |
|
<p>Not everyone set aside hypermedia, of course. There have been heroic |
|
efforts to continue to advance hypermedia outside of HTML, efforts like |
|
HyTime, VoiceXML, and HAL.</p> |
|
<p>But HTML, the most widely used hypermedia in the world, stopped |
|
making progress as a hypermedia. The web development world moved on, |
|
solving the interactivity problems with HTML by adopting |
|
JavaScript-based SPAs and, mostly inadvertently, a completely different |
|
system architecture.</p> |
|
<h2 id="_a_hypermedia_resurgence">A Hypermedia Resurgence?</h2> |
|
<p>It is interesting to think about how HTML <em class="test">could</em> have |
|
advanced. Instead of stalling as a hypermedia, how could HTML have |
|
continued to develop? Could it have kept adding new hypermedia controls |
|
and increasing the expressiveness of existing ones? Would it have been |
|
possible to build modern web applications within this original, |
|
hypermedia-oriented and RESTful model that made the early web so |
|
powerful, so flexible, so much fun?</p> |
|
<p>This might seem like idle speculation, but we have some good news on |
|
this score: in the last decade a few idiosyncratic, alternative front |
|
end libraries have arisen that attempt to get HTML moving again. |
|
Ironically, these libraries are written in JavaScript, the technology |
|
that supplanted HTML as the center of web development.</p> |
|
<p>However, these libraries use JavaScript not as a <em class="test">replacement</em> |
|
for the fundamental hypermedia system of the web.</p> |
|
<p>Instead, they use JavaScript to augment HTML itself <em class="test">as a |
|
hypermedia</em>.</p> |
|
<p>These <em class="test">hypermedia-oriented</em> libraries re-center hypermedia as |
|
the core technology in web applications.</p> |
|
<h3 id="_hypermedia_oriented_javascript_libraries">Hypermedia-Oriented |
|
JavaScript Libraries</h3> |
|
<p>In the web development world there is an ongoing debate between the |
|
Single Page Application (SPA) approach and what is now being called the |
|
“Multi-Page Application” (MPA) approach. MPA is a modern name for the |
|
old, Web 1.0 way of building web applications, using links and forms |
|
located on multiple web pages, submitting HTTP requests and getting HTML |
|
responses.</p> |
|
<p>MPA applications, by their nature, are Hypermedia-Driven |
|
Applications: after all, they are exactly what Roy Fielding was |
|
describing in his dissertation.</p> |
|
<p>These applications tend to be clunky, but they work reasonably well. |
|
Many web developers and teams choose to accept the limitations of plain |
|
HTML in the interest of simplicity and reliability.</p> |
|
<p>Rich Harris, creator of Svelte.js, a popular SPA library, and a |
|
thought-leader on the SPA side of the debate, has proposed a mix of this |
|
older MPA style and the newer SPA style. Harris calls this approach to |
|
building web applications “transitional,” in that it attempts to blend |
|
the MPA approach and the newer SPA approach into a coherent whole. (This |
|
is somewhat similar to the “transitional” trend in architecture, which |
|
combines traditional and modern architectural styles.)</p> |
|
<p>“Transitional” is a fitting term for mixed-style applications, and it |
|
offers a reasonable compromise between the two approaches, using either |
|
one as appropriate on a case-by-case basis.</p> |
|
<p>But this compromise still feels unsatisfactory.</p> |
|
<p>Must we default to having these two very different architectural |
|
models in our applications?</p> |
|
<p>Recall that the crux of the trade-off between SPAs and MPAs is the |
|
<em class="test">user experience</em>, or interactivity of the application. This |
|
typically drives the decision to choose one approach versus the other |
|
for an application or — in the case of a “transitional” application — |
|
for a particular feature.</p> |
|
<p>It turns out that by adopting a hypermedia-oriented library, the |
|
interactivity gap between the MPA and the SPA approach closes |
|
dramatically. You can use the MPA approach, that is, the hypermedia |
|
approach, for much more of your application without compromising your |
|
user interface. You might even be able to use the hypermedia approach |
|
for <em class="test">all</em> your application needs.</p> |
|
<p>Rather than having an SPA with a bit of hypermedia around the edges, |
|
or some mix of the two approaches, you can often create a web |
|
application that is <em class="test">primarily</em> or <em class="test">entirely</em> |
|
hypermedia-driven, and that still satisfies the interactivity that your |
|
users require.</p> |
|
<p>This can <em class="test">tremendously</em> simplify your web application and |
|
produce a much more coherent and understandable piece of software. While |
|
there are still times and places for the more complex SPA approach, |
|
which we will discuss later in the book, by adopting a hypermedia-first |
|
approach and using a hypermedia-oriented library to push HTML as far as |
|
possible, your web application can be powerful, interactive <em class="test">and</em> |
|
simple.</p> |
|
<p>One such hypermedia oriented library is <a href="https://htmx.org">htmx</a>. Htmx will be the focus of Part Two of |
|
this book. We show that you can, in fact, create many common “modern” UI |
|
features found in sophisticated Single Page Applications by instead |
|
using the hypermedia model.</p> |
|
<p>And, it is refreshingly fun and simple to do so.</p> |
|
<h3 id="_hypermedia_driven_applications">Hypermedia-Driven |
|
Applications</h3> |
|
<p>When building a web application with htmx the term Multi-Page |
|
Application applies <em class="test">roughly</em>, but it doesn’t fully characterize |
|
the core of the application architecture. As you will see, htmx doesn’t |
|
<em class="test">need</em> to replace entire pages, and, in fact, an htmx-based |
|
application can reside entirely within a single page. We don’t recommend |
|
this practice, but it is possible!</p> |
|
<p>So it isn’t quite right to call web applications built with htmx |
|
“Multi-Page Applications.” What the older Web 1.0 MPA approach and the |
|
newer hypermedia-oriented library powered applications have in common is |
|
their use of <em class="test">hypermedia</em> as their core technology and |
|
architecture.</p> |
|
<p>Therefore, we use the term <em class="test">Hypermedia-Driven Applications |
|
(HDAs)</em> to describe both.</p> |
|
<p>This clarifies that the core distinction between these two approaches |
|
and the SPA approach <em class="test">isn’t</em> the number of pages in the |
|
application, but rather the underlying <em class="test">system</em> architecture.</p> |
|
<dl> |
|
<dt>Hypermedia-Driven Application (HDA)</dt> |
|
<dd> |
|
<p>A web application that uses <em class="test">hypermedia</em> and <em class="test">hypermedia |
|
exchanges</em> as its primary mechanism for communicating with a |
|
server.</p> |
|
</dd> |
|
</dl> |
|
<p>So, what does an HDA look like up close?</p> |
|
<p>Let’s look at an htmx-powered implementation of the simple |
|
JavaScript-powered button above:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/1"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#contact-ui"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a> Fetch Contact</span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>An htmx implementation</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>issues a <code>GET</code> request to <code>/contacts/1</code>, |
|
replacing the <code>contact-ui</code>.</p></li> |
|
</ol> |
|
<p>As with the JavaScript powered button, this button has been annotated |
|
with some attributes. However, in this case we do not have any |
|
(explicit) JavaScript scripting.</p> |
|
<p>Instead, we have <em class="test">declarative</em> attributes much like the |
|
<code>href</code> attribute on anchor tags and the <code>action</code> |
|
attribute on form tags. The <code>hx-get</code> attribute tells htmx: |
|
“When the user clicks this button, issue a <code>GET</code> request to |
|
<code>/contacts/1</code>.” The <code>hx-target</code> attribute tells |
|
htmx: “When the response returns, take the resulting HTML and place it |
|
into the element with the id <code>contact-ui</code>.”</p> |
|
<p>Here we get to the crux of htmx and how it allows you to build |
|
Hypermedia-Driven Applications:</p> |
|
<p><em class="test">The HTTP response from the server is expected to be in HTML |
|
format, not JSON</em>.</p> |
|
<p>An HTTP response to this htmx-driven request might look something |
|
like this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">details</span><span class="dt">></span></span> |
|
<span id="cb8-2"><a aria-hidden="true" href="#cb8-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb8-3"><a aria-hidden="true" href="#cb8-3" tabindex="-1"></a> Contact: HTML Example</span> |
|
<span id="cb8-4"><a aria-hidden="true" href="#cb8-4" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb8-5"><a aria-hidden="true" href="#cb8-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb8-6"><a aria-hidden="true" href="#cb8-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:html-example@example.com"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb8-7"><a aria-hidden="true" href="#cb8-7" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb8-8"><a aria-hidden="true" href="#cb8-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">details</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>HTML</p></figcaption> |
|
</figure> |
|
<p>This small bit of HTML would be placed into the element in the DOM |
|
with the id <code>contact-ui</code>.</p> |
|
<p>Thus, this htmx-powered button is exchanging <em class="test">hypermedia</em> with |
|
the server, just like an anchor tag or form might, and thus the |
|
interaction is still using the basic hypermedia model of the web. Htmx |
|
<em class="test">is</em> adding functionality to this button (via JavaScript), but |
|
that functionality is <em class="test">augmenting</em> HTML as a hypermedia. Htmx |
|
extends the hypermedia system of the web, rather than <em class="test">replacing</em> |
|
that hypermedia system with a totally different architecture.</p> |
|
<p>Despite looking superficially similar to one another it turns out |
|
that this htmx-powered button and the JavaScript-based button are using |
|
extremely different system architectures and, thus, approaches to web |
|
development.</p> |
|
<p>As we walk through building a Hypermedia-Driven Application in this |
|
book, the differences between the two approaches will become more and |
|
more apparent.</p> |
|
<h2 id="_when_should_you_use_hypermedia">When Should You Use |
|
Hypermedia?</h2> |
|
<p>Hypermedia is often, though <em class="test">not always</em>, a great choice for a |
|
web application.</p> |
|
<p>Perhaps you are building a website or application that simply doesn’t |
|
<em class="test">need</em> a huge amount of user-interactivity. There are many useful |
|
web applications like this, and there is no shame in it! Applications |
|
like Amazon, eBay, any number of news sites, shopping sites, message |
|
boards and so on don’t need a massive amount of interactivity to be |
|
effective: they are mainly text and images, which is exactly what the |
|
web was designed for.</p> |
|
<p>Perhaps your application adds most of its value on the <em class="test">server |
|
side</em>, by coordinating users or by applying sophisticated data |
|
analysis and then presenting it to a user. Perhaps your application adds |
|
value by simply sitting in front of a well-designed database, with |
|
simple Create-Read-Update-Delete (CRUD) operations. Again, there is no |
|
shame in this!</p> |
|
<p>In any of these cases, using a hypermedia approach would likely be a |
|
great choice: the interactivity needs of these applications are not |
|
dramatic, and much of the value of these applications lives on the |
|
server side, rather than on the client side.</p> |
|
<p>All of these applications are amenable to what Roy Fielding called |
|
“large-grain hypermedia data transfers”: you can simply use anchor tags |
|
and forms, with responses that return entire HTML documents from |
|
requests, and things will work just fine. This is exactly what the web |
|
was designed to do!</p> |
|
<p>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.</p> |
|
<p>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 user interface and |
|
experience possibilities, making the set of applications that can be |
|
built using hypermedia <em class="test">much</em> larger.</p> |
|
<p>But more on that later.</p> |
|
<h2 id="_when_shouldnt_you_use_hypermedia">When Shouldn’t You Use |
|
Hypermedia?</h2> |
|
<p>So, what about that <em class="test">not always</em>? When isn’t hypermedia going |
|
to work well for an application?</p> |
|
<p>One example that springs immediately to mind is an online spreadsheet |
|
application. In the case of a spreadsheet, updating one cell could have |
|
a large number of cascading changes that need to be made across the |
|
entire sheet. Worse, this might need to happen <em class="test">on every |
|
keystroke</em>.</p> |
|
<p>In this case we have a highly dynamic user interface without clear |
|
boundaries as to what might need to be updated given a particular |
|
change. Introducing a hypermedia-style server round-trip on every cell |
|
change would hurt performance tremendously.</p> |
|
<p>This is simply not a situation amenable to the “large-grain |
|
hypermedia data transfer” approach of the web. For an application like |
|
this we would certainly recommend looking into using a sophisticated |
|
client-side JavaScript approach.</p> |
|
<p><em class="test">However</em> even in the case of an online spreadsheet there are |
|
likely areas where the hypermedia approach might help.</p> |
|
<p>The spreadsheet application likely also has a settings page. And |
|
perhaps that settings page <em class="test">is</em> amenable to the hypermedia |
|
approach. If it is simply a set of relatively straight-forward forms |
|
that need to be persisted to the server, the chances are good that |
|
hypermedia would, in fact, work great for this part of the app.</p> |
|
<p>And, by adopting hypermedia for that part of your application, you |
|
might be able to simplify that part of the application quite a bit. You |
|
could then save more of your application’s <em class="test">complexity budget</em> |
|
for the core, complicated spreadsheet logic, keeping the simple stuff |
|
simple.</p> |
|
<p>Why waste all the complexity associated with a heavy JavaScript |
|
framework on something as simple as a settings page?</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>A Complexity Budget</strong></p> |
|
</div> |
|
<div> |
|
<p>Any software project has a complexity budget, explicit or not: there |
|
is only so much complexity a given development team can tolerate and |
|
every new feature and implementation choice adds at least a bit more to |
|
the overall complexity of the system.</p> |
|
<p>What is particularly nasty about complexity is that it tends to grow |
|
exponentially: one day you can keep the entire system in your head and |
|
understand the ramifications of a particular change, and a week later |
|
the whole system seems intractable. Even worse, efforts to help control |
|
complexity, such as introducing abstractions or infrastructure to manage |
|
the complexity, often end up making things even more complex. Truly, the |
|
job of the good software engineer is to keep complexity under |
|
control.</p> |
|
<p>The sure-fire way to keep complexity down is also the hardest: say |
|
no. Pushing back on feature requests is an art and, if you can learn to |
|
do it well, making people feel like <em class="test">they</em> said no, you will go |
|
far.</p> |
|
<p>Sadly this is not always possible: some features will need to be |
|
built. At this point the question becomes: “what is the simplest thing |
|
that could possibly work?” Understanding the possibilities available in |
|
the hypermedia approach will give you another tool in your “simplest |
|
thing” tool chest.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h2 id="_hypermedia_a_sophisticated_modern_system_architecture">Hypermedia: |
|
A Sophisticated, Modern System Architecture</h2> |
|
<p>Hypermedia is often regarded as an old and antiquated technology in |
|
web development circles, useful perhaps for static websites but |
|
certainly not a realistic choice for modern, sophisticated web |
|
applications.</p> |
|
<p>Seriously? Are we claiming that modern web applications can be built |
|
using it?</p> |
|
<p>Yes, seriously.</p> |
|
<p>Contrary to current popular opinion, hypermedia is an |
|
<em class="test">innovative</em> and <em class="test">modern</em> system architecture for building |
|
applications, in some ways <em class="test">more modern</em> than the prevailing |
|
Single Page Application approaches. In the remainder of this book we |
|
will reintroduce you to the core, practical concepts of hypermedia and |
|
then demonstrate exactly how you can take advantage of this system |
|
architecture in your own software.</p> |
|
<p>In the coming chapters you will develop a firm understanding of all |
|
the benefits and techniques enabled by this approach. We hope that, in |
|
addition, you will also become as passionate about it as we are. <span id="_html_notes_div_soup"></span></p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: <div> Soup</h2> |
|
<p>The best-known kind of messy HTML is <code><div></code> |
|
soup.</p> |
|
<p>When developers fall back on the generic <code><div></code> and |
|
<code><span></code> elements instead of more meaningful tags, we |
|
either degrade the quality of our websites or create more work for |
|
ourselves — probably both.</p> |
|
<p>For example, instead of adding a button using the dedicated |
|
<code><button></code> element, a <code><div></code> element |
|
might have a <code>click</code> event listener added to it.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"bg-accent padding-4 rounded-2"</span><span class="ot"> onclick</span><span class="op">=</span><span class="st">"doStuff()"</span><span class="dt">></span></span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a> Do stuff</span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<p>There are two main issues with this button:</p> |
|
<ul> |
|
<li><p>It’s not focusable — the Tab key won’t get you to it.</p></li> |
|
<li><p>There’s no way for assistive tools to tell that it’s a |
|
button.</p></li> |
|
</ul> |
|
<p>Yes, we can fix that by adding <code>role="button"</code> and |
|
<code>tabindex="0"</code>:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb10"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb10-1"><a aria-hidden="true" href="#cb10-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"bg-accent padding-4 rounded-2"</span></span> |
|
<span id="cb10-2"><a aria-hidden="true" href="#cb10-2" tabindex="-1"></a><span class="ot"> role</span><span class="op">=</span><span class="st">"button"</span></span> |
|
<span id="cb10-3"><a aria-hidden="true" href="#cb10-3" tabindex="-1"></a><span class="ot"> tabindex</span><span class="op">=</span><span class="st">"0"</span></span> |
|
<span id="cb10-4"><a aria-hidden="true" href="#cb10-4" tabindex="-1"></a><span class="ot"> onclick</span><span class="op">=</span><span class="st">"doStuff()"</span><span class="dt">></span>Do stuff<span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<p>These are easy fixes, but they’re things you have to |
|
<em class="test">remember</em>. It’s also not obvious from the HTML source that this |
|
is a button, making the source harder to read and the absence of these |
|
attributes harder to spot. The source code of pages with div soup is |
|
difficult to edit and debug.</p> |
|
<p>To avoid div soup, become friendly with the HTML spec of available |
|
tags, and consider each tag another tool in your tool chest. There might |
|
be things there you don’t remember from before! (With the 113 elements |
|
currently defined in the spec, it’s more of a tool <em class="test">shed</em>).</p> |
|
<p>Of course, not every UI pattern has a designated HTML element. We |
|
often need to compose elements and augment them with attributes. Before |
|
you do, though, rummage through the HTML tool chest. Sometimes you might |
|
be surprised by how much is available.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Components Of A Hypermedia System</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_components_of_a_hypermedia_system">Components Of A Hypermedia |
|
System</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_the_hypermedia">The Hypermedia</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_hypermedia_protocols">Hypermedia Protocols</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_http_methods">HTTP methods</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_http_response_codes">HTTP response codes</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_caching_http_responses">Caching HTTP responses</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_hypermedia_servers">Hypermedia Servers</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_hypermedia_clients">Hypermedia Clients</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_rest">REST</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_the_constraints_of_rest">The “Constraints” of REST</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_the_client_server_constraint">The Client-Server Constraint</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_the_statelessness_constraint">The Statelessness Constraint</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_the_caching_constraint">The Caching Constraint</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_the_uniform_interface_constraint">The Uniform Interface |
|
Constraint</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_identification_of_resources">Identification of resources</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_manipulation_of_resources_through_representations">Manipulation |
|
of resources through representations</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_self_descriptive_messages">Self-descriptive messages</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_hypermedia_as_the_engine_of_application_state_hateoas">Hypermedia |
|
As The Engine of Application State (HATEOAS)</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_hateoas_api_churn">HATEOAS & API churn</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_layered_system">Layered System</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_an_optional_constraint_code_on_demand">An Optional Constraint: |
|
Code-On-Demand</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#_conclusion">Conclusion</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/components-of-a-hypermedia-system/#html-note-title">HTML Notes: HTML5 Soup</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>A <em class="test">hypermedia system</em> consists of a number of components, |
|
including:</p> |
|
<ul> |
|
<li><p>A hypermedia, such as HTML.</p></li> |
|
<li><p>A network protocol, such as HTTP.</p></li> |
|
<li><p>A server that presents a hypermedia API responding to network |
|
requests with hypermedia responses.</p></li> |
|
<li><p>A client that properly interprets those responses.</p></li> |
|
</ul> |
|
<p>In this chapter we will look at these components and their |
|
implementation in the context of the web.</p> |
|
<p>Once we have reviewed the major components of the web as a hypermedia |
|
system, we will look at some key ideas behind this system — especially |
|
as developed by Roy Fielding in his dissertation, “Architectural Styles |
|
and the Design of Network-based Software Architectures.” We will see |
|
where the terms REpresentational State Transfer (REST), RESTful and |
|
Hypermedia As The Engine Of Application State (HATEOAS) come from, and |
|
we will analyze these terms in the context of the web.</p> |
|
<p>This should give you a stronger understanding of the theoretical |
|
basis of the web as a hypermedia system, how it is supposed to fit |
|
together, and why Hypermedia-Driven Applications are RESTful, whereas |
|
JSON APIs — despite the way the term REST is currently used in the |
|
industry — are not.</p> |
|
<h2 id="_components_of_a_hypermedia_system">Components Of A Hypermedia |
|
System</h2> |
|
<h3 id="_the_hypermedia">The Hypermedia</h3> |
|
<p>The fundamental technology of a hypermedia system is a hypermedia |
|
that allows a client and server to communicate with one another in a |
|
dynamic, non-linear fashion. Again, what makes a hypermedia a hypermedia |
|
is the presence of <em class="test">hypermedia controls</em>: elements that allow |
|
users to select non-linear actions within the hypermedia. Users can |
|
<em class="test">interact</em> with the media in a manner beyond simply reading from |
|
start to end.</p> |
|
<p>We have already mentioned the two primary hypermedia controls in |
|
HTML, anchors and forms, which allow a browser to present links and |
|
operations to a user through a browser.</p> |
|
<p>In the case of HTML, these links and forms typically specify the |
|
target of their operations using <em class="test">Uniform Resource Locators |
|
(URLs)</em>:</p> |
|
<dl> |
|
<dt>Uniform Resource Locator</dt> |
|
<dd> |
|
<p>A uniform resource locator is a textual string that refers to, or |
|
<em class="test">points to</em> a location on a network where a <em class="test">resource</em> can |
|
be retrieved from, as well as the mechanism by which the resource can be |
|
retrieved.</p> |
|
</dd> |
|
</dl> |
|
<p>A URL is a string consisting of various subcomponents:</p> |
|
<figure> |
|
<pre><code>[scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment] |
|
</code></pre> |
|
<figcaption><p>URL Components</p></figcaption> |
|
</figure> |
|
<p>Many of these subcomponents are not required, and are often |
|
omitted.</p> |
|
<p>A typical URL might look like this:</p> |
|
<figure> |
|
<pre><code>https://hypermedia.systems/book/contents/ |
|
</code></pre> |
|
<figcaption><p>A simple URL</p></figcaption> |
|
</figure> |
|
<p>This particular URL is made up of the following components:</p> |
|
<ul> |
|
<li><p>A protocol or scheme (in this case, <code>https</code>)</p></li> |
|
<li><p>A domain (e.g., <code>hypermedia.systems</code>)</p></li> |
|
<li><p>A path (e.g., <code>/book/contents</code>)</p></li> |
|
</ul> |
|
<p>This URL uniquely identifies a retrievable <em class="test">resource</em> on the |
|
internet, to which an <em class="test">HTTP Request</em> can be issued by a |
|
hypermedia client that “speaks” HTTPS, such as a web browser. If this |
|
URL is found as the reference of a hypermedia control within an HTML |
|
document, it implies that there is a <em class="test">hypermedia server</em> on the |
|
other side of the network that understands HTTPS as well, and that can |
|
respond to this request with a <em class="test">representation</em> of the given |
|
resource (or redirect you to another location, etc.)</p> |
|
<p>Note that URLs are often not written out entirely within HTML. It is |
|
very common to see anchor tags that look like this, for example:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/book/contents/"</span><span class="dt">></span>Table Of Contents<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A Simple Link</p></figcaption> |
|
</figure> |
|
<p>Here we have a <em class="test">relative</em> hypermedia reference, where the |
|
protocol, host and port are <em class="test">implied</em> to be that of the “current |
|
document,” that is, the same as whatever the protocol and server were to |
|
retrieve the current HTML page. So, if this link was found in an HTML |
|
document retrieved from <code>https://hypermedia.systems/</code>, then |
|
the implied URL for this anchor would be |
|
<code>https://hypermedia.systems/book/contents/</code>.</p> |
|
<h3 id="_hypermedia_protocols">Hypermedia Protocols</h3> |
|
<p>The hypermedia control (link) above tells a browser: “When a user |
|
clicks on this text, issue a request to |
|
<code>https://hypermedia.systems/book/contents/</code> using the |
|
Hypertext Transfer Protocol,” or HTTP.</p> |
|
<p>HTTP is the <em class="test">protocol</em> used to transfer HTML (hypermedia) |
|
between browsers (hypermedia clients) and servers (hypermedia servers) |
|
and, as such, is the key network technology that binds the distributed |
|
hypermedia system of the web together.</p> |
|
<p>HTTP version 1.1 is a relatively simple network protocol, so lets |
|
take a look at what the <code>GET</code> request triggered by the anchor |
|
tag would look like. This is the request that would be sent to the |
|
server found at <code>hypermedia.systems</code>, on port <code>80</code> |
|
by default:</p> |
|
<figure> |
|
<pre class="http"><code>GET /book/contents/ HTTP/1.1 |
|
Accept: text/html,*/* |
|
Host: hypermedia.systems |
|
</code></pre> |
|
</figure> |
|
<p>The first line specifies that this is an HTTP <code>GET</code> |
|
request. It then specifies the path of the resource being requested. |
|
Finally, it contains the HTTP version for this request.</p> |
|
<p>After that are a series of HTTP <em class="test">request headers</em>: individual |
|
lines of name/value pairs separated by a colon. The request headers |
|
provide <em class="test">metadata</em> that can be used by the server to determine |
|
exactly how to respond to the client request. In this case, with the |
|
<code>Accept</code> header, the browser is saying it would prefer HTML |
|
as a response format, but will accept any server response.</p> |
|
<p>Next, it has a <code>Host</code> header that specifies which server |
|
the request has been sent to. This is useful when multiple domains are |
|
hosted on the same host.</p> |
|
<p>An HTTP response from a server to this request might look something |
|
like this:</p> |
|
<figure> |
|
<pre class="http"><code>HTTP/1.1 200 OK |
|
Content-Type: text/html; charset=utf-8 |
|
Content-Length: 870 |
|
Server: Werkzeug/2.0.2 Python/3.8.10 |
|
Date: Sat, 23 Apr 2022 18:27:55 GMT |
|
|
|
<html lang="en"> |
|
<body> |
|
<header> |
|
<h1>HYPERMEDIA SYSTEMS</h1> |
|
</header> |
|
... |
|
</body> |
|
</html> |
|
</code></pre> |
|
</figure> |
|
<p>In the first line, the HTTP Response specifies the HTTP version being |
|
used, followed by a <em class="test">response code</em> of <code>200</code>, |
|
indicating that the given resource was found and that the request |
|
succeeded. This is followed by a string, <code>OK</code> that |
|
corresponds to the response code. (The actual string doesn’t matter, it |
|
is the response code that tells the client the result of a request, as |
|
we will discuss in more detail below.)</p> |
|
<p>After the first line of the response, as with the HTTP Request, we |
|
see a series of <em class="test">response headers</em> that provide metadata to the |
|
client to assist in displaying the <em class="test">representation</em> of the |
|
resource correctly.</p> |
|
<p>Finally, we see some new HTML content. This content is the HTML |
|
<em class="test">representation</em> of the requested resource, in this case a table |
|
of contents of a book. The browser will use this HTML to replace the |
|
entire content in its display window, showing the user this new page, |
|
and updating the address bar to reflect the new URL.</p> |
|
<h4 id="_http_methods">HTTP methods</h4> |
|
<p>The anchor tag above issued an HTTP <code>GET</code>, where |
|
<code>GET</code> is the <em class="test">method</em> of the request. The particular |
|
method being used in an HTTP request is perhaps the most important piece |
|
of information about it, after the actual resource that the request is |
|
directed at.</p> |
|
<p>There are many methods available in HTTP; the ones of most practical |
|
importance to developers are the following:</p> |
|
<dl> |
|
<dt><code>GET</code></dt> |
|
<dd> |
|
<p>A GET request retrieves the representation of the specified resource. |
|
GET requests should not mutate data.</p> |
|
</dd> |
|
<dt><code>POST</code></dt> |
|
<dd> |
|
<p>A POST request submits data to the specified resource. This will |
|
often result in a mutation of state on the server.</p> |
|
</dd> |
|
<dt><code>PUT</code></dt> |
|
<dd> |
|
<p>A PUT request replaces the data of the specified resource. This |
|
results in a mutation of state on the server.</p> |
|
</dd> |
|
<dt><code>PATCH</code></dt> |
|
<dd> |
|
<p>A PATCH request replaces the data of the specified resource. This |
|
results in a mutation of state on the server.</p> |
|
</dd> |
|
<dt><code>DELETE</code></dt> |
|
<dd> |
|
<p>A DELETE request deletes the specified resource. This results in a |
|
mutation of state on the server.</p> |
|
</dd> |
|
</dl> |
|
<p>These methods <em class="test">roughly</em> line up with the |
|
“Create/Read/Update/Delete” or CRUD pattern found in many |
|
applications:</p> |
|
<ul> |
|
<li><p><code>POST</code> corresponds with Creating a resource.</p></li> |
|
<li><p><code>GET</code> corresponds with Reading a resource.</p></li> |
|
<li><p><code>PUT</code> and <code>PATCH</code> correspond with Updating |
|
a resource.</p></li> |
|
<li><p><code>DELETE</code> corresponds, well, with Deleting a |
|
resource.</p></li> |
|
</ul> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Put vs. Post</strong></p> |
|
</div> |
|
<div> |
|
<p>While HTTP Actions correspond roughly to CRUD, they are not the same. |
|
The technical specifications for these methods make no such connection, |
|
and are often somewhat difficult to read. Here, for example, is the |
|
documentation on the distinction between a <code>POST</code> and a |
|
<code>PUT</code> from <a href="https://www.rfc-editor.org/rfc/rfc9110">RFC-9110</a>.</p> |
|
<blockquote> |
|
<p>The target resource in a POST request is intended to handle the |
|
enclosed representation according to the resource’s own semantics, |
|
whereas the enclosed representation in a PUT request is defined as |
|
replacing the state of the target resource. Hence, the intent of PUT is |
|
idempotent and visible to intermediaries, even though the exact effect |
|
is only known by the origin server.</p> |
|
</blockquote><p class="quote-attribution"> RFC-9110, https://www.rfc-editor.org/rfc/rfc9110#section-9.3.4</p> |
|
<p>In plain terms, a <code>POST</code> can be handled by a server pretty |
|
much however it likes, whereas a <code>PUT</code> should be handled as a |
|
“replacement” of the resource, although the language, once again allows |
|
the server to do pretty much whatever it would like within the |
|
constraint of being <a href="https://developer.mozilla.org/en-US/docs/Glossary/Idempotent"><em class="test">idempotent</em></a>.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>In a properly structured HTML-based hypermedia system you would use |
|
an appropriate HTTP method for the operation a particular hypermedia |
|
control performs. For example, if a hypermedia control such as a button |
|
<em class="test">deletes</em> a resource, ideally it should issue an HTTP |
|
<code>DELETE</code> request to do so.</p> |
|
<p>A strange thing about HTML, though, is that the native hypermedia |
|
controls can only issue HTTP <code>GET</code> and <code>POST</code> |
|
requests.</p> |
|
<p>Anchor tags always issue a <code>GET</code> request.</p> |
|
<p>Forms can issue either a <code>GET</code> or <code>POST</code> using |
|
the <code>method</code> attribute.</p> |
|
<p>Despite the fact that HTML — the world’s most popular hypermedia — |
|
has been designed alongside HTTP (which is the Hypertext Transfer |
|
Protocol, after all!): if you wish to issue <code>PUT</code>, |
|
<code>PATCH</code> or <code>DELETE</code> requests you currently |
|
<em class="test">have to</em> resort to JavaScript to do so. Since a |
|
<code>POST</code> can do almost anything, it ends up being used for any |
|
mutation on the server, and <code>PUT</code>, <code>PATCH</code> and |
|
<code>DELETE</code> are left aside in plain HTML-based applications.</p> |
|
<p>This is an obvious shortcoming of HTML as a hypermedia; it would be |
|
wonderful to see this fixed in the HTML specification. For now, in |
|
Chapter 4, we’ll discuss ways to get around this.</p> |
|
<h4 id="_http_response_codes">HTTP response codes</h4> |
|
<p>HTTP request methods allow a client to tell a server <em class="test">what</em> to |
|
do to a given resource. HTTP responses contain <em class="test">response codes</em>, |
|
which tell a client what the result of the request was. HTTP response |
|
codes are numeric values that are embedded in the HTTP response, as we |
|
saw above.</p> |
|
<p>The most familiar response code for web developers is probably |
|
<code>404</code>, which stands for “Not Found.” This is the response |
|
code that is returned by web servers when a resource that does not exist |
|
is requested from them.</p> |
|
<p>HTTP breaks response codes up into various categories:</p> |
|
<dl> |
|
<dt><code>100</code>-<code>199</code></dt> |
|
<dd> |
|
<p>Informational responses that provide information about how the server |
|
is processing the response.</p> |
|
</dd> |
|
<dt><code>200</code>-<code>299</code></dt> |
|
<dd> |
|
<p>Successful responses indicating that the request succeeded.</p> |
|
</dd> |
|
<dt><code>300</code>-<code>399</code></dt> |
|
<dd> |
|
<p>Redirection responses indicating that the request should be sent to |
|
some other URL.</p> |
|
</dd> |
|
<dt><code>400</code>-<code>499</code></dt> |
|
<dd> |
|
<p>Client error responses indicating that the client made some sort of |
|
bad request (e.g., asking for something that didn’t exist in the case of |
|
<code>404</code> errors).</p> |
|
</dd> |
|
<dt><code>500</code>-<code>599</code></dt> |
|
<dd> |
|
<p>Server error responses indicating that the server encountered an |
|
error internally as it attempted to respond to the request.</p> |
|
</dd> |
|
</dl> |
|
<p>Within each of these categories there are multiple response codes for |
|
specific situations.</p> |
|
<p>Here are some of the more common or interesting ones:</p> |
|
<dl> |
|
<dt><code>200 OK</code></dt> |
|
<dd> |
|
<p>The HTTP request succeeded.</p> |
|
</dd> |
|
<dt><code>301 Moved Permanently</code></dt> |
|
<dd> |
|
<p>The URL for the requested resource has moved to a new location |
|
permanently, and the new URL will be provided in the |
|
<code>Location</code> response header.</p> |
|
</dd> |
|
<dt><code>302 Found</code></dt> |
|
<dd> |
|
<p>The URL for the requested resource has moved to a new location |
|
temporarily, and the new URL will be provided in the |
|
<code>Location</code> response header.</p> |
|
</dd> |
|
<dt><code>303 See Other</code></dt> |
|
<dd> |
|
<p>The URL for the requested resource has moved to a new location, and |
|
the new URL will be provided in the <code>Location</code> response |
|
header. Additionally, this new URL should be retrieved with a |
|
<code>GET</code> request.</p> |
|
</dd> |
|
<dt><code>401 Unauthorized</code></dt> |
|
<dd> |
|
<p>The client is not yet authenticated (yes, authenticated, despite the |
|
name) and must be authenticated to retrieve the given resource.</p> |
|
</dd> |
|
<dt><code>403 Forbidden</code></dt> |
|
<dd> |
|
<p>The client does not have access to this resource.</p> |
|
</dd> |
|
<dt><code>404 Not Found</code></dt> |
|
<dd> |
|
<p>The server cannot find the requested resource.</p> |
|
</dd> |
|
<dt><code>500 Internal Server Error</code></dt> |
|
<dd> |
|
<p>The server encountered an error when attempting to process the |
|
response.</p> |
|
</dd> |
|
</dl> |
|
<p>There are some fairly subtle differences between HTTP response codes |
|
(and, to be honest, some ambiguities between them). The difference |
|
between a <code>302</code> redirect and a <code>303</code> redirect, for |
|
example, is that the former will issue the request to the new URL using |
|
the same HTTP method as the initial request, whereas the latter will |
|
always use a <code>GET</code>. This is a small but often crucial |
|
difference, as we will see later in the book.</p> |
|
<p>A well crafted Hypermedia-Driven Application will take advantage of |
|
both HTTP methods and HTTP response codes to create a sensible |
|
hypermedia API. You do not want to build a Hypermedia-Driven Application |
|
that uses a <code>POST</code> method for all requests and responds with |
|
<code>200 OK</code> for every response, for example. (Some JSON Data |
|
APIs built on top of HTTP do exactly this!)</p> |
|
<p>When building a Hypermedia-Driven Application, you want, instead, to |
|
go “with the grain” of the web and use HTTP methods and response codes |
|
as they were designed to be used.</p> |
|
<h4 id="_caching_http_responses">Caching HTTP responses</h4> |
|
<p>A constraint of REST (and, therefore, a feature of HTTP) is the |
|
notion of caching responses: a server can indicate to a client (as well |
|
as intermediary HTTP servers) that a given response can be cached for |
|
future requests to the same URL.</p> |
|
<p>The cache behavior of an HTTP response from a server can be indicated |
|
with the <code>Cache-Control</code> response header. This header can |
|
have a number of different values indicating the cacheability of a given |
|
response. If, for example, the header contains the value |
|
<code>max-age=60</code>, this indicates that a client may cache this |
|
response for 60 seconds, and need not issue another HTTP request for |
|
that resource until that time limit has expired.</p> |
|
<p>Another important caching-related response header is |
|
<code>Vary</code>. This response header can be used to indicate exactly |
|
what headers in an HTTP Request form the unique identifier for a cached |
|
result. This becomes important to allow the browser to correctly cache |
|
content in situations where a particular header affects the form of the |
|
server response.</p> |
|
<p>A common pattern in htmx-powered applications, for example, is to use |
|
a custom header set by htmx, <code>HX-Request</code>, to differentiate |
|
between “normal” web requests and requests submitted by htmx. To |
|
properly cache the response to these requests, the |
|
<code>HX-Request</code> request header must be indicated by the |
|
<code>Vary</code> response header.</p> |
|
<p>A full discussion of caching HTTP responses is beyond the scope of |
|
this chapter; see the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching">MDN |
|
Article on HTTP Caching</a> if you would like to know more on the |
|
topic.</p> |
|
<h3 id="_hypermedia_servers">Hypermedia Servers</h3> |
|
<p>Hypermedia servers are any server that can respond to an HTTP request |
|
with an HTTP response. Because HTTP is so simple, this means that nearly |
|
any programming language can be used to build a hypermedia server. There |
|
are a vast number of libraries available for building HTTP-based |
|
hypermedia servers in nearly every programming language imaginable.</p> |
|
<p>This turns out to be one of the best aspects of adopting hypermedia |
|
as your primary technology for building a web application: it removes |
|
the pressure to adopt JavaScript as a backend technology. If you use a |
|
JavaScript-heavy Single Page Application-based front end, and you use |
|
JSON Data APIs, you are going to feel significant pressure to deploy |
|
JavaScript on the back end as well.</p> |
|
<p>In this latter situation, you already have a ton of code written in |
|
JavaScript. Why maintain two separate code bases in two different |
|
languages? Why not create reusable domain logic on the client-side as |
|
well as the server-side? Now that JavaScript has excellent server-side |
|
technologies available like Node and Deno, why not just use a single |
|
language for everything?</p> |
|
<p>In contrast, building a Hypermedia-Driven Application gives you a lot |
|
more freedom in picking the back end technology you want to use. Your |
|
decision can be based on the domain of your application, what languages |
|
and server software you are familiar with or are passionate about, or |
|
just what you feel like trying out.</p> |
|
<p>You certainly aren’t writing your server-side logic in HTML! And |
|
every major programming language has at least one good web framework and |
|
templating library that can be used to handle HTTP requests cleanly.</p> |
|
<p>If you are doing something in big data, perhaps you’d like to use |
|
Python, which has tremendous support for that domain.</p> |
|
<p>If you are doing AI work, perhaps you’d like to use Lisp, leaning on |
|
a language with a long history in that area of research.</p> |
|
<p>Maybe you are a functional programming enthusiast and want to use |
|
OCaml or Haskell. Perhaps you just really like Julia or Nim.</p> |
|
<p>These are all perfectly valid reasons for choosing a particular |
|
server-side technology!</p> |
|
<p>By using hypermedia as your system architecture, you are freed up to |
|
adopt any of these choices. There simply isn’t a large JavaScript code |
|
base on the front end pressuring you to adopt JavaScript on the back |
|
end.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Hypermedia On Whatever you’d Like (HOWL)</strong></p> |
|
</div> |
|
<div> |
|
<p>In the htmx community we call this (with tongue in cheek) the HOWL |
|
stack: Hypermedia On Whatever you’d Like. The htmx community is |
|
multi-language and multi-framework, there are rubyists as well as |
|
pythonistas, lispers as well as haskellers. There are even JavaScript |
|
enthusiasts! All these languages and frameworks are able to adopt |
|
hypermedia, and are able to still share techniques and offer support to |
|
one another because they share a common underlying architecture: they |
|
are all using the web as a hypermedia system.</p> |
|
<p>Hypermedia, in this sense, provides a “universal language” for the |
|
web that we can all use.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_hypermedia_clients">Hypermedia Clients</h3> |
|
<p>We now come to the final major component in a hypermedia system: the |
|
hypermedia client. Hypermedia <em class="test">clients</em> are software that |
|
understand how to interpret a particular hypermedia, and the hypermedia |
|
controls within it, properly. The canonical example, of course, is the |
|
web browser, which understands HTML and can present it to a user to |
|
interact with. Web browsers are incredibly sophisticated pieces of |
|
software. (So sophisticated, in fact, that they are often re-purposed |
|
away from being a hypermedia client, to being a sort of cross-platform |
|
virtual machine for launching Single Page Applications.)</p> |
|
<p>Browsers aren’t the only hypermedia clients out there, however. In |
|
the last section of this book we will look at Hyperview, a |
|
mobile-oriented hypermedia. One of the outstanding features of Hyperview |
|
is that it doesn’t simply provide a hypermedia, HXML, but also provides |
|
a <em class="test">working hypermedia client</em> for that hypermedia. This makes |
|
building a proper Hypermedia-Driven Application with Hyperview extremely |
|
easy.</p> |
|
<p>A crucial feature of a hypermedia system is what is known as <em class="test">the |
|
uniform interface</em>. We discuss this concept in depth in the next |
|
section on REST. What is often ignored in discussions about hypermedia |
|
is how important the hypermedia client is in taking advantage of this |
|
uniform interface. A hypermedia client must know how to properly |
|
interpret and present hypermedia controls found in a hypermedia response |
|
from a hypermedia server for the whole hypermedia system to hang |
|
together. Without a sophisticated client that can do this, hypermedia |
|
controls and a hypermedia-based API are much less useful.</p> |
|
<p>This is one reason why JSON APIs have rarely adopted hypermedia |
|
controls successfully: JSON APIs are typically consumed by code that is |
|
expecting a fixed format and that isn’t designed to be a hypermedia |
|
client. This is totally understandable: building a good hypermedia |
|
client is hard! For JSON API clients like this, the power of hypermedia |
|
controls embedded within an API response is irrelevant and often simply |
|
annoying:</p> |
|
<blockquote> |
|
<p>The short answer to this question is that HATEOAS isn’t a good fit |
|
for most modern use cases for APIs. That is why after almost 20 years, |
|
HATEOAS still hasn’t gained wide adoption among developers. GraphQL on |
|
the other hand is spreading like wildfire because it solves real-world |
|
problems.</p> |
|
</blockquote><p class="quote-attribution"> Freddie Karlbom, |
|
https://techblog.commercetools.com/graphql-and-rest-level-3-hateoas-70904ff1f9cf</p> |
|
<p>HATEOAS will be described in more detail below, but the takeaway here |
|
is that a good hypermedia client is a necessary component within a |
|
larger hypermedia system.</p> |
|
<h2 id="_rest">REST</h2> |
|
<p>Now that we have reviewed the major components of a hypermedia |
|
system, it’s time to look more deeply into the concept of REST. The term |
|
“REST” comes from Roy Fielding’s PhD dissertation on the architecture of |
|
the web. Fielding wrote his dissertation at U.C. Irvine, after having |
|
helped build much of the infrastructure of the early web, including the |
|
Apache web server. Roy was attempting to formalize and describe the |
|
novel distributed computing system that he had helped to build.</p> |
|
<p>We are going to focus on what we feel is the most important section |
|
of Fielding’s writing, from a web development perspective: Section 5.1. |
|
This section contains the core concepts (Fielding calls them |
|
<em class="test">constraints</em>) of Representational State Transfer, or REST.</p> |
|
<p>Before we get into the muck, however, it is important to understand |
|
that Fielding discusses REST as a <em class="test">network architecture</em>, that |
|
is, as an entirely different way to architect a distributed system. And, |
|
further, as a novel network architecture that should be |
|
<em class="test">contrasted</em> with earlier approaches to distributed systems.</p> |
|
<p>It is also important to emphasize that, at the time Fielding wrote |
|
his dissertation, JSON APIs and AJAX did not exist. He was describing |
|
the early web, with HTML being transferred over HTTP by early browsers, |
|
as a hypermedia system.</p> |
|
<p>Today, in a strange turn of events, the term “REST” is mainly |
|
associated with JSON Data APIs, rather than with HTML and hypermedia. |
|
This is extremely funny once you realize that the vast majority of JSON |
|
Data APIs aren’t RESTful, in the original sense, and, in fact, |
|
<em class="test">can’t</em> be RESTful, since they aren’t using a natural hypermedia |
|
format.</p> |
|
<p>To re-emphasize: REST, as coined by Fielding, describes the |
|
<em class="test">pre-API web</em>, and letting go of the current, common usage of the |
|
term REST to simply mean “a JSON API” is necessary to develop a proper |
|
understanding of the idea.</p> |
|
<h3 id="_the_constraints_of_rest">The “Constraints” of REST</h3> |
|
<p>In his dissertation, Fielding defines various “constraints” to |
|
describe how a RESTful system must behave. This approach can feel a |
|
little round-about and difficult to follow for many people, but it is an |
|
appropriate approach for an academic document. Given a bit of time |
|
thinking about the constraints he outlines and some concrete examples of |
|
those constraints it will become easy to assess whether a given system |
|
actually satisfies the architectural requirements of REST or not.</p> |
|
<p>Here are the constraints of REST Fielding outlines:</p> |
|
<ul> |
|
<li><p>It is a client-server architecture (section 5.1.2).</p></li> |
|
<li><p>It must be stateless; (section 5.1.3) that is, every request |
|
contains all information necessary to respond to that request.</p></li> |
|
<li><p>It must allow for caching (section 5.1.4).</p></li> |
|
<li><p>It must have a <em class="test">uniform interface</em> (section |
|
5.1.5).</p></li> |
|
<li><p>It is a layered system (section 5.1.6).</p></li> |
|
<li><p>Optionally, it can allow for Code-On-Demand (section 5.1.7), that |
|
is, scripting.</p></li> |
|
</ul> |
|
<p>Let’s go through each of these constraints in turn and discuss them |
|
in detail, looking at how (and to what extent) the web satisfies each of |
|
them.</p> |
|
<h3 id="_the_client_server_constraint">The Client-Server Constraint</h3> |
|
<p>See <a href="https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_2">Section |
|
5.1.2</a> for the Client-Server constraint.</p> |
|
<p>The REST model Fielding was describing involved both <em class="test">clients</em> |
|
(browsers, in the case of the web) and <em class="test">servers</em> (such as the |
|
Apache Web Server he had been working on) communicating via a network |
|
connection. This was the context of his work: he was describing the |
|
network architecture of the World Wide Web, and contrasting it with |
|
earlier architectures, notably thick-client networking models such as |
|
the Common Object Request Broker Architecture (CORBA).</p> |
|
<p>It should be obvious that any web application, regardless of how it |
|
is designed, will satisfy this requirement.</p> |
|
<h3 id="_the_statelessness_constraint">The Statelessness Constraint</h3> |
|
<p>See <a href="https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_3">Section |
|
5.1.3</a> for the Stateless constraint.</p> |
|
<p>As described by Fielding, a RESTful system is stateless: every |
|
request should encapsulate all information necessary to respond to that |
|
request, with no side state or context stored on either the client or |
|
the server.</p> |
|
<p>In practice, for many web applications today, we actually violate |
|
this constraint: it is common to establish a <em class="test">session cookie</em> |
|
that acts as a unique identifier for a given user and that is sent along |
|
with every request. While this session cookie is, by itself, not |
|
stateful (it is sent with every request), it is typically used as a key |
|
to look up information stored on the server, in what is usually termed |
|
“the session.”</p> |
|
<p>This session information is typically stored in some sort of shared |
|
storage across multiple web servers, holding things like the current |
|
user’s email or id, their roles, partially created domain objects, |
|
caches, and so forth.</p> |
|
<p>This violation of the Statelessness REST architectural constraint has |
|
proven to be useful for building web applications and does not appear to |
|
have had a major impact on the overall flexibility of the web. But it is |
|
worth bearing in mind that even Web 1.0 applications often violate the |
|
purity of REST in the interest of pragmatic trade-offs.</p> |
|
<p>And it must be said that sessions <em class="test">do</em> cause additional |
|
operational complexity headaches when deploying hypermedia servers; |
|
these may need shared access to session state information stored across |
|
an entire cluster. So Fielding was correct in pointing out that an ideal |
|
RESTful system, one that did not violate this constraint, would be |
|
simpler and therefore more robust.</p> |
|
<h3 id="_the_caching_constraint">The Caching Constraint</h3> |
|
<p>See <a href="https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_4">Section |
|
5.1.4</a> for the Caching constraint.</p> |
|
<p>This constraint states that a RESTful system should support the |
|
notion of caching, with explicit information on the cache-ability of |
|
responses for future requests of the same resource. This allows both |
|
clients as well as intermediary servers between a given client and final |
|
server to cache the results of a given request.</p> |
|
<p>As we discussed earlier, HTTP has a sophisticated caching mechanism |
|
via response headers that is often overlooked or underutilized when |
|
building hypermedia applications. Given the existence of this |
|
functionality, however, it is easy to see how this constraint is |
|
satisfied by the web.</p> |
|
<h3 id="_the_uniform_interface_constraint">The Uniform Interface |
|
Constraint</h3> |
|
<p>Now we come to the most interesting and, in our opinion, most |
|
innovative constraint in REST: that of the <em class="test">uniform |
|
interface</em>.</p> |
|
<p>This constraint is the source of much of the <em class="test">flexibility</em> and |
|
<em class="test">simplicity</em> of a hypermedia system, so we are going to spend |
|
some time on it.</p> |
|
<p>See <a href="https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_5">Section |
|
5.1.5</a> for the Uniform Interface constraint.</p> |
|
<p>In this section, Fielding says:</p> |
|
<blockquote> |
|
<p>The central feature that distinguishes the REST architectural style |
|
from other network-based styles is its emphasis on a uniform interface |
|
between components… In order to obtain a uniform interface, multiple |
|
architectural constraints are needed to guide the behavior of |
|
components. REST is defined by four interface constraints: |
|
identification of resources; manipulation of resources through |
|
representations; self-descriptive messages; and, hypermedia as the |
|
engine of application state</p> |
|
</blockquote><p class="quote-attribution"> Roy Fielding, Architectural Styles and the Design of Network-based |
|
Software Architectures</p> |
|
<p>So we have four sub-constraints that, taken together, form the |
|
Uniform Interface constraint.</p> |
|
<h4 id="_identification_of_resources">Identification of resources</h4> |
|
<p>In a RESTful system, resources should have a unique identifier. Today |
|
the concept of Universal Resource Locators (URLs) is common, but at the |
|
time of Fielding’s writing they were still relatively new and novel.</p> |
|
<p>What might be more interesting today is the notion of a |
|
<em class="test">resource</em>, thus being identified: in a RESTful system, |
|
<em class="test">any</em> sort of data that can be referenced, that is, the target of |
|
a hypermedia reference, is considered a resource. URLs, though common |
|
enough today, end up solving the very complex problem of uniquely |
|
identifying any and every resource on the internet.</p> |
|
<h4 id="_manipulation_of_resources_through_representations">Manipulation |
|
of resources through representations</h4> |
|
<p>In a RESTful system, <em class="test">representations</em> of the resource are |
|
transferred between clients and servers. These representations can |
|
contain both data and metadata about the request (such as “control data” |
|
like an HTTP method or response code). A particular data format or |
|
<em class="test">media type</em> may be used to present a given resource to a client, |
|
and that media type can be negotiated between the client and the |
|
server.</p> |
|
<p>We saw this latter aspect of the uniform interface in the |
|
<code>Accept</code> header in the requests above.</p> |
|
<h4 id="_self_descriptive_messages">Self-descriptive messages</h4> |
|
<p>The Self-Descriptive Messages constraint, combined with the next one, |
|
HATEOAS, form what we consider to be the core of the Uniform Interface, |
|
of REST and why hypermedia provides such a powerful system |
|
architecture.</p> |
|
<p>The Self-Descriptive Messages constraint requires that, in a RESTful |
|
system, messages must be <em class="test">self-describing</em>.</p> |
|
<p>This means that <em class="test">all information</em> necessary to both display |
|
<em class="test">and also operate</em> on the data being represented must be present |
|
in the response. In a properly RESTful system, there can be no |
|
additional “side” information necessary for a client to transform a |
|
response from a server into a useful user interface. Everything must “be |
|
in” the message itself, in the form of hypermedia controls.</p> |
|
<p>This might sound a little abstract so let’s look at a concrete |
|
example.</p> |
|
<p>Consider two different potential responses from an HTTP server for |
|
the URL <code>https://example.com/contacts/42</code>.</p> |
|
<p>Both responses will return information about a contact, but each |
|
response will take very different forms.</p> |
|
<p>The first implementation returns an HTML representation:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb6"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb6-1"><a aria-hidden="true" href="#cb6-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">html</span><span class="ot"> lang</span><span class="op">=</span><span class="st">"en"</span><span class="dt">></span></span> |
|
<span id="cb6-2"><a aria-hidden="true" href="#cb6-2" tabindex="-1"></a><span class="dt"><</span><span class="kw">body</span><span class="dt">></span></span> |
|
<span id="cb6-3"><a aria-hidden="true" href="#cb6-3" tabindex="-1"></a><span class="dt"><</span><span class="kw">h1</span><span class="dt">></span>Joe Smith<span class="dt"></</span><span class="kw">h1</span><span class="dt">></span></span> |
|
<span id="cb6-4"><a aria-hidden="true" href="#cb6-4" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb6-5"><a aria-hidden="true" href="#cb6-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span>Email: joe@example.bar<span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb6-6"><a aria-hidden="true" href="#cb6-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span>Status: Active<span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb6-7"><a aria-hidden="true" href="#cb6-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb6-8"><a aria-hidden="true" href="#cb6-8" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb6-9"><a aria-hidden="true" href="#cb6-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/42/archive"</span><span class="dt">></span>Archive<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb6-10"><a aria-hidden="true" href="#cb6-10" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb6-11"><a aria-hidden="true" href="#cb6-11" tabindex="-1"></a><span class="dt"></</span><span class="kw">body</span><span class="dt">></span></span> |
|
<span id="cb6-12"><a aria-hidden="true" href="#cb6-12" tabindex="-1"></a><span class="dt"></</span><span class="kw">html</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<p>The second implementation returns a JSON representation:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="fu">{</span></span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a> <span class="dt">"name"</span><span class="fu">:</span> <span class="st">"Joe Smith"</span><span class="fu">,</span></span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a> <span class="dt">"email"</span><span class="fu">:</span> <span class="st">"joe@example.org"</span><span class="fu">,</span></span> |
|
<span id="cb7-4"><a aria-hidden="true" href="#cb7-4" tabindex="-1"></a> <span class="dt">"status"</span><span class="fu">:</span> <span class="st">"Active"</span></span> |
|
<span id="cb7-5"><a aria-hidden="true" href="#cb7-5" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div> |
|
</figure> |
|
<p>What can we say about the differences between these two |
|
responses?</p> |
|
<p>One thing that may initially jump out at you is that the JSON |
|
representation is smaller than the HTML representation. Fielding notes |
|
exactly this trade-off when using a RESTful architecture:</p> |
|
<blockquote> |
|
<p>The trade-off, though, is that a uniform interface degrades |
|
efficiency, since information is transferred in a standardized form |
|
rather than one which is specific to an application’s needs.</p> |
|
</blockquote><p class="quote-attribution"> Roy Fielding, Architectural Styles and the Design of Network-based |
|
Software Architectures</p> |
|
<p>So REST <em class="test">trades off</em> representational efficiency for other |
|
goals.</p> |
|
<p>To understand these other goals, first notice that the HTML |
|
representation has a hyperlink in it to navigate to a page to archive |
|
the contact. The JSON representation, in contrast, does not have this |
|
link.</p> |
|
<p>What are the ramifications of this fact for a <em class="test">client</em> of the |
|
JSON API?</p> |
|
<p>What this means is that the JSON API client must know <em class="test">in |
|
advance</em> exactly what other URLs (and request methods) are available |
|
for working with the contact information. If the JSON client is able to |
|
update this contact in some way, it must know how to do so from some |
|
source of information <em class="test">external</em> to the JSON message. If the |
|
contact has a different status, say “Archived”, does this change the |
|
allowable actions? If so, what are the new allowable actions?</p> |
|
<p>The source of all this information might be API documentation, word |
|
of mouth or, if the developer controls both the server and the client, |
|
internal knowledge. But this information is implicit and |
|
<em class="test">outside</em> the response.</p> |
|
<p>Contrast this with the hypermedia (HTML) response. In this case, the |
|
hypermedia client (that is, the browser) needs only to know how to |
|
render the given HTML. It doesn’t need to understand what actions are |
|
available for this contact: they are simply encoded <em class="test">within</em> the |
|
HTML response itself as hypermedia controls. It doesn’t need to |
|
understand what the status field means. In fact, the client doesn’t even |
|
know what a contact is!</p> |
|
<p>The browser, our hypermedia client, simply renders the HTML and |
|
allows the user, who presumably understands the concept of a Contact, to |
|
make a decision on what action to pursue from the actions made available |
|
in the representation.</p> |
|
<p>This difference between the two responses demonstrates the crux of |
|
REST and hypermedia, what makes them so powerful and flexible: clients |
|
(again, web browsers) don’t need to understand <em class="test">anything</em> about |
|
the underlying resources being represented.</p> |
|
<p>Browsers only (only! As if it is easy!) need to understand how to |
|
interpret and display hypermedia, in this case HTML. This gives |
|
hypermedia-based systems unprecedented flexibility in dealing with |
|
changes to both the backing representations and to the system |
|
itself.</p> |
|
<h4 id="_hypermedia_as_the_engine_of_application_state_hateoas">Hypermedia |
|
As The Engine of Application State (HATEOAS)</h4> |
|
<p>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 “the hypermedia constraint” when discussing it.</p> |
|
<p>This constraint is closely related to the previous self-describing |
|
message constraint. Let us consider again the two different |
|
implementations of the endpoint <code>/contacts/42</code>, one returning |
|
HTML and one returning JSON. Let’s update the situation such that the |
|
contact identified by this URL has now been archived.</p> |
|
<p>What do our responses look like?</p> |
|
<p>The first implementation returns the following HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">html</span><span class="ot"> lang</span><span class="op">=</span><span class="st">"en"</span><span class="dt">></span></span> |
|
<span id="cb8-2"><a aria-hidden="true" href="#cb8-2" tabindex="-1"></a><span class="dt"><</span><span class="kw">body</span><span class="dt">></span></span> |
|
<span id="cb8-3"><a aria-hidden="true" href="#cb8-3" tabindex="-1"></a><span class="dt"><</span><span class="kw">h1</span><span class="dt">></span>Joe Smith<span class="dt"></</span><span class="kw">h1</span><span class="dt">></span></span> |
|
<span id="cb8-4"><a aria-hidden="true" href="#cb8-4" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb8-5"><a aria-hidden="true" href="#cb8-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span>Email: joe@example.bar<span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb8-6"><a aria-hidden="true" href="#cb8-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span>Status: Archived<span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb8-7"><a aria-hidden="true" href="#cb8-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb8-8"><a aria-hidden="true" href="#cb8-8" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb8-9"><a aria-hidden="true" href="#cb8-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/42/unarchive"</span><span class="dt">></span>Unarchive<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb8-10"><a aria-hidden="true" href="#cb8-10" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb8-11"><a aria-hidden="true" href="#cb8-11" tabindex="-1"></a><span class="dt"></</span><span class="kw">body</span><span class="dt">></span></span> |
|
<span id="cb8-12"><a aria-hidden="true" href="#cb8-12" tabindex="-1"></a><span class="dt"></</span><span class="kw">html</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<p>The second implementation returns the following JSON |
|
representation:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a><span class="fu">{</span></span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a> <span class="dt">"name"</span><span class="fu">:</span> <span class="st">"Joe Smith"</span><span class="fu">,</span></span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a> <span class="dt">"email"</span><span class="fu">:</span> <span class="st">"joe@example.org"</span><span class="fu">,</span></span> |
|
<span id="cb9-4"><a aria-hidden="true" href="#cb9-4" tabindex="-1"></a> <span class="dt">"status"</span><span class="fu">:</span> <span class="st">"Archived"</span></span> |
|
<span id="cb9-5"><a aria-hidden="true" href="#cb9-5" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div> |
|
</figure> |
|
<p>The important point to notice here is that, by virtue of being a |
|
self-describing message, the HTML response now shows that the “Archive” |
|
operation is no longer available, and a new “Unarchive” operation has |
|
become available. The HTML representation of the contact |
|
<em class="test">encodes</em> the state of the application; it encodes exactly what |
|
can and cannot be done with this particular representation, in a way |
|
that the JSON representation does not.</p> |
|
<p>A client interpreting the JSON response must, again, understand not |
|
only the general concept of a Contact, but also specifically what the |
|
“status” field with the value “Archived” means. It must know exactly |
|
what operations are available on an “Archived” contact, to appropriately |
|
display them to an end user. The state of the application is not encoded |
|
in the response, but rather conveyed through a mix of raw data and side |
|
channel information such as API documentation.</p> |
|
<p>Furthermore, in the majority of front end SPA frameworks today, this |
|
contact information would live <em class="test">in memory</em> in a JavaScript object |
|
representing a model of the contact, while the page data is held in the |
|
browser’s <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model">Document |
|
Object Model</a> (DOM). The DOM would be updated based on changes to |
|
this model, that is, the DOM would “react” to changes to this backing |
|
JavaScript model.</p> |
|
<p>This approach is certainly <em class="test">not</em> using Hypermedia As The |
|
Engine Of Application State: rather, it is using a JavaScript model as |
|
the engine of application state, and synchronizing that model with a |
|
server and with the browser.</p> |
|
<p>With the HTML approach, the Hypermedia is, indeed, The Engine Of |
|
Application State: there is no additional model on the client side, and |
|
all state is expressed directly in the hypermedia, in this case HTML. As |
|
state changes on the server, it is reflected in the representation (that |
|
is, HTML) sent back to the client. The hypermedia client (a browser) |
|
doesn’t know anything about contacts, what the concept of “Archiving” |
|
is, or anything else about the particular domain model for this |
|
response: it simply knows how to render HTML.</p> |
|
<p>Because a hypermedia client doesn’t need to know anything about the |
|
server model beyond how to render hypermedia to a client, it is |
|
incredibly flexible with respect to the representations it receives and |
|
displays to users.</p> |
|
<h4 id="_hateoas_api_churn">HATEOAS & API churn</h4> |
|
<p>This last point is critical to understanding the flexibility of |
|
hypermedia, so let’s look at a practical example of it in action. |
|
Consider a situation where a new feature has been added to the web |
|
application with these two end points. This feature allows you to send a |
|
message to a given Contact.</p> |
|
<p>How would this change each of the two responses—HTML and JSON—from |
|
the server?</p> |
|
<p>The HTML representation might now look like this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb10"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb10-1"><a aria-hidden="true" href="#cb10-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">html</span><span class="ot"> lang</span><span class="op">=</span><span class="st">"en"</span><span class="dt">></span></span> |
|
<span id="cb10-2"><a aria-hidden="true" href="#cb10-2" tabindex="-1"></a><span class="dt"><</span><span class="kw">body</span><span class="dt">></span></span> |
|
<span id="cb10-3"><a aria-hidden="true" href="#cb10-3" tabindex="-1"></a><span class="dt"><</span><span class="kw">h1</span><span class="dt">></span>Joe Smith<span class="dt"></</span><span class="kw">h1</span><span class="dt">></span></span> |
|
<span id="cb10-4"><a aria-hidden="true" href="#cb10-4" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb10-5"><a aria-hidden="true" href="#cb10-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span>Email: joe@example.bar<span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb10-6"><a aria-hidden="true" href="#cb10-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span>Status: Active<span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb10-7"><a aria-hidden="true" href="#cb10-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb10-8"><a aria-hidden="true" href="#cb10-8" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb10-9"><a aria-hidden="true" href="#cb10-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/42/archive"</span><span class="dt">></span>Archive<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb10-10"><a aria-hidden="true" href="#cb10-10" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/42/message"</span><span class="dt">></span>Message<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb10-11"><a aria-hidden="true" href="#cb10-11" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb10-12"><a aria-hidden="true" href="#cb10-12" tabindex="-1"></a><span class="dt"></</span><span class="kw">body</span><span class="dt">></span></span> |
|
<span id="cb10-13"><a aria-hidden="true" href="#cb10-13" tabindex="-1"></a><span class="dt"></</span><span class="kw">html</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<p>The JSON representation, on the other hand, might look like this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb11"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb11-1"><a aria-hidden="true" href="#cb11-1" tabindex="-1"></a><span class="fu">{</span></span> |
|
<span id="cb11-2"><a aria-hidden="true" href="#cb11-2" tabindex="-1"></a> <span class="dt">"name"</span><span class="fu">:</span> <span class="st">"Joe Smith"</span><span class="fu">,</span></span> |
|
<span id="cb11-3"><a aria-hidden="true" href="#cb11-3" tabindex="-1"></a> <span class="dt">"email"</span><span class="fu">:</span> <span class="st">"joe@example.org"</span><span class="fu">,</span></span> |
|
<span id="cb11-4"><a aria-hidden="true" href="#cb11-4" tabindex="-1"></a> <span class="dt">"status"</span><span class="fu">:</span> <span class="st">"Active"</span></span> |
|
<span id="cb11-5"><a aria-hidden="true" href="#cb11-5" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div> |
|
</figure> |
|
<p>Note that, once again, the JSON representation is unchanged. There is |
|
no indication of this new functionality. Instead, a client must |
|
<em class="test">know</em> about this change, presumably via some shared |
|
documentation between the client and the server.</p> |
|
<p>Contrast this with the HTML response. Because of the uniform |
|
interface of the RESTful model and, in particular, because we are using |
|
Hypermedia As The Engine of Application State, no such exchange of |
|
documentation is necessary! Instead, the client (a browser) simply |
|
renders the new HTML with this operation in it, making this operation |
|
available for the end user without any additional coding changes.</p> |
|
<p>A pretty neat trick!</p> |
|
<p>Now, in this case, if the JSON client is not properly updated, the |
|
error state is relatively benign: a new bit of functionality is simply |
|
not made available to users. But consider a more severe change to the |
|
API: what if the archive functionality was removed? Or what if the URLs |
|
or the HTTP methods for these operations changed in some way?</p> |
|
<p>In this case, the JSON client may be broken in a much more serious |
|
manner.</p> |
|
<p>The HTML response, however, would simply be updated to exclude the |
|
removed options or to update the URLs used for them. Clients would see |
|
the new HTML, display it properly, and allow users to select whatever |
|
the new set of operations happens to be. Once again, the uniform |
|
interface of REST has proven to be extremely flexible: despite a |
|
potentially radically new layout for our hypermedia API, clients |
|
continue to work.</p> |
|
<p>An important fact emerges from this: due to this flexibility, |
|
hypermedia APIs <em class="test">do not have the versioning headaches that JSON Data |
|
APIs do</em>.</p> |
|
<p>Once a Hypermedia-Driven Application has been “entered into” (that |
|
is, loaded through some entry point URL), all functionality and |
|
resources are surfaced through self-describing messages. Therefore, |
|
there is no need to exchange documentation with the client: the client |
|
simply renders the hypermedia (in this case HTML) and everything works |
|
out. When a change occurs, there is no need to create a new version of |
|
the API: clients simply retrieve updated hypermedia, which encodes the |
|
new operations and resources in it, and display it to users to work |
|
with.</p> |
|
<h3 id="_layered_system">Layered System</h3> |
|
<p>The final “required” constraint on a RESTful system that we will |
|
consider is The Layered System constraint. This constraint can be found |
|
in <a href="https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_6">Section |
|
5.1.6</a> of Fielding’s dissertation.</p> |
|
<p>To be frank, after the excitement of the uniform interface |
|
constraint, the “layered system” constraint is a bit of a let down. But |
|
it is still worth understanding and it is actually utilized effectively |
|
by The web. The constraint requires that a RESTful architecture be |
|
“layered,” allowing for multiple servers to act as intermediaries |
|
between a client and the eventual “source of truth” server.</p> |
|
<p>These intermediary servers can act as proxies, transform intermediate |
|
requests and responses and so forth.</p> |
|
<p>A common modern example of this layering feature of REST is the use |
|
of Content Delivery Networks (CDNs) to deliver unchanging static assets |
|
to clients more quickly, by storing the response from the origin server |
|
in intermediate servers more closely located to the client making a |
|
request.</p> |
|
<p>This allows content to be delivered more quickly to the end user and |
|
reduces load on the origin server.</p> |
|
<p>Not as exciting for web application developers as the uniform |
|
interface, at least in our opinion, but useful nonetheless.</p> |
|
<h3 id="_an_optional_constraint_code_on_demand">An Optional Constraint: |
|
Code-On-Demand</h3> |
|
<p>We called The Layered System constraint the final “required” |
|
constraint because Fielding mentions one additional constraint on a |
|
RESTful system. This Code On Demand constraint is somewhat awkwardly |
|
described as “optional” (Section 5.1.7).</p> |
|
<p>In this section, Fielding says:</p> |
|
<blockquote> |
|
<p>REST allows client functionality to be extended by downloading and |
|
executing code in the form of applets or scripts. This simplifies |
|
clients by reducing the number of features required to be |
|
pre-implemented. Allowing features to be downloaded after deployment |
|
improves system extensibility. However, it also reduces visibility, and |
|
thus is only an optional constraint within REST.</p> |
|
</blockquote><p class="quote-attribution"> Roy Fielding, Architectural Styles and the Design of Network-based |
|
Software Architectures</p> |
|
<p>So, scripting was and is a native aspect of the original RESTful |
|
model of the web, and thus should of course be allowed in a |
|
Hypermedia-Driven Application.</p> |
|
<p>However, in a Hypermedia-Driven Application the presence of scripting |
|
should <em class="test">not</em> change the fundamental networking model: hypermedia |
|
should continue to be the engine of application state, server |
|
communication should still consist of hypermedia exchanges rather than, |
|
for example, JSON data exchanges, and so on. (JSON Data API’s certainly |
|
have their place; in Chapter 10 we’ll discuss when and how to use |
|
them).</p> |
|
<p>Today, unfortunately, the scripting layer of the web, JavaScript, is |
|
quite often used to <em class="test">replace</em>, rather than augment the hypermedia |
|
model. We will elaborate in a later chapter what scripting that does not |
|
replace the underlying hypermedia system of the web looks like.</p> |
|
<h2 id="_conclusion">Conclusion</h2> |
|
<p>After this deep dive into the components and concepts behind |
|
hypermedia systems — including Roy Fielding’s insights into their |
|
operation — we hope you have much better understanding of REST, and in |
|
particular, of the uniform interface and HATEOAS. We hope you can see |
|
<em class="test">why</em> these characteristics make hypermedia systems so |
|
flexible.</p> |
|
<p>If you were not aware of the full significance of REST and HATEOAS |
|
before now, don’t feel bad: it took some of us over a decade of working |
|
in web development, and building a hypermedia-oriented library to boot, |
|
to understand the special nature of HTML, hypermedia and the web!</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: HTML5 Soup</h2> |
|
<blockquote> |
|
<p>The beginning of wisdom is to call things by their right names.</p> |
|
</blockquote><p class="quote-attribution"> Confucius</p> |
|
<p>Elements like <code><section></code>, |
|
<code><article></code>, <code><nav></code>, |
|
<code><header></code>, <code><footer></code>, |
|
<code><figure></code> have become a sort of shorthand for |
|
HTML.</p> |
|
<p>By using these elements, a page can make false promises, like |
|
<code><article></code> elements being self-contained, reusable |
|
entities, to clients like browsers, search engines and scrapers that |
|
can’t know better. To avoid this:</p> |
|
<ul> |
|
<li><p>Make sure that the element you’re using fits your use case. Check |
|
the HTML spec.</p></li> |
|
<li><p>Don’t try to be specific when you can’t or don’t need to. |
|
Sometimes, <code><div></code> is fine.</p></li> |
|
</ul> |
|
<p>The most authoritative resource for learning about HTML is the HTML |
|
specification. The current specification lives on <a href="https://html.spec.whatwg.org/multipage">https://html.spec.whatwg.org/multipage</a>.<a class="footnote-ref" href="#fn1" id="fnref1" role="doc-noteref"><sup>1</sup></a> There’s no need to rely on hearsay |
|
to keep up with developments in HTML.</p> |
|
<p>Section 4 of the spec features a list of all available elements, |
|
including what they represent, where they can occur, and what they are |
|
allowed to contain. It even tells you when you’re allowed to leave out |
|
closing tags!</p> |
|
</div> |
|
</div> |
|
<section class="footnotes footnotes-end-of-document" id="footnotes" role="doc-endnotes"> |
|
<hr/> |
|
<ol> |
|
<li id="fn1"><p>The single-page version is too slow to load and render |
|
on most computers. There’s also a “developers’ edition” at /dev, but the |
|
standard version has nicer styling.<a class="footnote-back" href="#fnref1" role="doc-backlink">↩︎</a></p></li> |
|
</ol> |
|
</section> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">A Web 1.0 Application</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_picking_a_web_stack">Picking A “Web Stack”</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_python">Python</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_introducing_flask_our_first_route">Introducing Flask: Our First |
|
Route</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_contact_app_functionality">Contact.app Functionality</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_showing_a_searchable_list_of_contacts">Showing A Searchable |
|
List Of Contacts</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_the_list_search_templates">The list & search templates</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_adding_a_new_contact">Adding A New Contact</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_handling_the_post_to_contactsnew">Handling the post to |
|
/contacts/new</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_viewing_the_details_of_a_contact">Viewing The Details Of A |
|
Contact</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_the_contact_detail_template">The Contact Detail Template</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_editing_and_deleting_a_contact">Editing And Deleting A |
|
Contact</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_handling_the_post_to_contactscontact_id">Handling the post to |
|
/contacts/<contact_id>/edit</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_deleting_a_contact">Deleting A Contact</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#_contact_app_implemented">Contact.app… Implemented!</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-web-1-0-application/#html-note-title">HTML Notes: Framework Soup</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>To start our journey into Hypermedia-Driven Applications, we are |
|
going to create a simple contact management web application called |
|
Contact.app. We will start with a basic, “Web 1.0-style” Multi-Page |
|
Application (MPA), in the grand CRUD (Create, Read, Update, Delete) |
|
tradition. It will not be the best contact management application in the |
|
world, but it will be simple and it will do its job.</p> |
|
<p>This application will also be easy to incrementally improve in the |
|
coming chapters by utilizing the hypermedia-oriented library htmx.</p> |
|
<p>By the time we are finished building and enhancing the application, |
|
over the next few chapters, it will have some very slick features that |
|
most developers today would assume requires the use of a SPA JavaScript |
|
framework.</p> |
|
<h2 id="_picking_a_web_stack">Picking A “Web Stack”</h2> |
|
<p>In order to demonstrate how web 1.0 applications work, we need to |
|
pick a server-side language and a library for handling HTTP requests. |
|
Colloquially, this is called our “Server-Side” or “Web” stack, and there |
|
are literally hundreds of options to choose from, many with passionate |
|
followings. You probably have a web framework that you prefer and, while |
|
we wish we could write this book for every possible stack out there, in |
|
the interest of simplicity (and sanity) we can only pick one.</p> |
|
<p>For this book we are going to use the following stack:</p> |
|
<ul> |
|
<li><p><a href="https://www.python.org/">Python</a> as our programming |
|
language.</p></li> |
|
<li><p><a href="https://palletsprojects.com/p/flask/">Flask</a> as our |
|
web framework, allowing us to connect HTTP requests to Python |
|
logic.</p></li> |
|
<li><p><a href="https://palletsprojects.com/p/jinja/">Jinja2</a> for our |
|
server-side templating language, allowing us to render HTML responses |
|
using a familiar and intuitive syntax.</p></li> |
|
</ul> |
|
<p>Why this particular stack?</p> |
|
<p>Python is the most popular programming language in the world, as of |
|
this writing, according to the <a href="https://www.tiobe.com/tiobe-index/">TIOBE index</a>, a respected |
|
measure of programming language popularity. More importantly, Python is |
|
easy to read even if you aren’t familiar with it.</p> |
|
<p>We chose the Flask web framework because it is simple and does not |
|
impose a lot of structure on top of the basics of HTTP request |
|
handling.</p> |
|
<p>This bare-bones approach is a good match for our needs: in other |
|
cases you might consider a more full-featured Python framework, such as |
|
<a href="https://www.djangoproject.com/">Django</a>, which supplies much |
|
more functionality out of the box than Flask does.</p> |
|
<p>By using Flask for our book, we will be able to keep our code focused |
|
on <em class="test">hypermedia exchanges</em>.</p> |
|
<p>We picked Jinja2 templates because they are the default templating |
|
language for Flask. They are simple enough and similar enough to most |
|
other server-side templating languages that most people who are familiar |
|
with any server-side (or client-side) templating library should be able |
|
to understand them quickly and easily.</p> |
|
<p>Even if this combination of technologies isn’t your preferred stack, |
|
please, keep reading: you will learn quite a bit from the patterns we |
|
introduce in the coming chapters and it shouldn’t be hard to map them |
|
into your preferred language and frameworks.</p> |
|
<p>With this stack we will be rendering HTML <em class="test">on the server-side</em> |
|
to return to clients, rather than producing JSON. This is the |
|
traditional approach to building web applications. However, with the |
|
rise of SPAs, this approach is not as widely used a technique as it once |
|
was. Today, as people are rediscovering this style of web applications, |
|
the term “Server-Side Rendering” or SSR is emerging as the way that |
|
people talk about it. This contrasts with “Client-Side Rendering”, that |
|
is, rendering templates in the browser with data retrieved in JSON form |
|
from the server, as is common in SPA libraries.</p> |
|
<p>In Contact.app we will intentionally keep things as simple as |
|
possible to maximize the teaching value of our code: it won’t be |
|
perfectly factored code, but it will be easy to follow for readers, even |
|
if they have little Python experience, and it should be easy to |
|
translate both the application and the techniques demonstrated into your |
|
preferred programming environment.</p> |
|
<h2 id="_python">Python</h2> |
|
<p>Since this book is for learning how to use hypermedia effectively, |
|
we’ll just briefly introduce the various technologies we use |
|
<em class="test">around</em> that hypermedia. This has some obvious drawbacks: if you |
|
aren’t comfortable with Python, for example, some example Python code in |
|
the book may be a bit confusing or mysterious at first.</p> |
|
<p>If you feel like you need a quick introduction to the language before |
|
diving into the code, we recommend the following books and websites:</p> |
|
<ul> |
|
<li><p><a href="https://nostarch.com/python-crash-course-3rd-edition">Python Crash |
|
Course</a> from No Starch Press</p></li> |
|
<li><p><a href="https://learnpythonthehardway.org/python3/">Learn Python |
|
The Hard Way</a> by Zed Shaw</p></li> |
|
<li><p><a href="https://www.py4e.com/">Python For Everybody</a> by Dr. |
|
Charles R. Severance</p></li> |
|
</ul> |
|
<p>We think most web developers, even developers who are unfamiliar with |
|
Python, should be able to follow along with our examples. Most of the |
|
authors of this book hadn’t written much Python before writing it, and |
|
we got the hang of it pretty quickly.</p> |
|
<h2 id="_introducing_flask_our_first_route">Introducing Flask: Our First |
|
Route</h2> |
|
<p>Flask is a simple but flexible web framework for Python. We’ll ease |
|
into it by touching on its core elements.</p> |
|
<p>A Flask application consists of a series of <em class="test">routes</em> tied to |
|
functions that execute when an HTTP request to a given path is made. It |
|
uses a Python feature called “decorators” to declare the route that will |
|
be handled, which is then followed by a function to handle requests to |
|
that route. We’ll use the term “handler” to refer to the functions |
|
associated with a route.</p> |
|
<p>Let’s create our first route definition, a simple “Hello World” |
|
route. In the following Python code you will see the <code>@app</code> |
|
symbol. This is the flask decorator that allows us to set up our routes. |
|
Don’t worry too much about how decorators work in Python, just know that |
|
this feature allows us to map a given <em class="test">path</em> to a particular |
|
function (i.e., handler). The Flask application, when started, will take |
|
HTTP requests and look up the matching handler and invoke it.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a aria-hidden="true" href="#cb1-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/"</span>) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb1-2"><a aria-hidden="true" href="#cb1-2" tabindex="-1"></a><span class="kw">def</span> index(): <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb1-3"><a aria-hidden="true" href="#cb1-3" tabindex="-1"></a> <span class="cf">return</span> <span class="st">"Hello World!"</span> <span class="op"><</span><span class="dv">3</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>A simple “Hello World” route</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Establishes we are mapping the <code>/</code> path as a |
|
route.</p></li> |
|
<li><p>The next method is the handler for that route.</p></li> |
|
<li><p>Returns the string “Hello World!” to the client.</p></li> |
|
</ol> |
|
<p>The <code>route()</code> method on the Flask decorator takes an |
|
argument: the path you wish the route to handle. Here we pass in the |
|
root or <code>/</code> path, as a string, to handle requests to the root |
|
path.</p> |
|
<p>This route declaration is then followed by a simple function |
|
definition, <code>index()</code>. In Python, decorators invoked in this |
|
manner apply to the function immediately following them. Therefore, this |
|
function becomes the “handler” for that route, and will be executed when |
|
an HTTP request to the given path is made.</p> |
|
<p>Note that the name of the function doesn’t matter, we can call it |
|
whatever we’d like so long as it is unique. In this case we chose |
|
<code>index()</code> because that fits with the route we are handling: |
|
the root “index” of the web application.</p> |
|
<p>So we have the <code>index()</code> function immediately following |
|
our route definition for the root, and this will become the handler for |
|
the root URL in our web application.</p> |
|
<p>The handler in this case is dead simple, it just returns a string, |
|
“Hello World!”, to the client. This isn’t hypermedia yet, but as we can |
|
see in <a class="ref" href="#fig-helloworld">[fig-helloworld]</a>, a |
|
browser will render it just fine.</p> |
|
<figure id="fig-helloworld"> |
|
<p><img src="https://hypermedia.systems/images/figure_2-1_hello_world.png"/></p> |
|
<figcaption><p>Hello World!</p></figcaption> |
|
</figure> |
|
<p>Great, there’s our first step into Flask, showing the core technique |
|
we are going to use to respond to HTTP requests: routes mapped to |
|
handlers.</p> |
|
<p>For Contact.app, rather than rendering “Hello World!” at the root |
|
path, we are going to do something a little fancy: we are going to |
|
redirect to another path, the <code>/contacts</code> path. Redirects are |
|
a feature of HTTP that allow you to redirect a client to another |
|
location with an HTTP response.</p> |
|
<p>We are going to display a list of contacts as our root page, and, |
|
arguably, redirecting to the <code>/contacts</code> path to display this |
|
information is a bit more consistent with the notion of resources with |
|
REST. This is a judgement call on our part, and not something we feel is |
|
too important, but it makes sense in terms of routes we will set up |
|
later in the application.</p> |
|
<p>To change our “Hello World” route to a redirect, we only need to |
|
change one line of code:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb2"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb2-1"><a aria-hidden="true" href="#cb2-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/"</span>)</span> |
|
<span id="cb2-2"><a aria-hidden="true" href="#cb2-2" tabindex="-1"></a><span class="kw">def</span> index():</span> |
|
<span id="cb2-3"><a aria-hidden="true" href="#cb2-3" tabindex="-1"></a> <span class="cf">return</span> redirect(<span class="st">"/contacts"</span>) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Changing “Hello World” to a redirect</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Update to a call to <code>redirect()</code></p></li> |
|
</ol> |
|
<p>Now the <code>index()</code> function returns the result of the |
|
Flask-supplied <code>redirect()</code> function with the path we’ve |
|
supplied. In this case the path is <code>/contacts</code>, passed in as |
|
a string argument. Now, if you navigate to the root path, |
|
<code>/</code>, our Flask application will forward you on to the |
|
<code>/contacts</code> path.</p> |
|
<h2 id="_contact_app_functionality">Contact.app Functionality</h2> |
|
<p>Now that we have some understanding of how to define routes, let’s |
|
get down to specifying and then implementing our web application.</p> |
|
<p>What will Contact.app do?</p> |
|
<p>Initially, it will allow users to:</p> |
|
<ul> |
|
<li><p>View a list of contacts, including first name, last name, phone |
|
and email address</p></li> |
|
<li><p>Search the contacts</p></li> |
|
<li><p>Add a new contact</p></li> |
|
<li><p>View the details of a contact</p></li> |
|
<li><p>Edit the details of a contact</p></li> |
|
<li><p>Delete a contact</p></li> |
|
</ul> |
|
<p>So, as you can see, Contact.app is a CRUD application, the sort of |
|
application that is perfect for an old-school web 1.0 approach.</p> |
|
<p>Note that the source code of Contact.app is available on <a href="https://github.com/bigskysoftware/contact-app">GitHub</a>.</p> |
|
<h3 id="_showing_a_searchable_list_of_contacts">Showing A Searchable |
|
List Of Contacts</h3> |
|
<p>Let’s add our first real bit of functionality: the ability to show |
|
all the contacts in our app in a list (really, in a table).</p> |
|
<p>This functionality is going to be found at the <code>/contacts</code> |
|
path, which is the path our previous route is redirecting to.</p> |
|
<p>We will use Flask to route the <code>/contacts</code> path to a |
|
handler function, <code>contacts()</code>. This function will do one of |
|
two things:</p> |
|
<ul> |
|
<li><p>If there is a search term found in the request, it will filter |
|
down to only contacts matching that term</p></li> |
|
<li><p>If not, it will simply list all contacts</p></li> |
|
</ul> |
|
<p>This is a common approach in web 1.0 style applications: the same URL |
|
that displays all instances of some resource also serves as the search |
|
results page for those resources. Taking this approach makes it easy to |
|
reuse the list display that is common to both types of request.</p> |
|
<p>Here is what the code looks like for this handler:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts"</span>)</span> |
|
<span id="cb3-2"><a aria-hidden="true" href="#cb3-2" tabindex="-1"></a><span class="kw">def</span> contacts():</span> |
|
<span id="cb3-3"><a aria-hidden="true" href="#cb3-3" tabindex="-1"></a> search <span class="op">=</span> request.args.get(<span class="st">"q"</span>) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb3-4"><a aria-hidden="true" href="#cb3-4" tabindex="-1"></a> <span class="cf">if</span> search <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span> |
|
<span id="cb3-5"><a aria-hidden="true" href="#cb3-5" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.search(search) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb3-6"><a aria-hidden="true" href="#cb3-6" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb3-7"><a aria-hidden="true" href="#cb3-7" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>() <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb3-8"><a aria-hidden="true" href="#cb3-8" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"index.html"</span>, contacts<span class="op">=</span>contacts_set)</span></code></pre></div> |
|
<figcaption><p>A handler for server-side search</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Look for the query parameter named <code>q</code>, which stands |
|
for “query.”</p></li> |
|
<li><p>If the parameter exists, call the <code>Contact.search()</code> |
|
function with it.</p></li> |
|
<li><p>If not, call the <code>Contact.all()</code> function.</p></li> |
|
<li><p>Pass the result to the <code>index.html</code> template to render |
|
to the client.</p></li> |
|
</ol> |
|
<p>We see the same sort of routing code we saw in our first example, but |
|
we have a more elaborate handler function. First, we check to see if a |
|
search query parameter named <code>q</code> is part of the request.</p> |
|
<dl> |
|
<dt>Query Strings</dt> |
|
<dd> |
|
<p>A “query string” is part of the URL specification. Here is an example |
|
URL with a query string in it: |
|
<code>https://example.com/contacts?q=joe</code>. The query string is |
|
everything after the <code>?</code>, and has a name-value pair format. |
|
In this URL, the query parameter <code>q</code> is set to the string |
|
value <code>joe</code>. In plain HTML, a query string can be included in |
|
a request either by being hardcoded in an anchor tag or, more |
|
dynamically, by using a form tag with a <code>GET</code> request.</p> |
|
</dd> |
|
</dl> |
|
<p>To return to our Flask route, if a query parameter named |
|
<code>q</code> is found, we call out to the <code>search()</code> method |
|
on a <code>Contact</code> model object to do the actual contact search |
|
and return all the matching contacts.</p> |
|
<p>If the query parameter is <em class="test">not</em> found, we simply get all |
|
contacts by invoking the <code>all()</code> method on the |
|
<code>Contact</code> object.</p> |
|
<p>Finally, we render a template, <code>index.html</code> that displays |
|
the given contacts, passing in the results of whichever of these two |
|
functions we end up calling.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>A Note On The Contact Class</strong></p> |
|
</div> |
|
<div> |
|
<p>The <code>Contact</code> Python class we’re using is the “domain |
|
model” or just “model” class for our application, providing the |
|
“business logic” around the management of Contacts.</p> |
|
<p>It could be working with a database (it isn’t) or a simple flat file |
|
(it is), but we’re going to skip over the internal details of the model. |
|
Think of it as a “normal” domain model class, with methods on it that |
|
act in a “normal” manner.</p> |
|
<p>We will treat <code>Contact</code> as a <em class="test">resource</em>, and focus |
|
on how to effectively provide hypermedia representations of that |
|
resource to clients.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h4 id="_the_list_search_templates">The list & search templates</h4> |
|
<p>Now that we have our handler logic written, we’ll create a template |
|
to render HTML in our response to the client. At a high level, our HTML |
|
response needs to have the following elements:</p> |
|
<ul> |
|
<li><p>A list of any matching or all contacts.</p></li> |
|
<li><p>A search box where a user may type and submit search |
|
terms.</p></li> |
|
<li><p>A bit of surrounding “chrome”: a header and footer for the |
|
website that will be the same regardless of the page you are |
|
on.</p></li> |
|
</ul> |
|
<p>We are using the Jinja2 templating language, which has the following |
|
features:</p> |
|
<ul> |
|
<li><p>We can use double-curly braces, <code>{{ }}</code>, to embed |
|
expression values in the template.</p></li> |
|
<li><p>we can use curly-percents, <code>{% %}</code>, for directives, |
|
like iteration or including other content.</p></li> |
|
</ul> |
|
<p>Beyond this basic syntax, Jinja2 is very similar to other templating |
|
languages used to generate content, and should be easy to follow for |
|
most web developers.</p> |
|
<p>Let’s look at the first few lines of code in the |
|
<code>index.html</code> template:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb4"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb4-1"><a aria-hidden="true" href="#cb4-1" tabindex="-1"></a>{% extends 'layout.html' %} <span class="er"><</span>1></span> |
|
<span id="cb4-2"><a aria-hidden="true" href="#cb4-2" tabindex="-1"></a></span> |
|
<span id="cb4-3"><a aria-hidden="true" href="#cb4-3" tabindex="-1"></a>{% block content %} <span class="er"><</span>2></span> |
|
<span id="cb4-4"><a aria-hidden="true" href="#cb4-4" tabindex="-1"></a></span> |
|
<span id="cb4-5"><a aria-hidden="true" href="#cb4-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"get"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"tool-bar"</span><span class="dt">></span> <span class="er"><</span>3></span> |
|
<span id="cb4-6"><a aria-hidden="true" href="#cb4-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"search"</span><span class="dt">></span>Search Term<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb4-7"><a aria-hidden="true" href="#cb4-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span></span> |
|
<span id="cb4-8"><a aria-hidden="true" href="#cb4-8" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span><span class="ot"> </span><span class="dt">/></span> <span class="er"><</span>4></span> |
|
<span id="cb4-9"><a aria-hidden="true" href="#cb4-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> type</span><span class="op">=</span><span class="st">"submit"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"Search"</span><span class="dt">/></span></span> |
|
<span id="cb4-10"><a aria-hidden="true" href="#cb4-10" tabindex="-1"></a> <span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Start of index.html</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Set the layout template for this template.</p></li> |
|
<li><p>Delimit the content to be inserted into the layout.</p></li> |
|
<li><p>Create a search form that will issue an HTTP <code>GET</code> to |
|
<code>/contacts</code>.</p></li> |
|
<li><p>Create an input for a user to type search queries.</p></li> |
|
</ol> |
|
<p>The first line of code references a base template, |
|
<code>layout.html</code>, with the <code>extends</code> directive. This |
|
layout template provides the layout for the page (again, sometimes |
|
called “the chrome”): it wraps the template content in an |
|
<code><html></code> tag, imports any necessary CSS and JavaScript |
|
in a <code><head></code> element, places a |
|
<code><body></code> tag around the main content and so forth. All |
|
the common content wrapped around the “normal” content for the entire |
|
application is located in this file.</p> |
|
<p>The next line of code declares the <code>content</code> section of |
|
this template. This content block is used by the |
|
<code>layout.html</code> template to inject the content of |
|
<code>index.html</code> within its HTML.</p> |
|
<p>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 <code>GET</code> request to the |
|
<code>/contacts</code> path. The form itself contains a label and an |
|
input with the name “q.” This input’s value will be submitted with the |
|
<code>GET</code> request to the <code>/contacts</code> path, as a query |
|
string (since this is a <code>GET</code> request.)</p> |
|
<p>Note that the value of this input is set to the Jinja expression |
|
<code>{{ request.args.get('q') or '' }}</code>. This expression is |
|
evaluated by Jinja and will insert the request value of “q” as the |
|
input’s value, if it exists. This will “preserve” the search value when |
|
a user does a search, so that when the results of a search are rendered |
|
the text input contains the term that was searched for. This makes for a |
|
better user experience since the user can see exactly what the current |
|
results match, rather than having a blank text box at the top of the |
|
screen.</p> |
|
<p>Finally, we have a submit-type input. This will render as a button |
|
and, when it is clicked, it will trigger the form to issue an HTTP |
|
request.</p> |
|
<p>This search interface forms the top of our contact page. Following it |
|
is a table of contacts, either all contacts or the contacts that match |
|
the search, if a search was done.</p> |
|
<p>Here is what the template code for the contact table looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">table</span><span class="dt">></span></span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">thead</span><span class="dt">></span></span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>First <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>Last <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>Phone <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>Email <span class="dt"><</span><span class="kw">th</span><span class="dt">/></span> <span class="er"><</span>1></span> |
|
<span id="cb5-5"><a aria-hidden="true" href="#cb5-5" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb5-6"><a aria-hidden="true" href="#cb5-6" tabindex="-1"></a> <span class="dt"></</span><span class="kw">thead</span><span class="dt">></span></span> |
|
<span id="cb5-7"><a aria-hidden="true" href="#cb5-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tbody</span><span class="dt">></span></span> |
|
<span id="cb5-8"><a aria-hidden="true" href="#cb5-8" tabindex="-1"></a> {% for contact in contacts %} <span class="er"><</span>2></span> |
|
<span id="cb5-9"><a aria-hidden="true" href="#cb5-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb5-10"><a aria-hidden="true" href="#cb5-10" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.first }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb5-11"><a aria-hidden="true" href="#cb5-11" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.last }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb5-12"><a aria-hidden="true" href="#cb5-12" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.phone }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb5-13"><a aria-hidden="true" href="#cb5-13" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.email }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span> <span class="er"><</span>3></span> |
|
<span id="cb5-14"><a aria-hidden="true" href="#cb5-14" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb5-15"><a aria-hidden="true" href="#cb5-15" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>View<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">td</span><span class="dt">></span> <span class="er"><</span>4></span> |
|
<span id="cb5-16"><a aria-hidden="true" href="#cb5-16" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb5-17"><a aria-hidden="true" href="#cb5-17" tabindex="-1"></a> {% endfor %}</span> |
|
<span id="cb5-18"><a aria-hidden="true" href="#cb5-18" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tbody</span><span class="dt">></span></span> |
|
<span id="cb5-19"><a aria-hidden="true" href="#cb5-19" tabindex="-1"></a><span class="dt"></</span><span class="kw">table</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The contacts table</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Output some headers for our table.</p></li> |
|
<li><p>Iterate over the contacts that were passed in to the |
|
template.</p></li> |
|
<li><p>Output the values of the current contact, first name, last name, |
|
etc.</p></li> |
|
<li><p>An “operations” column, with links to edit or view the contact |
|
details.</p></li> |
|
</ol> |
|
<p>This is the core of the page: we construct a table with appropriate |
|
headers matching the data we are going to show for each contact. We |
|
iterate over the contacts that were passed into the template by the |
|
handler method using the <code>for</code> loop directive in Jinja2. We |
|
then construct a series of rows, one for each contact, where we render |
|
the first and last name, phone and email of the contact as table cells |
|
in the row.</p> |
|
<p>Additionally, we have a table cell that includes two links:</p> |
|
<ul> |
|
<li><p>A link to the “Edit” page for the contact, located at |
|
<code>/contacts/{{ contact.id }}/edit</code> (e.g., For the contact with |
|
id 42, the edit link will point to |
|
<code>/contacts/42/edit</code>)</p></li> |
|
<li><p>A link to the “View” page for the contact |
|
<code>/contacts/{{ contact.id }}</code> (using our previous contact |
|
example, the view page would be at <code>/contacts/42</code>)</p></li> |
|
</ul> |
|
<p>Finally, we have a bit of end-matter: a link to add a new contact and |
|
a Jinja2 directive to end the <code>content</code> block:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb6"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb6-1"><a aria-hidden="true" href="#cb6-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb6-2"><a aria-hidden="true" href="#cb6-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/new"</span><span class="dt">></span>Add Contact<span class="dt"></</span><span class="kw">a</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb6-3"><a aria-hidden="true" href="#cb6-3" tabindex="-1"></a> <span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb6-4"><a aria-hidden="true" href="#cb6-4" tabindex="-1"></a></span> |
|
<span id="cb6-5"><a aria-hidden="true" href="#cb6-5" tabindex="-1"></a>{% endblock %} <span class="er"><</span>2></span></code></pre></div> |
|
<figcaption><p>The “add contact” link</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Link to the page that allows you to create a new |
|
contact.</p></li> |
|
<li><p>The closing element of the <code>content</code> block.</p></li> |
|
</ol> |
|
<p>And that’s our complete template. Using this simple server-side |
|
template, in combination with our handler method, we can respond with an |
|
HTML <em class="test">representation</em> of all the contacts requested. So far, so |
|
hypermedia.</p> |
|
<p><a class="ref" href="#fig-contactapp">[fig-contactapp]</a> is what |
|
the template looks like, rendered with a bit of contact information.</p> |
|
<figure id="fig-contactapp"> |
|
<p><img src="https://hypermedia.systems/images/figure_2-2_table_etc.png"/></p> |
|
<figcaption><p>Contact.app</p></figcaption> |
|
</figure> |
|
<p>Now, our application won’t win any design awards at this point, but |
|
notice that our template, when rendered, provides all the functionality |
|
necessary to see all the contacts and search them, and also provides |
|
links to edit them, view details of them or even create a new one.</p> |
|
<p>And it does all this without the client (that is, the browser) |
|
knowing a thing about what contacts are or how to work with them. |
|
Everything is encoded <em class="test">in</em> the hypermedia. A web browser |
|
accessing this application just knows how to issue HTTP requests and |
|
then render HTML, nothing more about the specifics of our applications |
|
end points or underlying domain model.</p> |
|
<p>As simple as our application is at this point, it is thoroughly |
|
RESTful.</p> |
|
<h3 id="_adding_a_new_contact">Adding A New Contact</h3> |
|
<p>The next bit of functionality that we will add to our application is |
|
the ability to add new contacts. To do this, we are going to need to |
|
handle that <code>/contacts/new</code> URL referenced in the “Add |
|
Contact” link above. Note that when a user clicks on that link, the |
|
browser will issue a <code>GET</code> request to the |
|
<code>/contacts/new</code> URL.</p> |
|
<p>All the other routes we have so far use <code>GET</code> as well, but |
|
we are actually going to use two different HTTP methods for this bit of |
|
functionality: an HTTP <code>GET</code> to render a form for adding a |
|
new contact, and then an HTTP <code>POST</code> <em class="test">to the same |
|
path</em> to actually create the contact, so we are going to be explicit |
|
about the HTTP method we want to handle when we declare this route.</p> |
|
<p>Here is the code:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/new"</span>, methods<span class="op">=</span>[<span class="st">'GET'</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a><span class="kw">def</span> contacts_new_get():</span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"new.html"</span>, contact<span class="op">=</span>Contact()) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>The “new contact” GET route</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Declare a route, explicitly handling <code>GET</code> requests to |
|
this path.</p></li> |
|
<li><p>Render the <code>new.html</code> template, passing in a new |
|
contact object.</p></li> |
|
</ol> |
|
<p>Simple enough. We just render a <code>new.html</code> template with a |
|
new Contact. (<code>Contact()</code> is how you construct a new instance |
|
of the <code>Contact</code> class in Python, if you aren’t familiar with |
|
it.)</p> |
|
<p>While the handler code for this route is very simple, the |
|
<code>new.html</code> template is more complicated.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p>For the remaining templates we are going to omit the layout directive |
|
and the content block declaration, but you can assume they are the same |
|
unless we say otherwise. This will let us focus on the “meat” of the |
|
template.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>If you are familiar with HTML you are probably expecting a form |
|
element here, and you will not be disappointed. We are going to use the |
|
standard form hypermedia control for collecting contact information and |
|
submitting it to the server.</p> |
|
<p>Here is what our HTML looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts/new"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"post"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb8-2"><a aria-hidden="true" href="#cb8-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">fieldset</span><span class="dt">></span></span> |
|
<span id="cb8-3"><a aria-hidden="true" href="#cb8-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">legend</span><span class="dt">></span>Contact Values<span class="dt"></</span><span class="kw">legend</span><span class="dt">></span></span> |
|
<span id="cb8-4"><a aria-hidden="true" href="#cb8-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb8-5"><a aria-hidden="true" href="#cb8-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"email"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">label</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb8-6"><a aria-hidden="true" href="#cb8-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"email"</span></span> |
|
<span id="cb8-7"><a aria-hidden="true" href="#cb8-7" tabindex="-1"></a><span class="ot"> type</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Email"</span></span> |
|
<span id="cb8-8"><a aria-hidden="true" href="#cb8-8" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.email or '' }}"</span><span class="dt">></span> <span class="er"><</span>3></span> |
|
<span id="cb8-9"><a aria-hidden="true" href="#cb8-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span></span> |
|
<span id="cb8-10"><a aria-hidden="true" href="#cb8-10" tabindex="-1"></a> {{ contact.errors['email'] }} <span class="er"><</span>4></span> |
|
<span id="cb8-11"><a aria-hidden="true" href="#cb8-11" tabindex="-1"></a> <span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb8-12"><a aria-hidden="true" href="#cb8-12" tabindex="-1"></a> <span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The “new contact” form</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A form that submits to the <code>/contacts/new</code> path, using |
|
an HTTP <code>POST</code>.</p></li> |
|
<li><p>A label for the first form input.</p></li> |
|
<li><p>The first form input, of type email.</p></li> |
|
<li><p>Any error messages associated with this field.</p></li> |
|
</ol> |
|
<p>In the first line of code we create a form that will submit back |
|
<em class="test">to the same path</em> that we are handling: |
|
<code>/contacts/new</code>. Rather than issuing an HTTP <code>GET</code> |
|
to this path, however, we will issue an HTTP <code>POST</code> to it. |
|
Using a <code>POST</code> in this manner will signal to the server that |
|
we want to create a new Contact, rather than get a form for creating |
|
one.</p> |
|
<p>We then have a label (always a good practice!) and an input that |
|
captures the email of the contact being created. The name of the input |
|
is <code>email</code> and, when this form is submitted, the value of |
|
this input will be submitted in the <code>POST</code> request, |
|
associated with the <code>email</code> key.</p> |
|
<p>Next we have inputs for the other fields for contacts:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"first_name"</span><span class="dt">></span>First Name<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"first_name"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"first_name"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span></span> |
|
<span id="cb9-4"><a aria-hidden="true" href="#cb9-4" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"First Name"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.first or '' }}"</span><span class="dt">></span></span> |
|
<span id="cb9-5"><a aria-hidden="true" href="#cb9-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['first'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb9-6"><a aria-hidden="true" href="#cb9-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb9-7"><a aria-hidden="true" href="#cb9-7" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb9-8"><a aria-hidden="true" href="#cb9-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"last_name"</span><span class="dt">></span>Last Name<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb9-9"><a aria-hidden="true" href="#cb9-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"last_name"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"last_name"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span></span> |
|
<span id="cb9-10"><a aria-hidden="true" href="#cb9-10" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Last Name"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.last or '' }}"</span><span class="dt">></span></span> |
|
<span id="cb9-11"><a aria-hidden="true" href="#cb9-11" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['last'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb9-12"><a aria-hidden="true" href="#cb9-12" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb9-13"><a aria-hidden="true" href="#cb9-13" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb9-14"><a aria-hidden="true" href="#cb9-14" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"phone"</span><span class="dt">></span>Phone<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb9-15"><a aria-hidden="true" href="#cb9-15" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"phone"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"phone"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Phone"</span></span> |
|
<span id="cb9-16"><a aria-hidden="true" href="#cb9-16" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.phone or '' }}"</span><span class="dt">></span></span> |
|
<span id="cb9-17"><a aria-hidden="true" href="#cb9-17" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['phone'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb9-18"><a aria-hidden="true" href="#cb9-18" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Inputs and labels for the “new contact” |
|
form</p></figcaption> |
|
</figure> |
|
<p>Finally, we have a button that will submit the form, the end of the |
|
form tag, and a link back to the main contacts table:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb10"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb10-1"><a aria-hidden="true" href="#cb10-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="dt">></span>Save<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb10-2"><a aria-hidden="true" href="#cb10-2" tabindex="-1"></a> <span class="dt"></</span><span class="kw">fieldset</span><span class="dt">></span></span> |
|
<span id="cb10-3"><a aria-hidden="true" href="#cb10-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span> |
|
<span id="cb10-4"><a aria-hidden="true" href="#cb10-4" tabindex="-1"></a></span> |
|
<span id="cb10-5"><a aria-hidden="true" href="#cb10-5" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb10-6"><a aria-hidden="true" href="#cb10-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts"</span><span class="dt">></span>Back<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb10-7"><a aria-hidden="true" href="#cb10-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The submit button for the “new contact” |
|
form</p></figcaption> |
|
</figure> |
|
<p>It is easy to miss in this straight-forward example: we are seeing |
|
the flexibility of hypermedia in action.</p> |
|
<p>If we add a new field, remove a field, or change the logic around how |
|
fields are validated or work with one another, this new state of affairs |
|
would be reflected in the new hypermedia representation given to users. |
|
A user would see the updated new form and be able to work with these new |
|
features, with no software update required.</p> |
|
<h4 id="_handling_the_post_to_contactsnew">Handling the post to |
|
/contacts/new</h4> |
|
<p>The next step in our application is to handle the <code>POST</code> |
|
that this form makes to <code>/contacts/new</code>.</p> |
|
<p>To do so, we need to add another route to our application that |
|
handles the <code>/contacts/new</code> path. The new route will handle |
|
an HTTP <code>POST</code> method instead of an HTTP <code>GET</code>. We |
|
will use the submitted form values to attempt to create a new |
|
Contact.</p> |
|
<p>If we are successful in creating a Contact, we will redirect the user |
|
to the list of contacts and show a success message. If we aren’t |
|
successful, then we will render the new contact form again with whatever |
|
values the user entered and render error messages about what issues need |
|
to be fixed so that the user can correct them.</p> |
|
<p>Here is our new request handler:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb11"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb11-1"><a aria-hidden="true" href="#cb11-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/new"</span>, methods<span class="op">=</span>[<span class="st">'POST'</span>])</span> |
|
<span id="cb11-2"><a aria-hidden="true" href="#cb11-2" tabindex="-1"></a><span class="kw">def</span> contacts_new():</span> |
|
<span id="cb11-3"><a aria-hidden="true" href="#cb11-3" tabindex="-1"></a> c <span class="op">=</span> Contact(</span> |
|
<span id="cb11-4"><a aria-hidden="true" href="#cb11-4" tabindex="-1"></a> <span class="va">None</span>,</span> |
|
<span id="cb11-5"><a aria-hidden="true" href="#cb11-5" tabindex="-1"></a> request.form[<span class="st">'first_name'</span>],</span> |
|
<span id="cb11-6"><a aria-hidden="true" href="#cb11-6" tabindex="-1"></a> request.form[<span class="st">'last_name'</span>],</span> |
|
<span id="cb11-7"><a aria-hidden="true" href="#cb11-7" tabindex="-1"></a> request.form[<span class="st">'phone'</span>],</span> |
|
<span id="cb11-8"><a aria-hidden="true" href="#cb11-8" tabindex="-1"></a> request.form[<span class="st">'email'</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb11-9"><a aria-hidden="true" href="#cb11-9" tabindex="-1"></a> <span class="cf">if</span> c.save(): <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb11-10"><a aria-hidden="true" href="#cb11-10" tabindex="-1"></a> flash(<span class="st">"Created New Contact!"</span>)</span> |
|
<span id="cb11-11"><a aria-hidden="true" href="#cb11-11" tabindex="-1"></a> <span class="cf">return</span> redirect(<span class="st">"/contacts"</span>) <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb11-12"><a aria-hidden="true" href="#cb11-12" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb11-13"><a aria-hidden="true" href="#cb11-13" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"new.html"</span>, contact<span class="op">=</span>c) <span class="op"><</span><span class="dv">4</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>The “new contact” controller code</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We construct a new contact object with the values from the |
|
form.</p></li> |
|
<li><p>We try to save it.</p></li> |
|
<li><p>On success, “flash” a success message & redirect to the |
|
<code>/contacts</code> page.</p></li> |
|
<li><p>On failure, re-render the form, showing any errors to the |
|
user.</p></li> |
|
</ol> |
|
<p>The logic in this handler is a bit more complex than other methods we |
|
have seen. The first thing we do is create a new Contact, again using |
|
the <code>Contact()</code> syntax in Python to construct the object. We |
|
pass in the values that the user submitted in the form by using the |
|
<code>request.form</code> object, a feature provided by Flask.</p> |
|
<p>This <code>request.form</code> allows us to access submitted form |
|
values in an easy and convenient way, by simply passing in the same name |
|
associated with the various inputs.</p> |
|
<p>We also pass in <code>None</code> as the first value to the |
|
<code>Contact</code> constructor. This is the “id” parameter, and by |
|
passing in <code>None</code> we are signaling that it is a new contact, |
|
and needs to have an ID generated for it. (Again, we are not going into |
|
the details of how this model object is implemented, our only concern is |
|
using it to generate hypermedia responses.)</p> |
|
<p>Next, we call the <code>save()</code> method on the Contact object. |
|
This method returns <code>true</code> if the save is successful, and |
|
<code>false</code> if the save is unsuccessful (for example, a bad email |
|
was submitted by the user).</p> |
|
<p>If we are able to save the contact (that is, there were no validation |
|
errors), we create a <em class="test">flash</em> message indicating success, and |
|
redirect the browser back to the list page. A “flash” is a common |
|
feature in web frameworks that allows you to store a message that will |
|
be available on the <em class="test">next</em> request, typically in a cookie or in a |
|
session store.</p> |
|
<p>Finally, if we are unable to save the contact, we re-render the |
|
<code>new.html</code> template with the contact. This will show the same |
|
template as above, but the inputs will be filled in with the submitted |
|
values, and any errors associated with the fields will be rendered to |
|
feedback to the user as to what validation failed.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>The Post/Redirect/Get Pattern</strong></p> |
|
</div> |
|
<div> |
|
<p>This handler implements a common strategy in web 1.0-style |
|
development called the <a href="https://en.wikipedia.org/wiki/Post/Redirect/Get">Post/Redirect/Get</a> |
|
or PRG pattern. By issuing an HTTP redirect once a contact has been |
|
created and forwarding the browser on to another location, we ensure |
|
that the <code>POST</code> does not end up in the browsers request |
|
cache.</p> |
|
<p>This means that if the user accidentally (or intentionally) refreshes |
|
the page, the browser will not submit another <code>POST</code>, |
|
potentially creating another contact. Instead, it will issue the |
|
<code>GET</code> that we redirect to, which should be side-effect |
|
free.</p> |
|
<p>We will use the PRG pattern in a few different places in this |
|
book.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>OK, so we have our server-side logic set up to save contacts. And, |
|
believe it or not, this is about as complicated as our handler logic |
|
will get, even when we look at adding more sophisticated htmx-driven |
|
behaviors.</p> |
|
<h3 id="_viewing_the_details_of_a_contact">Viewing The Details Of A |
|
Contact</h3> |
|
<p>The next piece of functionality we will implement is the detail page |
|
for a Contact. The user will navigate to this page by clicking the |
|
“View” link in one of the rows in the list of contacts. This will take |
|
them to the path <code>/contacts/<contact id></code> (e.g., |
|
<code>/contacts/42</code>).</p> |
|
<p>This is a common pattern in web development: contacts are treated as |
|
resources and the URLs around these resources are organized in a |
|
coherent manner.</p> |
|
<ul> |
|
<li><p>If you wish to view all contacts, you issue a <code>GET</code> to |
|
<code>/contacts</code>.</p></li> |
|
<li><p>If you want a hypermedia representation allowing you to create a |
|
new contact, you issue a <code>GET</code> to |
|
<code>/contacts/new</code>.</p></li> |
|
<li><p>If you wish to view a specific contact (with, say, an id of |
|
<code>42), you issue a `GET</code> to |
|
<code>/contacts/42</code>.</p></li> |
|
</ul> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>The Eternal Bike Shed of URL Design</strong></p> |
|
</div> |
|
<div> |
|
<p>It is easy to quibble about the particulars of the path scheme you |
|
use for your application:</p> |
|
<p>“Should we <code>POST</code> to <code>/contacts/new</code> or to |
|
<code>/contacts</code>?”</p> |
|
<p>We have seen many arguments online and in person advocating for one |
|
approach versus another. We feel it is more important to understand the |
|
overarching idea of <em class="test">resources</em> and <em class="test">hypermedia |
|
representations</em>, rather than getting worked up about the smaller |
|
details of your URL design.</p> |
|
<p>We recommend you just pick a reasonable, resource-oriented URL layout |
|
you like and then stay consistent. Remember, in a hypermedia system, you |
|
can always change your endpoints later, because you are using hypermedia |
|
as the engine of application state!</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>Our handler logic for the detail route is going to be <em class="test">very</em> |
|
simple: we just look the Contact up by id, which is embedded in the path |
|
of the URL for the route. To extract this ID we are going to need to |
|
introduce a final bit of Flask functionality: the ability to call out |
|
pieces of a path and have them automatically extracted and passed in to |
|
a handler function.</p> |
|
<p>Here is what the code looks like, just a few lines of simple |
|
Python:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb12"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb12-1"><a aria-hidden="true" href="#cb12-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/<contact_id>"</span>) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb12-2"><a aria-hidden="true" href="#cb12-2" tabindex="-1"></a><span class="kw">def</span> contacts_view(contact_id<span class="op">=</span><span class="dv">0</span>): <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb12-3"><a aria-hidden="true" href="#cb12-3" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id) <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb12-4"><a aria-hidden="true" href="#cb12-4" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"show.html"</span>, contact<span class="op">=</span>contact) <span class="op"><</span><span class="dv">4</span><span class="op">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>Map the path, with a path variable named |
|
<code>contact_id</code>.</p></li> |
|
<li><p>The handler takes the value of this path parameter.</p></li> |
|
<li><p>Look up the corresponding contact.</p></li> |
|
<li><p>Render the <code>show.html</code> template.</p></li> |
|
</ol> |
|
<p>You can see the syntax for extracting values from the path in the |
|
first line of code: you enclose the part of the path you wish to extract |
|
in <code><></code> and give it a name. This component of the path |
|
will be extracted and then passed into the handler function, via the |
|
parameter with the same name.</p> |
|
<p>So, if you were to navigate to the path <code>/contacts/42</code>, |
|
the value <code>42</code> would be passed into the |
|
<code>contacts_view()</code> function for the value of |
|
<code>contact_id</code>.</p> |
|
<p>Once we have the id of the contact we want to look up, we load it up |
|
using the <code>find</code> method on the <code>Contact</code> object. |
|
We then pass this contact into the <code>show.html</code> template and |
|
render a response.</p> |
|
<h3 id="_the_contact_detail_template">The Contact Detail Template</h3> |
|
<p>Our <code>show.html</code> template is relatively simple, just |
|
showing the same information as the table but in a slightly different |
|
format (perhaps for printing). If we add functionality like “notes” to |
|
the application later on, this will give us a good place to do so.</p> |
|
<p>Again, we will omit the “chrome” of the template and focus on the |
|
meat:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb13"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb13-1"><a aria-hidden="true" href="#cb13-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">h1</span><span class="dt">></span>{{contact.first}} {{contact.last}}<span class="dt"></</span><span class="kw">h1</span><span class="dt">></span></span> |
|
<span id="cb13-2"><a aria-hidden="true" href="#cb13-2" tabindex="-1"></a></span> |
|
<span id="cb13-3"><a aria-hidden="true" href="#cb13-3" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb13-4"><a aria-hidden="true" href="#cb13-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span>Phone: {{contact.phone}}<span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb13-5"><a aria-hidden="true" href="#cb13-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span>Email: {{contact.email}}<span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb13-6"><a aria-hidden="true" href="#cb13-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb13-7"><a aria-hidden="true" href="#cb13-7" tabindex="-1"></a></span> |
|
<span id="cb13-8"><a aria-hidden="true" href="#cb13-8" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb13-9"><a aria-hidden="true" href="#cb13-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{contact.id}}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb13-10"><a aria-hidden="true" href="#cb13-10" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts"</span><span class="dt">></span>Back<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb13-11"><a aria-hidden="true" href="#cb13-11" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The “contact details” template</p></figcaption> |
|
</figure> |
|
<p>We simply render a First Name and Last Name header, with the |
|
additional contact information below it, and a couple of links: a link |
|
to edit the contact and a link to navigate back to the full list of |
|
contacts.</p> |
|
<h3 id="_editing_and_deleting_a_contact">Editing And Deleting A |
|
Contact</h3> |
|
<p>Next up we will tackle the functionality on the other end of that |
|
“Edit” link. Editing a contact is going to look very similar to creating |
|
a new contact. As with adding a new contact, we are going to need two |
|
routes that handle the same path, but using different HTTP methods: a |
|
<code>GET</code> to <code>/contacts/<contact_id>/edit</code> will |
|
return a form allowing you to edit the contact and a <code>POST</code> |
|
to that path will update it.</p> |
|
<p>We are also going to piggyback the ability to delete a contact along |
|
with this editing functionality. To do this we will need to handle a |
|
<code>POST</code> to |
|
<code>/contacts/<contact_id>/delete</code>.</p> |
|
<p>Let’s look at the code to handle the <code>GET</code>, which, again, |
|
will return an HTML representation of an editing interface for the given |
|
resource:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb14"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb14-1"><a aria-hidden="true" href="#cb14-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/<contact_id>/edit"</span>, methods<span class="op">=</span>[<span class="st">"GET"</span>])</span> |
|
<span id="cb14-2"><a aria-hidden="true" href="#cb14-2" tabindex="-1"></a><span class="kw">def</span> contacts_edit_get(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb14-3"><a aria-hidden="true" href="#cb14-3" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id)</span> |
|
<span id="cb14-4"><a aria-hidden="true" href="#cb14-4" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"edit.html"</span>, contact<span class="op">=</span>contact)</span></code></pre></div> |
|
<figcaption><p>The “edit contact” controller code</p></figcaption> |
|
</figure> |
|
<p>As you can see this looks a lot like our “Show Contact” |
|
functionality. In fact, it is nearly identical except for the template: |
|
here we render <code>edit.html</code> rather than |
|
<code>show.html</code>.</p> |
|
<p>While our handler code looked similar to the “Show Contact” |
|
functionality, the <code>edit.html</code> template is going to look very |
|
similar to the template for the “New Contact” functionality: we will |
|
have a form that submits updated contact values to the same “edit” URL |
|
and that presents all the fields of a contact as inputs for editing, |
|
along with any error messages.</p> |
|
<p>Here is the first bit of the form:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb15"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb15-1"><a aria-hidden="true" href="#cb15-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"post"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb15-2"><a aria-hidden="true" href="#cb15-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">fieldset</span><span class="dt">></span></span> |
|
<span id="cb15-3"><a aria-hidden="true" href="#cb15-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">legend</span><span class="dt">></span>Contact Values<span class="dt"></</span><span class="kw">legend</span><span class="dt">></span></span> |
|
<span id="cb15-4"><a aria-hidden="true" href="#cb15-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb15-5"><a aria-hidden="true" href="#cb15-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"email"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb15-6"><a aria-hidden="true" href="#cb15-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span></span> |
|
<span id="cb15-7"><a aria-hidden="true" href="#cb15-7" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Email"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.email }}"</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb15-8"><a aria-hidden="true" href="#cb15-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['email'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb15-9"><a aria-hidden="true" href="#cb15-9" tabindex="-1"></a> <span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The “edit contact” form start</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Issue a <code>POST</code> to the |
|
<code>/contacts/{{ contact.id }}/edit</code> path.</p></li> |
|
<li><p>As with the <code>new.html</code> page, the input is tied to the |
|
contact’s email.</p></li> |
|
</ol> |
|
<p>This HTML is nearly identical to our <code>new.html</code> form, |
|
except that this form is going to submit a <code>POST</code> to a |
|
different path, based on the id of the contact that we want to update. |
|
(It’s worth mentioning here that, rather than <code>POST</code>, we |
|
would prefer to use a <code>PUT</code> or <code>PATCH</code>, but those |
|
are not available in plain HTML.)</p> |
|
<p>Following this we have the remainder of our form, again very similar |
|
to the <code>new.html</code> template, and our button to submit the |
|
form.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb16"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb16-1"><a aria-hidden="true" href="#cb16-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb16-2"><a aria-hidden="true" href="#cb16-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"first_name"</span><span class="dt">></span>First Name<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb16-3"><a aria-hidden="true" href="#cb16-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"first_name"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"first_name"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span></span> |
|
<span id="cb16-4"><a aria-hidden="true" href="#cb16-4" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"First Name"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.first }}"</span><span class="dt">></span></span> |
|
<span id="cb16-5"><a aria-hidden="true" href="#cb16-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['first'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb16-6"><a aria-hidden="true" href="#cb16-6" tabindex="-1"></a> <span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb16-7"><a aria-hidden="true" href="#cb16-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb16-8"><a aria-hidden="true" href="#cb16-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"last_name"</span><span class="dt">></span>Last Name<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb16-9"><a aria-hidden="true" href="#cb16-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"last_name"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"last_name"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span></span> |
|
<span id="cb16-10"><a aria-hidden="true" href="#cb16-10" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Last Name"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.last }}"</span><span class="dt">></span></span> |
|
<span id="cb16-11"><a aria-hidden="true" href="#cb16-11" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['last'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb16-12"><a aria-hidden="true" href="#cb16-12" tabindex="-1"></a> <span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb16-13"><a aria-hidden="true" href="#cb16-13" tabindex="-1"></a> <span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb16-14"><a aria-hidden="true" href="#cb16-14" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"phone"</span><span class="dt">></span>Phone<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb16-15"><a aria-hidden="true" href="#cb16-15" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"phone"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"phone"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span></span> |
|
<span id="cb16-16"><a aria-hidden="true" href="#cb16-16" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Phone"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.phone }}"</span><span class="dt">></span></span> |
|
<span id="cb16-17"><a aria-hidden="true" href="#cb16-17" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['phone'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb16-18"><a aria-hidden="true" href="#cb16-18" tabindex="-1"></a> <span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb16-19"><a aria-hidden="true" href="#cb16-19" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="dt">></span>Save<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb16-20"><a aria-hidden="true" href="#cb16-20" tabindex="-1"></a> <span class="dt"></</span><span class="kw">fieldset</span><span class="dt">></span></span> |
|
<span id="cb16-21"><a aria-hidden="true" href="#cb16-21" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The “edit contact” form body</p></figcaption> |
|
</figure> |
|
<p>In the final part of our template we have a small difference between |
|
the <code>new.html</code> and <code>edit.html</code>. Below the main |
|
editing form, we include a second form that allows you to delete a |
|
contact. It does this by issuing a <code>POST</code> to the |
|
<code>/contacts/<contact id>/delete</code> path. Just as we would |
|
prefer to use a <code>PUT</code> to update a contact, we would much |
|
rather use an HTTP <code>DELETE</code> request to delete one. |
|
Unfortunately that also isn’t possible in plain HTML.</p> |
|
<p>To finish up the page, there is a simple hyperlink back to the list |
|
of contacts.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb17"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb17-1"><a aria-hidden="true" href="#cb17-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/delete"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"post"</span><span class="dt">></span></span> |
|
<span id="cb17-2"><a aria-hidden="true" href="#cb17-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="dt">></span>Delete Contact<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb17-3"><a aria-hidden="true" href="#cb17-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span> |
|
<span id="cb17-4"><a aria-hidden="true" href="#cb17-4" tabindex="-1"></a></span> |
|
<span id="cb17-5"><a aria-hidden="true" href="#cb17-5" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb17-6"><a aria-hidden="true" href="#cb17-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/"</span><span class="dt">></span>Back<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb17-7"><a aria-hidden="true" href="#cb17-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The “edit contact” form footer</p></figcaption> |
|
</figure> |
|
<p>Given all the similarities between the <code>new.html</code> and |
|
<code>edit.html</code> templates, you may be wondering why we are not |
|
<em class="test">refactoring</em> these two templates to share logic between them. |
|
That’s a good observation and, in a production system, we would probably |
|
do just that.</p> |
|
<p>For our purposes, however, since our application is small and simple, |
|
we will leave the templates separate.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Factoring Your Applications</strong></p> |
|
</div> |
|
<div> |
|
<p>One thing that often trips people up who are coming to hypermedia |
|
applications from a JavaScript background is the notion of “components”. |
|
In JavaScript-oriented applications it is common to break your app up |
|
into small client-side components that are then composed together. These |
|
components are often developed and tested in isolation and provide a |
|
nice abstraction for developers to create testable code.</p> |
|
<p>With Hypermedia-Driven Applications, in contrast, you factor your |
|
application on the server side. As we said, the above form could be |
|
refactored into a shared template between the edit and create templates, |
|
allowing you to achieve a reusable and DRY (Don’t Repeat Yourself) |
|
implementation.</p> |
|
<p>Note that factoring on the server-side tends to be coarser-grained |
|
than on the client-side: you tend to split out common <em class="test">sections</em> |
|
rather than create lots of individual components. This has benefits (it |
|
tends to be simple) as well as drawbacks (it is not nearly as isolated |
|
as client-side components).</p> |
|
<p>Overall, a properly factored server-side hypermedia application can |
|
be extremely DRY.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h4 id="_handling_the_post_to_contactscontact_id">Handling the post to |
|
/contacts/<contact_id>/edit</h4> |
|
<p>Next we need to handle the HTTP <code>POST</code> request that the |
|
form in our <code>edit.html</code> template submits. We will declare |
|
another route that handles the same path as the <code>GET</code> |
|
above.</p> |
|
<p>Here is the new handler code:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb18"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb18-1"><a aria-hidden="true" href="#cb18-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/<contact_id>/edit"</span>, methods<span class="op">=</span>[<span class="st">"POST"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb18-2"><a aria-hidden="true" href="#cb18-2" tabindex="-1"></a><span class="kw">def</span> contacts_edit_post(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb18-3"><a aria-hidden="true" href="#cb18-3" tabindex="-1"></a> c <span class="op">=</span> Contact.find(contact_id) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb18-4"><a aria-hidden="true" href="#cb18-4" tabindex="-1"></a> c.update(</span> |
|
<span id="cb18-5"><a aria-hidden="true" href="#cb18-5" tabindex="-1"></a> request.form[<span class="st">'first_name'</span>],</span> |
|
<span id="cb18-6"><a aria-hidden="true" href="#cb18-6" tabindex="-1"></a> request.form[<span class="st">'last_name'</span>],</span> |
|
<span id="cb18-7"><a aria-hidden="true" href="#cb18-7" tabindex="-1"></a> request.form[<span class="st">'phone'</span>],</span> |
|
<span id="cb18-8"><a aria-hidden="true" href="#cb18-8" tabindex="-1"></a> request.form[<span class="st">'email'</span>]) <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb18-9"><a aria-hidden="true" href="#cb18-9" tabindex="-1"></a> <span class="cf">if</span> c.save(): <span class="op"><</span><span class="dv">4</span><span class="op">></span></span> |
|
<span id="cb18-10"><a aria-hidden="true" href="#cb18-10" tabindex="-1"></a> flash(<span class="st">"Updated Contact!"</span>)</span> |
|
<span id="cb18-11"><a aria-hidden="true" href="#cb18-11" tabindex="-1"></a> <span class="cf">return</span> redirect(<span class="st">"/contacts/"</span> <span class="op">+</span> <span class="bu">str</span>(contact_id)) <span class="op"><</span><span class="dv">5</span><span class="op">></span></span> |
|
<span id="cb18-12"><a aria-hidden="true" href="#cb18-12" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb18-13"><a aria-hidden="true" href="#cb18-13" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"edit.html"</span>, contact<span class="op">=</span>c) <span class="op"><</span><span class="dv">6</span><span class="op">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>Handle a <code>POST</code> to |
|
<code>/contacts/<contact_id>/edit</code>.</p></li> |
|
<li><p>Look the contact up by id.</p></li> |
|
<li><p>Update the contact with the new information from the |
|
form.</p></li> |
|
<li><p>Attempt to save it.</p></li> |
|
<li><p>On success, flash a success message & redirect to the detail |
|
page.</p></li> |
|
<li><p>On failure, re-render the edit template, showing any |
|
errors.</p></li> |
|
</ol> |
|
<p>The logic in this handler is very similar to the logic in the handler |
|
for adding a new contact. The only real difference is that, rather than |
|
creating a new Contact, we look the contact up by id and then call the |
|
<code>update()</code> method on it with the values that were entered in |
|
the form.</p> |
|
<p>Once again, this consistency between our CRUD operations is one of |
|
the nice and simplifying aspects of traditional CRUD web |
|
applications.</p> |
|
<h3 id="_deleting_a_contact">Deleting A Contact</h3> |
|
<p>We piggybacked contact delete functionality into the same template |
|
used to edit a contact. This second form will issue an HTTP |
|
<code>POST</code> to <code>/contacts/<contact_id>/delete</code>, |
|
and we will need to create a handler for that path as well.</p> |
|
<p>Here is what the controller looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb19"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb19-1"><a aria-hidden="true" href="#cb19-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/<contact_id>/delete"</span>, methods<span class="op">=</span>[<span class="st">"POST"</span>]) <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb19-2"><a aria-hidden="true" href="#cb19-2" tabindex="-1"></a><span class="kw">def</span> contacts_delete(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb19-3"><a aria-hidden="true" href="#cb19-3" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id)</span> |
|
<span id="cb19-4"><a aria-hidden="true" href="#cb19-4" tabindex="-1"></a> contact.delete() <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb19-5"><a aria-hidden="true" href="#cb19-5" tabindex="-1"></a> flash(<span class="st">"Deleted Contact!"</span>)</span> |
|
<span id="cb19-6"><a aria-hidden="true" href="#cb19-6" tabindex="-1"></a> <span class="cf">return</span> redirect(<span class="st">"/contacts"</span>) <span class="op"><</span><span class="dv">3</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>The “delete contact” controller code</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Handle a <code>POST</code> the |
|
<code>/contacts/<contact_id>/delete</code> path.</p></li> |
|
<li><p>Look up and then invoke the <code>delete()</code> method on the |
|
contact.</p></li> |
|
<li><p>Flash a success message and redirect to the main list of |
|
contacts.</p></li> |
|
</ol> |
|
<p>The handler code is very simple since we don’t need to do any |
|
validation or conditional logic: we simply look up the contact the same |
|
way we have been doing in our other handlers and invoke the |
|
<code>delete()</code> method on it, then redirect back to the list of |
|
contacts with a success flash message.</p> |
|
<p>No need for a template in this case, the contact is gone.</p> |
|
<h3 id="_contact_app_implemented">Contact.app… Implemented!</h3> |
|
<p>And, well… believe it or not, that’s our entire contact |
|
application!</p> |
|
<p>If you’ve struggled with parts of the code so far, don’t worry: we |
|
don’t expect you to be a Python or Flask expert (we aren’t!). You just |
|
need a basic understanding of how they work to benefit from the |
|
remainder of the book.</p> |
|
<p>This is a small and simple application, but it does demonstrate many |
|
of the aspects of traditional, web 1.0 applications: CRUD, the |
|
Post/Redirect/Get pattern, working with domain logic in a controller, |
|
organizing our URLs in a coherent, resource-oriented manner.</p> |
|
<p>And, furthermore, this is a deeply <em class="test">Hypermedia-Driven</em> web |
|
application. Without thinking about it very much, we have been using |
|
REST, HATEOAS and all the other hypermedia concepts we discussed |
|
earlier. We would bet that this simple little contact app of ours is |
|
more RESTful than 99% of all JSON APIs ever built!</p> |
|
<p>Just by virtue of using a <em class="test">hypermedia</em>, HTML, we naturally |
|
fall into the RESTful network architecture.</p> |
|
<p>So that’s great. But what’s the matter with this little web app? Why |
|
not end here and go off to develop web 1.0 style applications?</p> |
|
<p>Well, at some level, nothing is wrong with it. Particularly for an |
|
application as simple as this one, the older way of building web apps |
|
might be a perfectly acceptable approach.</p> |
|
<p>However, our application does suffer from that “clunkiness” that we |
|
mentioned earlier when discussing web 1.0 applications: every request |
|
replaces the entire screen, introducing a noticeable flicker when |
|
navigating between pages. You lose your scroll state. You have to click |
|
around a bit more than you might in a more sophisticated web |
|
application.</p> |
|
<p>Contact.app, at this point, just doesn’t feel like a “modern” web |
|
application.</p> |
|
<p>Is it time to reach for a JavaScript framework and JSON APIs to make |
|
our contact application more interactive?</p> |
|
<p>No. No it isn’t.</p> |
|
<p>It turns out that we can improve the user experience of this |
|
application while retaining its fundamental hypermedia architecture.</p> |
|
<p>In the next few chapters we will look at <a href="https://htmx.org">htmx</a>, a hypermedia-oriented library that |
|
will let us improve our contact application while retaining the |
|
hypermedia-based approach we have used so far.</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: Framework Soup</h2> |
|
<p>Components encapsulate a section of a page along with its dynamic |
|
behavior. While encapsulating behavior is a good way to organize code, |
|
it can also separate elements from their surrounding context, which can |
|
lead to wrong or inadequate relationships between elements. The result |
|
is what one might call <em class="test">component soup</em>, where information is |
|
hidden in component state, rather than being present in the HTML, which |
|
is now incomprehensible due to missing context.</p> |
|
<p>Before you reach for components for reuse, consider your options. |
|
Lower-level mechanisms often (allow you to) produce better HTML. In some |
|
cases, components can actually <em class="test">improve</em> the clarity of your |
|
HTML.</p> |
|
<blockquote> |
|
<p>The fact that the HTML document is something that you barely touch, |
|
because everything you need in there will be injected via JavaScript, |
|
puts the document and the page structure out of focus.</p> |
|
</blockquote><p class="quote-attribution"> Manuel Matuzović, <a href="https://www.matuzo.at/blog/2023/single-page-applications-criticism">Why |
|
I’m not the biggest fan of Single Page Applications</a></p> |
|
<p>In order to avoid <code><div></code> soup (or Markdown soup, or |
|
Component soup), you need to be aware of the markup you’re producing and |
|
be able to change it.</p> |
|
<p>Some SPA frameworks, and some web components, make this more |
|
difficult by putting layers of abstraction between the code the |
|
developer writes and the generated markup.</p> |
|
<p>While these abstractions can allow developers to create richer UI or |
|
work faster, their pervasiveness means that developers can lose sight of |
|
the actual HTML (and JavaScript) being sent to clients. Without diligent |
|
testing, this leads to inaccessibility, poor SEO, and bloat.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Extending HTML As Hypermedia</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_a_close_look_at_a_hyperlink">A Close Look At A Hyperlink</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_why_only_anchors_forms">Why Only Anchors & Forms?</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_why_only_click_submit_events">Why Only Click & Submit |
|
Events?</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_why_only_get_post">Why Only GET & POST?</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_why_only_replace_the_entire_screen">Why Only Replace The Entire |
|
Screen?</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_extending_html_as_a_hypermedia_with_htmx">Extending HTML as a |
|
Hypermedia with Htmx</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_installing_and_using_htmx">Installing and Using Htmx</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_no_javascript_required">No JavaScript Required…</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_triggering_http_requests">Triggering HTTP Requests</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_its_all_just_html">It’s All Just HTML</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_htmx_vs_plain_html_responses">Htmx vs. “Plain” HTML |
|
Responses</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_targeting_other_elements">Targeting Other Elements</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_swap_styles">Swap Styles</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_using_events">Using Events</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_htmx_html_extended">Htmx: HTML eXtended</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_passing_request_parameters">Passing Request Parameters</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_enclosing_forms">Enclosing Forms</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_including_inputs">Including Inputs</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_relative_css_selectors">Relative CSS selectors</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_inline_values">Inline Values</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_history_support">History Support</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#_conclusion">Conclusion</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/extending-html-as-hypermedia/#html-note-title">HTML Notes: Budgeting For HTML</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>In the previous chapter we introduced a simple Web 1.0-style |
|
hypermedia application to manage contacts. Our 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 hypermedia controls used to interact |
|
with servers. The application exchanges hypermedia (HTML) with the |
|
server over HTTP, issuing <code>GET</code> and <code>POST</code> HTTP |
|
requests and receiving back full HTML documents in response.</p> |
|
<p>It is a basic web application, but it is also definitely a |
|
Hypermedia-Driven Application. It is robust, it leverages the web’s |
|
native technologies, and it is simple to understand.</p> |
|
<p>So what’s not to like about the application?</p> |
|
<p>Unfortunately, our application has a few issues common to web 1.0 |
|
style applications:</p> |
|
<ul> |
|
<li><p>From a user experience perspective: there is a noticeable refresh |
|
when you move between pages of the application, or 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.</p></li> |
|
<li><p>From a technical perspective, all the updates are done with the |
|
<code>POST</code> HTTP method. This, despite the fact that more logical |
|
actions and HTTP request types like <code>PUT</code> and |
|
<code>DELETE</code> exist and would make more sense for some of the |
|
operations we implemented. After all, if we wanted to delete a resource, |
|
wouldn’t it make more sense to use an HTTP <code>DELETE</code> request |
|
to do so? Somewhat ironically, since we have used pure HTML, we are |
|
unable to access the full expressive power of HTTP, which was designed |
|
specifically <em class="test">for</em> HTML.</p></li> |
|
</ul> |
|
<p>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.</p> |
|
<p>We could address this issue by adopting a Single Page Application |
|
framework, and updating our server-side to provide JSON-based responses. |
|
Single Page Applications eliminate the clunkiness of web 1.0 |
|
applications by updating a web page without refreshing it: they can |
|
mutate parts of the Document Object Model (DOM) of the existing page |
|
without needing to replace (and re-render) the entire page.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>The DOM</strong></p> |
|
</div> |
|
<div> |
|
<p>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.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>There are a few 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 <a href="https://reactjs.org/">React</a> or <a href="https://vuejs.org/">Vue</a> <em class="test">reactively</em> update the DOM |
|
when a JavaScript model is updated: you make a change to a JavaScript |
|
object that is stored locally in memory in the browser, and the web page |
|
“magically” updates its state to reflect the change in the model.</p> |
|
<p>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.</p> |
|
<p>Many web developers today would not even consider the hypermedia |
|
approach due to the perceived “legacy” feel of these web 1.0 style |
|
applications.</p> |
|
<p>Now, the second more technical issue we mentioned may strike you as a |
|
bit pedantic, and we are the first to admit that conversations around |
|
REST and which HTTP Action is right for a given operation can become |
|
very tedious. But still, it’s odd that, when using plain HTML, it is |
|
impossible to use all the functionality of HTTP!</p> |
|
<p>Just seems wrong, doesn’t it?</p> |
|
<h2 id="_a_close_look_at_a_hyperlink">A Close Look At A Hyperlink</h2> |
|
<p>It turns out that we can boost the interactivity of our application |
|
and address both of these issues <em class="test">without</em> resorting to the SPA |
|
approach. We can do so by using a <em class="test">hypermedia-oriented</em> |
|
JavaScript library, <a href="https://htmx.org">htmx</a>. The authors of |
|
this book built htmx specifically to extend HTML as a hypermedia and |
|
address the issues with legacy HTML applications we mentioned above (as |
|
well as a few others.)</p> |
|
<p>Before we get into how htmx allows us to improve the UX of our Web |
|
1.0 style application, let’s revisit the hyperlink/anchor tag from |
|
Chapter 1. Recall, a hyperlink is what is known as a <em class="test">hypermedia |
|
control</em>, a mechanism that describes some sort of interaction with a |
|
server by encoding information about that interaction directly and |
|
completely within the control itself.</p> |
|
<p>Consider again this simple anchor tag which, when interpreted by a |
|
browser, creates a hyperlink to the website for this book:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb1"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb1-1"><a aria-hidden="true" href="#cb1-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"https://hypermedia.systems/"</span><span class="dt">></span></span> |
|
<span id="cb1-2"><a aria-hidden="true" href="#cb1-2" tabindex="-1"></a> Hypermedia Systems</span> |
|
<span id="cb1-3"><a aria-hidden="true" href="#cb1-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A simple hyperlink, revisited</p></figcaption> |
|
</figure> |
|
<p>Let’s break down exactly what happens with this link:</p> |
|
<ul> |
|
<li><p>The browser will render the text “Hypermedia Systems” to the |
|
screen, likely with a decoration indicating it is clickable.</p></li> |
|
<li><p>Then, when a user clicks on the text…</p></li> |
|
<li><p>The browser will issue an HTTP <code>GET</code> to |
|
<code>https://hypermedia.systems</code>…</p></li> |
|
<li><p>The browser will load the HTML body of the HTTP response into the |
|
browser window, replacing the current document.</p></li> |
|
</ul> |
|
<p>So we have four aspects of a simple hypermedia link like this, with |
|
the last three aspects supplying the mechanism that distinguishes a |
|
hyperlink from “normal” text and, thus, makes this a hypermedia |
|
control.</p> |
|
<p>Now, let’s take a moment and think about how we can |
|
<em class="test">generalize</em> these last three aspects of a hyperlink.</p> |
|
<h3 id="_why_only_anchors_forms">Why Only Anchors & Forms?</h3> |
|
<p>Consider: what makes anchor tags (and forms) so special?</p> |
|
<p>Why can’t other elements issue HTTP requests as well?</p> |
|
<p>For example, why shouldn’t <code>button</code> elements be able to |
|
issue HTTP requests? It seems arbitrary to have to wrap a form tag |
|
around a button just to make deleting contacts work in our application, |
|
for example.</p> |
|
<p>Maybe: other elements should be able to issue HTTP requests as well. |
|
Maybe other elements should be able to act as hypermedia controls on |
|
their own.</p> |
|
<p>This is our first opportunity to generalize HTML as a hypermedia.</p> |
|
<div id="important"> |
|
<div> |
|
<div> |
|
<p><strong>Opportunity 1</strong></p> |
|
</div> |
|
<div> |
|
<p>HTML could be extended to allow <em class="test">any</em> element to issue a |
|
request to the server and act as a hypermedia control.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_why_only_click_submit_events">Why Only Click & Submit |
|
Events?</h3> |
|
<p>Next, let’s consider the event that triggers the request to the |
|
server on our link: a click event.</p> |
|
<p>Well, what’s so special about clicking (in the case of anchors) or |
|
submitting (in the case of forms) things? 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.</p> |
|
<p>Why shouldn’t these other events be able to trigger requests as |
|
well?</p> |
|
<p>This gives us our second opportunity to expand the expressiveness of |
|
HTML:</p> |
|
<div id="important"> |
|
<div> |
|
<div> |
|
<p><strong>Opportunity 2</strong></p> |
|
</div> |
|
<div> |
|
<p>HTML could be extended to allow <em class="test">any</em> event — not just a |
|
click, as in the case of hyperlinks — to trigger HTTP requests.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_why_only_get_post">Why Only GET & POST?</h3> |
|
<p>Getting a bit more technical in our thinking leads us to the problem |
|
we noted earlier: plain HTML only give us access to the <code>GET</code> |
|
and <code>POST</code> actions of HTTP.</p> |
|
<p>HTTP <em class="test">stands</em> 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 <em class="test">have</em> to use JavaScript |
|
and issue an AJAX request to get at the other three: |
|
<code>DELETE</code>, <code>PUT</code> and <code>PATCH</code>.</p> |
|
<p>Let’s recall what these different HTTP request types are designed to |
|
represent:</p> |
|
<ul> |
|
<li><p><code>GET</code> corresponds with “getting” a representation for |
|
a resource from a URL: it is a pure read, with no mutation of the |
|
resource.</p></li> |
|
<li><p><code>POST</code> submits an entity (or data) to the given |
|
resource, often creating or mutating the resource and causing a state |
|
change.</p></li> |
|
<li><p><code>PUT</code> submits an entity (or data) to the given |
|
resource for update or replacement, again likely causing a state |
|
change.</p></li> |
|
<li><p><code>PATCH</code> is similar to <code>PUT</code> but implies a |
|
partial update and state change rather than a complete replacement of |
|
the entity.</p></li> |
|
<li><p><code>DELETE</code> deletes the given resource.</p></li> |
|
</ul> |
|
<p>These operations correspond closely to the CRUD operations we |
|
discussed in Chapter 2. By giving us access to only two of the five, |
|
HTML hamstrings our ability to take full advantage of HTTP.</p> |
|
<p>This gives us our third opportunity to expand the expressiveness of |
|
HTML:</p> |
|
<div id="important"> |
|
<div> |
|
<div> |
|
<p><strong>Opportunity 3</strong></p> |
|
</div> |
|
<div> |
|
<p>HTML could be extended so that it allows access to the missing three |
|
HTTP methods, <code>PUT</code>, <code>PATCH</code> and |
|
<code>DELETE</code>.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_why_only_replace_the_entire_screen">Why Only Replace The Entire |
|
Screen?</h3> |
|
<p>As a final observation, consider the last aspect of a hyperlink: it |
|
replaces the <em class="test">entire</em> screen when a user clicks on it.</p> |
|
<p>It turns out that this technical detail is the primary culprit for |
|
poor user experience in Web 1.0 Applications. A full page refresh can |
|
cause a flash of unstyled content, where content “jumps” on the screen |
|
as it transitions from its initial to its styled final form. It also |
|
destroys the scroll state of the user by scrolling to the top of the |
|
page, removes focus from a focused element and so forth.</p> |
|
<p>But, if you think about it, there is no rule saying that hypermedia |
|
exchanges <em class="test">must</em> replace the entire document.</p> |
|
<p>This gives us our fourth, final and perhaps most important |
|
opportunity to generalize HTML:</p> |
|
<div id="important"> |
|
<div> |
|
<div> |
|
<p><strong>Opportunity 4</strong></p> |
|
</div> |
|
<div> |
|
<p>HTML could be extended to allow the responses to requests to replace |
|
elements <em class="test">within</em> the current document, rather than requiring |
|
that they replace the <em class="test">entire</em> document.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>This is actually a very old concept in hypermedia. Ted Nelson, in his |
|
1980 book “Literary Machines” coined the term <em class="test">transclusion</em> 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.</p> |
|
<h2 id="_extending_html_as_a_hypermedia_with_htmx">Extending HTML as a |
|
Hypermedia with Htmx</h2> |
|
<p>These four opportunities present us a way to extend HTML well beyond |
|
its current abilities, but in a way that is <em class="test">entirely within</em> the |
|
hypermedia model of the web. The fundamentals of HTML, HTTP, the |
|
browser, and so on, won’t be changed dramatically. Rather, these |
|
generalizations of <em class="test">existing functionality</em> already found within |
|
HTML would simply let us accomplish <em class="test">more</em> using HTML.</p> |
|
<p>Htmx is a JavaScript library that extends HTML in exactly this |
|
manner, and it will be the focus of the next few chapters of this book. |
|
Again, htmx is not the only JavaScript library that takes this |
|
hypermedia-oriented approach (other excellent examples are <a href="https://unpoly.com">Unpoly</a> and <a href="https://hotwire.dev">Hotwire</a>), but htmx is the purest in its |
|
pursuit of extending HTML as a hypermedia.</p> |
|
<h3 id="_installing_and_using_htmx">Installing and Using Htmx</h3> |
|
<p>From a practical “getting started” perspective, htmx is a simple, |
|
dependency-free and stand-alone JavaScript library that can be added to |
|
a web application by simply including it via a <code>script</code> tag |
|
in your <code>head</code> element.</p> |
|
<p>Because of this simple installation model, you can take advantage of |
|
tools like public CDNs to install the library.</p> |
|
<p>Below is an example using the popular <a href="https://unpkg.com">unpkg</a> Content Delivery Network (CDN) to |
|
install version <code>1.9.2</code> of the library. We use an integrity |
|
hash to ensure that the delivered JavaScript content matches what we |
|
expect. This SHA can be found on the htmx website.</p> |
|
<p>We also mark the script as <code>crossorigin="anonymous"</code> so no |
|
credentials will be sent to the CDN.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb2"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb2-1"><a aria-hidden="true" href="#cb2-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">head</span><span class="dt">></span></span> |
|
<span id="cb2-2"><a aria-hidden="true" href="#cb2-2" tabindex="-1"></a><span class="dt"><</span><span class="kw">script</span><span class="ot"> src</span><span class="op">=</span><span class="st">"https://unpkg.com/htmx.org@1.9.2"</span></span> |
|
<span id="cb2-3"><a aria-hidden="true" href="#cb2-3" tabindex="-1"></a><span class="ot"> integrity</span><span class="op">=</span><span class="st">"sha384-L6OqL9pRWyyFU3+/bjdSri+iIphTN/</span></span> |
|
<span id="cb2-4"><a aria-hidden="true" href="#cb2-4" tabindex="-1"></a><span class="st"> bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h"</span></span> |
|
<span id="cb2-5"><a aria-hidden="true" href="#cb2-5" tabindex="-1"></a><span class="ot"> crossorigin</span><span class="op">=</span><span class="st">"anonymous"</span><span class="dt">></</span><span class="kw">script</span><span class="dt">></span></span> |
|
<span id="cb2-6"><a aria-hidden="true" href="#cb2-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">head</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Installing htmx</p></figcaption> |
|
</figure> |
|
<p>If you are used to modern JavaScript development, with complex build |
|
systems and large numbers of dependencies, it may be a pleasant surprise |
|
to find that that’s all it takes to install htmx.</p> |
|
<p>This is in the spirit of the early web, when you could simply include |
|
a script tag and things would “just work.”</p> |
|
<p>If you don’t want to use a CDN, 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 a build system that automatically |
|
installs dependencies. In this case you can use the Node Package Manager |
|
(npm) name for the library: <code>htmx.org</code> and install it in the |
|
usual manner that your build system supports.</p> |
|
<p>Once htmx has been installed, you can begin using it immediately.</p> |
|
<h3 id="_no_javascript_required">No JavaScript Required…</h3> |
|
<p>And here we get to the interesting part of htmx: htmx does not |
|
require you, the user of htmx, to actually write any JavaScript.</p> |
|
<p>Instead, you will use <em class="test">attributes</em> placed directly on elements |
|
in your HTML to drive more dynamic behavior. Htmx extends HTML as a |
|
hypermedia, and it is designed to make that extension feel as natural |
|
and consistent as possible with existing HTML concepts. Just as an |
|
anchor tag uses an <code>href</code> attribute to specify the URL to |
|
retrieve, and forms use an <code>action</code> attribute to specify the |
|
URL to submit the form to, htmx uses HTML <em class="test">attributes</em> to specify |
|
the URL that an HTTP request should be issued to.</p> |
|
<h2 id="_triggering_http_requests">Triggering HTTP Requests</h2> |
|
<p>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 |
|
provided by htmx, and it consists of five attributes that can be used to |
|
issue the five different developer-facing types of HTTP requests:</p> |
|
<ul> |
|
<li><p><code>hx-get</code> - issues an HTTP <code>GET</code> |
|
request.</p></li> |
|
<li><p><code>hx-post</code> - issues an HTTP <code>POST</code> |
|
request.</p></li> |
|
<li><p><code>hx-put</code> - issues an HTTP <code>PUT</code> |
|
request.</p></li> |
|
<li><p><code>hx-patch</code> - issues an HTTP <code>PATCH</code> |
|
request.</p></li> |
|
<li><p><code>hx-delete</code> - issues an HTTP <code>DELETE</code> |
|
request.</p></li> |
|
</ul> |
|
<p>Each of these attributes, when placed on an element, tells the htmx |
|
library: “When a user clicks (or whatever) this element, issue an HTTP |
|
request of the specified type.”</p> |
|
<p>The values of these attributes are similar to the values of both |
|
<code>href</code> on anchors and <code>action</code> 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.</p> |
|
<p>For example, if we wanted a button to issue a <code>GET</code> |
|
request to <code>/contacts</code> then we would write the following |
|
HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb3-2"><a aria-hidden="true" href="#cb3-2" tabindex="-1"></a> Get The Contacts</span> |
|
<span id="cb3-3"><a aria-hidden="true" href="#cb3-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A simple htmx-powered button</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A simple button that issues an HTTP <code>GET</code> to |
|
<code>/contacts</code>.</p></li> |
|
</ol> |
|
<p>The htmx library will see the <code>hx-get</code> attribute on this |
|
button, and hook up some JavaScript logic to issue an HTTP |
|
<code>GET</code> AJAX request to the <code>/contacts</code> path when |
|
the user clicks on it.</p> |
|
<p>Very easy to understand and very consistent with the rest of |
|
HTML.</p> |
|
<h3 id="_its_all_just_html">It’s All Just HTML</h3> |
|
<p>With the request issued by the button above, we get to perhaps the |
|
most important thing to understand about htmx: it expects the response |
|
to this AJAX request <em class="test">to be HTML</em>. Htmx is an extension of HTML. |
|
A native hypermedia control like an anchor tag will typically get an |
|
HTML response to an HTTP request it creates. Similarly, htmx expects the |
|
server to respond to the requests that <em class="test">it</em> makes with HTML.</p> |
|
<p>This may surprise web developers who are used to responding to an |
|
AJAX request with 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 use 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.</p> |
|
<p>Htmx simply goes another direction and expects HTML.</p> |
|
<h3 id="_htmx_vs_plain_html_responses">Htmx vs. “Plain” HTML |
|
Responses</h3> |
|
<p>There is an important difference between the HTTP responses to |
|
“normal” anchor or form driven HTTP requests and to htmx-powered |
|
requests: in the case of htmx triggered requests, responses can be |
|
<em class="test">partial</em> bits of HTML.</p> |
|
<p>In htmx-powered interactions, as you will see, we are often not |
|
replacing the entire document. Rather we are using “transclusion” to |
|
include content <em class="test">within</em> 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.</p> |
|
<p>This fact can be used to save bandwidth as well as resource loading |
|
time. Less overall content is transferred from the server to the client, |
|
and it isn’t necessary to reprocess a <code>head</code> tag with style |
|
sheets, script tags, and so forth.</p> |
|
<p>When the “Get Contacts” button is clicked, a <em class="test">partial</em> HTML |
|
response might look something like this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb4"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb4-1"><a aria-hidden="true" href="#cb4-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">ul</span><span class="dt">></span></span> |
|
<span id="cb4-2"><a aria-hidden="true" href="#cb4-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">li</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:joe@example.com"</span><span class="dt">></span>Joe<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">li</span><span class="dt">></span></span> |
|
<span id="cb4-3"><a aria-hidden="true" href="#cb4-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">li</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:sarah@example.com"</span><span class="dt">></span>Sarah<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">li</span><span class="dt">></span></span> |
|
<span id="cb4-4"><a aria-hidden="true" href="#cb4-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">li</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:fred@example.com"</span><span class="dt">></span>Fred<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">li</span><span class="dt">></span></span> |
|
<span id="cb4-5"><a aria-hidden="true" href="#cb4-5" tabindex="-1"></a><span class="dt"></</span><span class="kw">ul</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A partial HTML response to an htmx |
|
request</p></figcaption> |
|
</figure> |
|
<p>This is just an unordered list of contacts with some clickable |
|
elements in it. Note that there is no opening <code>html</code> tag, no |
|
<code>head</code> tag, and so forth: it is a <em class="test">raw</em> HTML list, |
|
without any decoration around it. A response in a real application might |
|
contain more sophisticated HTML than this simple list, but even if it |
|
were more complicated it wouldn’t need to be an entire page of HTML: it |
|
could just be the “inner” content of the HTML representation for this |
|
resource.</p> |
|
<p>Now, this simple list response is perfect for htmx. Htmx will simply |
|
take the returned content and then swap it in to the DOM in place of |
|
some element in the page. (More on exactly where it will be placed in |
|
the DOM in a moment.) Swapping in HTML content in this manner is fast |
|
and efficient because it leverages the existing native HTML parser in |
|
the browser, rather than requiring a significant amount of client-side |
|
JavaScript to be executed.</p> |
|
<p>This small HTML response shows how htmx stays within the hypermedia |
|
paradigm: just like a “normal” hypermedia control in a “normal” web |
|
application, we see hypermedia being transferred to the client in a |
|
stateless and uniform manner.</p> |
|
<p>This button just gives us a slightly more sophisticated mechanism for |
|
building a web application using hypermedia.</p> |
|
<h2 id="_targeting_other_elements">Targeting Other Elements</h2> |
|
<p>Now, given that htmx has issued a request and gotten back some HTML |
|
as a response, and that we are going to swap this content into the |
|
existing page (rather than replacing the entire page), the question |
|
becomes: where should this new content be placed?</p> |
|
<p>It turns out that the default htmx behavior is to simply put the |
|
returned content inside the element that triggered the request. That’s |
|
<em class="test">not</em> a good thing in the case of our button: we will end up with |
|
a list of contacts awkwardly embedded within the button element. That |
|
will look pretty silly and is obviously not what we want.</p> |
|
<p>Fortunately htmx provides another attribute, <code>hx-target</code> |
|
which can be used to specify exactly <em class="test">where</em> in the DOM the new |
|
content should be placed. The value of the <code>hx-target</code> |
|
attribute is a Cascading Style Sheet (CSS) <em class="test">selector</em> that allows |
|
you to specify the element to put the new hypermedia content into.</p> |
|
<p>Let’s add a <code>div</code> tag that encloses the button with the id |
|
<code>main</code>. We will then target this <code>div</code> with the |
|
response:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"main"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a></span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a> Get The Contacts</span> |
|
<span id="cb5-5"><a aria-hidden="true" href="#cb5-5" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb5-6"><a aria-hidden="true" href="#cb5-6" tabindex="-1"></a></span> |
|
<span id="cb5-7"><a aria-hidden="true" href="#cb5-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A simple htmx-powered button</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A <code>div</code> element that wraps the button.</p></li> |
|
<li><p>The <code>hx-target</code> attribute that specifies the target of |
|
the response.</p></li> |
|
</ol> |
|
<p>We have added <code>hx-target="#main"</code> to our button, where |
|
<code>#main</code> is a CSS selector that says “The thing with the ID |
|
‘main’.”</p> |
|
<p>By using CSS selectors, htmx builds on top of familiar and standard |
|
HTML concepts. This keeps the additional conceptual load for working |
|
with htmx to a minimum.</p> |
|
<p>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?</p> |
|
<p>It would look something like this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb6"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb6-1"><a aria-hidden="true" href="#cb6-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"main"</span><span class="dt">></span></span> |
|
<span id="cb6-2"><a aria-hidden="true" href="#cb6-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">ul</span><span class="dt">></span></span> |
|
<span id="cb6-3"><a aria-hidden="true" href="#cb6-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">li</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:joe@example.com"</span><span class="dt">></span>Joe<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">li</span><span class="dt">></span></span> |
|
<span id="cb6-4"><a aria-hidden="true" href="#cb6-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">li</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:sarah@example.com"</span><span class="dt">></span>Sarah<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">li</span><span class="dt">></span></span> |
|
<span id="cb6-5"><a aria-hidden="true" href="#cb6-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">li</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:fred@example.com"</span><span class="dt">></span>Fred<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">li</span><span class="dt">></span></span> |
|
<span id="cb6-6"><a aria-hidden="true" href="#cb6-6" tabindex="-1"></a> <span class="dt"></</span><span class="kw">ul</span><span class="dt">></span></span> |
|
<span id="cb6-7"><a aria-hidden="true" href="#cb6-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Our HTML after the htmx request finishes</p></figcaption> |
|
</figure> |
|
<p>The response HTML has been swapped into the <code>div</code>, |
|
replacing the button that triggered the request. Transclusion! And this |
|
has happened “in the background” via AJAX, without a clunky page |
|
refresh.</p> |
|
<h2 id="_swap_styles">Swap Styles</h2> |
|
<p>Now, perhaps we don’t want to load the content from the server |
|
response <em class="test">into</em> the div, as child elements. Perhaps, for whatever |
|
reason, we wish to <em class="test">replace</em> the entire div with the response. To |
|
handle this, htmx provides another attribute, <code>hx-swap</code>, that |
|
allows you to specify exactly <em class="test">how</em> the content should be swapped |
|
into the DOM.</p> |
|
<p>The <code>hx-swap</code> attribute supports the following values:</p> |
|
<ul> |
|
<li><p><code>innerHTML</code> - The default, replace the inner html of |
|
the target element.</p></li> |
|
<li><p><code>outerHTML</code> - Replace the entire target element with |
|
the response.</p></li> |
|
<li><p><code>beforebegin</code> - Insert the response before the target |
|
element.</p></li> |
|
<li><p><code>afterbegin</code> - Insert the response before the first |
|
child of the target element.</p></li> |
|
<li><p><code>beforeend</code> - Insert the response after the last child |
|
of the target element.</p></li> |
|
<li><p><code>afterend</code> - Insert the response after the target |
|
element.</p></li> |
|
<li><p><code>delete</code> - Deletes the target element regardless of |
|
the response.</p></li> |
|
<li><p><code>none</code> - No swap will be performed.</p></li> |
|
</ul> |
|
<p>The first two values, <code>innerHTML</code> and |
|
<code>outerHTML</code>, are taken from the standard DOM properties that |
|
allow you to replace content within an element or in place of an entire |
|
element respectively.</p> |
|
<p>The next four values are taken from the |
|
<code>Element.insertAdjacentHTML()</code> DOM API, which allow you to |
|
place an element or elements around a given element in various ways.</p> |
|
<p>The last two values, <code>delete</code> and <code>none</code> are |
|
specific to htmx. The first option will remove the target element from |
|
the DOM, while the second option will do nothing (you may want to only |
|
work with response headers, an advanced technique we will look at later |
|
in the book.)</p> |
|
<p>Again, you can see htmx stays as close as possible to existing web |
|
standards in order to minimize the conceptual load necessary for its |
|
use.</p> |
|
<p>So let’s consider that case where, rather than replacing the |
|
<code>innerHTML</code> content of the main div above, we want to replace |
|
the <em class="test">entire div</em> with the HTML response.</p> |
|
<p>To do so would require only a small change to our button, adding a |
|
new <code>hx-swap</code> attribute:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"main"</span><span class="dt">></span></span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a></span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb7-4"><a aria-hidden="true" href="#cb7-4" tabindex="-1"></a> Get The Contacts</span> |
|
<span id="cb7-5"><a aria-hidden="true" href="#cb7-5" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb7-6"><a aria-hidden="true" href="#cb7-6" tabindex="-1"></a></span> |
|
<span id="cb7-7"><a aria-hidden="true" href="#cb7-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Replacing the entire div</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The <code>hx-swap</code> attribute specifies how to swap in new |
|
content.</p></li> |
|
</ol> |
|
<p>Now, when a response is received, the <em class="test">entire</em> div will be |
|
replaced with the hypermedia content:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">ul</span><span class="dt">></span></span> |
|
<span id="cb8-2"><a aria-hidden="true" href="#cb8-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">li</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:joe@example.com"</span><span class="dt">></span>Joe<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">li</span><span class="dt">></span></span> |
|
<span id="cb8-3"><a aria-hidden="true" href="#cb8-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">li</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:sarah@example.com"</span><span class="dt">></span>Sarah<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">li</span><span class="dt">></span></span> |
|
<span id="cb8-4"><a aria-hidden="true" href="#cb8-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">li</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"mailto:fred@example.com"</span><span class="dt">></span>Fred<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">li</span><span class="dt">></span></span> |
|
<span id="cb8-5"><a aria-hidden="true" href="#cb8-5" tabindex="-1"></a><span class="dt"></</span><span class="kw">ul</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Our HTML after the htmx request finishes</p></figcaption> |
|
</figure> |
|
<p>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.</p> |
|
<p>Later in the book we will see additional uses for |
|
<code>hx-swap</code>, for example when we implement infinite scrolling |
|
in our contact management application.</p> |
|
<p>Note that with the <code>hx-get</code>, <code>hx-post</code>, |
|
<code>hx-put</code>, <code>hx-patch</code> and <code>hx-delete</code> |
|
attributes, we have addressed two of the four opportunities for |
|
improvement that we enumerated regarding plain HTML:</p> |
|
<ul> |
|
<li><p>Opportunity 1: We can now issue an HTTP request with <em class="test">any</em> |
|
element (in this case we are using a button).</p></li> |
|
<li><p>Opportunity 3: We can issue <em class="test">any sort</em> of HTTP request we |
|
want, <code>PUT</code>, <code>PATCH</code> and <code>DELETE</code>, in |
|
particular.</p></li> |
|
</ul> |
|
<p>And, with <code>hx-target</code> and <code>hx-swap</code> we have |
|
addressed a third shortcoming: the requirement that the entire page be |
|
replaced.</p> |
|
<ul> |
|
<li><p>Opportunity 4: We can now replace any element we want in our page |
|
via transclusion, and we can do so in any manner we want.</p></li> |
|
</ul> |
|
<p>So, with only seven relatively simple additional attributes, we have |
|
addressed most of the shortcomings of HTML as a hypermedia that we |
|
identified earlier.</p> |
|
<p>What’s next? Recall the one other opportunity we noted: the fact that |
|
only a <code>click</code> event (on an anchor) or a <code>submit</code> |
|
event (on a form) can trigger an HTTP request. Let’s look at how we can |
|
address that limitation.</p> |
|
<h2 id="_using_events">Using Events</h2> |
|
<p>Thus far we have been using a button to issue a request with htmx. |
|
You have probably intuitively understood that the button would issue its |
|
request when you clicked on the button since, well, that’s what you do |
|
with buttons: you click on them.</p> |
|
<p>And, yes, by default when an <code>hx-get</code> or another |
|
request-driving annotation from htmx is placed on a button, the request |
|
will be issued when the button is clicked.</p> |
|
<p>However, htmx generalizes this notion of an event triggering a |
|
request by using, you guessed it, another attribute: |
|
<code>hx-trigger</code>. The <code>hx-trigger</code> attribute allows |
|
you to specify one or more events that will cause the element to trigger |
|
an HTTP request.</p> |
|
<p>Often you don’t need to use <code>hx-trigger</code> because the |
|
default triggering event will be what you want. The default triggering |
|
event depends on the element type, and should be fairly intuitive:</p> |
|
<ul> |
|
<li><p>Requests on <code>input</code>, <code>textarea</code> & |
|
<code>select</code> elements are triggered by the <code>change</code> |
|
event.</p></li> |
|
<li><p>Requests on <code>form</code> elements are triggered on the |
|
<code>submit</code> event.</p></li> |
|
<li><p>Requests on all other elements are triggered by the |
|
<code>click</code> event.</p></li> |
|
</ul> |
|
<p>To demonstrate how <code>hx-trigger</code> works, consider the |
|
following situation: we want to trigger the request on our button when |
|
the mouse enters it. Now, this is certainly not a <em class="test">good</em> UX |
|
pattern, but bear with us: we are just using this as an example.</p> |
|
<p>To respond to a mouse entering the button, we would add the following |
|
attribute to our button:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"main"</span><span class="dt">></span></span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a></span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span></span> |
|
<span id="cb9-4"><a aria-hidden="true" href="#cb9-4" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"mouseenter"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb9-5"><a aria-hidden="true" href="#cb9-5" tabindex="-1"></a> Get The Contacts</span> |
|
<span id="cb9-6"><a aria-hidden="true" href="#cb9-6" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb9-7"><a aria-hidden="true" href="#cb9-7" tabindex="-1"></a></span> |
|
<span id="cb9-8"><a aria-hidden="true" href="#cb9-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A (bad?) button that triggers on mouse |
|
entry</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Issue a request on the… <code>mouseenter</code> event.</p></li> |
|
</ol> |
|
<p>Now, with this <code>hx-trigger</code> attribute in place, whenever |
|
the mouse enters this button, a request will be triggered. Silly, but it |
|
works.</p> |
|
<p>Let’s try something a bit more realistic and potentially useful: |
|
let’s add support for a keyboard shortcut for loading the contacts, |
|
<code>Ctrl-L</code> (for “Load”). To do this we will need to take |
|
advantage of additional syntax that the <code>hx-trigger</code> |
|
attribute supports: event filters and additional arguments.</p> |
|
<p>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: <code>someEvent[someFilter]</code>. 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, the request will not be triggered.</p> |
|
<p>In the case of keyboard shortcuts, we want to catch the |
|
<code>keyup</code> event in addition to the click event:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb10"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb10-1"><a aria-hidden="true" href="#cb10-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"main"</span><span class="dt">></span></span> |
|
<span id="cb10-2"><a aria-hidden="true" href="#cb10-2" tabindex="-1"></a></span> |
|
<span id="cb10-3"><a aria-hidden="true" href="#cb10-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span></span> |
|
<span id="cb10-4"><a aria-hidden="true" href="#cb10-4" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"click, keyup"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb10-5"><a aria-hidden="true" href="#cb10-5" tabindex="-1"></a> Get The Contacts</span> |
|
<span id="cb10-6"><a aria-hidden="true" href="#cb10-6" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb10-7"><a aria-hidden="true" href="#cb10-7" tabindex="-1"></a></span> |
|
<span id="cb10-8"><a aria-hidden="true" href="#cb10-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A start, trigger on keyup</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A trigger with two events.</p></li> |
|
</ol> |
|
<p>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. We still want to respond to the <code>click</code> |
|
event and load the contacts, in addition to handling the |
|
<code>Ctrl-L</code> keyboard shortcut.</p> |
|
<p>Unfortunately there are two problems with our <code>keyup</code> |
|
addition: As it stands, it will trigger requests on <em class="test">any</em> keyup |
|
event that occurs. And, worse, it will only trigger when a keyup occurs |
|
<em class="test">within</em> this button. The user would need to tab onto the button |
|
to make it active and then begin typing.</p> |
|
<p>Let’s fix these two issues. To fix the first one, we will use a |
|
trigger filter to test that Control key and the “L” key are pressed |
|
together:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb11"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb11-1"><a aria-hidden="true" href="#cb11-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"main"</span><span class="dt">></span></span> |
|
<span id="cb11-2"><a aria-hidden="true" href="#cb11-2" tabindex="-1"></a></span> |
|
<span id="cb11-3"><a aria-hidden="true" href="#cb11-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span></span> |
|
<span id="cb11-4"><a aria-hidden="true" href="#cb11-4" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"click, keyup[ctrlKey </span><span class="er">&&</span><span class="st"> key == 'l']"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb11-5"><a aria-hidden="true" href="#cb11-5" tabindex="-1"></a> Get The Contacts</span> |
|
<span id="cb11-6"><a aria-hidden="true" href="#cb11-6" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb11-7"><a aria-hidden="true" href="#cb11-7" tabindex="-1"></a></span> |
|
<span id="cb11-8"><a aria-hidden="true" href="#cb11-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Getting better with filter on keyup</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p><code>keyup</code> now has a filter, so the control key and L |
|
must be pressed.</p></li> |
|
</ol> |
|
<p>The trigger filter in this case is |
|
<code>ctrlKey && key == 'l'</code>. 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 <code>ctrlKey</code> and |
|
<code>key</code> 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.</p> |
|
<p>OK, so this filter limits the keyup events that will trigger the |
|
request to only <code>Ctrl-L</code> presses. However, we still have the |
|
problem that, as it stands, only <code>keyup</code> events |
|
<em class="test">within</em> the button will trigger the request.</p> |
|
<p>If you are not familiar with the JavaScript event bubbling model: |
|
events typically “bubble” up to parent elements. So an event like |
|
<code>keyup</code> will be triggered first on the focused element, and |
|
then on its parent (enclosing) element, and so on, until it reaches the |
|
top level <code>document</code> object that is the root of all other |
|
elements.</p> |
|
<p>To support a global keyboard shortcut that works regardless of what |
|
element has focus, we will take advantage of event bubbling and a |
|
feature that the <code>hx-trigger</code> attribute supports: the ability |
|
to listen to <em class="test">other elements</em> for events. The syntax for doing |
|
this is the <code>from:</code> modifier, which is added after an event |
|
name and that allows you to specify a specific element to listen for the |
|
given event on using a CSS selector.</p> |
|
<p>In this case, we want to listen to the <code>body</code> element, |
|
which is the parent element of all visible elements on the page.</p> |
|
<p>Here is what our updated <code>hx-trigger</code> attribute looks |
|
like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb12"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb12-1"><a aria-hidden="true" href="#cb12-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"main"</span><span class="dt">></span></span> |
|
<span id="cb12-2"><a aria-hidden="true" href="#cb12-2" tabindex="-1"></a></span> |
|
<span id="cb12-3"><a aria-hidden="true" href="#cb12-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span></span> |
|
<span id="cb12-4"><a aria-hidden="true" href="#cb12-4" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"click, keyup[ctrlKey </span><span class="er">&&</span><span class="st"> key == 'l'] from:body"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb12-5"><a aria-hidden="true" href="#cb12-5" tabindex="-1"></a> Get The Contacts</span> |
|
<span id="cb12-6"><a aria-hidden="true" href="#cb12-6" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb12-7"><a aria-hidden="true" href="#cb12-7" tabindex="-1"></a></span> |
|
<span id="cb12-8"><a aria-hidden="true" href="#cb12-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Even better, listen for keyup on the |
|
body</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Listen to the ‘keyup” event on the <code>body</code> |
|
tag.</p></li> |
|
</ol> |
|
<p>Now, in addition to clicks, the button will listen for |
|
<code>keyup</code> events on the body of the page. So it will issue a |
|
request when it is clicked on and also whenever someone hits |
|
<code>Ctrl-L</code> within the body of the page.</p> |
|
<p>And now we have a nice keyboard shortcut for our Hypermedia-Driven |
|
Application.</p> |
|
<p>The <code>hx-trigger</code> attribute supports many more modifiers, |
|
and it is more elaborate than other htmx attributes. This is because |
|
events, in general, are complicated and require a lot of details to get |
|
just right. The default trigger will often suffice, however, and you |
|
typically don’t need to reach for complicated <code>hx-trigger</code> |
|
features when using htmx.</p> |
|
<p>Even with more sophisticated trigger specifications like the keyboard |
|
shortcut we just added, the overall feel of htmx is <em class="test">declarative</em> |
|
rather than <em class="test">imperative</em>. That keeps htmx-powered applications |
|
“feeling like” standard web 1.0 applications in a way that adding |
|
significant amounts of JavaScript does not.</p> |
|
<h2 id="_htmx_html_extended">Htmx: HTML eXtended</h2> |
|
<p>And hey, check it out! With <code>hx-trigger</code> we have addressed |
|
the final opportunity for improvement of HTML that we outlined at the |
|
start of this chapter:</p> |
|
<ul> |
|
<li><p>Opportunity 2: We can use <em class="test">any</em> event to trigger an HTTP |
|
request.</p></li> |
|
</ul> |
|
<p>That’s a grand total of eight, count ‘em, <em class="test">eight</em> attributes |
|
that all fall squarely within the same conceptual model as normal HTML |
|
and that, by extending HTML as a hypermedia, open up a whole new world |
|
of user interaction possibilities within it.</p> |
|
<p>Here is a table summarizing those opportunities and which htmx |
|
attributes address them:</p> |
|
<dl> |
|
<dt>Any element should be able to make HTTP requests</dt> |
|
<dd> |
|
<p><code>hx-get</code>, <code>hx-post</code>, <code>hx-put</code>, |
|
<code>hx-patch</code>, <code>hx-delete</code></p> |
|
</dd> |
|
<dt>Any event should be able to trigger an HTTP request</dt> |
|
<dd> |
|
<p><code>hx-trigger</code></p> |
|
</dd> |
|
<dt>Any HTTP Action should be available</dt> |
|
<dd> |
|
<p><code>hx-put</code>, <code>hx-patch</code>, |
|
<code>hx-delete</code></p> |
|
</dd> |
|
<dt>Any place on the page should be replaceable (transclusion)</dt> |
|
<dd> |
|
<p><code>hx-target</code>, <code>hx-swap</code></p> |
|
</dd> |
|
</dl> |
|
<h2 id="_passing_request_parameters">Passing Request Parameters</h2> |
|
<p>So far we have just looked at a situation where a button makes a |
|
simple <code>GET</code> request. This is conceptually very close to what |
|
an anchor tag might do. But there is that other native hypermedia |
|
control in HTML-based applications: forms. Forms are used to pass |
|
additional information beyond just a URL up to the server in a |
|
request.</p> |
|
<p>This information is captured via input and input-like elements within |
|
the form via the various types of input tags available in HTML.</p> |
|
<p>Htmx allows you include this additional information in a way that |
|
mirrors HTML itself.</p> |
|
<h3 id="_enclosing_forms">Enclosing Forms</h3> |
|
<p>The simplest way to pass input values with a request in htmx is to |
|
enclose the element making a request within a form tag.</p> |
|
<p>Let’s take our original search form and convert it to use htmx |
|
instead:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb13"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb13-1"><a aria-hidden="true" href="#cb13-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"get"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"tool-bar"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb13-2"><a aria-hidden="true" href="#cb13-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"search"</span><span class="dt">></span>Search Term<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb13-3"><a aria-hidden="true" href="#cb13-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span><span class="ot"> </span></span> |
|
<span id="cb13-4"><a aria-hidden="true" href="#cb13-4" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span></span> |
|
<span id="cb13-5"><a aria-hidden="true" href="#cb13-5" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Search Contacts"</span><span class="dt">/></span></span> |
|
<span id="cb13-6"><a aria-hidden="true" href="#cb13-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-post</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb13-7"><a aria-hidden="true" href="#cb13-7" tabindex="-1"></a> Search</span> |
|
<span id="cb13-8"><a aria-hidden="true" href="#cb13-8" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb13-9"><a aria-hidden="true" href="#cb13-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>An htmx-powered search button</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>When an htmx-powered element is within an ancestor form tag, all |
|
input values within that form will be submitted for non-<code>GET</code> |
|
requests</p></li> |
|
<li><p>We have switched from an <code>input</code> of type |
|
<code>submit</code> to a <code>button</code> and added the |
|
<code>hx-post</code> attribute</p></li> |
|
</ol> |
|
<p>Now, when a user clicks on this button, the value of the input with |
|
the id <code>search</code> 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.)</p> |
|
<p>You might have noticed that the button was switched from a |
|
<code>GET</code> request to a <code>POST</code> request. This is |
|
because, by default, htmx does <em class="test">not</em> include the closest |
|
enclosing form for <code>GET</code> requests, but it <em class="test">does</em> |
|
include the form for all other types of requests.</p> |
|
<p>This may seem a little strange, but it avoids junking up URLs that |
|
are used within forms when dealing with history entries, which we will |
|
discuss in a bit. You can always include an enclosing form’s values with |
|
an element that uses a <code>GET</code> by using the |
|
<code>hx-include</code> attribute, which we will discuss next.</p> |
|
<p>Note also that we could have added the <code>hx-post</code> attribute |
|
to the form, rather than to the button but that would create a somewhat |
|
awkward duplication of the search URL in the <code>action</code> and |
|
<code>hx-post</code> attributes. This can be avoided by using the |
|
<code>hx-boost</code> attribute, which we discuss in the next |
|
chapter.</p> |
|
<h3 id="_including_inputs">Including Inputs</h3> |
|
<p>While enclosing all the inputs you want included in a request within |
|
a form is the most common approach for serializing inputs for 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 |
|
(<code>tr</code>) elements: the <code>form</code> tag is not a valid |
|
child or parent of table rows, so you can’t place a form within or |
|
around a row of data in a table.</p> |
|
<p>To address this issue, htmx provides a mechanism for including input |
|
values in requests: the <code>hx-include</code> attribute. The |
|
<code>hx-include</code> attribute allows you to select input values that |
|
you wish to include in a request via CSS selectors.</p> |
|
<p>Here is the above example reworked to include the input, dropping the |
|
form:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb14"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb14-1"><a aria-hidden="true" href="#cb14-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"main"</span><span class="dt">></span></span> |
|
<span id="cb14-2"><a aria-hidden="true" href="#cb14-2" tabindex="-1"></a></span> |
|
<span id="cb14-3"><a aria-hidden="true" href="#cb14-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"search"</span><span class="dt">></span>Search Contacts:<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb14-4"><a aria-hidden="true" href="#cb14-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> </span></span> |
|
<span id="cb14-5"><a aria-hidden="true" href="#cb14-5" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span></span> |
|
<span id="cb14-6"><a aria-hidden="true" href="#cb14-6" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Search Contacts"</span><span class="dt">/></span></span> |
|
<span id="cb14-7"><a aria-hidden="true" href="#cb14-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-post</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="ot"> hx-include</span><span class="op">=</span><span class="st">"#search"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb14-8"><a aria-hidden="true" href="#cb14-8" tabindex="-1"></a> Search</span> |
|
<span id="cb14-9"><a aria-hidden="true" href="#cb14-9" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb14-10"><a aria-hidden="true" href="#cb14-10" tabindex="-1"></a></span> |
|
<span id="cb14-11"><a aria-hidden="true" href="#cb14-11" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>An htmx-powered search button with |
|
<code>hx-include</code></p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p><code>hx-include</code> can be used to include values directly in |
|
a request.</p></li> |
|
</ol> |
|
<p>The <code>hx-include</code> 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 desired inputs.</p> |
|
<p>It is also useful when you do, in fact, want to submit values with a |
|
<code>GET</code> request and overcome the default behavior of htmx.</p> |
|
<h4 id="_relative_css_selectors">Relative CSS selectors</h4> |
|
<p>The <code>hx-include</code> attribute and, in fact, most attributes |
|
that take a CSS selector, also support <em class="test">relative</em> CSS selectors. |
|
These allow you to specify a CSS selector <em class="test">relative</em> to the |
|
element it is declared on. Here are some examples:</p> |
|
<dl> |
|
<dt><code>closest</code></dt> |
|
<dd> |
|
<p>Find the closest parent element matching the given selector, e.g., |
|
<code>closest form</code>.</p> |
|
</dd> |
|
<dt><code>next</code></dt> |
|
<dd> |
|
<p>Find the next element (scanning forward) matching the given selector, |
|
e.g., <code>next input</code>.</p> |
|
</dd> |
|
<dt><code>previous</code></dt> |
|
<dd> |
|
<p>Find the previous element (scanning backwards) matching the given |
|
selector, e.g., <code>previous input</code>.</p> |
|
</dd> |
|
<dt><code>find</code></dt> |
|
<dd> |
|
<p>Find the next element within this element matching the given |
|
selector, e.g., <code>find input</code>.</p> |
|
</dd> |
|
<dt><code>this</code></dt> |
|
<dd> |
|
<p>The current element.</p> |
|
</dd> |
|
</dl> |
|
<p>Using relative CSS selectors often allows you to avoid generating ids |
|
for elements, since you can take advantage of their local structural |
|
layout instead.</p> |
|
<h3 id="_inline_values">Inline Values</h3> |
|
<p>A final way to include values in htmx-driven requests is to use the |
|
<code>hx-vals</code> 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.)</p> |
|
<p>Here is an example of <code>hx-vals</code>:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb15"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb15-1"><a aria-hidden="true" href="#cb15-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-vals</span><span class="op">=</span><span class="st">'{"state":"MT"}'</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb15-2"><a aria-hidden="true" href="#cb15-2" tabindex="-1"></a> Get The Contacts In Montana</span> |
|
<span id="cb15-3"><a aria-hidden="true" href="#cb15-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>An htmx-powered button with |
|
<code>hx-vals</code></p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p><code>hx-vals</code>, a JSON value to include in the |
|
request.</p></li> |
|
</ol> |
|
<p>The parameter <code>state</code> with the value <code>MT</code> will |
|
be included in the <code>GET</code> request, resulting in a path and |
|
parameters that looks like this: <code>/contacts?state=MT</code>. Note |
|
that we switched the <code>hx-vals</code> 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.</p> |
|
<p>You can also prefix <code>hx-vals</code> with a <code>js:</code> 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.</p> |
|
<p>For example, if the <code>state</code> variable were maintained |
|
dynamically, via some JavaScript, and there existed a JavaScript |
|
function, <code>getCurrentState()</code>, that returned the currently |
|
selected state, it could be included dynamically in htmx requests like |
|
so:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb16"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb16-1"><a aria-hidden="true" href="#cb16-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span></span> |
|
<span id="cb16-2"><a aria-hidden="true" href="#cb16-2" tabindex="-1"></a><span class="ot"> hx-vals</span><span class="op">=</span><span class="st">'js:{"state":getCurrentState()}'</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb16-3"><a aria-hidden="true" href="#cb16-3" tabindex="-1"></a> Get The Contacts In The Selected State</span> |
|
<span id="cb16-4"><a aria-hidden="true" href="#cb16-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A dynamic value</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>With the <code>js:</code> prefix, this expression will evaluate |
|
at submit time.</p></li> |
|
</ol> |
|
<p>These three mechanisms, using <code>form</code> tags, using the |
|
<code>hx-include</code> attribute and using the <code>hx-vals</code> |
|
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.</p> |
|
<h2 id="_history_support">History Support</h2> |
|
<p>We have a final piece of functionality to close out our overview of |
|
htmx: browser history support. When you use normal HTML links and forms, |
|
your browser will keep track of all the pages that you have visited. You |
|
can then 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.</p> |
|
<p>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 browser’s history, which 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.</p> |
|
<p>However, there are likely to be a lot of AJAX driven interactions in |
|
a Single Page Application where it <em class="test">is</em> appropriate to create a |
|
history entry. There is a JavaScript API to work with browser history, |
|
but this API is deeply annoying and difficult to work with, and thus |
|
often ignored by JavaScript developers.</p> |
|
<p>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.</p> |
|
<p>In htmx, as with Single Page Application frameworks, you will often |
|
need to explicitly work with the history API. Fortunately, since htmx |
|
sticks so close to the native model of the web and since it is |
|
declarative, getting web history right is typically much easier to do in |
|
an htmx-based application.</p> |
|
<p>Consider the button we have been looking at to load contacts:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb17"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb17-1"><a aria-hidden="true" href="#cb17-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="dt">></span></span> |
|
<span id="cb17-2"><a aria-hidden="true" href="#cb17-2" tabindex="-1"></a> Get The Contacts</span> |
|
<span id="cb17-3"><a aria-hidden="true" href="#cb17-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Our trusty button</p></figcaption> |
|
</figure> |
|
<p>As it stands, if you click this button it will retrieve the content |
|
from <code>/contacts</code> and load it into the element with the id |
|
<code>main</code>, but it will <em class="test">not</em> create a new history |
|
entry.</p> |
|
<p>If we wanted it to create a history entry when this request happened, |
|
we would add a new attribute to the button, the <code>hx-push-url</code> |
|
attribute:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb18"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb18-1"><a aria-hidden="true" href="#cb18-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#main"</span><span class="ot"> hx-push-url</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb18-2"><a aria-hidden="true" href="#cb18-2" tabindex="-1"></a> Get The Contacts</span> |
|
<span id="cb18-3"><a aria-hidden="true" href="#cb18-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Our trusty button, now with history!</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p><code>hx-push-url</code> will create an entry in history when the |
|
button is clicked.</p></li> |
|
</ol> |
|
<p>Now, when the button is clicked, the <code>/contacts</code> 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 the original |
|
URL.</p> |
|
<p>Now, the name <code>hx-push-url</code> for this attribute might sound |
|
a little obscure, but it is based on the JavaScript API, |
|
<code>history.pushState()</code>. 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.</p> |
|
<p>With this relatively simple, declarative mechanism, htmx allows you |
|
to integrate with the back button in a way that mimics the “normal” |
|
behavior of HTML.</p> |
|
<p>Now, there is one additional thing we need to handle to get history |
|
“just right”: we have “pushed” the <code>/contacts</code> path into the |
|
browsers location bar successfully, and the back button works. But what |
|
if someone refreshes their browser while on the <code>/contacts</code> |
|
page?</p> |
|
<p>In this case, you will need to handle the htmx-based “partial” |
|
response as well as the non-htmx “full page” response. You can do this |
|
using HTTP headers, a topic we will go into in detail later in the |
|
book.</p> |
|
<h2 id="_conclusion">Conclusion</h2> |
|
<p>So that’s our whirlwind introduction to htmx. We’ve only seen about |
|
ten attributes from the library, but you can see a hint of just how |
|
powerful these attributes can be. Htmx enables a much more sophisticated |
|
web application than is possible in plain HTML, with minimal additional |
|
conceptual load compared to most JavaScript-based approaches.</p> |
|
<p>Htmx aims to incrementally improve HTML as a hypermedia in a manner |
|
that is conceptually coherent with the underlying markup language. Like |
|
any technical choice, this is not without trade-offs: by staying so |
|
close to HTML, htmx does not give developers a lot of infrastructure |
|
that many might feel should be there “by default”.</p> |
|
<p>By staying closer to the native model of the web, htmx aims to strike |
|
a balance between simplicity and functionality, deferring to other |
|
libraries for more elaborate frontend extensions on top of the existing |
|
web platform. The good news is that htmx plays well with others, so when |
|
these needs arise it is often easy enough to bring in another library to |
|
handle them.</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: Budgeting For HTML</h2> |
|
<p>The close relationship between content and markup means that good |
|
HTML is labor-intensive. Most sites have a separation between the |
|
authors, who are rarely familiar with HTML, and the developers, who need |
|
to develop a generic system able to handle any content that’s thrown at |
|
it — this separation usually taking the form of a CMS. As a result, |
|
having markup tailored to content, which is often necessary for advanced |
|
HTML, is rarely feasible.</p> |
|
<p>Furthermore, for internationalized sites, content in different |
|
languages being injected into the same elements can degrade markup |
|
quality as stylistic conventions differ between languages. It’s an |
|
expense few organizations can spare.</p> |
|
<p>Thus, we don’t expect every site to contain perfectly conformant |
|
HTML. What’s most important is to avoid <em class="test">wrong</em> HTML — it can be |
|
better to fall back on a more generic element than to be precisely |
|
incorrect.</p> |
|
<p>If you have the resources, however, putting more care in your HTML |
|
will produce a more polished site.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Htmx Patterns</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_installing_htmx">Installing Htmx</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_ajax_ifying_our_application">AJAX-ifying Our Application</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_boosted_links">Boosted Links</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_boosted_forms">Boosted Forms</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_attribute_inheritance">Attribute Inheritance</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_progressive_enhancement">Progressive Enhancement</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_adding_hx_boost_to_contact_app">Adding “hx-boost” to |
|
Contact.app</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_a_second_step_deleting_contacts_with_http_delete">A Second |
|
Step: Deleting Contacts With HTTP DELETE</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_updating_the_server_side_code">Updating The Server-Side |
|
Code</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_a_response_code_gotcha">A response code gotcha</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_targeting_the_right_element">Targeting The Right Element</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_updating_the_location_bar_url_properly">Updating The Location |
|
Bar URL Properly</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_one_more_thing">One More Thing…</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_progressive_enhancement_2">Progressive Enhancement?</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_next_steps_validating_contact_emails">Next Steps: Validating |
|
Contact Emails</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_updating_our_input_type">Updating Our Input Type</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_inline_validation">Inline Validation</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_validating_emails_server_side">Validating Emails |
|
Server-Side</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_taking_the_user_experience_further">Taking The User Experience |
|
Further</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_debouncing_our_validation_requests">Debouncing Our Validation |
|
Requests</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_ignoring_non_mutating_keys">Ignoring Non-Mutating Keys</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_another_application_improvement_paging">Another Application |
|
Improvement: Paging</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_click_to_load">Click To Load</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#_infinite_scroll">Infinite Scroll</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/htmx-patterns/#html-note-title">HTML Notes: Caution With Modals and “display: |
|
none”</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>Now that we’ve seen how htmx extends HTML as a hypermedia, it’s time |
|
to put it into action. As we use htmx, we will still be using |
|
hypermedia: we will issue HTTP requests and get back HTML. But, with the |
|
additional functionality that htmx provides, we will have a more |
|
<em class="test">powerful hypermedia</em> to work with, allowing us to accomplish |
|
much more sophisticated interfaces.</p> |
|
<p>This will allow us to address user experience issues, such as long |
|
feedback cycles or painful page refreshes, without needing to write |
|
much, if any, JavaScript, and without creating a JSON API. Everything |
|
will be implemented in hypermedia, using the core hypermedia concepts of |
|
the early web.</p> |
|
<h2 id="_installing_htmx">Installing Htmx</h2> |
|
<p>The first thing we need to do is install htmx in our web application. |
|
We are going to do this by downloading the source and saving it locally |
|
in our application, so we aren’t dependent on any external systems. This |
|
is known as “vendoring” the library. We can grab the latest version of |
|
htmx by navigating our browser to |
|
<code>https://unpkg.com/htmx.org</code>, which will redirect us to the |
|
source of the latest version of the library.</p> |
|
<p>We can save the content from this URL into the |
|
<code>static/js/htmx.js</code> file in our project.</p> |
|
<p>You can, of course, use a more sophisticated JavaScript package |
|
manager such as Node Package Manager (NPM) or yarn to install htmx. You |
|
do this by referring to its package name, <code>htmx.org</code>, in the |
|
manner appropriate for your tool. However, htmx is very small |
|
(approximately 12kb when compressed and zipped) and is dependency free, |
|
so using it does not require an elaborate mechanism or build tool.</p> |
|
<p>With htmx downloaded locally to our applications |
|
<code>/static/js</code> directory, we can now load it in to our |
|
application. We do this by adding the following <code>script</code> tag |
|
to the <code>head</code> tag in our <code>layout.html</code> file, which |
|
will make htmx available and active on every page in our |
|
application:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb1"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb1-1"><a aria-hidden="true" href="#cb1-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">head</span><span class="dt">></span></span> |
|
<span id="cb1-2"><a aria-hidden="true" href="#cb1-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">script</span><span class="ot"> src</span><span class="op">=</span><span class="st">"/js/htmx.js"</span><span class="dt">></</span><span class="kw">script</span><span class="dt">></span></span> |
|
<span id="cb1-3"><a aria-hidden="true" href="#cb1-3" tabindex="-1"></a> ...</span> |
|
<span id="cb1-4"><a aria-hidden="true" href="#cb1-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">head</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Installing htmx</p></figcaption> |
|
</figure> |
|
<p>Recall that the <code>layout.html</code> file is a <em class="test">layout</em> |
|
file included in most templates that wraps the content of those |
|
templates in common HTML, including a <code>head</code> element that we |
|
are using here to install htmx.</p> |
|
<p>Believe it or not, that’s it! This simple script tag will make htmx’s |
|
functionality available across our entire application.</p> |
|
<h2 id="_ajax_ifying_our_application">AJAX-ifying Our Application</h2> |
|
<p>To get our feet wet with htmx, the first feature we are going to take |
|
advantage of is known as “boosting.” This is a bit of a “magic” feature |
|
in that we don’t need to do much beyond adding a single attribute, |
|
<code>hx-boost</code>, to the application.</p> |
|
<p>When you put <code>hx-boost</code> on a given element with the value |
|
<code>true</code>, it will “boost” all anchor and form elements within |
|
that element. “Boost”, here, means that htmx will convert all those |
|
anchors and forms from “normal” hypermedia controls into AJAX-powered |
|
hypermedia controls. Rather than issuing “normal” HTTP requests that |
|
replace the whole page, the links and forms will issue AJAX requests. |
|
Htmx then swaps the inner content of the <code><body></code> tag |
|
in the response to these requests into the existing pages |
|
<code><body></code> tag.</p> |
|
<p>This makes navigation feel faster because the browser will not be |
|
re-interpreting most of the tags in the response |
|
<code><head></code> and so forth.</p> |
|
<h3 id="_boosted_links">Boosted Links</h3> |
|
<p>Let’s take a look at an example of a boosted link. Below is a link to |
|
a hypothetical settings page for a web application. Because it has |
|
<code>hx-boost="true"</code> on it, htmx will halt the normal link |
|
behavior of issuing a request to the <code>/settings</code> path and |
|
replacing the entire page with the response. Instead, htmx will issue an |
|
AJAX request to <code>/settings</code>, take the result and replace the |
|
<code>body</code> element with the new content.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb2"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb2-1"><a aria-hidden="true" href="#cb2-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/settings"</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span>Settings<span class="dt"></</span><span class="kw">a</span><span class="dt">></span> <span class="er"><</span>1></span></code></pre></div> |
|
<figcaption><p>A boosted link</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The <code>hx-boost</code> attribute makes this link |
|
AJAX-powered.</p></li> |
|
</ol> |
|
<p>You might reasonably ask: what’s the advantage here? We are issuing |
|
an AJAX request and simply replacing the entire body.</p> |
|
<p>Is that significantly different from just issuing a normal link |
|
request?</p> |
|
<p>Yes, it is in fact different: with a boosted link, the browser is |
|
able to avoid any processing associated with the head tag. The head tag |
|
often contains many scripts and CSS file references. In the boosted |
|
scenario, it is not necessary to re-process those resources: the scripts |
|
and styles have already been processed and will continue to apply to the |
|
new content. This can often be a very easy way to speed up your |
|
hypermedia application.</p> |
|
<p>A second question you might have is: does the response need to be |
|
formatted specially to work with <code>hx-boost</code>? After all, the |
|
settings page would normally render an <code>html</code> tag, with a |
|
<code>head</code> tag and so forth. Do you need to handle “boosted” |
|
requests specially?</p> |
|
<p>The answer is no: htmx is smart enough to pull out only the content |
|
of the <code>body</code> tag to swap in to the new page. The |
|
<code>head</code> tag is mostly ignored: only the title tag, if it is |
|
present, will be processed. This means you don’t need to do anything |
|
special on the server side to render templates that |
|
<code>hx-boost</code> can handle: just return the normal HTML for your |
|
page, and it should work fine.</p> |
|
<p>Note that boosted links (and forms) will also continue to update the |
|
navigation bar and history, just like normal links, so users will be |
|
able to use the browser back button, will be able to copy and paste URLs |
|
(or “deep links”) and so on. Links will act pretty much like “normal”, |
|
they will just be faster.</p> |
|
<h3 id="_boosted_forms">Boosted Forms</h3> |
|
<p>Boosted form tags work in a similar way to boosted anchor tags: a |
|
boosted form will use an AJAX request rather than the usual |
|
browser-issued request, and will replace the entire body with the |
|
response.</p> |
|
<p>Here is an example of a form that posts messages to the |
|
<code>/messages</code> endpoint using an HTTP <code>POST</code> request. |
|
By adding <code>hx-boost</code> to it, those requests will be done in |
|
AJAX, rather than the normal browser behavior.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/messages"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"post"</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb3-2"><a aria-hidden="true" href="#cb3-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"message"</span><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Enter A Message..."</span><span class="dt">></span></span> |
|
<span id="cb3-3"><a aria-hidden="true" href="#cb3-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="dt">></span>Post Your Message<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb3-4"><a aria-hidden="true" href="#cb3-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A boosted form</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>As with the link, <code>hx-boost</code> makes this form |
|
AJAX-powered.</p></li> |
|
</ol> |
|
<p>A big advantage of the AJAX-based request that <code>hx-boost</code> |
|
uses (and the lack of head processing that occurs) is that it avoids |
|
what is known as a <em class="test">flash of unstyled content</em>:</p> |
|
<dl> |
|
<dt>Flash Of Unstyled Content (FOUC)</dt> |
|
<dd> |
|
<p>A situation where a browser renders a web page before all the styling |
|
information is available for the page. A FOUC causes a disconcerting |
|
momentary “flash” of the unstyled content, which is then restyled when |
|
all the style information is available. You will notice this as a |
|
flicker when you move around the internet: text, images and other |
|
content can “jump around” on the page as styles are applied to it.</p> |
|
</dd> |
|
</dl> |
|
<p>With <code>hx-boost</code> the site’s styling is already loaded |
|
<em class="test">before</em> the new content is retrieved, so there is no such flash |
|
of unstyled content. This can make a “boosted” application feel both |
|
smoother and also snappier in general.</p> |
|
<h3 id="_attribute_inheritance">Attribute Inheritance</h3> |
|
<p>Let’s expand on our previous example of a boosted link, and add a few |
|
more boosted links alongside it. We’ll add links so that we have one to |
|
the <code>/contacts</code> page, the <code>/settings</code> page, and |
|
the <code>/help</code> page. All these links are boosted and will behave |
|
in the manner that we have described above.</p> |
|
<p>This feels a little redundant, doesn’t it? It seems silly to annotate |
|
all three links with the <code>hx-boost="true"</code> attribute right |
|
next to one another.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb4"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb4-1"><a aria-hidden="true" href="#cb4-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span>Contacts<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb4-2"><a aria-hidden="true" href="#cb4-2" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/settings"</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span>Settings<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb4-3"><a aria-hidden="true" href="#cb4-3" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/help"</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span>Help<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A set of boosted links</p></figcaption> |
|
</figure> |
|
<p>Htmx offers a feature to help reduce this redundancy: attribute |
|
inheritance. With most attributes in htmx, if you place it on a parent, |
|
the attribute will also apply to children elements. This is how |
|
Cascading Style Sheets work, and that idea inspired htmx to adopt a |
|
similar “cascading htmx attributes” feature.</p> |
|
<p>To avoid the redundancy in this example, let’s introduce a |
|
<code>div</code> element that encloses all the links and then “hoist” |
|
the <code>hx-boost</code> attribute up to that parent <code>div</code>. |
|
This will let us remove the redundant <code>hx-boost</code> attributes |
|
but ensure all the links are still boosted, inheriting that |
|
functionality from the parent <code>div</code>.</p> |
|
<p>Note that any legal HTML element could be used here, we just use a |
|
<code>div</code> out of habit.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts"</span><span class="dt">></span>Contacts<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/settings"</span><span class="dt">></span>Settings<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/help"</span><span class="dt">></span>Help<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb5-5"><a aria-hidden="true" href="#cb5-5" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Boosting links via the parent</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The <code>hx-boost</code> has been moved to the parent |
|
div.</p></li> |
|
</ol> |
|
<p>Now we don’t have to put an <code>hx-boost="true"</code> on every |
|
link and, in fact, we can add more links alongside the existing ones, |
|
and they, too, will be boosted, without us needing to explicitly |
|
annotate them.</p> |
|
<p>That’s fine, but what if you have a link that you <em class="test">don’t</em> want |
|
boosted within an element that has <code>hx-boost="true"</code> on it? A |
|
good example of this situation is when a link is to a resource to be |
|
downloaded, such as a PDF. Downloading a file can’t be handled well by |
|
an AJAX request, so you probably want that link to behave “normally”, |
|
issuing a full page request for the PDF, which the browser will then |
|
offer to save as a file on the user’s local system.</p> |
|
<p>To handle this situation, you simply override the parent |
|
<code>hx-boost</code> value with <code>hx-boost="false"</code> on the |
|
anchor tag that you don’t want to boost:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb6"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb6-1"><a aria-hidden="true" href="#cb6-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb6-2"><a aria-hidden="true" href="#cb6-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts"</span><span class="dt">></span>Contacts<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb6-3"><a aria-hidden="true" href="#cb6-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/settings"</span><span class="dt">></span>Settings<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb6-4"><a aria-hidden="true" href="#cb6-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/help"</span><span class="dt">></span>Help<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb6-5"><a aria-hidden="true" href="#cb6-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/help/documentation.pdf"</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"false"</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb6-6"><a aria-hidden="true" href="#cb6-6" tabindex="-1"></a> Download Docs</span> |
|
<span id="cb6-7"><a aria-hidden="true" href="#cb6-7" tabindex="-1"></a> <span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb6-8"><a aria-hidden="true" href="#cb6-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Disabling boosting</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The <code>hx-boost</code> is still on the parent div.</p></li> |
|
<li><p>The boosting behavior is overridden for this link.</p></li> |
|
</ol> |
|
<p>Here we have a new link to a documentation PDF that we wish to |
|
function like a regular link. We have added |
|
<code>hx-boost="false"</code> to the link and this declaration will |
|
override the <code>hx-boost="true"</code> on the parent |
|
<code>div</code>, reverting it to regular link behavior and, thus, |
|
allowing for the file download behavior that we want.</p> |
|
<h3 id="_progressive_enhancement">Progressive Enhancement</h3> |
|
<p>A nice aspect of <code>hx-boost</code> is that it is an example of |
|
<em class="test">progressive enhancement</em>:</p> |
|
<dl> |
|
<dt>Progressive Enhancement</dt> |
|
<dd> |
|
<p>A software design philosophy that aims to provide as much essential |
|
content and functionality to as many users as possible, while delivering |
|
a better experience to users with more advanced web browsers.</p> |
|
</dd> |
|
</dl> |
|
<p>Consider the links in the example above. What would happen if someone |
|
did not have JavaScript enabled?</p> |
|
<p>No problem. The application would continue to work, but it would |
|
issue regular HTTP requests, rather than AJAX-based HTTP requests. This |
|
means that your web application will work for the maximum number of |
|
users; those with modern browsers (or users who have not turned off |
|
JavaScript) can take advantage of the benefits of the AJAX-style |
|
navigation that htmx offers, and others can still use the app just |
|
fine.</p> |
|
<p>Compare the behavior of htmx’s <code>hx-boost</code> attribute with a |
|
JavaScript heavy Single Page Application: such an application often |
|
won’t function <em class="test">at all</em> without JavaScript enabled. It is often |
|
very difficult to adopt a progressive enhancement approach when you use |
|
an SPA framework.</p> |
|
<p>This is <em class="test">not</em> to say that every htmx feature offers |
|
progressive enhancement. It is certainly possible to build features that |
|
do not offer a “No JS” fallback in htmx, and, in fact, many of the |
|
features we will build later in the book will fall into this category. |
|
We will note when a feature is progressive enhancement friendly and when |
|
it is not.</p> |
|
<p>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.</p> |
|
<h3 id="_adding_hx_boost_to_contact_app">Adding “hx-boost” to |
|
Contact.app</h3> |
|
<p>For the contact app we are building, we want this htmx “boost” |
|
behavior… well, everywhere.</p> |
|
<p>Right? Why not?</p> |
|
<p>How could we accomplish that?</p> |
|
<p>Well, it’s easy (and pretty common in htmx-powered web applications): |
|
we can just add <code>hx-boost</code> on the <code>body</code> tag of |
|
our <code>layout.html</code> template, and we are done.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">html</span><span class="dt">></span></span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a>...</span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a><span class="dt"><</span><span class="kw">body</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb7-4"><a aria-hidden="true" href="#cb7-4" tabindex="-1"></a>...</span> |
|
<span id="cb7-5"><a aria-hidden="true" href="#cb7-5" tabindex="-1"></a><span class="dt"></</span><span class="kw">body</span><span class="dt">></span></span> |
|
<span id="cb7-6"><a aria-hidden="true" href="#cb7-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">html</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Boosting the entire contact.app</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>All links and forms will be boosted now!</p></li> |
|
</ol> |
|
<p>Now every link and form in our application will use AJAX by default, |
|
making it feel much snappier. Consider the “New Contact” link that we |
|
created on the main page:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/new"</span><span class="dt">></span>Add Contact<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A newly boosted “add contact” link</p></figcaption> |
|
</figure> |
|
<p>Even though we haven’t touched anything on this link or on the |
|
server-side handling of the URL it targets, it will now “just work” as a |
|
boosted link, using AJAX for a snappier user experience, including |
|
updating history, back button support and so on. And, if JavaScript |
|
isn’t enabled, it will fall back to the normal link behavior.</p> |
|
<p>All this with one htmx attribute.</p> |
|
<p>The <code>hx-boost</code> attribute is neat, but is different than |
|
other htmx attributes in that it is pretty “magical”: by making one |
|
small change you modify the behavior of a large number of elements on |
|
the page, turning them into AJAX-powered elements. Most other htmx |
|
attributes are generally lower level and require more explicit |
|
annotations in order to specify exactly what you want htmx to do. In |
|
general, this is the design philosophy of htmx: prefer explicit over |
|
implicit and obvious over “magic.”</p> |
|
<p>However, the <code>hx-boost</code> attribute was too useful to allow |
|
dogma to override practicality, and so it is included as a feature in |
|
the library.</p> |
|
<h2 id="_a_second_step_deleting_contacts_with_http_delete">A Second |
|
Step: Deleting Contacts With HTTP DELETE</h2> |
|
<p>For our next step with htmx, recall that Contact.app has a small form |
|
on the edit page of a contact that is used to delete the contact:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/delete"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"post"</span><span class="dt">></span></span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="dt">></span>Delete Contact<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Plain HTML form to delete a contact</p></figcaption> |
|
</figure> |
|
<p>This form issued an HTTP <code>POST</code> to, for example, |
|
<code>/contacts/42/delete</code>, in order to delete the contact with |
|
the ID 42.</p> |
|
<p>We mentioned previously that one of the annoying things about HTML is |
|
that you can’t issue an HTTP <code>DELETE</code> (or <code>PUT</code> or |
|
<code>PATCH</code>) request directly, even though these are all part of |
|
HTTP and HTTP is <em class="test">obviously designed</em> for transferring HTML.</p> |
|
<p>Thankfully, now, with htmx, we have a chance to rectify this |
|
situation.</p> |
|
<p>The “right thing,” from a RESTful, resource-oriented perspective is, |
|
rather than issuing an HTTP <code>POST</code> to |
|
<code>/contacts/42/delete</code>, to issue an HTTP <code>DELETE</code> |
|
to <code>/contacts/42</code>. We want to delete the contact. The contact |
|
is a resource. The URL for that resource is <code>/contacts/42</code>. |
|
So the ideal is a <code>DELETE</code> request to |
|
<code>/contacts/42/</code>.</p> |
|
<p>Let’s update our application to do this by adding the htmx |
|
<code>hx-delete</code> attribute to the “Delete Contact” button:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb10"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb10-1"><a aria-hidden="true" href="#cb10-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>Delete Contact<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>An htmx-powered button for deleting a |
|
contact</p></figcaption> |
|
</figure> |
|
<p>Now, when a user clicks this button, htmx will issue an HTTP |
|
<code>DELETE</code> request via AJAX to the URL for the contact in |
|
question.</p> |
|
<p>A couple of things to notice:</p> |
|
<ul> |
|
<li><p>We no longer need a <code>form</code> tag to wrap the button, |
|
because the button itself carries the hypermedia action that it performs |
|
directly on itself.</p></li> |
|
<li><p>We no longer need to use the somewhat awkward |
|
<code>"/contacts/{{ contact.id }}/delete"</code> route, but can simply |
|
use the <code>"/contacts/{{ contact.id }}</code> route, since we are |
|
issuing a <code>DELETE</code>. By using a <code>DELETE</code> we |
|
disambiguate between a request intended to update the contact and a |
|
request intended to delete it, using the native HTTP tools available for |
|
exactly this reason.</p></li> |
|
</ul> |
|
<p>Note that we have done something pretty magical here: we have turned |
|
this button into a <em class="test">hypermedia control</em>. It is no longer |
|
necessary that this button be placed within a larger <code>form</code> |
|
tag in order to trigger an HTTP request: it is a stand-alone, and fully |
|
featured hypermedia control on its own. This is at the heart of htmx, |
|
allowing any element to become a hypermedia control and fully |
|
participate in a Hypermedia-Driven Application.</p> |
|
<p>We should also note that, unlike with the <code>hx-boost</code> |
|
examples above, this solution will <em class="test">not</em> degrade gracefully. To |
|
make this solution degrade gracefully, we would need to wrap the button |
|
in a form element and handle a <code>POST</code> on the server side as |
|
well.</p> |
|
<p>In the interest of keeping our application simple, we are going to |
|
omit that more elaborate solution.</p> |
|
<h3 id="_updating_the_server_side_code">Updating The Server-Side |
|
Code</h3> |
|
<p>We have updated the client-side code (if HTML can be considered code) |
|
so it now issues a <code>DELETE</code> request to the appropriate URL, |
|
but we still have some work to do. Since we updated both the route and |
|
the HTTP method we are using, we are going to need to update the |
|
server-side implementation as well to handle this new HTTP request.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb11"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb11-1"><a aria-hidden="true" href="#cb11-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/<contact_id>/delete"</span>, methods<span class="op">=</span>[<span class="st">"POST"</span>])</span> |
|
<span id="cb11-2"><a aria-hidden="true" href="#cb11-2" tabindex="-1"></a><span class="kw">def</span> contacts_delete(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb11-3"><a aria-hidden="true" href="#cb11-3" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id)</span> |
|
<span id="cb11-4"><a aria-hidden="true" href="#cb11-4" tabindex="-1"></a> contact.delete()</span> |
|
<span id="cb11-5"><a aria-hidden="true" href="#cb11-5" tabindex="-1"></a> flash(<span class="st">"Deleted Contact!"</span>)</span> |
|
<span id="cb11-6"><a aria-hidden="true" href="#cb11-6" tabindex="-1"></a> <span class="cf">return</span> redirect(<span class="st">"/contacts"</span>)</span></code></pre></div> |
|
<figcaption><p>The original server-side code for deleting a |
|
contact</p></figcaption> |
|
</figure> |
|
<p>We’ll need to make two changes to our handler: update the route, and |
|
update the HTTP method we are using to delete contacts.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb12"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb12-1"><a aria-hidden="true" href="#cb12-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/<contact_id>"</span>, methods<span class="op">=</span>[<span class="st">"DELETE"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb12-2"><a aria-hidden="true" href="#cb12-2" tabindex="-1"></a><span class="kw">def</span> contacts_delete(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb12-3"><a aria-hidden="true" href="#cb12-3" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id)</span> |
|
<span id="cb12-4"><a aria-hidden="true" href="#cb12-4" tabindex="-1"></a> contact.delete()</span> |
|
<span id="cb12-5"><a aria-hidden="true" href="#cb12-5" tabindex="-1"></a> flash(<span class="st">"Deleted Contact!"</span>)</span> |
|
<span id="cb12-6"><a aria-hidden="true" href="#cb12-6" tabindex="-1"></a> <span class="cf">return</span> redirect(<span class="st">"/contacts"</span>)</span></code></pre></div> |
|
<figcaption><p>Updated handler with new route and |
|
method</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>An updated path and method for the handler.</p></li> |
|
</ol> |
|
<p>Pretty simple, and much cleaner.</p> |
|
<h4 id="_a_response_code_gotcha">A response code gotcha</h4> |
|
<p>Unfortunately, there is a problem with our updated handler: by |
|
default, in Flask the <code>redirect()</code> method responds with a |
|
<code>302 Found</code> HTTP Response Code.</p> |
|
<p>According to the Mozilla Developer Network (MDN) web docs on the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302"><code>302 Found</code></a> |
|
response, this means that the HTTP <em class="test">method</em> of the request |
|
<em class="test">will be unchanged</em> when the redirected HTTP request is |
|
issued.</p> |
|
<p>We are now issuing a <code>DELETE</code> request with htmx and then |
|
being redirected to the <code>/contacts</code> path by flask. According |
|
to this logic, that would mean that the redirected HTTP request would |
|
still be a <code>DELETE</code> method. This means that, as it stands, |
|
the browser will issue a <code>DELETE</code> request to |
|
<code>/contacts</code>.</p> |
|
<p>This is definitely <em class="test">not</em> what we want: we would like the HTTP |
|
redirect to issue a <code>GET</code> request, slightly modifying the |
|
Post/Redirect/Get behavior we discussed earlier to be a |
|
Delete/Redirect/Get.</p> |
|
<p>Fortunately, there is a different response code, <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303"><code>303 See Other</code></a>, |
|
that does what we want: when a browser receives a |
|
<code>303 See Other</code> redirect response, it will issue a |
|
<code>GET</code> to the new location.</p> |
|
<p>So we want to update our code to use the <code>303</code> response |
|
code in the controller.</p> |
|
<p>Thankfully, this is very easy: there is a second parameter to |
|
<code>redirect()</code> that takes the numeric response code you wish to |
|
send.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb13"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb13-1"><a aria-hidden="true" href="#cb13-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/<contact_id>"</span>, methods<span class="op">=</span>[<span class="st">"DELETE"</span>])</span> |
|
<span id="cb13-2"><a aria-hidden="true" href="#cb13-2" tabindex="-1"></a><span class="kw">def</span> contacts_delete(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb13-3"><a aria-hidden="true" href="#cb13-3" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id)</span> |
|
<span id="cb13-4"><a aria-hidden="true" href="#cb13-4" tabindex="-1"></a> contact.delete()</span> |
|
<span id="cb13-5"><a aria-hidden="true" href="#cb13-5" tabindex="-1"></a> flash(<span class="st">"Deleted Contact!"</span>)</span> |
|
<span id="cb13-6"><a aria-hidden="true" href="#cb13-6" tabindex="-1"></a> <span class="cf">return</span> redirect(<span class="st">"/contacts"</span>, <span class="dv">303</span>) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Updated handler with <code>303</code> redirect |
|
response</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The response code is now a 303.</p></li> |
|
</ol> |
|
<p>Now, when you want to remove a given contact, you can simply issue a |
|
<code>DELETE</code> to the same URL as you used to access the contact in |
|
the first place.</p> |
|
<p>This is a natural HTTP-based approach to deleting a resource.</p> |
|
<h3 id="_targeting_the_right_element">Targeting The Right Element</h3> |
|
<p>We aren’t quite finished with our updated delete button. Recall that, |
|
by default, htmx “targets” the element that triggers a request, and will |
|
place the HTML returned by the server inside that element. Right now, |
|
the “Delete Contact” button is targeting itself.</p> |
|
<p>That means that, since the redirect to the <code>/contacts</code> URL |
|
is going to re-render the entire contact list, we will end up with that |
|
contact list placed <em class="test">inside</em> the “Delete Contact” button.</p> |
|
<p>Mis-targeting like this comes up from time to time when you are |
|
working with htmx and can lead to some pretty funny situations.</p> |
|
<p>The fix for this is easy: add an explicit target to the button, and |
|
target the <code>body</code> element with the response:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb14"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb14-1"><a aria-hidden="true" href="#cb14-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span></span> |
|
<span id="cb14-2"><a aria-hidden="true" href="#cb14-2" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb14-3"><a aria-hidden="true" href="#cb14-3" tabindex="-1"></a> Delete Contact</span> |
|
<span id="cb14-4"><a aria-hidden="true" href="#cb14-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A fixed htmx-powered button for deleting a |
|
contact</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>An explicit target added to the button.</p></li> |
|
</ol> |
|
<p>Now our button behaves as expected: clicking on the button will issue |
|
an HTTP <code>DELETE</code> to the server against the URL for the |
|
current contact, delete the contact and redirect back to the contact |
|
list page, with a nice flash message.</p> |
|
<p>Is everything working smoothly now?</p> |
|
<h3 id="_updating_the_location_bar_url_properly">Updating The Location |
|
Bar URL Properly</h3> |
|
<p>Well, almost.</p> |
|
<p>If you click on the button you will notice that, despite the |
|
redirect, the URL in the location bar is not correct. It still points to |
|
<code>/contacts/{{ contact.id }}</code>. That’s because we haven’t told |
|
htmx to update the URL: it just issues the <code>DELETE</code> request |
|
and then updates the DOM with the response.</p> |
|
<p>As we mentioned, boosting via <code>hx-boost</code> will naturally |
|
update the location bar for you, mimicking normal anchors and forms, but |
|
in this case we are building a custom button hypermedia control to issue |
|
a <code>DELETE</code>. We need to let htmx know that we want the |
|
resulting URL from this request “pushed” into the location bar.</p> |
|
<p>We can achieve this by adding the <code>hx-push-url</code> attribute |
|
with the value <code>true</code> to our button:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb15"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb15-1"><a aria-hidden="true" href="#cb15-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span></span> |
|
<span id="cb15-2"><a aria-hidden="true" href="#cb15-2" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span></span> |
|
<span id="cb15-3"><a aria-hidden="true" href="#cb15-3" tabindex="-1"></a><span class="ot"> hx-push-url</span><span class="op">=</span><span class="st">"true"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb15-4"><a aria-hidden="true" href="#cb15-4" tabindex="-1"></a> Delete Contact</span> |
|
<span id="cb15-5"><a aria-hidden="true" href="#cb15-5" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Deleting a contact, now with proper location |
|
information</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We tell htmx to push the redirected URL up into the location |
|
bar.</p></li> |
|
</ol> |
|
<p><em class="test">Now</em> we are done.</p> |
|
<p>We have a button that, all by itself, is able to issue a properly |
|
formatted HTTP <code>DELETE</code> request to the correct URL, and the |
|
UI and location bar are all updated correctly. This was accomplished |
|
with three declarative attributes placed directly on the button: |
|
<code>hx-delete</code>, <code>hx-target</code> and |
|
<code>hx-push-url</code>.</p> |
|
<p>This required more work than the <code>hx-boost</code> change, but |
|
the explicit code makes it easy to see what the button is doing as a |
|
custom hypermedia control. The resulting solution feels clean; it takes |
|
advantage of the built-in features of the web as a hypermedia system |
|
without any URL hacks.</p> |
|
<h3 id="_one_more_thing">One More Thing…</h3> |
|
<p>There is one additional “bonus” feature we can add to our “Delete |
|
Contact” button: a confirmation dialog. Deleting a contact is a |
|
destructive operation and as it stands right now, if the user |
|
inadvertently clicked the “Delete Contact” button, the application would |
|
just delete that contact. Too bad, so sad for the user.</p> |
|
<p>Fortunately htmx has an easy mechanism for adding a confirmation |
|
message on destructive operations like this: the <code>hx-confirm</code> |
|
attribute. You can place this attribute on an element, with a message as |
|
its value, and the JavaScript method <code>confirm()</code> will be |
|
called before a request is issued, which will show a simple confirmation |
|
dialog to the user asking them to confirm the action. Very easy and a |
|
great way to prevent accidents.</p> |
|
<p>Here is how we would add confirmation of the contact delete |
|
operation:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb16"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb16-1"><a aria-hidden="true" href="#cb16-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span></span> |
|
<span id="cb16-2"><a aria-hidden="true" href="#cb16-2" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span></span> |
|
<span id="cb16-3"><a aria-hidden="true" href="#cb16-3" tabindex="-1"></a><span class="ot"> hx-push-url</span><span class="op">=</span><span class="st">"true"</span></span> |
|
<span id="cb16-4"><a aria-hidden="true" href="#cb16-4" tabindex="-1"></a><span class="ot"> hx-confirm</span><span class="op">=</span><span class="st">"Are you sure you want to delete this contact?"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb16-5"><a aria-hidden="true" href="#cb16-5" tabindex="-1"></a> Delete Contact</span> |
|
<span id="cb16-6"><a aria-hidden="true" href="#cb16-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Confirming deletion</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This message will be shown to the user, asking them to confirm |
|
the delete.</p></li> |
|
</ol> |
|
<p>Now, when someone clicks on the “Delete Contact” button, they will be |
|
presented with a prompt that asks “Are you sure you want to delete this |
|
contact?” and they will have an opportunity to cancel if they clicked |
|
the button in error. Very nice.</p> |
|
<p>With this final change we now have a pretty solid “delete contact” |
|
mechanism: we are using the correct RESTful routes and HTTP Methods, we |
|
are confirming the deletion, and we have removed a lot of the cruft that |
|
normal HTML imposes on us, all while using declarative attributes in our |
|
HTML and staying firmly within the normal hypermedia model of the |
|
web.</p> |
|
<h3 id="_progressive_enhancement_2">Progressive Enhancement?</h3> |
|
<p>As we noted earlier about this solution: it is <em class="test">not</em> a |
|
progressive enhancement to our web application. If someone has disabled |
|
JavaScript then this “Delete Contact” button will no longer work. We |
|
would need to do additional work to keep the older form-based mechanism |
|
working in a JavaScript-disabled environment.</p> |
|
<p>Progressive Enhancement can be a hot-button topic in web development, |
|
with lots of passionate opinions and perspectives. Like nearly all |
|
JavaScript libraries, htmx makes it possible to create applications that |
|
do not function in the absence of JavaScript. Retaining support for |
|
non-JavaScript clients requires additional work and complexity in your |
|
application. It is important to determine exactly how important |
|
supporting non-JavaScript clients is before you begin using htmx, or any |
|
other JavaScript framework, for improving your web applications.</p> |
|
<h2 id="_next_steps_validating_contact_emails">Next Steps: Validating |
|
Contact Emails</h2> |
|
<p>Let’s move on to another improvement in our application. A big part |
|
of any web app is validating the data that is submitted to the server: |
|
ensuring emails are correctly formatted and unique, numeric values are |
|
valid, dates are acceptable, and so forth.</p> |
|
<p>Currently, our application has a small amount of validation that is |
|
done entirely server-side and that displays an error message when an |
|
error is detected.</p> |
|
<p>We are not going to go into the details of how validation works in |
|
the model objects, but recall what the code for updating a contact looks |
|
like from Chapter 3:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb17"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb17-1"><a aria-hidden="true" href="#cb17-1" tabindex="-1"></a><span class="kw">def</span> contacts_edit_post(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb17-2"><a aria-hidden="true" href="#cb17-2" tabindex="-1"></a> c <span class="op">=</span> Contact.find(contact_id)</span> |
|
<span id="cb17-3"><a aria-hidden="true" href="#cb17-3" tabindex="-1"></a> c.update(</span> |
|
<span id="cb17-4"><a aria-hidden="true" href="#cb17-4" tabindex="-1"></a> request.form[<span class="st">'first_name'</span>],</span> |
|
<span id="cb17-5"><a aria-hidden="true" href="#cb17-5" tabindex="-1"></a> request.form[<span class="st">'last_name'</span>],</span> |
|
<span id="cb17-6"><a aria-hidden="true" href="#cb17-6" tabindex="-1"></a> request.form[<span class="st">'phone'</span>],</span> |
|
<span id="cb17-7"><a aria-hidden="true" href="#cb17-7" tabindex="-1"></a> request.form[<span class="st">'email'</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb17-8"><a aria-hidden="true" href="#cb17-8" tabindex="-1"></a> <span class="cf">if</span> c.save():</span> |
|
<span id="cb17-9"><a aria-hidden="true" href="#cb17-9" tabindex="-1"></a> flash(<span class="st">"Updated Contact!"</span>)</span> |
|
<span id="cb17-10"><a aria-hidden="true" href="#cb17-10" tabindex="-1"></a> <span class="cf">return</span> redirect(<span class="st">"/contacts/"</span> <span class="op">+</span> <span class="bu">str</span>(contact_id))</span> |
|
<span id="cb17-11"><a aria-hidden="true" href="#cb17-11" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb17-12"><a aria-hidden="true" href="#cb17-12" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"edit.html"</span>, contact<span class="op">=</span>c) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Server-side validation on contact update</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We attempt to save the contact.</p></li> |
|
<li><p>If the save does not succeed we re-render the form to display |
|
error messages.</p></li> |
|
</ol> |
|
<p>So we attempt to save the contact, and, if the <code>save()</code> |
|
method returns true, we redirect to the contact’s detail page. If the |
|
<code>save()</code> method does not return true, that indicates that |
|
there was a validation error; instead of redirecting, we re-render the |
|
HTML for editing the contact. This gives the user a chance to correct |
|
the errors, which are displayed alongside the inputs.</p> |
|
<p>Let’s take a look at the HTML for the email input:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb18"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb18-1"><a aria-hidden="true" href="#cb18-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb18-2"><a aria-hidden="true" href="#cb18-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"email"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb18-3"><a aria-hidden="true" href="#cb18-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"text"</span></span> |
|
<span id="cb18-4"><a aria-hidden="true" href="#cb18-4" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Email"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.email }}"</span><span class="dt">></span></span> |
|
<span id="cb18-5"><a aria-hidden="true" href="#cb18-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['email'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb18-6"><a aria-hidden="true" href="#cb18-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Validation error messages</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Display any errors associated with the email field</p></li> |
|
</ol> |
|
<p>We have a label for the input, an input of type <code>text</code> and |
|
then a bit of HTML to display any error messages associated with the |
|
email. When the template is rendered on the server, if there are errors |
|
associated with the contact’s email, they will be displayed in this |
|
span, which will be highlighted red.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Server-Side Validation Logic</strong></p> |
|
</div> |
|
<div> |
|
<p>Right now there is a bit of logic in the contact class that checks if |
|
there are any other contacts with the same email address, and adds an |
|
error to the contact model if so, since we do not want to have duplicate |
|
emails in the database. This is a very common validation example: emails |
|
are usually unique and adding two contacts with the same email is almost |
|
certainly a user error.</p> |
|
<p>Again, we are not going into the details of how validation works in |
|
our models, but almost all server-side frameworks provide ways to |
|
validate data and collect errors to display to the user. This sort of |
|
infrastructure is very common in Web 1.0 server-side frameworks.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>The error message shown when a user attempts to save a contact with a |
|
duplicate email is “Email Must Be Unique”, as seen in <a class="ref" href="#fig-emailerror">[fig-emailerror]</a>.</p> |
|
<figure id="fig-emailerror"> |
|
<p><img src="https://hypermedia.systems/images/screenshot_validation_error.png"/></p> |
|
<figcaption><p>Email validation error</p></figcaption> |
|
</figure> |
|
<p>All of this is done using plain HTML and using Web 1.0 techniques, |
|
and it works well.</p> |
|
<p>However, as the application currently stands, there are two |
|
annoyances.</p> |
|
<ul> |
|
<li><p>First, there is no email format validation: you can enter |
|
whatever characters you’d like as an email and, as long as they are |
|
unique, the system will allow it.</p></li> |
|
<li><p>Second, we only check the email’s uniqueness when all the data is |
|
submitted: if a user has entered a duplicate email, they will not find |
|
out until they have filled in all the fields. This could be quite |
|
annoying if the user was accidentally reentering a contact and had to |
|
put all the contact information in before being made aware of this |
|
fact.</p></li> |
|
</ul> |
|
<h3 id="_updating_our_input_type">Updating Our Input Type</h3> |
|
<p>For the first issue, we have a pure HTML mechanism for improving our |
|
application: HTML 5 supports inputs of type <code>email</code>. All we |
|
need to do is switch our input from type <code>text</code> to type |
|
<code>email</code>, and the browser will enforce that the value entered |
|
properly matches the email format:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb19"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb19-1"><a aria-hidden="true" href="#cb19-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb19-2"><a aria-hidden="true" href="#cb19-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"email"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb19-3"><a aria-hidden="true" href="#cb19-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb19-4"><a aria-hidden="true" href="#cb19-4" tabindex="-1"></a> placeholder="Email" value="{{ contact.email }}"></span> |
|
<span id="cb19-5"><a aria-hidden="true" href="#cb19-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['email'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb19-6"><a aria-hidden="true" href="#cb19-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Changing the input to type |
|
<code>email</code></p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A change of the <code>type</code> attribute to <code>email</code> |
|
ensures that values entered are valid emails.</p></li> |
|
</ol> |
|
<p>With this change, when the user enters a value that isn’t a valid |
|
email, the browser will display an error message asking for a properly |
|
formed email in that field.</p> |
|
<p>So a simple single-attribute change done in pure HTML improves our |
|
validation and addresses the first problem we noted.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Server-Side vs. Client-Side Validations</strong></p> |
|
</div> |
|
<div> |
|
<p>Experienced web developers might be grinding their teeth at the code |
|
above: this validation is done on <em class="test">the client-side</em>. That is, we |
|
are relying on the browser to detect the malformed email and correct the |
|
user. Unfortunately, the client-side is not trustworthy: a browser may |
|
have a bug in it that allows the user to circumvent this validation |
|
code. Or, worse, the user may be malicious and figure out a mechanism |
|
around our validation entirely, such as using the developer console to |
|
edit the HTML.</p> |
|
<p>This is a perpetual danger in web development: all validations done |
|
on the client-side cannot be trusted and, if the validation is |
|
important, <em class="test">must be redone</em> on the server-side. This is less of a |
|
problem in Hypermedia-Driven Applications than in Single Page |
|
Applications, because the focus of HDAs is the server-side, but it is |
|
worth bearing in mind as you build your application.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_inline_validation">Inline Validation</h3> |
|
<p>While we have improved our validation experience a bit, the user must |
|
still submit the form to get any feedback on duplicate emails. We can |
|
next use htmx to improve this user experience.</p> |
|
<p>It would be better if the user were able to see a duplicate email |
|
error immediately after entering the email value. It turns out that |
|
inputs fire a <code>change</code> event and, in fact, the |
|
<code>change</code> event is the <em class="test">default trigger</em> for inputs in |
|
htmx. So, putting this feature to work, we can implement the following |
|
behavior: when the user enters an email, immediately issue a request to |
|
the server and validate that email, and render an error message if |
|
necessary.</p> |
|
<p>Recall the current HTML for our email input:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb20"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb20-1"><a aria-hidden="true" href="#cb20-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb20-2"><a aria-hidden="true" href="#cb20-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"email"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb20-3"><a aria-hidden="true" href="#cb20-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"email"</span></span> |
|
<span id="cb20-4"><a aria-hidden="true" href="#cb20-4" tabindex="-1"></a><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Email"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.email }}"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb20-5"><a aria-hidden="true" href="#cb20-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['email'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb20-6"><a aria-hidden="true" href="#cb20-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The initial email configuration</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This is the input that we want to have drive an HTTP request to |
|
validate the email.</p></li> |
|
<li><p>This is the span we want to put the error message, if any, |
|
into.</p></li> |
|
</ol> |
|
<p>So we want to add an <code>hx-get</code> attribute to this input. |
|
This will cause the input to issue an HTTP <code>GET</code> request to a |
|
given URL to validate the email. We then want to target the error span |
|
following the input with any error message returned from the server.</p> |
|
<p>Let’s make those changes to our HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb21"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb21-1"><a aria-hidden="true" href="#cb21-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb21-2"><a aria-hidden="true" href="#cb21-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"email"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb21-3"><a aria-hidden="true" href="#cb21-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"email"</span></span> |
|
<span id="cb21-4"><a aria-hidden="true" href="#cb21-4" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/email"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb21-5"><a aria-hidden="true" href="#cb21-5" tabindex="-1"></a> hx-target="next .error" <span class="er"><</span>2></span> |
|
<span id="cb21-6"><a aria-hidden="true" href="#cb21-6" tabindex="-1"></a> placeholder="Email" value="{{ contact.email }}"></span> |
|
<span id="cb21-7"><a aria-hidden="true" href="#cb21-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['email'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb21-8"><a aria-hidden="true" href="#cb21-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Our updated HTML</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Issue an HTTP <code>GET</code> to the <code>email</code> endpoint |
|
for the contact.</p></li> |
|
<li><p>Target the next element with the class <code>error</code> on |
|
it.</p></li> |
|
</ol> |
|
<p>Note that in the <code>hx-target</code> attribute we are using a |
|
<em class="test">relative positional</em> selector, <code>next</code>. This is a |
|
feature of htmx and an extension to normal CSS. Htmx supports prefixes |
|
that will find targets <em class="test">relative</em> to the current element.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Relative Positional Expressions in Htmx</strong></p> |
|
</div> |
|
<div> |
|
<dl> |
|
<dt><code>next</code></dt> |
|
<dd> |
|
<p>Scan forward in the DOM for the next matching element, e.g., |
|
<code>next .error</code></p> |
|
</dd> |
|
<dt><code>previous</code></dt> |
|
<dd> |
|
<p>Scan backwards in the DOM for the closest previous matching element, |
|
e.g., <code>previous .alert</code></p> |
|
</dd> |
|
<dt><code>closest</code></dt> |
|
<dd> |
|
<p>Scan the parents of this element for matching element, e.g., |
|
<code>closest table</code></p> |
|
</dd> |
|
<dt><code>find</code></dt> |
|
<dd> |
|
<p>Scan the children of this element for matching element, e.g., |
|
<code>find span</code></p> |
|
</dd> |
|
<dt><code>this</code></dt> |
|
<dd> |
|
<p>the current element is the target (default)</p> |
|
</dd> |
|
</dl> |
|
</div> |
|
</div> |
|
</div> |
|
<p>By using relative positional expressions we can avoid adding explicit |
|
ids to elements and take advantage of the local structure of HTML.</p> |
|
<p>So, in our example with added <code>hx-get</code> and |
|
<code>hx-target</code> attributes, whenever someone changes the value of |
|
the input (remember, <code>change</code> is the <em class="test">default</em> trigger |
|
for inputs in htmx) an HTTP <code>GET</code> request will be issued to |
|
the given URL. If there are any errors, they will be loaded into the |
|
error span.</p> |
|
<h3 id="_validating_emails_server_side">Validating Emails |
|
Server-Side</h3> |
|
<p>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 |
|
<code>validate()</code> method on it.</p> |
|
<p>That method will validate the email is unique and so forth. At that |
|
point we can return any errors associated with the email directly, or |
|
the empty string if none exist.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb22"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb22-1"><a aria-hidden="true" href="#cb22-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/<contact_id>/email"</span>, methods<span class="op">=</span>[<span class="st">"GET"</span>])</span> |
|
<span id="cb22-2"><a aria-hidden="true" href="#cb22-2" tabindex="-1"></a><span class="kw">def</span> contacts_email_get(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb22-3"><a aria-hidden="true" href="#cb22-3" tabindex="-1"></a> c <span class="op">=</span> Contact.find(contact_id) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb22-4"><a aria-hidden="true" href="#cb22-4" tabindex="-1"></a> c.email <span class="op">=</span> request.args.get(<span class="st">'email'</span>) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb22-5"><a aria-hidden="true" href="#cb22-5" tabindex="-1"></a> c.validate() <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb22-6"><a aria-hidden="true" href="#cb22-6" tabindex="-1"></a> <span class="cf">return</span> c.errors.get(<span class="st">'email'</span>) <span class="kw">or</span> <span class="st">""</span> <span class="op"><</span><span class="dv">4</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Code for our email validation endpoint</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Look up the contact by id.</p></li> |
|
<li><p>Update its email (note that since this is a <code>GET</code>, we |
|
use the <code>args</code> property rather than the <code>form</code> |
|
property).</p></li> |
|
<li><p>Validate the contact.</p></li> |
|
<li><p>Return a string, either the errors associated with the email |
|
field or, if there are none, the empty string.</p></li> |
|
</ol> |
|
<p>With this small bit of server-side code in place, we now have the |
|
following user experience: when a user enters an email and tabs to the |
|
next input field, they are immediately notified if the email is already |
|
taken.</p> |
|
<p>Note that the email validation is <em class="test">still</em> done when the entire |
|
contact is submitted for an update, so there is no danger of allowing |
|
duplicate email contacts to slip through: we have simply made it |
|
possible for users to catch this situation earlier by use of htmx.</p> |
|
<p>It is also worth noting that this particular email validation |
|
<em class="test">must</em> be done on the server side: you cannot determine that an |
|
email is unique across all contacts unless you have access to the data |
|
store of record. This is another simplifying aspect of Hypermedia-Driven |
|
Applications: since validations are done server-side, you have access to |
|
all the data you might need to do any sort of validation you’d like.</p> |
|
<p>Here again we want to stress that this interaction is done entirely |
|
within the hypermedia model: we are using declarative attributes and |
|
exchanging hypermedia with the server in a manner very similar to how |
|
links or forms work. But we have managed to improve our user experience |
|
dramatically.</p> |
|
<h3 id="_taking_the_user_experience_further">Taking The User Experience |
|
Further</h3> |
|
<p>Despite the fact that we haven’t added a lot of code here, we have a |
|
fairly sophisticated user interface, at least when compared with plain |
|
HTML-based applications. However, if you have used more advanced Single |
|
Page Applications you have probably seen the pattern where an email |
|
field (or a similar sort of input) is validated <em class="test">as you |
|
type</em>.</p> |
|
<p>This seems like the sort of interactivity that is only possible with |
|
a sophisticated, complex JavaScript framework, right?</p> |
|
<p>Well, no.</p> |
|
<p>It turns out that you can implement this functionality in htmx, using |
|
pure HTML attributes.</p> |
|
<p>In fact, all we need to do is to change our trigger. Currently, we |
|
are using the default trigger for inputs, which is the |
|
<code>change</code> event. To validate as the user types, we would want |
|
to capture the <code>keyup</code> event as well:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb23"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb23-1"><a aria-hidden="true" href="#cb23-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb23-2"><a aria-hidden="true" href="#cb23-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"email"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb23-3"><a aria-hidden="true" href="#cb23-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"email"</span></span> |
|
<span id="cb23-4"><a aria-hidden="true" href="#cb23-4" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/email"</span></span> |
|
<span id="cb23-5"><a aria-hidden="true" href="#cb23-5" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"next .error"</span></span> |
|
<span id="cb23-6"><a aria-hidden="true" href="#cb23-6" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"change, keyup"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb23-7"><a aria-hidden="true" href="#cb23-7" tabindex="-1"></a> placeholder="Email" value="{{ contact.email }}"></span> |
|
<span id="cb23-8"><a aria-hidden="true" href="#cb23-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['email'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb23-9"><a aria-hidden="true" href="#cb23-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Triggering With <code>keyup</code> |
|
events</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>An explicit <code>keyup</code> trigger has been added along with |
|
<code>change</code>.</p></li> |
|
</ol> |
|
<p>With this tiny change, every time a user types a character we will |
|
issue a request and validate the email. Simple.</p> |
|
<h3 id="_debouncing_our_validation_requests">Debouncing Our Validation |
|
Requests</h3> |
|
<p>Simple, yes, but probably not what we want: issuing a new request on |
|
every key up event would be very wasteful and could potentially |
|
overwhelm your server. What we want instead is only issue the request if |
|
the user has paused for a small amount of time. This is called |
|
“debouncing” the input, where requests are delayed until things have |
|
“settled down”.</p> |
|
<p>Htmx supports a <code>delay</code> modifier for triggers that allows |
|
you to debounce a request by adding a delay before the request is sent. |
|
If another event of the same kind appears within that interval, htmx |
|
will not issue the request and will reset the timer.</p> |
|
<p>This turns out to be exactly what we want for our email input: if the |
|
user is busy typing in an email we won’t interrupt them, but as soon as |
|
they pause or leave the field, we’ll issue a request.</p> |
|
<p>Let’s add a delay of 200 milliseconds to the <code>keyup</code> |
|
trigger, which is long enough to detect that the user has stopped |
|
typing.:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb24"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb24-1"><a aria-hidden="true" href="#cb24-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb24-2"><a aria-hidden="true" href="#cb24-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"email"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb24-3"><a aria-hidden="true" href="#cb24-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"email"</span></span> |
|
<span id="cb24-4"><a aria-hidden="true" href="#cb24-4" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/email"</span></span> |
|
<span id="cb24-5"><a aria-hidden="true" href="#cb24-5" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"next .error"</span></span> |
|
<span id="cb24-6"><a aria-hidden="true" href="#cb24-6" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"change, keyup delay:200ms"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb24-7"><a aria-hidden="true" href="#cb24-7" tabindex="-1"></a> placeholder="Email" value="{{ contact.email }}"></span> |
|
<span id="cb24-8"><a aria-hidden="true" href="#cb24-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['email'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb24-9"><a aria-hidden="true" href="#cb24-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Debouncing the <code>keyup</code> event</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We debounce the <code>keyup</code> event by adding a |
|
<code>delay</code> modifier.</p></li> |
|
</ol> |
|
<p>Now we no longer issue a stream of validation requests as the user |
|
types. Instead, we wait until the user pauses for a bit and then issue |
|
the request. Much better for our server, and still a great user |
|
experience.</p> |
|
<h3 id="_ignoring_non_mutating_keys">Ignoring Non-Mutating Keys</h3> |
|
<p>There is one last issue we should address with the keyup event: as it |
|
stands we will issue a request no matter <em class="test">which</em> keys are |
|
pressed, even if they are keys that have no effect on the value of the |
|
input, such as arrow keys. It would be better if there were a way to |
|
only issue a request if the input value has changed.</p> |
|
<p>And it turns out that htmx has support for that exact pattern, by |
|
using the <code>changed</code> modifier for events. (Not to be confused |
|
with the <code>change</code> event triggered by the DOM on input |
|
elements.)</p> |
|
<p>By adding <code>changed</code> to our <code>keyup</code> trigger, the |
|
input will not issue validation requests unless the keyup event actually |
|
updates the inputs value:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb25"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb25-1"><a aria-hidden="true" href="#cb25-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb25-2"><a aria-hidden="true" href="#cb25-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"email"</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb25-3"><a aria-hidden="true" href="#cb25-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> name</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> id</span><span class="op">=</span><span class="st">"email"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"email"</span></span> |
|
<span id="cb25-4"><a aria-hidden="true" href="#cb25-4" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/email"</span></span> |
|
<span id="cb25-5"><a aria-hidden="true" href="#cb25-5" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"next .error"</span></span> |
|
<span id="cb25-6"><a aria-hidden="true" href="#cb25-6" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"change, keyup delay:200ms changed"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb25-7"><a aria-hidden="true" href="#cb25-7" tabindex="-1"></a> placeholder="Email" value="{{ contact.email }}"></span> |
|
<span id="cb25-8"><a aria-hidden="true" href="#cb25-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> class</span><span class="op">=</span><span class="st">"error"</span><span class="dt">></span>{{ contact.errors['email'] }}<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb25-9"><a aria-hidden="true" href="#cb25-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Only sending requests when the input value |
|
changes</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We do away with pointless requests by only issuing them when the |
|
input’s value has actually changed.</p></li> |
|
</ol> |
|
<p>That’s some pretty good-looking and powerful HTML, providing an |
|
experience that most developers would think requires a complicated |
|
client-side solution.</p> |
|
<p>With a total of three attributes and a simple new server-side |
|
endpoint, we have added a fairly sophisticated user experience to our |
|
web application. Even better, any email validation rules we add on the |
|
server side will <em class="test">automatically</em> just work using this model: |
|
because we are using hypermedia as our communication mechanism there is |
|
no need to keep a client-side and server-side model in sync with one |
|
another.</p> |
|
<p>A great demonstration of the power of the hypermedia |
|
architecture!</p> |
|
<h2 id="_another_application_improvement_paging">Another Application |
|
Improvement: Paging</h2> |
|
<p>Let’s move on from the contact editing page for a bit and improve the |
|
root page of the application, found at the <code>/contacts</code> path |
|
and rendering the <code>index.html</code> template.</p> |
|
<p>Currently, Contact.app does not support paging: if there are 10,000 |
|
contacts in the database we will show all 10,000 contacts on the root |
|
page. Showing so much data can bog a browser (and a server) down, so |
|
most web applications adopt a concept of “paging” to deal with data sets |
|
this large, where only one “page” of a smaller number of items is shown, |
|
with the ability to navigate around the pages in the data set.</p> |
|
<p>Let’s fix our application so that we only show ten contacts at a time |
|
with a “Next” and “Previous” link if there are more than 10 contacts in |
|
the contact database.</p> |
|
<p>The first change we will make is to add a simple paging widget to our |
|
<code>index.html</code> template.</p> |
|
<p>We will conditionally include two links:</p> |
|
<ul> |
|
<li><p>If we are beyond the “first” page, we will include a link to the |
|
previous page</p></li> |
|
<li><p>If there are ten contacts in the current result set, we will |
|
include a link to the next page</p></li> |
|
</ul> |
|
<p>This isn’t a perfect paging widget: ideally we’d show the number of |
|
pages and offer the ability to do more specific page navigation, and |
|
there is the possibility that the next page might have 0 results in it |
|
since we aren’t checking the total results count, but it will do for now |
|
for our simple application.</p> |
|
<p>Let’s look at the jinja template code for this in |
|
<code>index.html</code>.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb26"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb26-1"><a aria-hidden="true" href="#cb26-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb26-2"><a aria-hidden="true" href="#cb26-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> style</span><span class="op">=</span><span class="st">"float: right"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb26-3"><a aria-hidden="true" href="#cb26-3" tabindex="-1"></a> {% if page > 1 %}</span> |
|
<span id="cb26-4"><a aria-hidden="true" href="#cb26-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts?page={{ page - 1 }}"</span><span class="dt">></span>Previous<span class="dt"></</span><span class="kw">a</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb26-5"><a aria-hidden="true" href="#cb26-5" tabindex="-1"></a> {% endif %}</span> |
|
<span id="cb26-6"><a aria-hidden="true" href="#cb26-6" tabindex="-1"></a> {% if contacts|length == 10 %}</span> |
|
<span id="cb26-7"><a aria-hidden="true" href="#cb26-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts?page={{ page + 1 }}"</span><span class="dt">></span>Next<span class="dt"></</span><span class="kw">a</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb26-8"><a aria-hidden="true" href="#cb26-8" tabindex="-1"></a> {% endif %}</span> |
|
<span id="cb26-9"><a aria-hidden="true" href="#cb26-9" tabindex="-1"></a> <span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb26-10"><a aria-hidden="true" href="#cb26-10" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding paging widgets to our list of |
|
contacts</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Include a new div under the table to hold our navigation |
|
links.</p></li> |
|
<li><p>If we are beyond page 1, include an anchor tag with the page |
|
decremented by one.</p></li> |
|
<li><p>If there are 10 contacts in the current page, include an anchor |
|
tag linking to the next page by incrementing it by one.</p></li> |
|
</ol> |
|
<p>Note that here we are using a special jinja filter syntax |
|
<code>contacts|length</code> to compute the length of the contacts list. |
|
The details of this filter syntax is beyond the scope of this book, but |
|
in this case you can think of it as invoking the |
|
<code>contacts.length</code> property and then comparing that with |
|
<code>10</code>.</p> |
|
<p>Now that we have these links in place, let’s address the server-side |
|
implementation of paging.</p> |
|
<p>We are using the <code>page</code> request parameter to encode the |
|
paging state of the UI. So, in our handler, we need to look for that |
|
<code>page</code> parameter and pass that through to our model, as an |
|
integer, so the model knows which page of contacts to return:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb27"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb27-1"><a aria-hidden="true" href="#cb27-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts"</span>)</span> |
|
<span id="cb27-2"><a aria-hidden="true" href="#cb27-2" tabindex="-1"></a><span class="kw">def</span> contacts():</span> |
|
<span id="cb27-3"><a aria-hidden="true" href="#cb27-3" tabindex="-1"></a> search <span class="op">=</span> request.args.get(<span class="st">"q"</span>)</span> |
|
<span id="cb27-4"><a aria-hidden="true" href="#cb27-4" tabindex="-1"></a> page <span class="op">=</span> <span class="bu">int</span>(request.args.get(<span class="st">"page"</span>, <span class="dv">1</span>)) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb27-5"><a aria-hidden="true" href="#cb27-5" tabindex="-1"></a> <span class="cf">if</span> search <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span> |
|
<span id="cb27-6"><a aria-hidden="true" href="#cb27-6" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.search(search)</span> |
|
<span id="cb27-7"><a aria-hidden="true" href="#cb27-7" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb27-8"><a aria-hidden="true" href="#cb27-8" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>(page) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb27-9"><a aria-hidden="true" href="#cb27-9" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"index.html"</span>,</span> |
|
<span id="cb27-10"><a aria-hidden="true" href="#cb27-10" tabindex="-1"></a> contacts<span class="op">=</span>contacts_set, page<span class="op">=</span>page)</span></code></pre></div> |
|
<figcaption><p>Adding paging to our request handler</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Resolve the page parameter, defaulting to page 1 if no page is |
|
passed in.</p></li> |
|
<li><p>Pass the page through to the model when loading all contacts so |
|
it knows which page of 10 contacts to return.</p></li> |
|
</ol> |
|
<p>This is fairly straightforward: we just need to get another |
|
parameter, like the <code>q</code> parameter we passed in for searching |
|
contacts earlier, convert it to an integer and then pass it through to |
|
the <code>Contact</code> model, so it knows which page to return.</p> |
|
<p>And, with that small change, we are done: we now have a very basic |
|
paging mechanism for our web application.</p> |
|
<p>And, believe it or not, it is already using AJAX, thanks to our use |
|
of <code>hx-boost</code> in the application. Easy!</p> |
|
<h3 id="_click_to_load">Click To Load</h3> |
|
<p>This paging mechanism is fine for a basic web application, and it is |
|
used extensively on the internet. But it has some drawbacks associated |
|
with it: every time you click the “Next” or “Previous” buttons you get a |
|
whole new page of contacts and lose any context you had on the previous |
|
page.</p> |
|
<p>Sometimes a more advanced paging UI pattern might be better. Maybe, |
|
rather than loading in a new page of elements and replacing the current |
|
elements, it would be nicer to append the next page of elements |
|
<em class="test">inline</em>, after the current elements.</p> |
|
<p>This is the common “click to load” UX pattern, found in more advanced |
|
web applications.</p> |
|
<figure id="fig-clicktoload"> |
|
<p><img src="https://hypermedia.systems/images/screenshot_click_to_load.png"/></p> |
|
<figcaption><p>A Click To Load UI</p></figcaption> |
|
</figure> |
|
<p>In <a class="ref" href="#fig-clicktoload">[fig-clicktoload]</a>, you |
|
have a button that you can click, and it will load the next set of |
|
contacts directly into the page, rather than “paging” to the next page. |
|
This allows you to keep the current contacts “in context” visually on |
|
the page, but still progress through them as you would in a normal, |
|
paged user interface.</p> |
|
<p>Let’s see how we can implement this UX pattern in htmx.</p> |
|
<p>It’s actually surprisingly simple: we can just take the existing |
|
“Next” link and repurpose it a bit using nothing but a few htmx |
|
attributes!</p> |
|
<p>We want to have a button that, when clicked, appends the rows from |
|
the next page of contacts to the current, existing table, rather than |
|
re-rendering the whole table. This can be achieved by adding a new row |
|
to our table that has just such a button in it:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb28"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb28-1"><a aria-hidden="true" href="#cb28-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">tbody</span><span class="dt">></span></span> |
|
<span id="cb28-2"><a aria-hidden="true" href="#cb28-2" tabindex="-1"></a>{% for contact in contacts %}</span> |
|
<span id="cb28-3"><a aria-hidden="true" href="#cb28-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb28-4"><a aria-hidden="true" href="#cb28-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.first }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb28-5"><a aria-hidden="true" href="#cb28-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.last }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb28-6"><a aria-hidden="true" href="#cb28-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.phone }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb28-7"><a aria-hidden="true" href="#cb28-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.email }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb28-8"><a aria-hidden="true" href="#cb28-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb28-9"><a aria-hidden="true" href="#cb28-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb28-10"><a aria-hidden="true" href="#cb28-10" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>View<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb28-11"><a aria-hidden="true" href="#cb28-11" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb28-12"><a aria-hidden="true" href="#cb28-12" tabindex="-1"></a>{% endfor %}</span> |
|
<span id="cb28-13"><a aria-hidden="true" href="#cb28-13" tabindex="-1"></a>{% if contacts|length == 10 %} <span class="er"><</span>1></span> |
|
<span id="cb28-14"><a aria-hidden="true" href="#cb28-14" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb28-15"><a aria-hidden="true" href="#cb28-15" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="ot"> colspan</span><span class="op">=</span><span class="st">"5"</span><span class="ot"> style</span><span class="op">=</span><span class="st">"text-align: center"</span><span class="dt">></span></span> |
|
<span id="cb28-16"><a aria-hidden="true" href="#cb28-16" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"closest tr"</span><span class="ot"> </span><span class="er"><2</span><span class="dt">></span></span> |
|
<span id="cb28-17"><a aria-hidden="true" href="#cb28-17" tabindex="-1"></a> hx-swap="outerHTML" <span class="er"><</span>3></span> |
|
<span id="cb28-18"><a aria-hidden="true" href="#cb28-18" tabindex="-1"></a> hx-select="tbody > tr" <span class="er"><</span>4></span> |
|
<span id="cb28-19"><a aria-hidden="true" href="#cb28-19" tabindex="-1"></a> hx-get="/contacts?page={{ page + 1 }}"></span> |
|
<span id="cb28-20"><a aria-hidden="true" href="#cb28-20" tabindex="-1"></a> Load More</span> |
|
<span id="cb28-21"><a aria-hidden="true" href="#cb28-21" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb28-22"><a aria-hidden="true" href="#cb28-22" tabindex="-1"></a> <span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb28-23"><a aria-hidden="true" href="#cb28-23" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb28-24"><a aria-hidden="true" href="#cb28-24" tabindex="-1"></a>{% endif %}</span> |
|
<span id="cb28-25"><a aria-hidden="true" href="#cb28-25" tabindex="-1"></a><span class="dt"></</span><span class="kw">tbody</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Changing to “click to load”</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Only show “Load More” if there are 10 contact results in the |
|
current page.</p></li> |
|
<li><p>Target the closest enclosing row.</p></li> |
|
<li><p>Replace the entire row with the response from the |
|
server.</p></li> |
|
<li><p>Select out the table rows from the response.</p></li> |
|
</ol> |
|
<p>Let’s go through each attribute in detail here.</p> |
|
<p>First, we are using <code>hx-target</code> to target the “closest” |
|
<code>tr</code> element, that is, the closest <em class="test">parent</em> table |
|
row.</p> |
|
<p>Second, we want to replace this <em class="test">entire</em> row with whatever |
|
content comes back from the server.</p> |
|
<p>Third, we want to yank out only the <code>tr</code> elements in the |
|
response. We are replacing this <code>tr</code> element with a new set |
|
of <code>tr</code> elements, which will have additional contact |
|
information in them, as well as, if necessary, a new “Load More” button |
|
that points to the <em class="test">next</em> next page. To do this, we use a CSS |
|
selector <code>tbody > tr</code> to ensure we only pull out the rows |
|
in the body of the table in the response. This avoids including rows in |
|
the table header, for example.</p> |
|
<p>Finally, we issue an HTTP <code>GET</code> to the url that will serve |
|
the next page of contacts, which looks just like the “Next” link from |
|
above.</p> |
|
<p>Somewhat surprisingly, no server-side changes are necessary for this |
|
new functionality. This is because of the flexibility that htmx gives |
|
you with respect to how it processes server responses.</p> |
|
<p>So, four attributes, and we now have a sophisticated “Click To Load” |
|
UX, via htmx.</p> |
|
<h3 id="_infinite_scroll">Infinite Scroll</h3> |
|
<p>Another common pattern for dealing with large sets of things is known |
|
as the “Infinite Scroll” pattern. In this pattern, as the last item of a |
|
list or table of elements is scrolled into view, more elements are |
|
loaded and appended to the list or table.</p> |
|
<p>Now, this behavior makes more sense in situations where a user is |
|
exploring a category or series of social media posts, rather than in the |
|
context of a contact application. However, for completeness, and to just |
|
show what you can do with htmx, we will implement this pattern as |
|
well.</p> |
|
<p>It turns out that we can repurpose the “Click To Load” code to |
|
implement this new pattern quite easily: if you think about it for a |
|
moment, infinite scroll is really just the “Click To Load” logic, but |
|
rather than loading when a click event occurs, we want to load when an |
|
element is “revealed” in the view portal of the browser.</p> |
|
<p>As luck would have it, htmx offers a synthetic (non-standard) DOM |
|
event, <code>revealed</code> that can be used in tandem with the |
|
<code>hx-trigger</code> attribute, to trigger a request when, well, when |
|
an element is revealed.</p> |
|
<p>So let’s convert our button to a span and take advantage of this |
|
event:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb29"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb29-1"><a aria-hidden="true" href="#cb29-1" tabindex="-1"></a>{% if contacts|length == 10 %}</span> |
|
<span id="cb29-2"><a aria-hidden="true" href="#cb29-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb29-3"><a aria-hidden="true" href="#cb29-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="ot"> colspan</span><span class="op">=</span><span class="st">"5"</span><span class="ot"> style</span><span class="op">=</span><span class="st">"text-align: center"</span><span class="dt">></span></span> |
|
<span id="cb29-4"><a aria-hidden="true" href="#cb29-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">span</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"closest tr"</span></span> |
|
<span id="cb29-5"><a aria-hidden="true" href="#cb29-5" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"revealed"</span></span> |
|
<span id="cb29-6"><a aria-hidden="true" href="#cb29-6" tabindex="-1"></a><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span></span> |
|
<span id="cb29-7"><a aria-hidden="true" href="#cb29-7" tabindex="-1"></a><span class="ot"> hx-select</span><span class="op">=</span><span class="st">"tbody > tr"</span></span> |
|
<span id="cb29-8"><a aria-hidden="true" href="#cb29-8" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts?page={{ page + 1 }}"</span><span class="dt">></span>Loading More...<span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span> |
|
<span id="cb29-9"><a aria-hidden="true" href="#cb29-9" tabindex="-1"></a> <span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb29-10"><a aria-hidden="true" href="#cb29-10" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb29-11"><a aria-hidden="true" href="#cb29-11" tabindex="-1"></a>{% endif %}</span></code></pre></div> |
|
<figcaption><p>Changing to “infinite scroll”</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We have converted our element from a button to a span, since the |
|
user will not be clicking on it.</p></li> |
|
<li><p>We trigger the request when the element is revealed, that is when |
|
it comes into view in the portal.</p></li> |
|
</ol> |
|
<p>All we needed to do to convert from “Click to Load” to “Infinite |
|
Scroll” was to update our element to be a span and then add the |
|
<code>revealed</code> event trigger.</p> |
|
<p>The fact that switching to infinite scroll was so easy shows how well |
|
htmx generalizes HTML: just a few attributes allow us to dramatically |
|
expand what we can achieve in the hypermedia.</p> |
|
<p>And, again, we are doing all this while taking advantage of the |
|
RESTful model of the web. Despite all this new behavior, we are still |
|
exchanging hypermedia with the server, with no JSON API response to be |
|
seen.</p> |
|
<p>As the web was designed.</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: Caution With Modals and “display: |
|
none”</h2> |
|
<p><em class="test">Think twice about modals.</em> Modal windows have become popular, |
|
almost standard, in many web applications today.</p> |
|
<p>Unfortunately, modal windows do not play well with much of the |
|
infrastructure of the web and introduce client-side state that can be |
|
difficult (though not impossible) to integrate cleanly with the |
|
hypermedia-based approach.</p> |
|
<p>Modal windows can be used safely for views that don’t constitute a |
|
resource or correspond to a domain entity:</p> |
|
<ul> |
|
<li><p>Alerts</p></li> |
|
<li><p>Confirmation dialogs</p></li> |
|
<li><p>Forms for creating/updating entities</p></li> |
|
</ul> |
|
<p>Otherwise, consider using alternatives such as inline editing, or a |
|
separate page, rather than a modal.</p> |
|
<p><em class="test">Use <code>display: none;</code> with care</em>. The issue is that |
|
it is not purely cosmetic — it also removes elements from the |
|
accessibility tree and keyboard focus. This is sometimes done to present |
|
the same content to visual and aural interfaces. If you want to hide an |
|
element visually without hiding it from assistive technology (e.g. the |
|
element contains information that is communicated through styling), you |
|
can use this utility class:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb30"><pre class="sourceCode css"><code class="sourceCode css"><span id="cb30-1"><a aria-hidden="true" href="#cb30-1" tabindex="-1"></a><span class="fu">.vh</span> {</span> |
|
<span id="cb30-2"><a aria-hidden="true" href="#cb30-2" tabindex="-1"></a> <span class="kw">position</span><span class="ch">:</span> <span class="dv">absolute</span><span class="op">;</span></span> |
|
<span id="cb30-3"><a aria-hidden="true" href="#cb30-3" tabindex="-1"></a> clip<span class="ch">:</span> rect<span class="fu">(</span><span class="dv">0</span> <span class="dv">0</span> <span class="dv">0</span> <span class="dv">0</span><span class="fu">)</span><span class="op">;</span></span> |
|
<span id="cb30-4"><a aria-hidden="true" href="#cb30-4" tabindex="-1"></a> <span class="kw">clip-path</span><span class="ch">:</span> <span class="fu">inset(</span><span class="dv">50</span><span class="dt">%</span><span class="fu">)</span><span class="op">;</span></span> |
|
<span id="cb30-5"><a aria-hidden="true" href="#cb30-5" tabindex="-1"></a> <span class="kw">block-size</span><span class="ch">:</span> <span class="dv">1</span><span class="dt">px</span><span class="op">;</span></span> |
|
<span id="cb30-6"><a aria-hidden="true" href="#cb30-6" tabindex="-1"></a> <span class="kw">inline-size</span><span class="ch">:</span> <span class="dv">1</span><span class="dt">px</span><span class="op">;</span></span> |
|
<span id="cb30-7"><a aria-hidden="true" href="#cb30-7" tabindex="-1"></a> <span class="kw">overflow</span><span class="ch">:</span> <span class="dv">hidden</span><span class="op">;</span></span> |
|
<span id="cb30-8"><a aria-hidden="true" href="#cb30-8" tabindex="-1"></a> <span class="kw">white-space</span><span class="ch">:</span> <span class="dv">nowrap</span><span class="op">;</span></span> |
|
<span id="cb30-9"><a aria-hidden="true" href="#cb30-9" tabindex="-1"></a>}</span></code></pre></div> |
|
</figure> |
|
<p><code>vh</code> is short for “visually hidden.” This class uses |
|
multiple methods and workarounds to make sure no browser removes the |
|
element’s function.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">More Htmx Patterns</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_active_search">Active Search</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_our_current_search_ui">Our Current Search UI</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_adding_active_search">Adding Active Search</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_targeting_the_correct_element">Targeting The Correct |
|
Element</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_paring_down_our_content">Paring Down Our Content</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_http_request_headers_in_htmx">HTTP Request Headers In Htmx</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_factoring_your_templates">Factoring Your Templates</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_using_our_new_template">Using Our New Template</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_updating_the_navigation_bar_with_hx_push_url">Updating the |
|
Navigation Bar With “hx-push-url”</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_adding_a_request_indicator">Adding A Request Indicator</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_lazy_loading">Lazy Loading</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_pulling_out_the_expensive_code">Pulling Out The Expensive |
|
Code</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_adding_an_indicator">Adding An Indicator</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_but_thats_not_lazy">But That’s Not Lazy!</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_inline_delete">Inline Delete</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_narrowing_our_target">Narrowing Our Target</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_updating_the_server_side">Updating The Server Side</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_the_htmx_swapping_model">The Htmx Swapping Model</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_taking_advantage_of_htmx_swapping">Taking Advantage of |
|
“htmx-swapping”</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_bulk_delete">Bulk Delete</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_the_delete_selected_contacts_button">The “Delete Selected |
|
Contacts” Button</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#_the_server_side_for_delete_selected_contacts">The Server Side |
|
for Delete Selected Contacts</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/more-htmx-patterns/#html-note-title">HTML Notes: Accessible by Default?</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<h2 id="_active_search">Active Search</h2> |
|
<p>So far so good with Contact.app: we have a nice little web |
|
application with some significant improvements over a plain HTML-based |
|
application. We’ve added a proper “Delete Contact” button, done some |
|
dynamic validation of input and looked at different approaches to add |
|
paging to the application. As we have said, many web developers would |
|
expect that a lot of JavaScript-based scripting would be required to get |
|
these features, but we’ve done it all in relatively pure HTML, using |
|
only htmx attributes.</p> |
|
<p>We <em class="test">will</em> eventually add some client-side scripting to our |
|
application: hypermedia is powerful, but it isn’t <em class="test">all powerful</em> |
|
and sometimes scripting might be the best (or only) way to achieve a |
|
given goal. For now, however, let’s see what we can accomplish with |
|
hypermedia.</p> |
|
<p>The first advanced htmx feature we will create is known as the |
|
“Active Search” pattern. Active Search is when, as a user types text |
|
into a search box, the results of that search are dynamically shown. |
|
This pattern was made popular when Google adopted it for search results, |
|
and many applications now implement it.</p> |
|
<p>To implement Active Search, we are going to use techniques closely |
|
related to the way we did email validation in the previous chapter. If |
|
you think about it, the two features are similar in many ways: in both |
|
cases we want to issue a request as the user types into an input and |
|
then update some other element with a response. The server-side |
|
implementations will, of course, be very different, but the frontend |
|
code will look fairly similar due to htmx’s general approach of “issue a |
|
request on an event and replace something on the screen.”</p> |
|
<h3 id="_our_current_search_ui">Our Current Search UI</h3> |
|
<p>Let’s recall what the search field in our application currently looks |
|
like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb1"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb1-1"><a aria-hidden="true" href="#cb1-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"get"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"tool-bar"</span><span class="dt">></span></span> |
|
<span id="cb1-2"><a aria-hidden="true" href="#cb1-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"search"</span><span class="dt">></span>Search Term<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb1-3"><a aria-hidden="true" href="#cb1-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span></span> |
|
<span id="cb1-4"><a aria-hidden="true" href="#cb1-4" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb1-5"><a aria-hidden="true" href="#cb1-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> type</span><span class="op">=</span><span class="st">"submit"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"Search"</span><span class="dt">/></span></span> |
|
<span id="cb1-6"><a aria-hidden="true" href="#cb1-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Our search form</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The <code>q</code> or “query” parameter our client-side code uses |
|
to search.</p></li> |
|
</ol> |
|
<p>Recall that we have some server-side code that looks for the |
|
<code>q</code> parameter and, if it is present, searches the contacts |
|
for that term.</p> |
|
<p>As it stands right now, the user must hit enter when the search input |
|
is focused, or click the “Search” button. Both of these events will |
|
trigger a <code>submit</code> event on the form, causing it to issue an |
|
HTTP <code>GET</code> and re-rendering the whole page.</p> |
|
<p>Currently, thanks to <code>hx-boost</code>, the form will use an AJAX |
|
request for this <code>GET</code>, but we don’t yet get that nice |
|
search-as-you-type behavior we want.</p> |
|
<h3 id="_adding_active_search">Adding Active Search</h3> |
|
<p>To add active search behavior, we will attach a few htmx attributes |
|
to the search input. We will leave the current form as it is, with an |
|
<code>action</code> and <code>method</code>, so that the normal search |
|
behavior works even if a user does not have JavaScript enabled. This |
|
will make our “Active Search” improvement a nice “progressive |
|
enhancement.”</p> |
|
<p>So, in addition to the regular form behavior, we <em class="test">also</em> want |
|
to issue an HTTP <code>GET</code> request when a key up occurs. We want |
|
to issue this request to the same URL as the normal form submission. |
|
Finally, we only want to do this after a small pause in typing has |
|
occurred.</p> |
|
<p>As we said, this functionality is very similar to what we needed for |
|
email validation. In fact, we can copy the <code>hx-trigger</code> |
|
attribute directly from our email validation example, with its small |
|
200-millisecond delay, to wait for a user to stop typing before a |
|
request is triggered.</p> |
|
<p>This is another example of how common patterns come up again and |
|
again when using htmx.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb2"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb2-1"><a aria-hidden="true" href="#cb2-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"get"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"tool-bar"</span><span class="dt">></span></span> |
|
<span id="cb2-2"><a aria-hidden="true" href="#cb2-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"search"</span><span class="dt">></span>Search Term<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb2-3"><a aria-hidden="true" href="#cb2-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span></span> |
|
<span id="cb2-4"><a aria-hidden="true" href="#cb2-4" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb2-5"><a aria-hidden="true" href="#cb2-5" tabindex="-1"></a> hx-get="/contacts" <span class="er"><</span>2></span> |
|
<span id="cb2-6"><a aria-hidden="true" href="#cb2-6" tabindex="-1"></a> hx-trigger="search, keyup delay:200ms changed"/> <span class="er"><</span>3></span> |
|
<span id="cb2-7"><a aria-hidden="true" href="#cb2-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> type</span><span class="op">=</span><span class="st">"submit"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"Search"</span><span class="dt">/></span></span> |
|
<span id="cb2-8"><a aria-hidden="true" href="#cb2-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding active search behavior</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Keep the original attributes, so search will work if JavaScript |
|
is not available.</p></li> |
|
<li><p>Issue a <code>GET</code> to the same URL as the form.</p></li> |
|
<li><p>Nearly the same <code>hx-trigger</code> specification as for the |
|
email input validation.</p></li> |
|
</ol> |
|
<p>We made a small change to the <code>hx-trigger</code> attribute: we |
|
switched out the <code>change</code> event for the <code>search</code> |
|
event. The <code>search</code> event is triggered when someone clears |
|
the search or hits the enter key. It is a non-standard event, but it |
|
doesn’t hurt to include here. The main functionality of the feature is |
|
provided by the second triggering event, the <code>keyup</code>. As in |
|
the email example, this trigger is delayed with the |
|
<code>delay:200ms</code> modifier to “debounce” the input requests and |
|
avoid hammering our server with requests on every keyup.</p> |
|
<h3 id="_targeting_the_correct_element">Targeting The Correct |
|
Element</h3> |
|
<p>What we have is close to what we want, but we need to set up the |
|
correct target. Recall that the default target for an element is itself. |
|
As things currently stand, an HTTP <code>GET</code> request will be |
|
issued to the <code>/contacts</code> path, which will, as of now, return |
|
an entire HTML document of search results, and then this whole document |
|
will be inserted into the <em class="test">inner</em> HTML of the search input.</p> |
|
<p>This is, in fact, nonsense: <code>input</code> elements aren’t |
|
allowed to have any HTML inside of them. The browser will, sensibly, |
|
just ignore the htmx request to put the response HTML inside the input. |
|
So, at this point, when a user types anything into our input, a request |
|
will be issued (you can see it in your browser development console if |
|
you try it out) but, unfortunately, it will appear to the user as if |
|
nothing has happened at all.</p> |
|
<p>To fix this issue, what do we want to target with the update instead? |
|
Ideally we’d like to just target the actual results: there is no reason |
|
to update the header or search input, and that could cause an annoying |
|
flash as focus jumps around.</p> |
|
<p>The <code>hx-target</code> attribute allows us to do exactly that. |
|
Let’s use it to target the results body, the <code>tbody</code> element |
|
in the table of contacts:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"get"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"tool-bar"</span><span class="dt">></span></span> |
|
<span id="cb3-2"><a aria-hidden="true" href="#cb3-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">label</span><span class="ot"> for</span><span class="op">=</span><span class="st">"search"</span><span class="dt">></span>Search Term<span class="dt"></</span><span class="kw">label</span><span class="dt">></span></span> |
|
<span id="cb3-3"><a aria-hidden="true" href="#cb3-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span></span> |
|
<span id="cb3-4"><a aria-hidden="true" href="#cb3-4" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span></span> |
|
<span id="cb3-5"><a aria-hidden="true" href="#cb3-5" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span></span> |
|
<span id="cb3-6"><a aria-hidden="true" href="#cb3-6" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"search, keyup delay:200ms changed"</span></span> |
|
<span id="cb3-7"><a aria-hidden="true" href="#cb3-7" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"tbody"</span><span class="dt">/></span> <span class="er"><</span>1></span> |
|
<span id="cb3-8"><a aria-hidden="true" href="#cb3-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> type</span><span class="op">=</span><span class="st">"submit"</span><span class="ot"> value</span><span class="op">=</span><span class="st">"Search"</span><span class="dt">/></span></span> |
|
<span id="cb3-9"><a aria-hidden="true" href="#cb3-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span></span> |
|
<span id="cb3-10"><a aria-hidden="true" href="#cb3-10" tabindex="-1"></a><span class="dt"><</span><span class="kw">table</span><span class="dt">></span></span> |
|
<span id="cb3-11"><a aria-hidden="true" href="#cb3-11" tabindex="-1"></a> ...</span> |
|
<span id="cb3-12"><a aria-hidden="true" href="#cb3-12" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tbody</span><span class="dt">></span></span> |
|
<span id="cb3-13"><a aria-hidden="true" href="#cb3-13" tabindex="-1"></a> ...</span> |
|
<span id="cb3-14"><a aria-hidden="true" href="#cb3-14" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tbody</span><span class="dt">></span></span> |
|
<span id="cb3-15"><a aria-hidden="true" href="#cb3-15" tabindex="-1"></a><span class="dt"></</span><span class="kw">table</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding active search behavior</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Target the <code>tbody</code> tag on the page.</p></li> |
|
</ol> |
|
<p>Because there is only one <code>tbody</code> on the page, we can use |
|
the general CSS selector <code>tbody</code> and htmx will target the |
|
body of the table on the page.</p> |
|
<p>Now if you try typing something into the search box, we’ll see some |
|
results: a request is made and the results are inserted into the |
|
document within the <code>tbody</code>. Unfortunately, the content that |
|
is coming back is still an entire HTML document.</p> |
|
<p>Here we end up with a “double render” situation, where an entire |
|
document has been inserted <em class="test">inside</em> another element, with all the |
|
navigation, headers and footers and so forth re-rendered within that |
|
element. This is an example of one of those mis-targeting issues we |
|
mentioned earlier.</p> |
|
<p>Thankfully, it is pretty easy to fix.</p> |
|
<h3 id="_paring_down_our_content">Paring Down Our Content</h3> |
|
<p>Now, we could use the same trick we reached for in the “Click To |
|
Load” and “Infinite Scroll” features: the <code>hx-select</code> |
|
attribute. Recall that the <code>hx-select</code> attribute allows us to |
|
pick out the part of the response we are interested in using a CSS |
|
selector.</p> |
|
<p>So we could add this to our input:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb4"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb4-1"><a aria-hidden="true" href="#cb4-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span></span> |
|
<span id="cb4-2"><a aria-hidden="true" href="#cb4-2" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span></span> |
|
<span id="cb4-3"><a aria-hidden="true" href="#cb4-3" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span></span> |
|
<span id="cb4-4"><a aria-hidden="true" href="#cb4-4" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"change, keyup delay:200ms changed"</span></span> |
|
<span id="cb4-5"><a aria-hidden="true" href="#cb4-5" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"tbody"</span></span> |
|
<span id="cb4-6"><a aria-hidden="true" href="#cb4-6" tabindex="-1"></a><span class="ot"> hx-select</span><span class="op">=</span><span class="st">"tbody tr"</span><span class="dt">/></span> <span class="er"><</span>1></span></code></pre></div> |
|
<figcaption><p>Using “hx-select” for active search</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Adding an <code>hx-select</code> that picks out the table rows in |
|
the <code>tbody</code> of the response.</p></li> |
|
</ol> |
|
<p>However, that isn’t the only fix for this problem, and, in this case, |
|
it isn’t the most efficient one. Instead, let’s change the |
|
<em class="test">server-side</em> of our Hypermedia-Driven Application to serve |
|
<em class="test">only the HTML content needed</em>.</p> |
|
<h3 id="_http_request_headers_in_htmx">HTTP Request Headers In Htmx</h3> |
|
<p>In this section, we’ll look at another, more advanced technique for |
|
dealing with a situation where we only want a <em class="test">partial bit</em> of |
|
HTML, rather than a full document. Currently, we are letting the server |
|
create the full HTML document as response and then, on the client side, |
|
we filter the HTML down to the bits that we want. This is easy to do, |
|
and, in fact, might be necessary if we don’t control the server side or |
|
can’t easily modify responses.</p> |
|
<p>In our application, however, since we are doing “Full Stack” |
|
development (that is: we control both frontend <em class="test">and</em> backend |
|
code, and can easily modify either) we have another option: we can |
|
modify our server responses to return only the content necessary, and |
|
remove the need to do client-side filtering.</p> |
|
<p>This turns out to be more efficient, since we aren’t returning all |
|
the content surrounding the bit we are interested in, saving bandwidth |
|
as well as CPU and memory on the server side. So let’s explore returning |
|
different HTML content based on the context information that htmx |
|
provides with the HTTP requests it makes.</p> |
|
<p>Here’s a look again at the current server-side code for our search |
|
logic:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts"</span>)</span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a><span class="kw">def</span> contacts():</span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a> search <span class="op">=</span> request.args.get(<span class="st">"q"</span>)</span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a> <span class="cf">if</span> search <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span> |
|
<span id="cb5-5"><a aria-hidden="true" href="#cb5-5" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.search(search) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb5-6"><a aria-hidden="true" href="#cb5-6" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb5-7"><a aria-hidden="true" href="#cb5-7" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>()</span> |
|
<span id="cb5-8"><a aria-hidden="true" href="#cb5-8" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"index.html"</span>, contacts<span class="op">=</span>contacts_set) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Server-side search</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This is where the search logic happens.</p></li> |
|
<li><p>We simply re-render the <code>index.html</code> template every |
|
time, no matter what.</p></li> |
|
</ol> |
|
<p>How do we want to change this? We want to render two different bits |
|
of HTML content <em class="test">conditionally</em>:</p> |
|
<ul> |
|
<li><p>If this is a “normal” request for the entire page, we want to |
|
render the <code>index.html</code> template in the current manner. In |
|
fact, we don’t want anything to change if this is a “normal” |
|
request.</p></li> |
|
<li><p>However, if this is an “Active Search” request, we only want to |
|
render the content that is within the <code>tbody</code>, that is, just |
|
the table rows of the page.</p></li> |
|
</ul> |
|
<p>So we need some way to determine exactly which of these two different |
|
types of requests to the <code>/contact</code> URL is being made, in |
|
order to know exactly which content we want to render.</p> |
|
<p>It turns out that htmx helps us distinguish between these two cases |
|
by including a number of HTTP <em class="test">Request Headers</em> when it makes |
|
requests. Request Headers are a feature of HTTP, allowing clients (e.g., |
|
web browsers) to include name/value pairs of metadata associated with |
|
requests to help the server understand what the client is |
|
requesting.</p> |
|
<p>Here is an example of (some of) the headers the FireFox browser |
|
issues when requesting <code>https://hypermedia.systems</code>:</p> |
|
<figure> |
|
<pre class="http"><code>GET / HTTP/2 |
|
Host: hypermedia.systems |
|
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0 |
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 |
|
Accept-Encoding: gzip, deflate, br |
|
Accept-Language: en-US,en;q=0.5 |
|
Cache-Control: no-cache |
|
Connection: keep-alive |
|
DNT: 1 |
|
Pragma: no-cache |
|
</code></pre> |
|
<figcaption><p>HTTP headers</p></figcaption> |
|
</figure> |
|
<p>Htmx takes advantage of this feature of HTTP and adds additional |
|
headers and, therefore, additional <em class="test">context</em> to the HTTP requests |
|
that it makes. This allows you to inspect those headers and choose what |
|
logic to execute on the server, and what sort of HTML response you want |
|
to send to the client.</p> |
|
<p>Here is a table of the HTTP headers that htmx includes in HTTP |
|
requests:</p> |
|
<dl> |
|
<dt><code>HX-Boosted</code></dt> |
|
<dd> |
|
<p>This will be the string “true” if the request is made via an element |
|
using hx-boost</p> |
|
</dd> |
|
<dt><code>HX-Current-URL</code></dt> |
|
<dd> |
|
<p>This will be the current URL of the browser</p> |
|
</dd> |
|
<dt><code>HX-History-Restore-Request</code></dt> |
|
<dd> |
|
<p>This will be the string “true” if the request is for history |
|
restoration after a miss in the local history cache</p> |
|
</dd> |
|
<dt><code>HX-Prompt</code></dt> |
|
<dd> |
|
<p>This will contain the user response to an hx-prompt</p> |
|
</dd> |
|
<dt><code>HX-Request</code></dt> |
|
<dd> |
|
<p>This value is always “true” for htmx-based requests</p> |
|
</dd> |
|
<dt><code>HX-Target</code></dt> |
|
<dd> |
|
<p>This value will be the id of the target element if it exists</p> |
|
</dd> |
|
<dt><code>HX-Trigger-Name</code></dt> |
|
<dd> |
|
<p>This value will be the name of the triggered element if it exists</p> |
|
</dd> |
|
<dt><code>HX-Trigger</code></dt> |
|
<dd> |
|
<p>This value will be the id of the triggered element if it exists</p> |
|
</dd> |
|
</dl> |
|
<p>Looking through this list of headers, the last one stands out: we |
|
have an id, <code>search</code> on our search input. So the value of the |
|
<code>HX-Trigger</code> header should be set to <code>search</code> when |
|
the request is coming from the search input, which has the id |
|
<code>search</code>.</p> |
|
<p>Let’s add some conditional logic to our controller to look for that |
|
header and, if the value is <code>search</code>, we render only the rows |
|
rather than the whole <code>index.html</code> template:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts"</span>)</span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a><span class="kw">def</span> contacts():</span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a> search <span class="op">=</span> request.args.get(<span class="st">"q"</span>)</span> |
|
<span id="cb7-4"><a aria-hidden="true" href="#cb7-4" tabindex="-1"></a> <span class="cf">if</span> search <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span> |
|
<span id="cb7-5"><a aria-hidden="true" href="#cb7-5" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.search(search)</span> |
|
<span id="cb7-6"><a aria-hidden="true" href="#cb7-6" tabindex="-1"></a> <span class="cf">if</span> request.headers.get(<span class="st">'HX-Trigger'</span>) <span class="op">==</span> <span class="st">'search'</span>: <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb7-7"><a aria-hidden="true" href="#cb7-7" tabindex="-1"></a> <span class="co"># </span><span class="al">TODO</span><span class="co">: render only the rows here <2></span></span> |
|
<span id="cb7-8"><a aria-hidden="true" href="#cb7-8" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb7-9"><a aria-hidden="true" href="#cb7-9" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>()</span> |
|
<span id="cb7-10"><a aria-hidden="true" href="#cb7-10" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"index.html"</span>, contacts<span class="op">=</span>contacts_set)</span></code></pre></div> |
|
<figcaption><p>Updating our server-side search</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>If the request header <code>HX-Trigger</code> is equal to |
|
“search” we want to do something different.</p></li> |
|
<li><p>We need to learn how to render just the table rows.</p></li> |
|
</ol> |
|
<p>OK, so how do we render only the result rows?</p> |
|
<h3 id="_factoring_your_templates">Factoring Your Templates</h3> |
|
<p>Now we come to a common pattern in htmx: we want to <em class="test">factor</em> |
|
our server-side templates. This means that we want to break our |
|
templates up a bit so that they can be called from multiple contexts. In |
|
this case, we want to break the rows of the results table out to a |
|
separate template we will call <code>rows.html</code>. We will include |
|
it from the original <code>index.html</code> template, and also use it |
|
in our controller to render it by itself when we want to respond with |
|
only the rows for Active Search requests.</p> |
|
<p>Here’s what the table in our <code>index.html</code> file currently |
|
looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">table</span><span class="dt">></span></span> |
|
<span id="cb8-2"><a aria-hidden="true" href="#cb8-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">thead</span><span class="dt">></span></span> |
|
<span id="cb8-3"><a aria-hidden="true" href="#cb8-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb8-4"><a aria-hidden="true" href="#cb8-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>First <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>Last <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>Phone <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>Email <span class="dt"><</span><span class="kw">th</span><span class="dt">/></span></span> |
|
<span id="cb8-5"><a aria-hidden="true" href="#cb8-5" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb8-6"><a aria-hidden="true" href="#cb8-6" tabindex="-1"></a> <span class="dt"></</span><span class="kw">thead</span><span class="dt">></span></span> |
|
<span id="cb8-7"><a aria-hidden="true" href="#cb8-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tbody</span><span class="dt">></span></span> |
|
<span id="cb8-8"><a aria-hidden="true" href="#cb8-8" tabindex="-1"></a> {% for contact in contacts %}</span> |
|
<span id="cb8-9"><a aria-hidden="true" href="#cb8-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb8-10"><a aria-hidden="true" href="#cb8-10" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.first }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb8-11"><a aria-hidden="true" href="#cb8-11" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.last }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb8-12"><a aria-hidden="true" href="#cb8-12" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.phone }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb8-13"><a aria-hidden="true" href="#cb8-13" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.email }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb8-14"><a aria-hidden="true" href="#cb8-14" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb8-15"><a aria-hidden="true" href="#cb8-15" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>View<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb8-16"><a aria-hidden="true" href="#cb8-16" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb8-17"><a aria-hidden="true" href="#cb8-17" tabindex="-1"></a> {% endfor %}</span> |
|
<span id="cb8-18"><a aria-hidden="true" href="#cb8-18" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tbody</span><span class="dt">></span></span> |
|
<span id="cb8-19"><a aria-hidden="true" href="#cb8-19" tabindex="-1"></a><span class="dt"></</span><span class="kw">table</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The contacts table</p></figcaption> |
|
</figure> |
|
<p>The <code>for</code> loop in this template is what produces all the |
|
rows in the final content generated by <code>index.html</code>. What we |
|
want to do is to move the <code>for</code> loop and, therefore, the rows |
|
it creates out to a <em class="test">separate template file</em> so that only that |
|
small bit of HTML can be rendered independently from |
|
<code>index.html</code>.</p> |
|
<p>Again, let’s call this new template <code>rows.html</code>:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a>{% for contact in contacts %}</span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.first }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb9-4"><a aria-hidden="true" href="#cb9-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.last }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb9-5"><a aria-hidden="true" href="#cb9-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.phone }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb9-6"><a aria-hidden="true" href="#cb9-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.email }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb9-7"><a aria-hidden="true" href="#cb9-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb9-8"><a aria-hidden="true" href="#cb9-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>View<span class="dt"></</span><span class="kw">a</span><span class="dt">></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb9-9"><a aria-hidden="true" href="#cb9-9" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb9-10"><a aria-hidden="true" href="#cb9-10" tabindex="-1"></a>{% endfor %}</span></code></pre></div> |
|
<figcaption><p>Our new <code>rows.html</code> file</p></figcaption> |
|
</figure> |
|
<p>Using this template we can render only the <code>tr</code> elements |
|
for a given collection of contacts.</p> |
|
<p>Of course, we still want to include this content in the |
|
<code>index.html</code> template: we are <em class="test">sometimes</em> going to be |
|
rendering the entire page, and sometimes only rendering the rows. In |
|
order to keep the <code>index.html</code> template rendering properly, |
|
we can include the <code>rows.html</code> template by using the jinja |
|
<code>include</code> directive at the position we want the content from |
|
<code>rows.html</code> inserted:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb10"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb10-1"><a aria-hidden="true" href="#cb10-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">table</span><span class="dt">></span></span> |
|
<span id="cb10-2"><a aria-hidden="true" href="#cb10-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">thead</span><span class="dt">></span></span> |
|
<span id="cb10-3"><a aria-hidden="true" href="#cb10-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb10-4"><a aria-hidden="true" href="#cb10-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>First<span class="dt"></</span><span class="kw">th</span><span class="dt">></span></span> |
|
<span id="cb10-5"><a aria-hidden="true" href="#cb10-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>Last<span class="dt"></</span><span class="kw">th</span><span class="dt">></span></span> |
|
<span id="cb10-6"><a aria-hidden="true" href="#cb10-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>Phone<span class="dt"></</span><span class="kw">th</span><span class="dt">></span></span> |
|
<span id="cb10-7"><a aria-hidden="true" href="#cb10-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">th</span><span class="dt">></span>Email<span class="dt"></</span><span class="kw">th</span><span class="dt">></span></span> |
|
<span id="cb10-8"><a aria-hidden="true" href="#cb10-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">th</span><span class="dt">></</span><span class="kw">th</span><span class="dt">></span></span> |
|
<span id="cb10-9"><a aria-hidden="true" href="#cb10-9" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb10-10"><a aria-hidden="true" href="#cb10-10" tabindex="-1"></a> <span class="dt"></</span><span class="kw">thead</span><span class="dt">></span></span> |
|
<span id="cb10-11"><a aria-hidden="true" href="#cb10-11" tabindex="-1"></a> <span class="dt"><</span><span class="kw">tbody</span><span class="dt">></span></span> |
|
<span id="cb10-12"><a aria-hidden="true" href="#cb10-12" tabindex="-1"></a> {% include 'rows.html' %} <span class="er"><</span>1></span> |
|
<span id="cb10-13"><a aria-hidden="true" href="#cb10-13" tabindex="-1"></a> <span class="dt"></</span><span class="kw">tbody</span><span class="dt">></span></span> |
|
<span id="cb10-14"><a aria-hidden="true" href="#cb10-14" tabindex="-1"></a><span class="dt"></</span><span class="kw">table</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Including the new file</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This directive “includes” the <code>rows.html</code> file, |
|
inserting its content into the current template.</p></li> |
|
</ol> |
|
<p>So far, so good: our <code>/contacts</code> page is still rendering |
|
properly, just as it did before we split the rows out of the |
|
<code>index.html</code> template.</p> |
|
<h3 id="_using_our_new_template">Using Our New Template</h3> |
|
<p>The last step in factoring our templates is to modify our web |
|
controller to take advantage of the new <code>rows.html</code> template |
|
file when it responds to an active search request.</p> |
|
<p>Since <code>rows.html</code> is just another template, just like |
|
<code>index.html</code>, all we need to do is call the |
|
<code>render_template</code> function with <code>rows.html</code> rather |
|
than <code>index.html</code>. This will render <em class="test">only</em> the row |
|
content rather than the entire page:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb11"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb11-1"><a aria-hidden="true" href="#cb11-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts"</span>)</span> |
|
<span id="cb11-2"><a aria-hidden="true" href="#cb11-2" tabindex="-1"></a><span class="kw">def</span> contacts():</span> |
|
<span id="cb11-3"><a aria-hidden="true" href="#cb11-3" tabindex="-1"></a> search <span class="op">=</span> request.args.get(<span class="st">"q"</span>)</span> |
|
<span id="cb11-4"><a aria-hidden="true" href="#cb11-4" tabindex="-1"></a> <span class="cf">if</span> search <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span> |
|
<span id="cb11-5"><a aria-hidden="true" href="#cb11-5" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.search(search)</span> |
|
<span id="cb11-6"><a aria-hidden="true" href="#cb11-6" tabindex="-1"></a> <span class="cf">if</span> request.headers.get(<span class="st">'HX-Trigger'</span>) <span class="op">==</span> <span class="st">'search'</span>:</span> |
|
<span id="cb11-7"><a aria-hidden="true" href="#cb11-7" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"rows.html"</span>, contacts<span class="op">=</span>contacts_set) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb11-8"><a aria-hidden="true" href="#cb11-8" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb11-9"><a aria-hidden="true" href="#cb11-9" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>()</span> |
|
<span id="cb11-10"><a aria-hidden="true" href="#cb11-10" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"index.html"</span>, contacts<span class="op">=</span>contacts_set)</span></code></pre></div> |
|
<figcaption><p>Updating our server-side search</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Render the new template in the case of an active search.</p></li> |
|
</ol> |
|
<p>Now, when an Active Search request is made, rather than getting an |
|
entire HTML document back, we only get a partial bit of HTML, the table |
|
rows for the contacts that match the search. These rows are then |
|
inserted into the <code>tbody</code> on the index page, without any need |
|
for <code>hx-select</code> or other client-side processing.</p> |
|
<p>And, as a bonus, the old form-based search <em class="test">still works</em>. We |
|
conditionally render the rows only when the <code>search</code> input |
|
issues the HTTP request via htmx. Again, this is a progressive |
|
enhancement to our application.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>HTTP Headers & Caching</strong></p> |
|
</div> |
|
<div> |
|
<p>One subtle aspect of the approach we are taking here, using headers |
|
to determine the content of what we return, is a feature baked into |
|
HTTP: caching. In our request handler, we are now returning different |
|
content depending on the value of the <code>HX-Trigger</code> header. If |
|
we were to use HTTP Caching, we might get into a situation where someone |
|
makes a <em class="test">non-htmx</em> request (e.g., refreshing a page) and yet the |
|
<em class="test">htmx</em> content is returned from the HTTP cache, resulting in a |
|
partial page of content for the user.</p> |
|
<p>The solution to this problem is to use the HTTP Response |
|
<code>Vary</code> header and call out the htmx headers that you are |
|
using to determine what content you are returning. A full explanation of |
|
HTTP Caching is beyond the scope of this book, but the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching">MDN |
|
article on the topic</a> is quite good, and the <a href="https://htmx.org/docs/#caching">htmx documentation</a> discusses |
|
this issue as well.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_updating_the_navigation_bar_with_hx_push_url">Updating the |
|
Navigation Bar With “hx-push-url”</h3> |
|
<p>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:</p> |
|
<figure> |
|
<pre><code>https://example.com/contacts?q=joe |
|
</code></pre> |
|
<figcaption><p>The updated location after a form search</p></figcaption> |
|
</figure> |
|
<p>This is a nice feature of browsers: it allows you to bookmark this |
|
search or to copy the URL and send it to someone else. All they have to |
|
do is to click on the link, and they will repeat the exact same search. |
|
This is also tied in with the browser’s notion of history: if you click |
|
the back button it will take you to the previous URL that you came from. |
|
If you submit two searches and want to go back to the first one, you can |
|
simply hit back and the browser will “return” to that search.</p> |
|
<p>As it stands right now, during our Active Search, we are not updating |
|
the browser’s navigation bar. So, users aren’t getting links that can be |
|
copied and pasted, and you aren’t getting history entries either, which |
|
means no back button support. Fortunately, we’ve already seen how to fix |
|
this: with the <code>hx-push-url</code> attribute.</p> |
|
<p>The <code>hx-push-url</code> attribute lets you tell htmx “Please |
|
push the URL of this request into the browser’s navigation bar.” Push |
|
might seem like an odd verb to use here, but that’s the term that the |
|
underlying browser history API uses, which stems from the fact that it |
|
models browser history as a “stack” of locations: when you go to a new |
|
location, that location is “pushed” onto the stack of history elements, |
|
and when you click “back”, that location is “popped” off the history |
|
stack.</p> |
|
<p>So, to get proper history support for our Active Search, all we need |
|
to do is to set the <code>hx-push-url</code> attribute to |
|
<code>true</code>.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb13"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb13-1"><a aria-hidden="true" href="#cb13-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span></span> |
|
<span id="cb13-2"><a aria-hidden="true" href="#cb13-2" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span></span> |
|
<span id="cb13-3"><a aria-hidden="true" href="#cb13-3" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span></span> |
|
<span id="cb13-4"><a aria-hidden="true" href="#cb13-4" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"change, keyup delay:200ms changed"</span></span> |
|
<span id="cb13-5"><a aria-hidden="true" href="#cb13-5" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"tbody"</span></span> |
|
<span id="cb13-6"><a aria-hidden="true" href="#cb13-6" tabindex="-1"></a><span class="ot"> hx-push-url</span><span class="op">=</span><span class="st">"true"</span><span class="dt">/></span> <span class="er"><</span>1></span></code></pre></div> |
|
<figcaption><p>Updating the URL during active search</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>By adding the <code>hx-push-url</code> attribute with the value |
|
<code>true</code>, htmx will update the URL when it makes a |
|
request.</p></li> |
|
</ol> |
|
<p>Now, as Active Search requests are sent, the URL in the browser’s |
|
navigation bar is updated to have the proper query in it, just like when |
|
the form is submitted.</p> |
|
<p>You might not <em class="test">want</em> this behavior. You might feel it would be |
|
confusing to users to see the navigation bar updated and have history |
|
entries for every Active Search made, for example. Which is fine: you |
|
can simply omit the <code>hx-push-url</code> attribute and it will go |
|
back to the behavior you want. The goal with htmx is to be flexible |
|
enough to achieve the UX that <em class="test">you</em> want, while staying within |
|
the declarative HTML model.</p> |
|
<h3 id="_adding_a_request_indicator">Adding A Request Indicator</h3> |
|
<p>A final touch for our Active Search pattern is to add a request |
|
indicator to let the user know that a search is in progress. As it |
|
stands the user has no explicit signal that the active search |
|
functionality is handling a request. If the search takes a bit, a user |
|
may end up thinking that the feature isn’t working. By adding a request |
|
indicator we let the user know that the hypermedia application is busy |
|
and they should wait (hopefully not too long!) for the request to |
|
complete.</p> |
|
<p>Htmx provides support for request indicators via the |
|
<code>hx-indicator</code> attribute. This attribute takes, you guessed |
|
it, a CSS selector that points to the indicator for a given element. The |
|
indicator can be anything, but it is typically some sort of animated |
|
image, such as a gif or svg file, that spins or otherwise communicates |
|
visually that “something is happening.”</p> |
|
<p>Let’s add a spinner after our search input:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb14"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb14-1"><a aria-hidden="true" href="#cb14-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span></span> |
|
<span id="cb14-2"><a aria-hidden="true" href="#cb14-2" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span></span> |
|
<span id="cb14-3"><a aria-hidden="true" href="#cb14-3" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span></span> |
|
<span id="cb14-4"><a aria-hidden="true" href="#cb14-4" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"change, keyup delay:200ms changed"</span></span> |
|
<span id="cb14-5"><a aria-hidden="true" href="#cb14-5" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"tbody"</span></span> |
|
<span id="cb14-6"><a aria-hidden="true" href="#cb14-6" tabindex="-1"></a><span class="ot"> hx-push-url</span><span class="op">=</span><span class="st">"true"</span></span> |
|
<span id="cb14-7"><a aria-hidden="true" href="#cb14-7" tabindex="-1"></a><span class="ot"> hx-indicator</span><span class="op">=</span><span class="st">"#spinner"</span><span class="dt">/></span> <span class="er"><</span>1></span> |
|
<span id="cb14-8"><a aria-hidden="true" href="#cb14-8" tabindex="-1"></a><span class="dt"><</span><span class="kw">img</span><span class="ot"> id</span><span class="op">=</span><span class="st">"spinner"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"htmx-indicator"</span></span> |
|
<span id="cb14-9"><a aria-hidden="true" href="#cb14-9" tabindex="-1"></a><span class="ot"> src</span><span class="op">=</span><span class="st">"/static/img/spinning-circles.svg"</span></span> |
|
<span id="cb14-10"><a aria-hidden="true" href="#cb14-10" tabindex="-1"></a><span class="ot"> alt</span><span class="op">=</span><span class="st">"Request In Flight..."</span><span class="dt">/></span> <span class="er"><</span>2></span></code></pre></div> |
|
<figcaption><p>Adding a request indicator to search</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The <code>hx-indicator</code> attribute points to the indicator |
|
image after the input.</p></li> |
|
<li><p>The indicator is a spinning circle svg file, and has the |
|
<code>htmx-indicator</code> class on it.</p></li> |
|
</ol> |
|
<p>We have added the spinner right after the input. This visually |
|
co-locates the request indicator with the element making the request, |
|
and makes it easy for a user to see that something is in fact |
|
happening.</p> |
|
<p>It just works, but how does htmx make the spinner appear and |
|
disappear? Note that the indicator <code>img</code> tag has the |
|
<code>htmx-indicator</code> class on it. <code>htmx-indicator</code> is |
|
a CSS class that is automatically injected into the page by htmx. This |
|
class sets the default <code>opacity</code> of an element to |
|
<code>0</code>, which hides the element from view, while at the same |
|
time not disrupting the layout of the page.</p> |
|
<p>When an htmx request is triggered that points to this indicator, |
|
another class, <code>htmx-request</code> is added to the indicator which |
|
transitions its opacity to 1. So you can use just about anything as an |
|
indicator, and it will be hidden by default. Then, when a request is in |
|
flight, it will be shown. This is all done via standard CSS classes, |
|
allowing you to control the transitions and even the mechanism by which |
|
the indicator is shown (e.g., you might use <code>display</code> rather |
|
than <code>opacity</code>).</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Use Request Indicators!</strong></p> |
|
</div> |
|
<div> |
|
<p>Request indicators are an important UX aspect of any distributed |
|
application. It is unfortunate that browsers have de-emphasized their |
|
native request indicators over time, and it is doubly unfortunate that |
|
request indicators are not part of the JavaScript ajax APIs.</p> |
|
<p>Be sure not to neglect this significant aspect of your application. |
|
Requests might seem instant when you are working on your application |
|
locally, but in the real world they can take quite a bit longer due to |
|
network latency. It’s often a good idea to take advantage of browser |
|
developer tools that allow you to throttle your local browser’s response |
|
times. This will give you a better idea of what real world users are |
|
seeing, and show you where indicators might help users understand |
|
exactly what is going on.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>With this request indicator, we now have a pretty sophisticated user |
|
experience when compared with plain HTML, but we’ve built it all as a |
|
hypermedia-driven feature. No JSON or JavaScript to be seen. And our |
|
implementation has the benefit of being a progressive enhancement; the |
|
application will continue to work for clients that don’t have JavaScript |
|
enabled.</p> |
|
<h2 id="_lazy_loading">Lazy Loading</h2> |
|
<p>With Active Search behind us, let’s move on to a very different sort |
|
of enhancement: lazy loading. Lazy loading is when the loading of a |
|
particular bit of content is deferred until later, when needed. This is |
|
commonly used as a performance enhancement: you avoid the processing |
|
resources necessary to produce some data until that data is actually |
|
needed.</p> |
|
<p>Let’s add a count of the total number of contacts to Contact.app, |
|
just below the bottom of our contacts table. This will give us a |
|
potentially expensive operation that we can use to demonstrate how to |
|
add lazy loading with htmx.</p> |
|
<p>First let’s update our server code in the <code>/contacts</code> |
|
request handler to get a count of the total number of contacts. We will |
|
pass that count through to the template to render some new HTML.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb15"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb15-1"><a aria-hidden="true" href="#cb15-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts"</span>)</span> |
|
<span id="cb15-2"><a aria-hidden="true" href="#cb15-2" tabindex="-1"></a><span class="kw">def</span> contacts():</span> |
|
<span id="cb15-3"><a aria-hidden="true" href="#cb15-3" tabindex="-1"></a> search <span class="op">=</span> request.args.get(<span class="st">"q"</span>)</span> |
|
<span id="cb15-4"><a aria-hidden="true" href="#cb15-4" tabindex="-1"></a> page <span class="op">=</span> <span class="bu">int</span>(request.args.get(<span class="st">"page"</span>, <span class="dv">1</span>))</span> |
|
<span id="cb15-5"><a aria-hidden="true" href="#cb15-5" tabindex="-1"></a> count <span class="op">=</span> Contact.count() <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb15-6"><a aria-hidden="true" href="#cb15-6" tabindex="-1"></a> <span class="cf">if</span> search <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span> |
|
<span id="cb15-7"><a aria-hidden="true" href="#cb15-7" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.search(search)</span> |
|
<span id="cb15-8"><a aria-hidden="true" href="#cb15-8" tabindex="-1"></a> <span class="cf">if</span> request.headers.get(<span class="st">'HX-Trigger'</span>) <span class="op">==</span> <span class="st">'search'</span>:</span> |
|
<span id="cb15-9"><a aria-hidden="true" href="#cb15-9" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"rows.html"</span>,</span> |
|
<span id="cb15-10"><a aria-hidden="true" href="#cb15-10" tabindex="-1"></a> contacts<span class="op">=</span>contacts_set, page<span class="op">=</span>page, count<span class="op">=</span>count) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb15-11"><a aria-hidden="true" href="#cb15-11" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb15-12"><a aria-hidden="true" href="#cb15-12" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>(page)</span> |
|
<span id="cb15-13"><a aria-hidden="true" href="#cb15-13" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"index.html"</span>,</span> |
|
<span id="cb15-14"><a aria-hidden="true" href="#cb15-14" tabindex="-1"></a> contacts<span class="op">=</span>contacts_set, page<span class="op">=</span>page, count<span class="op">=</span>count)</span></code></pre></div> |
|
<figcaption><p>Adding a count to the UI</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Get the total count of contacts from the Contact model.</p></li> |
|
<li><p>Pass the count out to the <code>index.html</code> template to use |
|
when rendering.</p></li> |
|
</ol> |
|
<p>As with the rest of the application, in the interest of staying |
|
focused on the <em class="test">hypermedia</em> part of Contact.app, we’ll skip over |
|
the details of how <code>Contact.count()</code> works. We just need to |
|
know that:</p> |
|
<ul> |
|
<li><p>It returns the total count of contacts in the contact |
|
database.</p></li> |
|
<li><p>It may be slow (for the sake of our example).</p></li> |
|
</ul> |
|
<p>Next lets add some HTML to our <code>index.html</code> that takes |
|
advantage of this new bit of data, showing a message next to the “Add |
|
Contact” link with the total count of users. Here is what our HTML looks |
|
like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb16"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb16-1"><a aria-hidden="true" href="#cb16-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb16-2"><a aria-hidden="true" href="#cb16-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/new"</span><span class="dt">></span>Add Contact<span class="dt"></</span><span class="kw">a</span></span> |
|
<span id="cb16-3"><a aria-hidden="true" href="#cb16-3" tabindex="-1"></a> <span class="dt">></span> <span class="dt"><</span><span class="kw">span</span><span class="dt">></span>({{ count }} total Contacts)<span class="dt"></</span><span class="kw">span</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb16-4"><a aria-hidden="true" href="#cb16-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding a contact count element to the |
|
application</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A simple span with some text showing the total number of |
|
contacts.</p></li> |
|
</ol> |
|
<p>Well that was easy, wasn’t it? Now our users will see the total |
|
number of contacts next to the link to add new contacts, to give them a |
|
sense of how large the contact database is. This sort of rapid |
|
development is one of the joys of developing web applications the old |
|
way.</p> |
|
<p><a class="ref" href="#fig-totalcontacts">[fig-totalcontacts]</a> is |
|
what the feature looks like in our application. Beautiful.</p> |
|
<figure id="fig-totalcontacts"> |
|
<p><img src="https://hypermedia.systems/images/screenshot_total_contacts.png"/></p> |
|
<figcaption><p>Total contact count display</p></figcaption> |
|
</figure> |
|
<p>Of course, as you probably suspected, all is not perfect. |
|
Unfortunately, upon shipping this feature to production, we start |
|
getting complaints from 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.</p> |
|
<p>It turns out, surprisingly, that the problem is that innocent looking |
|
<code>Contacts.count()</code> call, which is taking up to a second and a |
|
half to complete. Unfortunately, for reasons beyond the scope of this |
|
book, it is not possible to improve that load time, nor is possible to |
|
cache the result.</p> |
|
<p>This leaves us with two options:</p> |
|
<ul> |
|
<li><p>Remove the feature.</p></li> |
|
<li><p>Come up with some other way to mitigate the performance |
|
issue.</p></li> |
|
</ul> |
|
<p>Let’s assume that we can’t remove the feature, and therefore look at |
|
how we can mitigate this performance issue by using htmx instead.</p> |
|
<h3 id="_pulling_out_the_expensive_code">Pulling Out The Expensive |
|
Code</h3> |
|
<p>The first step in implementing the Lazy Load pattern is to pull the |
|
expensive code — that is, the call to <code>Contacts.count()</code> — |
|
out of the request handler for the <code>/contacts</code> endpoint.</p> |
|
<p>Let’s put this function call into its own HTTP request handler as a |
|
new HTTP endpoint that we will put at <code>/contacts/count</code>. For |
|
this new endpoint, we won’t need to render a template at all: its sole |
|
job is going to be to render that small bit of text that is in the span, |
|
“(22 total Contacts).”</p> |
|
<p>Here is what the new code will look like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb17"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb17-1"><a aria-hidden="true" href="#cb17-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts"</span>)</span> |
|
<span id="cb17-2"><a aria-hidden="true" href="#cb17-2" tabindex="-1"></a><span class="kw">def</span> contacts():</span> |
|
<span id="cb17-3"><a aria-hidden="true" href="#cb17-3" tabindex="-1"></a> search <span class="op">=</span> request.args.get(<span class="st">"q"</span>)</span> |
|
<span id="cb17-4"><a aria-hidden="true" href="#cb17-4" tabindex="-1"></a> page <span class="op">=</span> <span class="bu">int</span>(request.args.get(<span class="st">"page"</span>, <span class="dv">1</span>)) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb17-5"><a aria-hidden="true" href="#cb17-5" tabindex="-1"></a> <span class="cf">if</span> search <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span> |
|
<span id="cb17-6"><a aria-hidden="true" href="#cb17-6" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.search(search)</span> |
|
<span id="cb17-7"><a aria-hidden="true" href="#cb17-7" tabindex="-1"></a> <span class="cf">if</span> request.headers.get(<span class="st">'HX-Trigger'</span>) <span class="op">==</span> <span class="st">'search'</span>:</span> |
|
<span id="cb17-8"><a aria-hidden="true" href="#cb17-8" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"rows.html"</span>,</span> |
|
<span id="cb17-9"><a aria-hidden="true" href="#cb17-9" tabindex="-1"></a> contacts<span class="op">=</span>contacts_set, page<span class="op">=</span>page)</span> |
|
<span id="cb17-10"><a aria-hidden="true" href="#cb17-10" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb17-11"><a aria-hidden="true" href="#cb17-11" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>(page)</span> |
|
<span id="cb17-12"><a aria-hidden="true" href="#cb17-12" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"index.html"</span>,</span> |
|
<span id="cb17-13"><a aria-hidden="true" href="#cb17-13" tabindex="-1"></a> contacts<span class="op">=</span>contacts_set, page<span class="op">=</span>page) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb17-14"><a aria-hidden="true" href="#cb17-14" tabindex="-1"></a></span> |
|
<span id="cb17-15"><a aria-hidden="true" href="#cb17-15" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/count"</span>)</span> |
|
<span id="cb17-16"><a aria-hidden="true" href="#cb17-16" tabindex="-1"></a><span class="kw">def</span> contacts_count():</span> |
|
<span id="cb17-17"><a aria-hidden="true" href="#cb17-17" tabindex="-1"></a> count <span class="op">=</span> Contact.count() <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb17-18"><a aria-hidden="true" href="#cb17-18" tabindex="-1"></a> <span class="cf">return</span> <span class="st">"("</span> <span class="op">+</span> <span class="bu">str</span>(count) <span class="op">+</span> <span class="st">" total Contacts)"</span> <span class="op"><</span><span class="dv">4</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Pulling the expensive code out</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We no longer call <code>Contacts.count()</code> in this |
|
handler.</p></li> |
|
<li><p><code>Count</code> is no longer passed out to the template to |
|
render in the <code>/contacts</code> handler.</p></li> |
|
<li><p>We create a new handler at the <code>/contacts/count</code> path |
|
that does the expensive calculation.</p></li> |
|
<li><p>Return the string with the total number of contacts.</p></li> |
|
</ol> |
|
<p>So now we have moved the performance issue out of the |
|
<code>/contacts</code> handler code, which renders the main contacts |
|
table, and created a new HTTP endpoint that will produce this |
|
expensive-to-create count string for us.</p> |
|
<p>Now we need to get the content from this new handler <em class="test">into</em> |
|
the span, somehow. As we said earlier, the default behavior of htmx is |
|
to place any content it receives for a given request into the |
|
<code>innerHTML</code> of an element, and that turns out to be exactly |
|
what we want here: we want to retrieve this text and put it into the |
|
<code>span</code>. So we can simply place an <code>hx-get</code> |
|
attribute on the span, pointing to this new path, and do exactly |
|
that.</p> |
|
<p>However, recall that the default <em class="test">event</em> that will trigger a |
|
request for a <code>span</code> element in htmx is the |
|
<code>click</code> event. Well, that’s not what we want! Instead, we |
|
want this request to trigger immediately, when the page loads.</p> |
|
<p>To do this, we can add the <code>hx-trigger</code> attribute to |
|
update the trigger of the requests for the element, and use the |
|
<code>load</code> event.</p> |
|
<p>The <code>load</code> event is a special event that htmx triggers on |
|
all content when it is loaded into the DOM. By setting |
|
<code>hx-trigger</code> to <code>load</code>, we will cause htmx to |
|
issue the <code>GET</code> request when the <code>span</code> element is |
|
loaded into the page.</p> |
|
<p>Here is our updated template code:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb18"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb18-1"><a aria-hidden="true" href="#cb18-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">p</span><span class="dt">></span></span> |
|
<span id="cb18-2"><a aria-hidden="true" href="#cb18-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/new"</span><span class="dt">></span>Add Contact<span class="dt"></</span><span class="kw">a</span></span> |
|
<span id="cb18-3"><a aria-hidden="true" href="#cb18-3" tabindex="-1"></a> <span class="dt">></span> <span class="dt"><</span><span class="kw">span</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/count"</span><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"load"</span><span class="dt">></</span><span class="kw">span</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb18-4"><a aria-hidden="true" href="#cb18-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">p</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding a contact count element to the |
|
application</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Issue a <code>GET</code> to <code>/contacts/count</code> when the |
|
<code>load</code> event occurs.</p></li> |
|
</ol> |
|
<p>Note that the <code>span</code> starts empty: we have removed the |
|
content from it, and we are allowing the request to |
|
<code>/contacts/count</code> to populate it instead.</p> |
|
<p>And, check it out, our <code>/contacts</code> page is fast again! |
|
When you navigate to the page it feels very snappy and profiling shows |
|
that yes, indeed, the page is loading much more quickly. Why is that? |
|
Well, we’ve deferred the expensive calculation to a secondary request, |
|
allowing the initial request to finish loading faster.</p> |
|
<p>You might say “OK, great, but it’s still taking a second or two to |
|
get the total count on the page.” True, but often the user may not be |
|
particularly interested in the total count. They may just want to come |
|
to the page and search for an existing user, or perhaps they may want to |
|
edit or add a user. The total count of contacts is just a “nice to have” |
|
bit of information in these cases.</p> |
|
<p>By deferring the calculation of the count in this manner we let users |
|
get on with their use of the application while we perform the expensive |
|
calculation.</p> |
|
<p>Yes, the total time to get all the information on the screen takes |
|
just as long. It actually will be a bit longer, since we now need two |
|
HTTP requests to get all the information for the page. But the |
|
<em class="test">perceived performance</em> for the end user will be much better: |
|
they can do what they want nearly immediately, even if some information |
|
isn’t available instantaneously.</p> |
|
<p>Lazy Loading is a great tool to have in your belt when optimizing web |
|
application performance.</p> |
|
<h3 id="_adding_an_indicator">Adding An Indicator</h3> |
|
<p>A shortcoming of the current implementation is that currently there |
|
is no indication that the count request is in flight, it just appears at |
|
some point when the request finishes.</p> |
|
<p>This isn’t ideal. What we want here is an indicator, just like we |
|
added in our Active Search example. And, in fact, we can simply reuse |
|
that same exact spinner image, copy-and-pasted into the new HTML we have |
|
created.</p> |
|
<p>Now, in this case, we have a one-time request and, once the request |
|
is over, we are not going to need the spinner anymore. So it doesn’t |
|
make sense to use the exact same approach we did with the active search |
|
example. Recall that in that case we placed a spinner <em class="test">after</em> the |
|
span and using the <code>hx-indicator</code> attribute to point to |
|
it.</p> |
|
<p>In this case, since the spinner is only used once, we can put it |
|
<em class="test">inside</em> the content of the span. When the request completes the |
|
content in the response will be placed inside the span, replacing the |
|
spinner with the computed contact count. It turns out that htmx allows |
|
you to place indicators with the <code>htmx-indicator</code> class on |
|
them inside of elements that issue htmx-powered requests. In the absence |
|
of an <code>hx-indicator</code> attribute, these internal indicators |
|
will be shown when a request is in flight.</p> |
|
<p>So let’s add that spinner from the active search example as the |
|
initial content in our span:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb19"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb19-1"><a aria-hidden="true" href="#cb19-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">span</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/count"</span><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"load"</span><span class="dt">></span></span> |
|
<span id="cb19-2"><a aria-hidden="true" href="#cb19-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">img</span><span class="ot"> id</span><span class="op">=</span><span class="st">"spinner"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"htmx-indicator"</span></span> |
|
<span id="cb19-3"><a aria-hidden="true" href="#cb19-3" tabindex="-1"></a><span class="ot"> src</span><span class="op">=</span><span class="st">"/static/img/spinning-circles.svg"</span><span class="dt">/></span> <span class="er"><</span>1></span> |
|
<span id="cb19-4"><a aria-hidden="true" href="#cb19-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding an indicator to our lazily loaded |
|
content</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Yep, that’s it.</p></li> |
|
</ol> |
|
<p>Now when the user loads the page, rather than having the total |
|
contact count magically appear, there is a nice spinner indicating that |
|
something is coming. Much better.</p> |
|
<p>Note that all we had to do was copy and paste our indicator from the |
|
active search example into the <code>span</code>. Once again we see how |
|
htmx provides flexible, composable features and building blocks. |
|
Implementing a new feature is often just copy-and-paste, maybe a tweak |
|
or two, and you are done.</p> |
|
<h3 id="_but_thats_not_lazy">But That’s Not Lazy!</h3> |
|
<p>You might say “OK, but that’s not really lazy. We are still loading |
|
the count immediately when the page is loaded, we are just doing it in a |
|
second request. You aren’t really waiting until the value is actually |
|
needed.”</p> |
|
<p>Fine. Let’s make it <em class="test">lazy</em> lazy: we’ll only issue the request |
|
when the <code>span</code> scrolls into view.</p> |
|
<p>To do that, let’s recall how we set up the infinite scroll example: |
|
we used the <code>revealed</code> event for our trigger. That’s all we |
|
want here, right? When the element is revealed we issue the request?</p> |
|
<p>Yep, that’s it. Once again, we can mix and match concepts across |
|
various UX patterns to come up with solutions to new problems in |
|
htmx.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb20"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb20-1"><a aria-hidden="true" href="#cb20-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">span</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/count"</span><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"revealed"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb20-2"><a aria-hidden="true" href="#cb20-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">img</span><span class="ot"> id</span><span class="op">=</span><span class="st">"spinner"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"htmx-indicator"</span></span> |
|
<span id="cb20-3"><a aria-hidden="true" href="#cb20-3" tabindex="-1"></a><span class="ot"> src</span><span class="op">=</span><span class="st">"/static/img/spinning-circles.svg"</span><span class="dt">/></span></span> |
|
<span id="cb20-4"><a aria-hidden="true" href="#cb20-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">span</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Making it truly lazy</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Change the <code>hx-trigger</code> to |
|
<code>revealed</code>.</p></li> |
|
</ol> |
|
<p>Now we have a truly lazy implementation, deferring the expensive |
|
computation until we are absolutely sure we need it. A pretty cool |
|
trick, and, again, a simple one-attribute change demonstrates the |
|
flexibility of both htmx and the hypermedia approach.</p> |
|
<h2 id="_inline_delete">Inline Delete</h2> |
|
<p>For our next hypermedia trick, we are going to implement the “Inline |
|
Delete” pattern. With this feature, a contact can be deleted directly |
|
from the table of all contacts, rather than requiring the user to |
|
navigate all the way to the edit view of particular contact, in order to |
|
access the “Delete Contact” button we added in the last chapter.</p> |
|
<p>Recall that we already have “Edit” and “View” links for each row, in |
|
the <code>rows.html</code> template:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb21"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb21-1"><a aria-hidden="true" href="#cb21-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb21-2"><a aria-hidden="true" href="#cb21-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb21-3"><a aria-hidden="true" href="#cb21-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>View<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb21-4"><a aria-hidden="true" href="#cb21-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The existing row actions</p></figcaption> |
|
</figure> |
|
<p>Now we want to add a “Delete” link as well. And, thinking on it, we |
|
want that link to act an awful lot like the “Delete Contact” button from |
|
<code>edit.html</code>, don’t we? We’d like to issue an HTTP |
|
<code>DELETE</code> to the URL for the given contact and we want a |
|
confirmation dialog to ensure the user doesn’t accidentally delete a |
|
contact.</p> |
|
<p>Here is the “Delete Contact” button html:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb22"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb22-1"><a aria-hidden="true" href="#cb22-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span></span> |
|
<span id="cb22-2"><a aria-hidden="true" href="#cb22-2" tabindex="-1"></a><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span></span> |
|
<span id="cb22-3"><a aria-hidden="true" href="#cb22-3" tabindex="-1"></a><span class="ot"> hx-push-url</span><span class="op">=</span><span class="st">"true"</span></span> |
|
<span id="cb22-4"><a aria-hidden="true" href="#cb22-4" tabindex="-1"></a><span class="ot"> hx-confirm</span><span class="op">=</span><span class="st">"Are you sure you want to delete this contact?"</span></span> |
|
<span id="cb22-5"><a aria-hidden="true" href="#cb22-5" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="dt">></span></span> |
|
<span id="cb22-6"><a aria-hidden="true" href="#cb22-6" tabindex="-1"></a> Delete Contact</span> |
|
<span id="cb22-7"><a aria-hidden="true" href="#cb22-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The existing row actions</p></figcaption> |
|
</figure> |
|
<p>As you may suspect by now, this is going to be another copy-and-paste |
|
job.</p> |
|
<p>One thing to note is that, in the case of the “Delete Contact” |
|
button, we wanted to re-render the whole screen and update the URL, |
|
since we are going to be returning from the edit view for the contact to |
|
the list view of all contacts. In the case of this link, however, we are |
|
already on the list of contacts, so there is no need to update the URL, |
|
and we can omit the <code>hx-push-url</code> attribute.</p> |
|
<p>Here is the code for our inline “Delete” link:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb23"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb23-1"><a aria-hidden="true" href="#cb23-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb23-2"><a aria-hidden="true" href="#cb23-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb23-3"><a aria-hidden="true" href="#cb23-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>View<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb23-4"><a aria-hidden="true" href="#cb23-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"#"</span><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span></span> |
|
<span id="cb23-5"><a aria-hidden="true" href="#cb23-5" tabindex="-1"></a><span class="ot"> hx-confirm</span><span class="op">=</span><span class="st">"Are you sure you want to delete this contact?"</span></span> |
|
<span id="cb23-6"><a aria-hidden="true" href="#cb23-6" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="dt">></span>Delete<span class="dt"></</span><span class="kw">a</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb23-7"><a aria-hidden="true" href="#cb23-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The existing row actions</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Almost a straight copy of the “Delete Contact” button.</p></li> |
|
</ol> |
|
<p>As you can see, we have added a new anchor tag and given it a blank |
|
target (the <code>#</code> value in its <code>href</code> attribute) to |
|
retain the correct mouse-over styling behavior of the link. We’ve also |
|
copied the <code>hx-delete</code>, <code>hx-confirm</code> and |
|
<code>hx-target</code> attributes from the “Delete Contact” button, but |
|
omitted the <code>hx-push-url</code> attributes since we don’t want to |
|
update the URL of the browser.</p> |
|
<p>We now have inline delete working, even with a confirmation dialog. A |
|
user can click on the “Delete” link and the row will disappear from the |
|
UI as the entire page is re-rendered.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>A Style Sidebar</strong></p> |
|
</div> |
|
<div> |
|
<p>One side effect of adding this delete link is that we are starting to |
|
pile up the actions in a contact row:</p> |
|
<figure id="fig-stacked-actions"> |
|
<p><img src="https://hypermedia.systems/images/screenshot_stacked_actions.png"/></p> |
|
<figcaption><p>That’s a lot of actions</p></figcaption> |
|
</figure> |
|
<p>It would be nice if we didn’t show the actions all in a row, and, |
|
additionally, it would be nice if we only showed the actions when the |
|
user indicated interest in a given row. We will return to this problem |
|
after we look at the relationship between scripting and a |
|
Hypermedia-Driven Application in a later chapter.</p> |
|
<p>For now, let’s just tolerate this less-than-ideal user interface, |
|
knowing that we will fix it later.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_narrowing_our_target">Narrowing Our Target</h3> |
|
<p>We can get even fancier here, however. What if, rather than |
|
re-rendering the whole page, we just removed the row for the contact? |
|
The user is looking at the row anyway, so is there really a need to |
|
re-render the whole page?</p> |
|
<p>To do this, we’ll need to do a couple of things:</p> |
|
<ul> |
|
<li><p>We’ll need to update this link to target the row that it is |
|
in.</p></li> |
|
<li><p>We’ll need to change the swap to <code>outerHTML</code>, since we |
|
want to replace (really, remove) the entire row.</p></li> |
|
<li><p>We’ll need to update the server side to render empty content when |
|
the <code>DELETE</code> is issued from a “Delete” link rather than from |
|
the “Delete Contact” button on the contact edit page.</p></li> |
|
</ul> |
|
<p>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 <code>closest</code> feature |
|
to target the closest <code>tr</code>, like we did in our “Click To |
|
Load” and “Infinite Scroll” features:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb24"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb24-1"><a aria-hidden="true" href="#cb24-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb24-2"><a aria-hidden="true" href="#cb24-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb24-3"><a aria-hidden="true" href="#cb24-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>View<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb24-4"><a aria-hidden="true" href="#cb24-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"#"</span><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span></span> |
|
<span id="cb24-5"><a aria-hidden="true" href="#cb24-5" tabindex="-1"></a><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span></span> |
|
<span id="cb24-6"><a aria-hidden="true" href="#cb24-6" tabindex="-1"></a><span class="ot"> hx-confirm</span><span class="op">=</span><span class="st">"Are you sure you want to delete this contact?"</span></span> |
|
<span id="cb24-7"><a aria-hidden="true" href="#cb24-7" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"closest tr"</span><span class="dt">></span>Delete<span class="dt"></</span><span class="kw">a</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb24-8"><a aria-hidden="true" href="#cb24-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The existing row actions</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Updated to target the closest enclosing <code>tr</code> (table |
|
row) of the link.</p></li> |
|
</ol> |
|
<h3 id="_updating_the_server_side">Updating The Server Side</h3> |
|
<p>Now we need to update the server side. We want to keep the “Delete |
|
Contact” button working as well, and in that case the current logic is |
|
correct. So we’ll need some way to differentiate between |
|
<code>DELETE</code> requests that are triggered by the button and |
|
<code>DELETE</code> requests that come from this anchor.</p> |
|
<p>The cleanest way to do this is to add an <code>id</code> attribute to |
|
the “Delete Contact” button, so that we can inspect the |
|
<code>HX-Trigger</code> HTTP Request header to determine if the delete |
|
button was the cause of the request. This is a simple change to the |
|
existing HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb25"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb25-1"><a aria-hidden="true" href="#cb25-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> id</span><span class="op">=</span><span class="st">"delete-btn"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb25-2"><a aria-hidden="true" href="#cb25-2" tabindex="-1"></a> hx-delete="/contacts/{{ contact.id }}"</span> |
|
<span id="cb25-3"><a aria-hidden="true" href="#cb25-3" tabindex="-1"></a> hx-push-url="true"</span> |
|
<span id="cb25-4"><a aria-hidden="true" href="#cb25-4" tabindex="-1"></a> hx-confirm="Are you sure you want to delete this contact?"</span> |
|
<span id="cb25-5"><a aria-hidden="true" href="#cb25-5" tabindex="-1"></a> hx-target="body"></span> |
|
<span id="cb25-6"><a aria-hidden="true" href="#cb25-6" tabindex="-1"></a> Delete Contact</span> |
|
<span id="cb25-7"><a aria-hidden="true" href="#cb25-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding an <code>id</code> to the “delete contact” |
|
button</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>An <code>id</code> attribute has been added to the |
|
button.</p></li> |
|
</ol> |
|
<p>By giving this button an id attribute, we now have a mechanism for |
|
differentiating between the delete button in the <code>edit.html</code> |
|
template and the delete links in the <code>rows.html</code> template. |
|
When this button issues a request, it will look something like this:</p> |
|
<figure> |
|
<pre class="http"><code>DELETE http://example.org/contacts/42 HTTP/1.1 |
|
Accept: text/html,*/* |
|
Host: example.org |
|
... |
|
HX-Trigger: delete-btn |
|
... |
|
</code></pre> |
|
</figure> |
|
<p>You can see that the request now includes the <code>id</code> of the |
|
button. This allows us to write code very similar to what we did for the |
|
active search pattern, using a conditional on the |
|
<code>HX-Trigger</code> header to determine what we want to do. If that |
|
header has the value <code>delete-btn</code>, then we know the request |
|
came from the button on the edit page, and we can do what we are |
|
currently doing: delete the contact and redirect to |
|
<code>/contacts</code> page.</p> |
|
<p>If it <em class="test">does not</em> have that value, then we can simply delete |
|
the contact and return an empty string. This empty string will replace |
|
the target, in this case the row for the given contact, thereby removing |
|
the row from the UI.</p> |
|
<p>Let’s refactor our server-side code to do this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb27"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb27-1"><a aria-hidden="true" href="#cb27-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/<contact_id>"</span>, methods<span class="op">=</span>[<span class="st">"DELETE"</span>])</span> |
|
<span id="cb27-2"><a aria-hidden="true" href="#cb27-2" tabindex="-1"></a><span class="kw">def</span> contacts_delete(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb27-3"><a aria-hidden="true" href="#cb27-3" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id)</span> |
|
<span id="cb27-4"><a aria-hidden="true" href="#cb27-4" tabindex="-1"></a> contact.delete()</span> |
|
<span id="cb27-5"><a aria-hidden="true" href="#cb27-5" tabindex="-1"></a> <span class="cf">if</span> request.headers.get(<span class="st">'HX-Trigger'</span>) <span class="op">==</span> <span class="st">'delete-btn'</span>: <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb27-6"><a aria-hidden="true" href="#cb27-6" tabindex="-1"></a> flash(<span class="st">"Deleted Contact!"</span>)</span> |
|
<span id="cb27-7"><a aria-hidden="true" href="#cb27-7" tabindex="-1"></a> <span class="cf">return</span> redirect(<span class="st">"/contacts"</span>, <span class="dv">303</span>)</span> |
|
<span id="cb27-8"><a aria-hidden="true" href="#cb27-8" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb27-9"><a aria-hidden="true" href="#cb27-9" tabindex="-1"></a> <span class="cf">return</span> <span class="st">""</span> <span class="op"><</span><span class="dv">2</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Updating our server code to handle two different delete) |
|
patterns</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>If the delete button on the edit page submitted this request, |
|
then continue to do the previous logic.</p></li> |
|
<li><p>If not, simply return an empty string, which will delete the |
|
row.</p></li> |
|
</ol> |
|
<p>And that’s our server-side implementation: when a user clicks |
|
“Delete” on a contact row and confirms the delete, the row will |
|
disappear from the UI. Once again, we have a situation where just |
|
changing a few lines of simple code gives us a dramatically different |
|
behavior. Hypermedia is powerful in this manner.</p> |
|
<h3 id="_the_htmx_swapping_model">The Htmx Swapping Model</h3> |
|
<p>This is pretty cool, but there is another improvement we can make if |
|
we take some time to understand the htmx content swapping model: it |
|
would be nice if, rather than just instantly deleting the row, we faded |
|
it out before we removed it. The fade would make it clear that the row |
|
is being removed, giving the user some nice visual feedback on the |
|
deletion.</p> |
|
<p>It turns out we can do this pretty easily with htmx, but to do so |
|
we’ll need to dig in to exactly how htmx swaps content.</p> |
|
<p>You might think that htmx simply puts the new content into the DOM, |
|
but that’s not in fact how it works. Instead, content goes through a |
|
series of steps as it is added to the DOM:</p> |
|
<ul> |
|
<li><p>When content is received and about to be swapped into the DOM, |
|
the <code>htmx-swapping</code> CSS class is added to the target |
|
element.</p></li> |
|
<li><p>A small delay then occurs (we will discuss why this delay exists |
|
in a moment).</p></li> |
|
<li><p>Next, the <code>htmx-swapping</code> class is removed from the |
|
target and the <code>htmx-settling</code> class is added.</p></li> |
|
<li><p>The new content is swapped into the DOM.</p></li> |
|
<li><p>Another small delay occurs.</p></li> |
|
<li><p>Finally, the <code>htmx-settling</code> class is removed from the |
|
target.</p></li> |
|
</ul> |
|
<p>There is more to the swap mechanic (settling, for example, is a more |
|
advanced topic that we will discuss in a later chapter) but this is |
|
enough for now.</p> |
|
<p>Now, there are small delays in the process here, typically on the |
|
order of a few milliseconds. Why so? It turns out that these small |
|
delays allow <em class="test">CSS transitions</em> to occur.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>CSS Transitions</strong></p> |
|
</div> |
|
<div> |
|
<p>CSS transitions are a technology that allow you to animate a |
|
transition from one style to another. So, for example, if you changed |
|
the height of something from 10 pixels to 20 pixels, by using a CSS |
|
transition you can make the element smoothly animate to the new height. |
|
These sorts of animations are fun, often increase application usability, |
|
and are a great mechanism to add polish to your web application.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>Unfortunately, CSS transitions are difficult to access in plain HTML: |
|
you usually have to use JavaScript and add or remove classes to get them |
|
to trigger. This is why the htmx swap model is more complicated than you |
|
might initially think. By swapping in classes and adding small delays, |
|
you can access CSS transitions purely within HTML, without needing to |
|
write any JavaScript!</p> |
|
<h3 id="_taking_advantage_of_htmx_swapping">Taking Advantage of |
|
“htmx-swapping”</h3> |
|
<p>OK, so, let’s go back and look at our inline delete mechanic: we |
|
click an htmx-enhanced link which deletes the contact and then swaps |
|
some empty content in for the row. We know that before the |
|
<code>tr</code> element is removed, it will have the |
|
<code>htmx-swapping</code> class added to it. We can take advantage of |
|
that to write a CSS transition that fades the opacity of the row to 0. |
|
Here is what that CSS looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb28"><pre class="sourceCode css"><code class="sourceCode css"><span id="cb28-1"><a aria-hidden="true" href="#cb28-1" tabindex="-1"></a>tr<span class="fu">.htmx-swapping</span> { <1<span class="op">></span></span> |
|
<span id="cb28-2"><a aria-hidden="true" href="#cb28-2" tabindex="-1"></a> opacity<span class="in">:</span> 0; <2<span class="op">></span></span> |
|
<span id="cb28-3"><a aria-hidden="true" href="#cb28-3" tabindex="-1"></a> transition<span class="in">:</span> opacity 1s ease-out; <3<span class="op">></span></span> |
|
<span id="cb28-4"><a aria-hidden="true" href="#cb28-4" tabindex="-1"></a>}</span></code></pre></div> |
|
<figcaption><p>Adding a fade out transition</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We want this style to apply to <code>tr</code> elements with the |
|
<code>htmx-swapping</code> class on them.</p></li> |
|
<li><p>The <code>opacity</code> will be 0, making it invisible.</p></li> |
|
<li><p>The <code>opacity</code> will transition to 0 over a 1 second |
|
time period, using the <code>ease-out</code> function.</p></li> |
|
</ol> |
|
<p>Again, this is not a CSS book and we are not going to go deeply into |
|
the details of CSS transitions, but hopefully the above makes sense to |
|
you, even if this is the first time you’ve seen CSS transitions.</p> |
|
<p>So, think about what this means from the htmx swapping model: when |
|
htmx gets content back to swap into the row it will put the |
|
<code>htmx-swapping</code> class on the row and wait a bit. This will |
|
allow the transition to a zero opacity to occur, fading the row out. |
|
Then the new (empty) content will be swapped in, which will effectively |
|
remove the row.</p> |
|
<p>Sounds good, and we are nearly there. There is one more thing we need |
|
to do: the default “swap delay” for htmx is very short, a few |
|
milliseconds. That makes sense in most cases: you don’t want to have |
|
much of a delay before you put the new content into the DOM. But, in |
|
this case, we want to give the CSS animation time to complete before we |
|
do the swap, we want to give it a second, in fact.</p> |
|
<p>Fortunately htmx has an option for the <code>hx-swap</code> |
|
annotation that allows you to set the swap delay: following the swap |
|
type you can add <code>swap:</code> followed by a timing value to tell |
|
htmx to wait a specific amount of time before it swaps. Let’s update our |
|
HTML to allow a one second delay before the swap is done for the delete |
|
action:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb29"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb29-1"><a aria-hidden="true" href="#cb29-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb29-2"><a aria-hidden="true" href="#cb29-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb29-3"><a aria-hidden="true" href="#cb29-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>View<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb29-4"><a aria-hidden="true" href="#cb29-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> href</span><span class="op">=</span><span class="st">"#"</span><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span></span> |
|
<span id="cb29-5"><a aria-hidden="true" href="#cb29-5" tabindex="-1"></a><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML swap:1s"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb29-6"><a aria-hidden="true" href="#cb29-6" tabindex="-1"></a> hx-confirm="Are you sure you want to delete this contact?"</span> |
|
<span id="cb29-7"><a aria-hidden="true" href="#cb29-7" tabindex="-1"></a> hx-target="closest tr">Delete<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb29-8"><a aria-hidden="true" href="#cb29-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The existing row actions</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A swap delay changes how long htmx waits before it swaps in new |
|
content.</p></li> |
|
</ol> |
|
<p>With this modification, the existing row will stay in the DOM for an |
|
additional second, with the <code>htmx-swapping</code> class on it. This |
|
will give the row time to transition to an opacity of zero, giving the |
|
fade out effect we want.</p> |
|
<p>Now, when a user clicks on a “Delete” link and confirms the delete, |
|
the row will slowly fade out and then, once it has faded to a 0 opacity, |
|
it will be removed. Pretty fancy, and all done in a declarative, |
|
hypermedia-oriented manner, no JavaScript required. (Well, obviously |
|
htmx is written in JavaScript, but you know what we mean: we didn’t have |
|
to write any JavaScript to implement the feature.)</p> |
|
<h2 id="_bulk_delete">Bulk Delete</h2> |
|
<p>The final feature we are going to implement in this chapter is a |
|
“Bulk Delete.” The current mechanism for deleting users is nice, but it |
|
would be annoying if a user wanted to delete five or ten contacts at a |
|
time, wouldn’t it? For the bulk delete feature, we want to add the |
|
ability to select rows via a checkbox input and delete them all in a |
|
single go by clicking a “Delete Selected Contacts” button.</p> |
|
<p>To get started with this feature, we’ll need to add a checkbox input |
|
to each row in the <code>rows.html</code> template. This input will have |
|
the name <code>selected_contact_ids</code> and its value will be the |
|
<code>id</code> of the contact for the current row.</p> |
|
<p>Here is what the updated code for <code>rows.html</code> looks |
|
like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb30"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb30-1"><a aria-hidden="true" href="#cb30-1" tabindex="-1"></a>{% for contact in contacts %}</span> |
|
<span id="cb30-2"><a aria-hidden="true" href="#cb30-2" tabindex="-1"></a><span class="dt"><</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb30-3"><a aria-hidden="true" href="#cb30-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">><</span><span class="kw">input</span><span class="ot"> type</span><span class="op">=</span><span class="st">"checkbox"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"selected_contact_ids"</span></span> |
|
<span id="cb30-4"><a aria-hidden="true" href="#cb30-4" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.id }}"</span><span class="dt">></</span><span class="kw">td</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb30-5"><a aria-hidden="true" href="#cb30-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">td</span><span class="dt">></span>{{ contact.first }}<span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb30-6"><a aria-hidden="true" href="#cb30-6" tabindex="-1"></a> ... omitted</span> |
|
<span id="cb30-7"><a aria-hidden="true" href="#cb30-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">tr</span><span class="dt">></span></span> |
|
<span id="cb30-8"><a aria-hidden="true" href="#cb30-8" tabindex="-1"></a>{% endfor %}</span></code></pre></div> |
|
<figcaption><p>Adding a checkbox to each row</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A new cell with the checkbox input whose value is set to the |
|
current contact’s id.</p></li> |
|
</ol> |
|
<p>We’ll also need to add an empty column in the header for the table to |
|
accommodate the checkbox column. With that done we now get a series of |
|
check boxes, one for each row, a pattern no doubt familiar to you from |
|
the web (<a class="ref" href="#fig-checkboxes">[fig-checkboxes]</a>).</p> |
|
<figure id="fig-checkboxes"> |
|
<p><img src="https://hypermedia.systems/images/screenshot_checkboxes.png"/></p> |
|
<figcaption><p>Checkboxes for our contact rows</p></figcaption> |
|
</figure> |
|
<p>If you are not familiar with or have forgotten the way checkboxes |
|
work in HTML: a checkbox will submit its value associated with the name |
|
of the input if and only if it is checked. So if, for example, you |
|
checked the contacts with the ids 3, 7 and 9, then those three values |
|
would all be submitted to the server. Since all the checkboxes in this |
|
case have the same name, <code>selected_contact_ids</code>, all three |
|
values would be submitted with the name |
|
<code>selected_contact_ids</code>.</p> |
|
<h3 id="_the_delete_selected_contacts_button">The “Delete Selected |
|
Contacts” Button</h3> |
|
<p>The next step is to add a button below the table that will delete all |
|
the selected contacts. We want this button, like our delete links in |
|
each row, to issue an HTTP <code>DELETE</code>, but rather than issuing |
|
it to the URL for a given contact, like we do with the inline delete |
|
links and with the delete button on the edit page, here we want to issue |
|
the <code>DELETE</code> to the <code>/contacts</code> URL.</p> |
|
<p>As with the other delete elements, we want to confirm that the user |
|
wishes to delete the contacts, and, for this case, we are going to |
|
target the body of page, since we are going to re-render the whole |
|
table.</p> |
|
<p>Here is what the button code looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb31"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb31-1"><a aria-hidden="true" href="#cb31-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span></span> |
|
<span id="cb31-2"><a aria-hidden="true" href="#cb31-2" tabindex="-1"></a><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb31-3"><a aria-hidden="true" href="#cb31-3" tabindex="-1"></a> hx-confirm="Are you sure you want to delete these contacts?" <span class="er"><</span>2></span> |
|
<span id="cb31-4"><a aria-hidden="true" href="#cb31-4" tabindex="-1"></a> hx-target="body"> <span class="er"><</span>3></span> |
|
<span id="cb31-5"><a aria-hidden="true" href="#cb31-5" tabindex="-1"></a> Delete Selected Contacts</span> |
|
<span id="cb31-6"><a aria-hidden="true" href="#cb31-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The “delete selected contacts” button</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Issue a <code>DELETE</code> to <code>/contacts</code>.</p></li> |
|
<li><p>Confirm that the user wants to delete the selected |
|
contacts.</p></li> |
|
<li><p>Target the body.</p></li> |
|
</ol> |
|
<p>Pretty easy. One question though: how are we going to include the |
|
values of all the selected checkboxes in the request? As it stands right |
|
now, this is just a stand-alone button, and it doesn’t have any |
|
information indicating that it should include any other information in |
|
the <code>DELETE</code> request it makes.</p> |
|
<p>Fortunately, htmx has a few different ways to include values of |
|
inputs with a request.</p> |
|
<p>One way would be to use the <code>hx-include</code> attribute, which |
|
allows you to use a CSS selector to specify the elements you want to |
|
include in the request. That would work fine here, but we are going to |
|
use another approach that is a bit simpler in this case.</p> |
|
<p>By default, if an element is a child of a <code>form</code> element |
|
and makes a non-<code>GET</code> request, htmx will include all the |
|
values of inputs within that form. In situations like this, where there |
|
is a bulk operation for a table, it is common to enclose the whole table |
|
in a form tag, so that it is easy to add buttons that operate on the |
|
selected items.</p> |
|
<p>Let’s add that form tag around the table, and be sure to enclose the |
|
button in it as well:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb32"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb32-1"><a aria-hidden="true" href="#cb32-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb32-2"><a aria-hidden="true" href="#cb32-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">table</span><span class="dt">></span></span> |
|
<span id="cb32-3"><a aria-hidden="true" href="#cb32-3" tabindex="-1"></a> ... omitted</span> |
|
<span id="cb32-4"><a aria-hidden="true" href="#cb32-4" tabindex="-1"></a> <span class="dt"></</span><span class="kw">table</span><span class="dt">></span></span> |
|
<span id="cb32-5"><a aria-hidden="true" href="#cb32-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span></span> |
|
<span id="cb32-6"><a aria-hidden="true" href="#cb32-6" tabindex="-1"></a><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts"</span></span> |
|
<span id="cb32-7"><a aria-hidden="true" href="#cb32-7" tabindex="-1"></a><span class="ot"> hx-confirm</span><span class="op">=</span><span class="st">"Are you sure you want to delete these contacts?"</span></span> |
|
<span id="cb32-8"><a aria-hidden="true" href="#cb32-8" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="dt">></span></span> |
|
<span id="cb32-9"><a aria-hidden="true" href="#cb32-9" tabindex="-1"></a> Delete Selected Contacts</span> |
|
<span id="cb32-10"><a aria-hidden="true" href="#cb32-10" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb32-11"><a aria-hidden="true" href="#cb32-11" tabindex="-1"></a><span class="dt"></</span><span class="kw">form</span><span class="dt">></span> <span class="er"><</span>2></span></code></pre></div> |
|
<figcaption><p>The “delete selected contacts” button</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The form tag encloses the entire table.</p></li> |
|
<li><p>The form tag also encloses the button.</p></li> |
|
</ol> |
|
<p>Now, when the button issues a <code>DELETE</code>, it will include |
|
all the contact ids that have been selected as the |
|
<code>selected_contact_ids</code> request variable.</p> |
|
<h3 id="_the_server_side_for_delete_selected_contacts">The Server Side |
|
for Delete Selected Contacts</h3> |
|
<p>The server-side implementation is going to look like our original |
|
server-side code for deleting a contact. In fact, once again, we can |
|
just copy and paste, and make a few fixes:</p> |
|
<ul> |
|
<li><p>We want to change the URL to <code>/contacts</code>.</p></li> |
|
<li><p>We want the handler to get <em class="test">all</em> the ids submitted as |
|
<code>selected_contact_ids</code> and iterate over each one, deleting |
|
the given contact.</p></li> |
|
</ul> |
|
<p>Those are the only changes we need to make! Here is what the |
|
server-side code looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb33"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb33-1"><a aria-hidden="true" href="#cb33-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/"</span>, methods<span class="op">=</span>[<span class="st">"DELETE"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb33-2"><a aria-hidden="true" href="#cb33-2" tabindex="-1"></a><span class="kw">def</span> contacts_delete_all():</span> |
|
<span id="cb33-3"><a aria-hidden="true" href="#cb33-3" tabindex="-1"></a> contact_ids <span class="op">=</span> [</span> |
|
<span id="cb33-4"><a aria-hidden="true" href="#cb33-4" tabindex="-1"></a> <span class="bu">int</span>(<span class="bu">id</span>)</span> |
|
<span id="cb33-5"><a aria-hidden="true" href="#cb33-5" tabindex="-1"></a> <span class="co"># note: in htmx 1.0 we would use the request.form property instead</span></span> |
|
<span id="cb33-6"><a aria-hidden="true" href="#cb33-6" tabindex="-1"></a> <span class="cf">for</span> <span class="bu">id</span> <span class="kw">in</span> request.args.getlist(<span class="st">"selected_contact_ids"</span>)</span> |
|
<span id="cb33-7"><a aria-hidden="true" href="#cb33-7" tabindex="-1"></a> ] <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb33-8"><a aria-hidden="true" href="#cb33-8" tabindex="-1"></a> <span class="cf">for</span> contact_id <span class="kw">in</span> contact_ids: <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb33-9"><a aria-hidden="true" href="#cb33-9" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id)</span> |
|
<span id="cb33-10"><a aria-hidden="true" href="#cb33-10" tabindex="-1"></a> contact.delete() <span class="op"><</span><span class="dv">4</span><span class="op">></span></span> |
|
<span id="cb33-11"><a aria-hidden="true" href="#cb33-11" tabindex="-1"></a> flash(<span class="st">"Deleted Contacts!"</span>) <span class="op"><</span><span class="dv">5</span><span class="op">></span></span> |
|
<span id="cb33-12"><a aria-hidden="true" href="#cb33-12" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>()</span> |
|
<span id="cb33-13"><a aria-hidden="true" href="#cb33-13" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"index.html"</span>, contacts<span class="op">=</span>contacts_set)</span></code></pre></div> |
|
<figcaption><p>The “delete selected contacts” button</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We handle a <code>DELETE</code> request to the |
|
<code>/contacts/</code> path.</p></li> |
|
<li><p>Convert the <code>selected_contact_ids</code> values submitted to |
|
the server from a list of strings to a list integers.</p></li> |
|
<li><p>Iterate over all of the ids.</p></li> |
|
<li><p>Delete the given contact with each id.</p></li> |
|
<li><p>Beyond that, it’s the same code as our original delete handler: |
|
flash a message and render the <code>index.html</code> |
|
template.</p></li> |
|
</ol> |
|
<p>So, we took the original delete logic and slightly modified it to |
|
deal with an array of ids, rather than a single id.</p> |
|
<p>You might notice one other small change: we did away with the |
|
redirect that was in the original delete code. We did so because we are |
|
already on the page we want to re-render, so there is no reason to |
|
redirect and have the URL update to something new. We can just re-render |
|
the page, and the new list of contacts (sans the contacts that were |
|
deleted) will be re-rendered.</p> |
|
<p>And there we go, we now have a bulk delete feature for our |
|
application. Once again, not a huge amount of code, and we are |
|
implementing these features entirely by exchanging hypermedia with a |
|
server in the traditional, RESTful manner of the web.</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: Accessible by Default?</h2> |
|
<p>Accessibility problems can arise when we try to implement controls |
|
that aren’t built into HTML.</p> |
|
<p>Earlier, in Chapter 1, we looked at the example of a <div> |
|
improvised to work like a button. Let’s look at a different example: |
|
what if you make something that looks like a set of tabs, but you use |
|
radio buttons and CSS hacks to build it? It’s a neat hack that makes the |
|
rounds in web development communities from time to time.</p> |
|
<p>The problem here is that tabs have requirements beyond clicking to |
|
change content. Your improvised tabs may be missing features that will |
|
lead to user confusion and frustration, as well as some undesirable |
|
behaviors. From the <a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabs/">ARIA Authoring |
|
Practices Guide on tabs</a>:</p> |
|
<ul> |
|
<li><p>Keyboard interaction</p> |
|
<ul> |
|
<li><p>Can the tabs be focused with the Tab key?</p></li> |
|
</ul></li> |
|
<li><p>ARIA roles, states, and properties</p> |
|
<ul> |
|
<li><p>“[The element that contains the tabs] has role |
|
<code>tablist</code>.”</p></li> |
|
<li><p>“Each [tab] has role <code>tab</code> […]”</p></li> |
|
<li><p>“Each element that contains the content panel for a |
|
<code>tab</code> has role <code>tabpanel</code>.”</p></li> |
|
<li><p>“Each [tab] has the property <code>aria-controls</code> referring |
|
to its associated tabpanel element.”</p></li> |
|
<li><p>“The active <code>tab</code> element has the state |
|
<code>aria-selected</code> set to <code>true</code> and all other |
|
<code>tab</code> elements have it set to <code>false</code>.”</p></li> |
|
<li><p>“Each element with role <code>tabpanel</code> has the property |
|
<code>aria-labelledby</code> referring to its associated |
|
<code>tab</code> element.”</p></li> |
|
</ul></li> |
|
</ul> |
|
<p>You would need to write a lot of code to make your improvised tabs |
|
fulfill all of these requirements. Some of the ARIA attributes can be |
|
added directly in HTML, but they are repetitive and others (like |
|
<code>aria-selected</code>) need to be set through JavaScript since they |
|
are dynamic. The keyboard interactions can be error-prone too.</p> |
|
<p>It’s not impossible, not even that hard, to make your own tab set |
|
implementation. However, it’s difficult to trust that a new |
|
implementation will work for all users in all environments, since most |
|
of us have limited resources for testing.</p> |
|
<p><em class="test">Stick with established libraries</em> for UI interactions. If a |
|
use case requires a bespoke solution, <em class="test">test exhaustively</em> for |
|
keyboard interaction and accessibility. Test manually. Test |
|
automatically. Test with screen readers, test with a keyboard, test on |
|
different browsers and hardware, and run linters (while coding and/or in |
|
CI). Testing is critical to ensure machine readability, or human |
|
readability, or page weight.</p> |
|
<p>Also consider: Does the information need to be presented as tabs? |
|
Sometimes the answer is yes, but if not, a sequence of details and |
|
disclosures fulfills a very similar purpose.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb34"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb34-1"><a aria-hidden="true" href="#cb34-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">details</span><span class="dt">><</span><span class="kw">summary</span><span class="dt">></span>Disclosure 1<span class="dt"></</span><span class="kw">summary</span><span class="dt">></span></span> |
|
<span id="cb34-2"><a aria-hidden="true" href="#cb34-2" tabindex="-1"></a> Disclosure 1 contents</span> |
|
<span id="cb34-3"><a aria-hidden="true" href="#cb34-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">details</span><span class="dt">></span></span> |
|
<span id="cb34-4"><a aria-hidden="true" href="#cb34-4" tabindex="-1"></a><span class="dt"><</span><span class="kw">details</span><span class="dt">><</span><span class="kw">summary</span><span class="dt">></span>Disclosure 2<span class="dt"></</span><span class="kw">summary</span><span class="dt">></span></span> |
|
<span id="cb34-5"><a aria-hidden="true" href="#cb34-5" tabindex="-1"></a> Disclosure 2 contents</span> |
|
<span id="cb34-6"><a aria-hidden="true" href="#cb34-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">details</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<p>Compromising UX just to avoid JavaScript is bad development. But |
|
sometimes it’s possible to achieve an equal (or better!) quality of UX |
|
while allowing for a simpler and more robust implementation by changing |
|
the design.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">A Dynamic Archive UI</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_ui_requirements">UI Requirements</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_beginning_our_implementation">Beginning Our Implementation</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_adding_the_archiving_endpoint">Adding the Archiving |
|
Endpoint</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_conditionally_rendering_a_progress_ui">Conditionally Rendering |
|
A Progress UI</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_polling">Polling</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_using_polling_to_update_the_archive_ui">Using Polling To Update |
|
The Archive UI</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_adding_the_progress_bar_ui">Adding The Progress Bar UI</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_downloading_the_result">Downloading The Result</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_downloading_the_completed_archive">Downloading The Completed |
|
Archive</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_smoothing_things_out_animations_in_htmx">Smoothing Things Out: |
|
Animations in Htmx</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_the_settling_step_in_htmx">The “Settling” Step in Htmx</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_our_smoothing_solution">Our Smoothing Solution</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_dismissing_the_download_ui">Dismissing The Download UI</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_an_alternative_ux_auto_download">An Alternative UX: |
|
Auto-Download</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#_a_dynamic_archive_ui_complete">A Dynamic Archive UI: |
|
Complete</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/a-dynamic-archive-ui/#html-note-title">HTML Notes: Markdown soup</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>Contact.app has come a long way from a traditional web 1.0-style web |
|
application: we’ve added active search, bulk delete, some nice |
|
animations, and a slew of other features. We have reached a level of |
|
interactivity that most web developers would assume requires some sort |
|
of Single-Page Application JavaScript framework, but we’ve done it using |
|
htmx-powered hypermedia instead.</p> |
|
<p>Let’s look at how we can add a final significant feature to |
|
Contact.app: downloading an archive of all the contacts.</p> |
|
<p>From a hypermedia perspective, downloading a file isn’t exactly |
|
rocket science: using the HTTP <code>Content-Disposition</code> response |
|
header, we can easily tell the browser to download and save a file to |
|
the local computer.</p> |
|
<p>However, let’s make this problem more interesting: let’s add in the |
|
fact that the export can take a bit of time, from five to ten seconds, |
|
or sometimes even longer, to complete.</p> |
|
<p>This means that if we implemented the download as a “normal” HTTP |
|
request, driven by a link or a button, the user might sit with very |
|
little visual feedback, wondering if the download is actually happening, |
|
while the export is being completed. They might even give up in |
|
frustration and click the download hypermedia control again, causing a |
|
<em class="test">second</em> archive request. Not good.</p> |
|
<p>This turns out to be a classic problem in web app development. When |
|
faced with potentially long-running process like this, we ultimately |
|
have two options:</p> |
|
<ul> |
|
<li><p>When the user triggers the action, block until it is complete and |
|
then respond with the result.</p></li> |
|
<li><p>Begin the action and return immediately, showing some sort of UI |
|
indicating that things are in progress.</p></li> |
|
</ul> |
|
<p>Blocking and waiting for the action to complete is certainly the |
|
simpler way to handle it, but it can be a bad user experience, |
|
especially if the action takes a while to complete. If you’ve ever |
|
clicked on something in a web 1.0-style application and then had to sit |
|
there for what seems like an eternity before anything happens, you’ve |
|
seen the practical results of this choice.</p> |
|
<p>The second option, starting the action asynchronously (say, by |
|
creating a thread, or submitting it to a job runner system) is much |
|
nicer from a user experience perspective: the server can respond |
|
immediately and the user doesn’t need to sit there wondering what’s |
|
going on.</p> |
|
<p>But the question is, what do you respond <em class="test">with</em>? The job |
|
probably isn’t complete yet, so you can’t provide a link to the |
|
results.</p> |
|
<p>We have seen a few different “simple” approaches in this scenario in |
|
various web applications:</p> |
|
<ul> |
|
<li><p>Let the user know that the process has started and that they will |
|
be emailed a link to the completed process results when it is |
|
finished.</p></li> |
|
<li><p>Let the user know that the process has started and recommend that |
|
they should manually refresh the page to see the status of the |
|
process.</p></li> |
|
<li><p>Let the user know that the process has started and automatically |
|
refresh the page every few seconds using some JavaScript.</p></li> |
|
</ul> |
|
<p>All of these will work, but none of them is a great user |
|
experience.</p> |
|
<p>What we’d <em class="test">really</em> like in this scenario is something more |
|
like what you see when, for example, you download a large file via the |
|
browser: a nice progress bar indicating where in the process you are, |
|
and, when the process is complete, a link to click immediately to view |
|
the result of the process.</p> |
|
<p>This may sound like something impossible to implement with |
|
hypermedia, and, to be honest, we’ll need to push htmx pretty hard to |
|
make this all work, but, when it is done, it won’t be <em class="test">that</em> much |
|
code, and we will be able to achieve the user experience we want for |
|
this archiving feature.</p> |
|
<h2 id="_ui_requirements">UI Requirements</h2> |
|
<p>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 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.</p> |
|
<p>In order to actually do the archiving, we are going to use a python |
|
class, <code>Archiver</code>, that implements all the functionality that |
|
we need. As with the <code>Contact</code> class, we aren’t going to go |
|
into the implementation details of <code>Archiver</code>, because that’s |
|
beyond the scope of this book. For now you just need to know is that it |
|
provides all the server-side behavior necessary to start a contact |
|
archive process and get the results when that process is done.</p> |
|
<p><code>Archiver</code> gives us the following methods to work |
|
with:</p> |
|
<ul> |
|
<li><p><code>status()</code> - A string representing the status of the |
|
download, either <code>Waiting</code>, <code>Running</code> or |
|
<code>Complete</code></p></li> |
|
<li><p><code>progress()</code> - A number between 0 and 1, indicating |
|
how much progress the archive job has made</p></li> |
|
<li><p><code>run()</code> - Starts a new archive job (if the current |
|
status is <code>Waiting</code>)</p></li> |
|
<li><p><code>reset()</code> - Cancels the current archive job, if any, |
|
and resets to the “Waiting” state</p></li> |
|
<li><p><code>archive_file()</code> - The path to the archive file that |
|
has been created on the server, so we can send it to the client</p></li> |
|
<li><p><code>get()</code> - A class method that lets us get the Archiver |
|
for the current user</p></li> |
|
</ul> |
|
<p>A fairly uncomplicated API.</p> |
|
<p>The only somewhat tricky aspect to the whole API is that the |
|
<code>run()</code> method is <em class="test">non-blocking</em>. This means that it |
|
does not <em class="test">immediately</em> create the archive file, but rather it |
|
starts a background job (as a thread) to do the actual archiving. This |
|
can be confusing if you aren’t used to multithreading in code: you might |
|
be expecting the <code>run()</code> method to “block”, that is, to |
|
actually execute the entire export and only return when it is finished. |
|
But, if it did that, we wouldn’t be able to start the archive process |
|
and immediately render our desired archive progress UI.</p> |
|
<h2 id="_beginning_our_implementation">Beginning Our Implementation</h2> |
|
<p>We now have everything we need to begin implementing our UI: a |
|
reasonable outline of what it is going to look like, and the domain |
|
logic to support it.</p> |
|
<p>So, to start, note that this UI is largely self-contained: we want to |
|
replace the button with the download progress bar, and then the progress |
|
bar with a link to download the results of the completed archive |
|
process.</p> |
|
<p>The fact that our archive user interface is all going to be within a |
|
specific part of the UI is a strong hint that we will want to create a |
|
new template to handle it. Let’s call this template |
|
<code>archive_ui.html</code>.</p> |
|
<p>Also note that we are going to want to replace the entire download UI |
|
in multiple cases:</p> |
|
<ul> |
|
<li><p>When we start the download, we will want to replace the button |
|
with a progress bar.</p></li> |
|
<li><p>As the archive process proceeds, we will want to replace/update |
|
the progress bar.</p></li> |
|
<li><p>When the archive process completes, we will want to replace the |
|
progress bar with a download link.</p></li> |
|
</ul> |
|
<p>To update the UI in this way, we need to set a good target for the |
|
updates. So, let’s wrap the entire UI in a <code>div</code> tag, and |
|
then use that <code>div</code> as the target for all our operations.</p> |
|
<p>Here is the start of the template for our new archive user |
|
interface:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb1"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb1-1"><a aria-hidden="true" href="#cb1-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"archive-ui"</span></span> |
|
<span id="cb1-2"><a aria-hidden="true" href="#cb1-2" tabindex="-1"></a><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"this"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb1-3"><a aria-hidden="true" href="#cb1-3" tabindex="-1"></a> hx-swap="outerHTML"> <span class="er"><</span>2></span> |
|
<span id="cb1-4"><a aria-hidden="true" href="#cb1-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Our initial archive UI template</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This div will be the target for all elements within it.</p></li> |
|
<li><p>Replace the entire div every time using |
|
<code>outerHTML</code>.</p></li> |
|
</ol> |
|
<p>Next, lets add the “Download Contact Archive” button to the |
|
<code>div</code> that will kick off the archive-then-download process. |
|
We’ll use a <code>POST</code> to the path <code>/contacts/archive</code> |
|
to trigger the start of the archiving process:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb2"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb2-1"><a aria-hidden="true" href="#cb2-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"archive-ui"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"this"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span><span class="dt">></span></span> |
|
<span id="cb2-2"><a aria-hidden="true" href="#cb2-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-post</span><span class="op">=</span><span class="st">"/contacts/archive"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb2-3"><a aria-hidden="true" href="#cb2-3" tabindex="-1"></a> Download Contact Archive</span> |
|
<span id="cb2-4"><a aria-hidden="true" href="#cb2-4" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb2-5"><a aria-hidden="true" href="#cb2-5" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding the archive button</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This button will issue a <code>POST</code> to |
|
<code>/contacts/archive</code>.</p></li> |
|
</ol> |
|
<p>Finally, let’s include this new template in our main |
|
<code>index.html</code> template, above the contacts table:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a>{% block content %}</span> |
|
<span id="cb3-2"><a aria-hidden="true" href="#cb3-2" tabindex="-1"></a> {% include 'archive_ui.html' %} <span class="er"><</span>1></span> |
|
<span id="cb3-3"><a aria-hidden="true" href="#cb3-3" tabindex="-1"></a></span> |
|
<span id="cb3-4"><a aria-hidden="true" href="#cb3-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">form</span><span class="ot"> action</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> method</span><span class="op">=</span><span class="st">"get"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"tool-bar"</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Our initial archive UI template</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This template will now be included in the main template.</p></li> |
|
</ol> |
|
<p>With that done, we now have a button showing up in our web |
|
application to get the download going. Since the enclosing |
|
<code>div</code> has an <code>hx-target="this"</code> on it, the button |
|
will inherit that target and replace that enclosing <code>div</code> |
|
with whatever HTML comes back from the <code>POST</code> to |
|
<code>/contacts/archive</code>.</p> |
|
<h2 id="_adding_the_archiving_endpoint">Adding the Archiving |
|
Endpoint</h2> |
|
<p>Our next step is to handle the <code>POST</code> that our button is |
|
making. We want to get the <code>Archiver</code> for the current user |
|
and invoke the <code>run()</code> method on it. This will start the |
|
archive process running. Then we will render some new content indicating |
|
that the process is running.</p> |
|
<p>To do that, we want to reuse the <code>archive_ui</code> template to |
|
handle rendering the archive UI for both states, when the archiver is |
|
“Waiting” and when it is “Running.” (We will handle the “Complete” state |
|
in a bit).</p> |
|
<p>This is a very common pattern: we put all the different potential UIs |
|
for a given chunk of the user interface into a single template, and |
|
conditionally render the appropriate interface. By keeping everything in |
|
one file, it makes it much easier for other developers (or for us, if we |
|
come back after a while!) to understand exactly how the UI works on the |
|
client side.</p> |
|
<p>Since we are going to conditionally render different user interfaces |
|
based on the state of the archiver, we will need to pass the archiver |
|
out to the template as a parameter. So, again: we need to invoke |
|
<code>run()</code> on the archiver in our controller and then pass the |
|
archiver along to the template, so it can render the UI appropriate for |
|
the current status of the archive process.</p> |
|
<p>Here is what the code looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb4"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb4-1"><a aria-hidden="true" href="#cb4-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/archive"</span>, methods<span class="op">=</span>[<span class="st">"POST"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb4-2"><a aria-hidden="true" href="#cb4-2" tabindex="-1"></a><span class="kw">def</span> start_archive():</span> |
|
<span id="cb4-3"><a aria-hidden="true" href="#cb4-3" tabindex="-1"></a> archiver <span class="op">=</span> Archiver.get() <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb4-4"><a aria-hidden="true" href="#cb4-4" tabindex="-1"></a> archiver.run() <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb4-5"><a aria-hidden="true" href="#cb4-5" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"archive_ui.html"</span>, archiver<span class="op">=</span>archiver) <span class="op"><</span><span class="dv">4</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Server-side code to start the archive |
|
process</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Handle <code>POST</code> to |
|
<code>/contacts/archive</code>.</p></li> |
|
<li><p>Look up the Archiver.</p></li> |
|
<li><p>Invoke the non-blocking <code>run()</code> method on it.</p></li> |
|
<li><p>Render the <code>archive_ui.html</code> template, passing in the |
|
archiver.</p></li> |
|
</ol> |
|
<h2 id="_conditionally_rendering_a_progress_ui">Conditionally Rendering |
|
A Progress UI</h2> |
|
<p>Now let’s turn our attention to updating our archiving UI by setting |
|
<code>archive_ui.html</code> to conditionally render different content |
|
depending on the state of the archive process.</p> |
|
<p>Recall that the archiver has a <code>status()</code> method. When we |
|
pass the archiver through as a variable to the template, we can consult |
|
this <code>status()</code> method to see the status of the archive |
|
process.</p> |
|
<p>If the archiver has the status <code>Waiting</code>, we want to |
|
render the “Download Contact Archive” button. If the status is |
|
<code>Running</code>, we want to render a message indicating that |
|
progress is happening. Let’s update our template code to do just |
|
that:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"archive-ui"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"this"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span><span class="dt">></span></span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a> {% if archiver.status() == "Waiting" %} <span class="er"><</span>1></span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-post</span><span class="op">=</span><span class="st">"/contacts/archive"</span><span class="dt">></span></span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a> Download Contact Archive</span> |
|
<span id="cb5-5"><a aria-hidden="true" href="#cb5-5" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb5-6"><a aria-hidden="true" href="#cb5-6" tabindex="-1"></a> {% elif archiver.status() == "Running" %} <span class="er"><</span>2></span> |
|
<span id="cb5-7"><a aria-hidden="true" href="#cb5-7" tabindex="-1"></a> Running... <span class="er"><</span>3></span> |
|
<span id="cb5-8"><a aria-hidden="true" href="#cb5-8" tabindex="-1"></a> {% endif %}</span> |
|
<span id="cb5-9"><a aria-hidden="true" href="#cb5-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding conditional rendering</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Only render the archive button if the status is |
|
“Waiting.”</p></li> |
|
<li><p>Render different content when status is “Running.”</p></li> |
|
<li><p>For now, just some text saying the process is running.</p></li> |
|
</ol> |
|
<p>OK, great, we have some conditional logic in our template view, and |
|
the server-side logic to support kicking off the archive process. We |
|
don’t have a progress bar yet, but we’ll get there! Let’s see how this |
|
works as it stands, and refresh the main page of our application…</p> |
|
<figure> |
|
<pre><code>UndefinedError |
|
jinja2.exceptions.UndefinedError: 'archiver' is undefined |
|
</code></pre> |
|
<figcaption><p>Something Went Wrong</p></figcaption> |
|
</figure> |
|
<p>Ouch!</p> |
|
<p>We get an error message right out of the box. Why? Ah, we are |
|
including the <code>archive_ui.html</code> in the |
|
<code>index.html</code> template, but now the |
|
<code>archive_ui.html</code> template expects the archiver to be passed |
|
through to it, so it can conditionally render the correct UI.</p> |
|
<p>That’s an easy fix: we just need to pass the archiver through when we |
|
render the <code>index.html</code> template as well:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts"</span>)</span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a><span class="kw">def</span> contacts():</span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a> search <span class="op">=</span> request.args.get(<span class="st">"q"</span>)</span> |
|
<span id="cb7-4"><a aria-hidden="true" href="#cb7-4" tabindex="-1"></a> <span class="cf">if</span> search <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span> |
|
<span id="cb7-5"><a aria-hidden="true" href="#cb7-5" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.search(search)</span> |
|
<span id="cb7-6"><a aria-hidden="true" href="#cb7-6" tabindex="-1"></a> <span class="cf">if</span> request.headers.get(<span class="st">'HX-Trigger'</span>) <span class="op">==</span> <span class="st">'search'</span>:</span> |
|
<span id="cb7-7"><a aria-hidden="true" href="#cb7-7" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"rows.html"</span>, contacts<span class="op">=</span>contacts_set)</span> |
|
<span id="cb7-8"><a aria-hidden="true" href="#cb7-8" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb7-9"><a aria-hidden="true" href="#cb7-9" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>()</span> |
|
<span id="cb7-10"><a aria-hidden="true" href="#cb7-10" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"index.html"</span>,</span> |
|
<span id="cb7-11"><a aria-hidden="true" href="#cb7-11" tabindex="-1"></a> contacts<span class="op">=</span>contacts_set, archiver<span class="op">=</span>Archiver.get()) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Including the archiver when we render |
|
index.html</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Pass through archiver to the main template</p></li> |
|
</ol> |
|
<p>Now with that done, we can load up the page. And, sure enough, we can |
|
see the “Download Contact Archive” button.</p> |
|
<p>When we click on it, the button is replaced with the content |
|
“Running…”, and we can see in our development console on the server-side |
|
that the job is indeed getting kicked off properly.</p> |
|
<h2 id="_polling">Polling</h2> |
|
<p>That’s definitely progress, but we don’t exactly have the best |
|
progress indicator here: just some static text telling the user that the |
|
process is running.</p> |
|
<p>We want to make the content update as the process makes progress and, |
|
ideally, show a progress bar indicating how far along it is. How can we |
|
do that in htmx using plain old hypermedia?</p> |
|
<p>The technique we want to use here is called “polling”, where we issue |
|
a request on an interval and update the UI based on the new state of the |
|
server.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Polling? Really?</strong></p> |
|
</div> |
|
<div> |
|
<p>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.</p> |
|
<p>But, say what one will, polling <em class="test">works</em> 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.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>Htmx offers two types of polling. The first is “fixed rate polling”, |
|
which uses a special <code>hx-trigger</code> syntax to indicate that |
|
something should be polled on a fixed interval.</p> |
|
<p>Here is an example:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/messages"</span><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"every 3s"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb8-2"><a aria-hidden="true" href="#cb8-2" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Fixed interval polling</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Trigger a <code>GET</code> to <code>/messages</code> every three |
|
seconds.</p></li> |
|
</ol> |
|
<p>This works great in situations when you want to poll indefinitely, |
|
for example if you want to constantly poll for new messages to display |
|
to the user. However, fixed rate polling isn’t ideal when you have a |
|
definite process after which you want to stop polling: it keeps polling |
|
forever, until the element it is on is removed from the DOM.</p> |
|
<p>In our case, we have a definite process with an ending to it. So, it |
|
will be better to use the second polling technique, known as “load |
|
polling.” In load polling, we take advantage of the fact that htmx |
|
triggers a <code>load</code> event when content is loaded into the DOM. |
|
We can create a trigger on this <code>load</code> event, and add a bit |
|
of a delay so that the request doesn’t trigger immediately.</p> |
|
<p>With this, we can conditionally render the <code>hx-trigger</code> on |
|
every request: when a process has completed we simply do not include the |
|
<code>load</code> trigger, and the load polling stops. This offers a |
|
nice and simple way to poll until a definite process finishes.</p> |
|
<h3 id="_using_polling_to_update_the_archive_ui">Using Polling To Update |
|
The Archive UI</h3> |
|
<p>Let’s use load polling to update our UI as the archiver makes |
|
progress. To show the progress, let’s use a CSS-based progress bar, |
|
taking advantage of the <code>progress()</code> method which returns a |
|
number between 0 and 1 indicating how close the archive process is to |
|
completion.</p> |
|
<p>Here is the snippet of HTML we will use:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress"</span><span class="dt">></span></span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress-bar"</span></span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a><span class="ot"> style</span><span class="op">=</span><span class="st">"width:{{ archiver.progress() * 100 }}%"</span><span class="dt">></</span><span class="kw">div</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb9-4"><a aria-hidden="true" href="#cb9-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A CSS-based progress bar</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The width of the inner element corresponds to the |
|
progress.</p></li> |
|
</ol> |
|
<p>This CSS-based progress bar has two components: an outer |
|
<code>div</code> that provides the wire frame for the progress bar, and |
|
an inner <code>div</code> that is the actual progress bar indicator. We |
|
set the width of the inner progress bar to some percentage (note we need |
|
to multiply the <code>progress()</code> result by 100 to get a |
|
percentage) and that will make the progress indicator the appropriate |
|
width within the parent div.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>What About The <span id="progress"></span> |
|
Element?</strong></p> |
|
</div> |
|
<div> |
|
<p>We are perhaps dipping our toes into the “div soup” here, using a |
|
<code>div</code> tag when there is a perfectly good HTML5 tag, the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress"><code>progress</code></a> |
|
element, that is designed specifically for showing, well, progress.</p> |
|
<p>We decided not to use the <code>progress</code> element for this |
|
example because we want our progress bar to update smoothly, and we will |
|
need to use a CSS technique not available for the <code>progress</code> |
|
element to make that happen. That’s unfortunate, but sometimes we have |
|
to play with the cards we are dealt.</p> |
|
<p>We will, however, use the proper <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/roles/progressbar_role">progress |
|
bar roles</a> to make our <code>div</code>-based progress bar play well |
|
with assistive technologies.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>Let’s update our progress bar to have the proper ARIA roles and |
|
values:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb10"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb10-1"><a aria-hidden="true" href="#cb10-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress"</span><span class="dt">></span></span> |
|
<span id="cb10-2"><a aria-hidden="true" href="#cb10-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress-bar"</span></span> |
|
<span id="cb10-3"><a aria-hidden="true" href="#cb10-3" tabindex="-1"></a><span class="ot"> role</span><span class="op">=</span><span class="st">"progressbar"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb10-4"><a aria-hidden="true" href="#cb10-4" tabindex="-1"></a> aria-valuenow="{{ archiver.progress() * 100 }}" <span class="er"><</span>2></span> |
|
<span id="cb10-5"><a aria-hidden="true" href="#cb10-5" tabindex="-1"></a> style="width:{{ archiver.progress() * 100 }}%"><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb10-6"><a aria-hidden="true" href="#cb10-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A CSS-based progress bar</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This element will act as a progress bar</p></li> |
|
<li><p>The progress will be the percentage completeness of the archiver, |
|
with 100 indicating fully complete</p></li> |
|
</ol> |
|
<p>Finally, for completeness, here is the CSS we’ll use for this |
|
progress bar:</p> |
|
<figure id="lst:progress-bar-css"> |
|
<div class="sourceCode" id="cb11"><pre class="sourceCode css"><code class="sourceCode css"><span id="cb11-1"><a aria-hidden="true" href="#cb11-1" tabindex="-1"></a><span class="fu">.progress</span> {</span> |
|
<span id="cb11-2"><a aria-hidden="true" href="#cb11-2" tabindex="-1"></a> <span class="kw">height</span><span class="ch">:</span> <span class="dv">20</span><span class="dt">px</span><span class="op">;</span></span> |
|
<span id="cb11-3"><a aria-hidden="true" href="#cb11-3" tabindex="-1"></a> <span class="kw">margin-bottom</span><span class="ch">:</span> <span class="dv">20</span><span class="dt">px</span><span class="op">;</span></span> |
|
<span id="cb11-4"><a aria-hidden="true" href="#cb11-4" tabindex="-1"></a> <span class="kw">overflow</span><span class="ch">:</span> <span class="dv">hidden</span><span class="op">;</span></span> |
|
<span id="cb11-5"><a aria-hidden="true" href="#cb11-5" tabindex="-1"></a> <span class="kw">background-color</span><span class="ch">:</span> <span class="cn">#f5f5f5</span><span class="op">;</span></span> |
|
<span id="cb11-6"><a aria-hidden="true" href="#cb11-6" tabindex="-1"></a> <span class="kw">border-radius</span><span class="ch">:</span> <span class="dv">4</span><span class="dt">px</span><span class="op">;</span></span> |
|
<span id="cb11-7"><a aria-hidden="true" href="#cb11-7" tabindex="-1"></a> <span class="kw">box-shadow</span><span class="ch">:</span> <span class="dv">inset</span> <span class="dv">0</span> <span class="dv">1</span><span class="dt">px</span> <span class="dv">2</span><span class="dt">px</span> <span class="fu">rgba(</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">.1</span><span class="fu">)</span><span class="op">;</span></span> |
|
<span id="cb11-8"><a aria-hidden="true" href="#cb11-8" tabindex="-1"></a>}</span> |
|
<span id="cb11-9"><a aria-hidden="true" href="#cb11-9" tabindex="-1"></a></span> |
|
<span id="cb11-10"><a aria-hidden="true" href="#cb11-10" tabindex="-1"></a><span class="fu">.progress-bar</span> {</span> |
|
<span id="cb11-11"><a aria-hidden="true" href="#cb11-11" tabindex="-1"></a> <span class="kw">float</span><span class="ch">:</span> <span class="dv">left</span><span class="op">;</span></span> |
|
<span id="cb11-12"><a aria-hidden="true" href="#cb11-12" tabindex="-1"></a> <span class="kw">width</span><span class="ch">:</span> <span class="dv">0</span><span class="dt">%</span><span class="op">;</span></span> |
|
<span id="cb11-13"><a aria-hidden="true" href="#cb11-13" tabindex="-1"></a> <span class="kw">height</span><span class="ch">:</span> <span class="dv">100</span><span class="dt">%</span><span class="op">;</span></span> |
|
<span id="cb11-14"><a aria-hidden="true" href="#cb11-14" tabindex="-1"></a> <span class="kw">font-size</span><span class="ch">:</span> <span class="dv">12</span><span class="dt">px</span><span class="op">;</span></span> |
|
<span id="cb11-15"><a aria-hidden="true" href="#cb11-15" tabindex="-1"></a> <span class="kw">line-height</span><span class="ch">:</span> <span class="dv">20</span><span class="dt">px</span><span class="op">;</span></span> |
|
<span id="cb11-16"><a aria-hidden="true" href="#cb11-16" tabindex="-1"></a> <span class="kw">color</span><span class="ch">:</span> <span class="cn">#fff</span><span class="op">;</span></span> |
|
<span id="cb11-17"><a aria-hidden="true" href="#cb11-17" tabindex="-1"></a> <span class="kw">text-align</span><span class="ch">:</span> <span class="dv">center</span><span class="op">;</span></span> |
|
<span id="cb11-18"><a aria-hidden="true" href="#cb11-18" tabindex="-1"></a> <span class="kw">background-color</span><span class="ch">:</span> <span class="cn">#337ab7</span><span class="op">;</span></span> |
|
<span id="cb11-19"><a aria-hidden="true" href="#cb11-19" tabindex="-1"></a> <span class="kw">box-shadow</span><span class="ch">:</span> <span class="dv">inset</span> <span class="dv">0</span> <span class="dv">-1</span><span class="dt">px</span> <span class="dv">0</span> <span class="fu">rgba(</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">0</span><span class="op">,</span><span class="dv">.15</span><span class="fu">)</span><span class="op">;</span></span> |
|
<span id="cb11-20"><a aria-hidden="true" href="#cb11-20" tabindex="-1"></a> <span class="kw">transition</span><span class="ch">:</span> <span class="dv">width .6</span><span class="dt">s</span> <span class="dv">ease</span><span class="op">;</span></span> |
|
<span id="cb11-21"><a aria-hidden="true" href="#cb11-21" tabindex="-1"></a>}</span></code></pre></div> |
|
<figcaption><p>The CSS for our progress bar</p></figcaption> |
|
</figure> |
|
<figure> |
|
<p><img src="https://hypermedia.systems/images/screenshot_progress_bar.png"/></p> |
|
<figcaption><p>Our CSS-Based Progress Bar, as implemented in <span class="citation" data-cites="lst">[lst]</span>:progress-bar-css</p></figcaption> |
|
</figure> |
|
<h4 id="_adding_the_progress_bar_ui">Adding The Progress Bar UI</h4> |
|
<p>Let’s add the code for our progress bar into our |
|
<code>archive_ui.html</code> template for the case when the archiver is |
|
running, and let’s update the copy to say “Creating Archive…”:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb12"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb12-1"><a aria-hidden="true" href="#cb12-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"archive-ui"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"this"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span><span class="dt">></span></span> |
|
<span id="cb12-2"><a aria-hidden="true" href="#cb12-2" tabindex="-1"></a> {% if archiver.status() == "Waiting" %}</span> |
|
<span id="cb12-3"><a aria-hidden="true" href="#cb12-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-post</span><span class="op">=</span><span class="st">"/contacts/archive"</span><span class="dt">></span></span> |
|
<span id="cb12-4"><a aria-hidden="true" href="#cb12-4" tabindex="-1"></a> Download Contact Archive</span> |
|
<span id="cb12-5"><a aria-hidden="true" href="#cb12-5" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb12-6"><a aria-hidden="true" href="#cb12-6" tabindex="-1"></a> {% elif archiver.status() == "Running" %}</span> |
|
<span id="cb12-7"><a aria-hidden="true" href="#cb12-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb12-8"><a aria-hidden="true" href="#cb12-8" tabindex="-1"></a> Creating Archive...</span> |
|
<span id="cb12-9"><a aria-hidden="true" href="#cb12-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb12-10"><a aria-hidden="true" href="#cb12-10" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress-bar"</span><span class="ot"> role</span><span class="op">=</span><span class="st">"progressbar"</span></span> |
|
<span id="cb12-11"><a aria-hidden="true" href="#cb12-11" tabindex="-1"></a><span class="ot"> aria-valuenow</span><span class="op">=</span><span class="st">"{{ archiver.progress() * 100}}"</span></span> |
|
<span id="cb12-12"><a aria-hidden="true" href="#cb12-12" tabindex="-1"></a><span class="ot"> style</span><span class="op">=</span><span class="st">"width:{{ archiver.progress() * 100 }}%"</span><span class="dt">></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb12-13"><a aria-hidden="true" href="#cb12-13" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb12-14"><a aria-hidden="true" href="#cb12-14" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb12-15"><a aria-hidden="true" href="#cb12-15" tabindex="-1"></a> {% endif %}</span> |
|
<span id="cb12-16"><a aria-hidden="true" href="#cb12-16" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Adding the progress bar</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Our shiny new progress bar</p></li> |
|
</ol> |
|
<p>Now when we click the “Download Contact Archive” button, we get the |
|
progress bar. But it still doesn’t update because we haven’t implemented |
|
load polling yet: it just sits there, at zero.</p> |
|
<p>To get the progress bar updating dynamically, we’ll need to implement |
|
load polling using <code>hx-trigger</code>. We can add this to pretty |
|
much any element inside the conditional block for when the archiver is |
|
running, so let’s add it to that <code>div</code> that is wrapping |
|
around the “Creating Archive…” text and the progress bar.</p> |
|
<p>Let’s make it poll by issuing an HTTP <code>GET</code> to the same |
|
path as the <code>POST</code>: <code>/contacts/archive</code>.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb13"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb13-1"><a aria-hidden="true" href="#cb13-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"archive-ui"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"this"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span><span class="dt">></span></span> |
|
<span id="cb13-2"><a aria-hidden="true" href="#cb13-2" tabindex="-1"></a> {% if archiver.status() == "Waiting" %}</span> |
|
<span id="cb13-3"><a aria-hidden="true" href="#cb13-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-post</span><span class="op">=</span><span class="st">"/contacts/archive"</span><span class="dt">></span></span> |
|
<span id="cb13-4"><a aria-hidden="true" href="#cb13-4" tabindex="-1"></a> Download Contact Archive</span> |
|
<span id="cb13-5"><a aria-hidden="true" href="#cb13-5" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb13-6"><a aria-hidden="true" href="#cb13-6" tabindex="-1"></a> {% elif archiver.status() == "Running" %}</span> |
|
<span id="cb13-7"><a aria-hidden="true" href="#cb13-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/archive"</span><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"load delay:500ms"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb13-8"><a aria-hidden="true" href="#cb13-8" tabindex="-1"></a> Creating Archive...</span> |
|
<span id="cb13-9"><a aria-hidden="true" href="#cb13-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress"</span><span class="ot"> </span><span class="dt">></span></span> |
|
<span id="cb13-10"><a aria-hidden="true" href="#cb13-10" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress-bar"</span><span class="ot"> role</span><span class="op">=</span><span class="st">"progressbar"</span></span> |
|
<span id="cb13-11"><a aria-hidden="true" href="#cb13-11" tabindex="-1"></a><span class="ot"> aria-valuenow</span><span class="op">=</span><span class="st">"{{ archiver.progress() * 100}}"</span></span> |
|
<span id="cb13-12"><a aria-hidden="true" href="#cb13-12" tabindex="-1"></a><span class="ot"> style</span><span class="op">=</span><span class="st">"width:{{ archiver.progress() * 100 }}%"</span><span class="dt">></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb13-13"><a aria-hidden="true" href="#cb13-13" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb13-14"><a aria-hidden="true" href="#cb13-14" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb13-15"><a aria-hidden="true" href="#cb13-15" tabindex="-1"></a> {% endif %}</span> |
|
<span id="cb13-16"><a aria-hidden="true" href="#cb13-16" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Implementing load polling</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Issue a <code>GET</code> to <code>/contacts/archive</code> 500 |
|
milliseconds after the content loads.</p></li> |
|
</ol> |
|
<p>When this <code>GET</code> is issued to |
|
<code>/contacts/archive</code>, it is going to replace the |
|
<code>div</code> with the id <code>archive-ui</code>, not just itself. |
|
The <code>hx-target</code> attribute on the <code>div</code> with the id |
|
<code>archive-ui</code> is <em class="test">inherited</em> by all child elements |
|
within that <code>div</code>, so the children will all target that |
|
outermost <code>div</code> in the <code>archive_ui.html</code> file.</p> |
|
<p>Now we need to handle the <code>GET</code> to |
|
<code>/contacts/archive</code> on the server. Thankfully, this is quite |
|
easy: all we want to do is re-render <code>archive_ui.html</code> with |
|
the archiver:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb14"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb14-1"><a aria-hidden="true" href="#cb14-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/archive"</span>, methods<span class="op">=</span>[<span class="st">"GET"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb14-2"><a aria-hidden="true" href="#cb14-2" tabindex="-1"></a><span class="kw">def</span> archive_status():</span> |
|
<span id="cb14-3"><a aria-hidden="true" href="#cb14-3" tabindex="-1"></a> archiver <span class="op">=</span> Archiver.get()</span> |
|
<span id="cb14-4"><a aria-hidden="true" href="#cb14-4" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"archive_ui.html"</span>, archiver<span class="op">=</span>archiver) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Handling progress updates</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>handle <code>GET</code> to the <code>/contacts/archive</code> |
|
path</p></li> |
|
<li><p>just re-render the <code>archive_ui.html</code> template</p></li> |
|
</ol> |
|
<p>Like so much else with hypermedia, the code is very readable and not |
|
complicated.</p> |
|
<p>Now, when we click the “Download Contact Archive”, sure enough, we |
|
get a progress bar that updates every 500 milliseconds. As the result of |
|
the call to <code>archiver.progress()</code> incrementally updates from |
|
0 to 1, the progress bar moves across the screen for us. Very cool!</p> |
|
<h3 id="_downloading_the_result">Downloading The Result</h3> |
|
<p>We have one final state to handle, the case when |
|
<code>archiver.status()</code> is set to “Complete”, and there is a JSON |
|
archive of the data ready to download. When the archiver is complete, we |
|
can get the local JSON file on the server from the archiver via the |
|
<code>archive_file()</code> call.</p> |
|
<p>Let’s add another case to our if statement to handle the “Complete” |
|
state, and, when the archive job is complete, lets render a link to a |
|
new path, <code>/contacts/archive/file</code>, which will respond with |
|
the archived JSON file. Here is the new code:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb15"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb15-1"><a aria-hidden="true" href="#cb15-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"archive-ui"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"this"</span><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"outerHTML"</span><span class="dt">></span></span> |
|
<span id="cb15-2"><a aria-hidden="true" href="#cb15-2" tabindex="-1"></a> {% if archiver.status() == "Waiting" %}</span> |
|
<span id="cb15-3"><a aria-hidden="true" href="#cb15-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-post</span><span class="op">=</span><span class="st">"/contacts/archive"</span><span class="dt">></span></span> |
|
<span id="cb15-4"><a aria-hidden="true" href="#cb15-4" tabindex="-1"></a> Download Contact Archive</span> |
|
<span id="cb15-5"><a aria-hidden="true" href="#cb15-5" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb15-6"><a aria-hidden="true" href="#cb15-6" tabindex="-1"></a> {% elif archiver.status() == "Running" %}</span> |
|
<span id="cb15-7"><a aria-hidden="true" href="#cb15-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/archive"</span><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"load delay:500ms"</span><span class="dt">></span></span> |
|
<span id="cb15-8"><a aria-hidden="true" href="#cb15-8" tabindex="-1"></a> Creating Archive...</span> |
|
<span id="cb15-9"><a aria-hidden="true" href="#cb15-9" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress"</span><span class="ot"> </span><span class="dt">></span></span> |
|
<span id="cb15-10"><a aria-hidden="true" href="#cb15-10" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress-bar"</span><span class="ot"> role</span><span class="op">=</span><span class="st">"progressbar"</span></span> |
|
<span id="cb15-11"><a aria-hidden="true" href="#cb15-11" tabindex="-1"></a><span class="ot"> aria-valuenow</span><span class="op">=</span><span class="st">"{{ archiver.progress() * 100}}"</span></span> |
|
<span id="cb15-12"><a aria-hidden="true" href="#cb15-12" tabindex="-1"></a><span class="ot"> style</span><span class="op">=</span><span class="st">"width:{{ archiver.progress() * 100 }}%"</span><span class="dt">></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb15-13"><a aria-hidden="true" href="#cb15-13" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb15-14"><a aria-hidden="true" href="#cb15-14" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb15-15"><a aria-hidden="true" href="#cb15-15" tabindex="-1"></a> {% elif archiver.status() == "Complete" %} <span class="er"><</span>1></span> |
|
<span id="cb15-16"><a aria-hidden="true" href="#cb15-16" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"false"</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/archive/file"</span><span class="dt">></span></span> |
|
<span id="cb15-17"><a aria-hidden="true" href="#cb15-17" tabindex="-1"></a> Archive Ready! Click here to download. <span class="dv">&downarrow;</span></span> |
|
<span id="cb15-18"><a aria-hidden="true" href="#cb15-18" tabindex="-1"></a> <span class="dt"></</span><span class="kw">a</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb15-19"><a aria-hidden="true" href="#cb15-19" tabindex="-1"></a> {% endif %}</span> |
|
<span id="cb15-20"><a aria-hidden="true" href="#cb15-20" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Rendering A Download Link When Archiving |
|
Completes</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>If the status is “Complete”, render a download link.</p></li> |
|
<li><p>The link will issue a <code>GET</code> to |
|
<code>/contacts/archive/file</code>.</p></li> |
|
</ol> |
|
<p>Note that the link has <code>hx-boost</code> set to |
|
<code>false</code>. It has this so that the link will not inherit the |
|
boost behavior that is present for other links and, thus, will not be |
|
issued via AJAX. We want this “normal” link behavior because an AJAX |
|
request cannot download a file directly, whereas a plain anchor tag |
|
can.</p> |
|
<h3 id="_downloading_the_completed_archive">Downloading The Completed |
|
Archive</h3> |
|
<p>The final step is to handle the <code>GET</code> request to |
|
<code>/contacts/archive/file</code>. We want to send the file that the |
|
archiver created down to the client. We are in luck: Flask has a |
|
mechanism for sending a file as a downloaded response, the |
|
<code>send_file()</code> method.</p> |
|
<p>As you see in the code that follows, we pass three arguments to |
|
<code>send_file()</code>: the path to the archive file that the archiver |
|
created, the name of the file that we want the browser to create, and if |
|
we want it sent “as an attachment.” This last argument tells Flask to |
|
set the HTTP response header <code>Content-Disposition</code> to |
|
<code>attachment</code> with the given filename; this is what triggers |
|
the browser’s file-downloading behavior.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb16"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb16-1"><a aria-hidden="true" href="#cb16-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/archive/file"</span>, methods<span class="op">=</span>[<span class="st">"GET"</span>])</span> |
|
<span id="cb16-2"><a aria-hidden="true" href="#cb16-2" tabindex="-1"></a><span class="kw">def</span> archive_content():</span> |
|
<span id="cb16-3"><a aria-hidden="true" href="#cb16-3" tabindex="-1"></a> manager <span class="op">=</span> Archiver.get()</span> |
|
<span id="cb16-4"><a aria-hidden="true" href="#cb16-4" tabindex="-1"></a> <span class="cf">return</span> send_file(</span> |
|
<span id="cb16-5"><a aria-hidden="true" href="#cb16-5" tabindex="-1"></a> manager.archive_file(), <span class="st">"archive.json"</span>, as_attachment<span class="op">=</span><span class="va">True</span>) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Sending A File To The Client</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Send the file to the client via Flask’s <code>send_file()</code> |
|
method.</p></li> |
|
</ol> |
|
<p>Perfect. Now we have an archive UI that is very slick. You click the |
|
“Download Contacts Archive” button and a progress bar appears. When the |
|
progress bar reaches 100%, it disappears and a link to download the |
|
archive file appears. The user can then click on that link and download |
|
their archive.</p> |
|
<p>We’re offering a user experience that is much more user-friendly than |
|
the common click-and-wait experience of many websites.</p> |
|
<h2 id="_smoothing_things_out_animations_in_htmx">Smoothing Things Out: |
|
Animations in Htmx</h2> |
|
<p>As nice as this UI is, there is one minor annoyance: as the progress |
|
bar updates it “jumps” from one position to the next. This feels a bit |
|
like a full page refresh in web 1.0 style applications. Is there a way |
|
we can fix this? (Obviously there is, this why we went with a |
|
<code>div</code> rather than a <code>progress</code> element!)</p> |
|
<p>Let’s walk through the cause of this visual problem and how we might |
|
fix it. (If you’re in a hurry to get to an answer, feel free to jump |
|
ahead to “our solution.”)</p> |
|
<p>It turns out that there is a native HTML technology for smoothing out |
|
changes on an element from one state to another: the CSS Transitions |
|
API, the same one that we discussed in Chapter 4. Using CSS Transitions, |
|
you can smoothly animate an element between different styling by using |
|
the <code>transition</code> property.</p> |
|
<p>If you look back at our CSS definition of the |
|
<code>.progress-bar</code> class, you will see the following transition |
|
definition: <code>transition: width .6s ease;</code>. This means that |
|
when the width of the progress bar is changed from, say 20% to 30%, the |
|
browser will animate over a period of .6 seconds using the “ease” |
|
function (which has a nice accelerate/decelerate effect).</p> |
|
<p>So why isn’t that transition being applied in our current UI? The |
|
reason is that, in our example, htmx is <em class="test">replacing</em> the progress |
|
bar with a new one every time it polls. It isn’t updating the width of |
|
an <em class="test">existing</em> element. CSS transitions, unfortunately, only apply |
|
when the properties of an existing element change inline, not when the |
|
element is replaced.</p> |
|
<p>This is a reason why pure HTML-based applications can feel jerky and |
|
unpolished when compared with their SPA counterparts: it is hard to use |
|
CSS transitions without some JavaScript.</p> |
|
<p>But there is some good news: htmx has a way to utilize CSS |
|
transitions even when it replaces content in the DOM.</p> |
|
<h3 id="_the_settling_step_in_htmx">The “Settling” Step in Htmx</h3> |
|
<p>When we discussed the htmx swap model in Chapter 4, we focused on the |
|
classes that htmx adds and removes, but we skipped over the process of |
|
“settling.” In htmx, settling involves several steps: when htmx is about |
|
to replace a chunk of content, it looks through the new content and |
|
finds all elements with an <code>id</code> on it. It then looks in the |
|
<em class="test">existing</em> content for elements with the same |
|
<code>id</code>.</p> |
|
<p>If there is one, it does the following somewhat elaborate |
|
shuffle:</p> |
|
<ul> |
|
<li><p>The <em class="test">new</em> content gets the attributes of the <em class="test">old</em> |
|
content temporarily.</p></li> |
|
<li><p>The new content is inserted.</p></li> |
|
<li><p>After a small delay, the new content has its attributes reverted |
|
to their actual values.</p></li> |
|
</ul> |
|
<p>So, what is this strange little dance supposed to achieve?</p> |
|
<p>Well, if an element has a stable id between swaps, you can now write |
|
CSS transitions between various states. Since the <em class="test">new</em> content |
|
briefly has the <em class="test">old</em> attributes, the normal CSS transition |
|
mechanism will kick in when the actual values are restored.</p> |
|
<h3 id="_our_smoothing_solution">Our Smoothing Solution</h3> |
|
<p>So, we arrive at our fix.</p> |
|
<p>All we need to do is add a stable ID to our <code>progress-bar</code> |
|
element.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb17"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb17-1"><a aria-hidden="true" href="#cb17-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress"</span><span class="ot"> </span><span class="dt">></span></span> |
|
<span id="cb17-2"><a aria-hidden="true" href="#cb17-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> id</span><span class="op">=</span><span class="st">"archive-progress"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"progress-bar"</span><span class="ot"> role</span><span class="op">=</span><span class="st">"progressbar"</span></span> |
|
<span id="cb17-3"><a aria-hidden="true" href="#cb17-3" tabindex="-1"></a><span class="ot"> aria-valuenow</span><span class="op">=</span><span class="st">"{{ archiver.progress() * 100 }}"</span></span> |
|
<span id="cb17-4"><a aria-hidden="true" href="#cb17-4" tabindex="-1"></a><span class="ot"> style</span><span class="op">=</span><span class="st">"width:{{ archiver.progress() * 100 }}%"</span><span class="dt">></</span><span class="kw">div</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb17-5"><a aria-hidden="true" href="#cb17-5" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Smoothing things out</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The progress bar div now has a stable id across |
|
requests.</p></li> |
|
</ol> |
|
<p>Despite the complicated mechanics going on behind the scenes in htmx, |
|
the solution is as simple as adding a stable <code>id</code> attribute |
|
to the element we want to animate.</p> |
|
<p>Now, rather than jumping 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. The htmx swapping model allows us |
|
to achieve this even though we are replacing the content with new |
|
HTML.</p> |
|
<p>And voila: we have a nice, smoothly animated progress bar for our |
|
contact archiving feature. The result has the look and feel of a |
|
JavaScript-based solution, but we did it with the simplicity of an |
|
HTML-based approach.</p> |
|
<p>Now that, dear reader, does spark joy.</p> |
|
<h2 id="_dismissing_the_download_ui">Dismissing The Download UI</h2> |
|
<p>Some users may change their mind, and decide not to download the |
|
archive. They may never witness our glorious progress bar, but that’s |
|
OK. We’re going to give these users a button to dismiss the download |
|
link and return to the original export UI state.</p> |
|
<p>To do this, we’ll add a button that issues a <code>DELETE</code> to |
|
the path <code>/contacts/archive</code>, indicating that the current |
|
archive can be removed or cleaned up.</p> |
|
<p>We’ll add it after the download link, like so:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb18"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb18-1"><a aria-hidden="true" href="#cb18-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"false"</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/archive/file"</span><span class="dt">></span></span> |
|
<span id="cb18-2"><a aria-hidden="true" href="#cb18-2" tabindex="-1"></a> Archive Ready! Click here to download. <span class="dv">&downarrow;</span></span> |
|
<span id="cb18-3"><a aria-hidden="true" href="#cb18-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb18-4"><a aria-hidden="true" href="#cb18-4" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts/archive"</span><span class="dt">></span>Clear Download<span class="dt"></</span><span class="kw">button</span><span class="dt">></span> <span class="er"><</span>1></span></code></pre></div> |
|
<figcaption><p>Clearing the download</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A simple button that issues a <code>DELETE</code> to |
|
<code>/contacts/archive</code>.</p></li> |
|
</ol> |
|
<p>Now the user has a button that they can click on to dismiss the |
|
archive download link. But we will need to hook it up on the server |
|
side. As usual, this is pretty straightforward: we create a new handler |
|
for the <code>DELETE</code> HTTP Action, invoke the <code>reset()</code> |
|
method on the archiver, and re-render the <code>archive_ui.html</code> |
|
template.</p> |
|
<p>Since this button is picking up the same <code>hx-target</code> and |
|
<code>hx-swap</code> configuration as everything else, it “just |
|
works.”</p> |
|
<p>Here is the server-side code:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb19"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb19-1"><a aria-hidden="true" href="#cb19-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/contacts/archive"</span>, methods<span class="op">=</span>[<span class="st">"DELETE"</span>])</span> |
|
<span id="cb19-2"><a aria-hidden="true" href="#cb19-2" tabindex="-1"></a><span class="kw">def</span> reset_archive():</span> |
|
<span id="cb19-3"><a aria-hidden="true" href="#cb19-3" tabindex="-1"></a> archiver <span class="op">=</span> Archiver.get()</span> |
|
<span id="cb19-4"><a aria-hidden="true" href="#cb19-4" tabindex="-1"></a> archiver.reset() <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb19-5"><a aria-hidden="true" href="#cb19-5" tabindex="-1"></a> <span class="cf">return</span> render_template(<span class="st">"archive_ui.html"</span>, archiver<span class="op">=</span>archiver)</span></code></pre></div> |
|
<figcaption><p>The handler to reset the download</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Call <code>reset()</code> on the archiver</p></li> |
|
</ol> |
|
<p>This looks pretty similar to our other handlers, doesn’t it?</p> |
|
<p>Sure does! That’s the idea!</p> |
|
<h2 id="_an_alternative_ux_auto_download">An Alternative UX: |
|
Auto-Download</h2> |
|
<p>While we prefer the current user experience for archiving contacts, |
|
there are other alternatives. Currently, a progress bar shows the |
|
progress of the process and, when it completes, the user is presented |
|
with a link to actually download the file. Another pattern that we see |
|
on the web is “auto-downloading”, where the file downloads immediately |
|
without the user needing to click a link.</p> |
|
<p>We can add this functionality quite easily to our application with |
|
just a bit of scripting. We will discuss scripting in a |
|
Hypermedia-Driven Application in more depth in Chapter 9, but, put |
|
briefly: scripting is perfectly acceptable in a HDA, as long as it |
|
doesn’t replace the core hypermedia mechanics of the application.</p> |
|
<p>For our auto-download feature we will use <a href="https://hyperscript.org">_hyperscript</a>, our preferred scripting |
|
option. JavaScript would also work here, and would be nearly as simple; |
|
again, we’ll discuss scripting options in detail in Chapter 9.</p> |
|
<p>All we need to do to implement the auto-download feature is the |
|
following: when the download link renders, automatically click on the |
|
link for the user.</p> |
|
<p>The _hyperscript code reads almost the same as the previous sentence |
|
(which is a major reason why we love hyperscript):</p> |
|
<figure> |
|
<div class="sourceCode" id="cb20"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb20-1"><a aria-hidden="true" href="#cb20-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> hx-boost</span><span class="op">=</span><span class="st">"false"</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/archive/file"</span></span> |
|
<span id="cb20-2"><a aria-hidden="true" href="#cb20-2" tabindex="-1"></a><span class="ot"> _</span><span class="op">=</span><span class="st">"on load click() me"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb20-3"><a aria-hidden="true" href="#cb20-3" tabindex="-1"></a> Archive Downloading! Click here if the download does not start.</span> |
|
<span id="cb20-4"><a aria-hidden="true" href="#cb20-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Auto-downloading</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A bit of _hyperscript to make the file auto-download.</p></li> |
|
</ol> |
|
<p>Crucially, the scripting here is simply <em class="test">enhancing</em> the |
|
existing hypermedia, rather than replacing it with a non-hypermedia |
|
request. This is hypermedia-friendly scripting, as we will cover in more |
|
depth in a bit.</p> |
|
<h2 id="_a_dynamic_archive_ui_complete">A Dynamic Archive UI: |
|
Complete</h2> |
|
<p>In this chapter we’ve managed to create a dynamic UI for our contact |
|
archive functionality, with a progress bar and auto-downloading, and |
|
we’ve done nearly all of it — with the exception of a small bit of |
|
scripting for auto-download — in pure hypermedia. It took about 16 lines |
|
of front end code and 16 lines of backend code to build the whole |
|
thing.</p> |
|
<p>HTML, with a bit of help from a hypermedia-oriented JavaScript |
|
library such as htmx, can in fact be extremely powerful and |
|
expressive.</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: Markdown soup</h2> |
|
<p><em class="test">Markdown soup</em> is the lesser known sibling of |
|
<code><div></code> soup. This is the result of web developers |
|
limiting themselves to the set of elements that the Markdown language |
|
provides shorthand for, even when these elements are incorrect. More |
|
seriously, it’s important to be aware of the full power of our tools, |
|
including HTML. Consider the following example of an IEEE-style |
|
citation:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb21"><pre class="sourceCode markdown"><code class="sourceCode markdown"><span id="cb21-1"><a aria-hidden="true" href="#cb21-1" tabindex="-1"></a><span class="co">[</span><span class="ot">1</span><span class="co">]</span> C.H. Gross, A. Stepinski, and D. Akşimşek, <1></span> |
|
<span id="cb21-2"><a aria-hidden="true" href="#cb21-2" tabindex="-1"></a> _Hypermedia Systems_, <2></span> |
|
<span id="cb21-3"><a aria-hidden="true" href="#cb21-3" tabindex="-1"></a> Bozeman, MT, USA: Big Sky Software.</span> |
|
<span id="cb21-4"><a aria-hidden="true" href="#cb21-4" tabindex="-1"></a> Available: <span class="ot"><https://hypermedia.systems/></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>The reference number is written in brackets.</p></li> |
|
<li><p>Underscores around the book title creates an <em> |
|
element.</p></li> |
|
</ol> |
|
<p>Here, <em> is used because it’s the only Markdown element that |
|
is presented in italics by default. This indicates that the book title |
|
is being stressed, but the purpose is to mark it as the title of a work. |
|
HTML has the <code><cite></code> element that’s intended for this |
|
exact purpose.</p> |
|
<p>Furthermore, even though this is a numbered list perfect for the |
|
<code><ol></code> element, which Markdown supports, plain text is |
|
used for the reference numbers instead. Why could this be? The IEEE |
|
citation style requires that these numbers are presented in square |
|
brackets. This could be achieved on an <code><ol></code> with CSS, |
|
but Markdown doesn’t have a way to add a class to elements meaning the |
|
square brackets would apply to all ordered lists.</p> |
|
<p>Don’t shy away from using embedded HTML in Markdown. For larger |
|
sites, also consider Markdown extensions.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb22"><pre class="sourceCode markdown"><code class="sourceCode markdown"><span id="cb22-1"><a aria-hidden="true" href="#cb22-1" tabindex="-1"></a>{.ieee-reference-list} <1></span> |
|
<span id="cb22-2"><a aria-hidden="true" href="#cb22-2" tabindex="-1"></a><span class="ss">1. </span>C.H. Gross, A. Stepinski, and D. Akşimşek, <2></span> |
|
<span id="cb22-3"><a aria-hidden="true" href="#cb22-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">cite</span><span class="dt">></span>Hypermedia Systems<span class="dt"></</span><span class="kw">cite</span><span class="dt">></span>, <3></span> |
|
<span id="cb22-4"><a aria-hidden="true" href="#cb22-4" tabindex="-1"></a> Bozeman, MT, USA: Big Sky Software.</span> |
|
<span id="cb22-5"><a aria-hidden="true" href="#cb22-5" tabindex="-1"></a> Available: <span class="ot"><https://hypermedia.systems/></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>Many Markdown dialects let us add ids, classes and attributes |
|
using curly braces.</p></li> |
|
<li><p>We can now use the <ol> element, and create the brackets in |
|
CSS.</p></li> |
|
<li><p>We use <code><cite></code> to mark the title of the work |
|
being cited (not the whole citation!)</p></li> |
|
</ol> |
|
<p>You can also use custom processors to produce extra-detailed HTML |
|
instead of writing it by hand:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb23"><pre class="sourceCode markdown"><code class="sourceCode markdown"><span id="cb23-1"><a aria-hidden="true" href="#cb23-1" tabindex="-1"></a>{% reference_list %} <1></span> |
|
<span id="cb23-2"><a aria-hidden="true" href="#cb23-2" tabindex="-1"></a><span class="ot">[hypers2023]: </span><2></span> |
|
<span id="cb23-3"><a aria-hidden="true" href="#cb23-3" tabindex="-1"></a> C.H. Gross, A. Stepinski, and D. Akşimşek, _Hypermedia Systems_,</span> |
|
<span id="cb23-4"><a aria-hidden="true" href="#cb23-4" tabindex="-1"></a> Bozeman, MT, USA: Big Sky Software, 2023.</span> |
|
<span id="cb23-5"><a aria-hidden="true" href="#cb23-5" tabindex="-1"></a> Available: <span class="ot"><https://hypermedia.systems/></span></span> |
|
<span id="cb23-6"><a aria-hidden="true" href="#cb23-6" tabindex="-1"></a>{% end %}</span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p><code>reference_list</code> is a macro that will transform the |
|
plain text to highly-detailed HTML.</p></li> |
|
<li><p>A processor can also resolve identifiers, so we don’t have to |
|
manually keep the reference list in order and the in-text citations in |
|
sync.</p></li> |
|
</ol> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Tricks Of The Htmx Masters</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#htmx-attributes">Htmx Attributes</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#hx-swap">hx-swap</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#hx-trigger">hx-trigger</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#trigger-filters">Trigger filters</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#synthetic-events">Synthetic events</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#_other_attributes">Other Attributes</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#events">Events</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#htmx-generated-events">Htmx-Generated Events</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#using-the-htmx-configrequest-event">Using the htmx:configRequest Event</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#canceling-a-request-using-htmx-abort">Canceling a Request Using htmx:abort</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#_server_generated_events">Server Generated Events</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#_http_requests_responses">HTTP Requests & Responses</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#http-response-codes">HTTP Response Codes</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#_updating_other_content">Updating Other Content</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#_expanding_your_selection">Expanding Your Selection</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#out-of-band-swaps">Out of Band Swaps</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#events-1">Events</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#being-pragmatic">Being Pragmatic</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#debugging">Debugging</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#_logging_htmx_events">Logging Htmx Events</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#_monitoring_events_in_chrome">Monitoring Events in Chrome</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#security-considerations">Security Considerations</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#content-security-policies---htmx">Content Security Policies & Htmx</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#configuring">Configuring</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/tricks-of-the-htmx-masters/#html-note-title">HTML Notes: Semantic HTML</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>In this chapter we are going to look deeper into the htmx toolkit. |
|
We’ve accomplished quite a bit with what we’ve learned so far. Still, |
|
when you are developing Hypermedia-Driven Applications, there will be |
|
times when you need to reach for additional options and techniques.</p> |
|
<p>We will go over the more advanced attributes in htmx, as well as |
|
expand on the advanced details of attributes we have already used.</p> |
|
<p>Additionally, we will look at functionality that htmx offers beyond |
|
simple HTML attributes: how htmx extends standard HTTP request and |
|
responses, how htmx works with (and produces) events, and how to |
|
approach situations where there isn’t a simple, single target on the |
|
page to be updated.</p> |
|
<p>Finally, we will take a look at practical considerations when doing |
|
htmx development: how to debug htmx-based applications effectively, |
|
security considerations you will need to take into account when working |
|
with htmx, and how to configure the behavior of htmx.</p> |
|
<p>With the features and techniques in this chapter, you will be able to |
|
pull off extremely sophisticated user interfaces using only htmx and |
|
perhaps a small bit of hypermedia-friendly client-side scripting.</p> |
|
<h2 id="htmx-attributes">Htmx Attributes</h2> |
|
<p>Thus far we have used about fifteen different attributes from htmx in |
|
our application. The most important ones have been:</p> |
|
<dl> |
|
<dt><code>hx-get</code>, <code>hx-post</code>, etc.</dt> |
|
<dd> |
|
<p>To specify the AJAX request an element should make</p> |
|
</dd> |
|
<dt><code>hx-trigger</code></dt> |
|
<dd> |
|
<p>To specify the event that triggers a request</p> |
|
</dd> |
|
<dt><code>hx-swap</code></dt> |
|
<dd> |
|
<p>To specify how to swap the returned HTML content into the DOM</p> |
|
</dd> |
|
<dt><code>hx-target</code></dt> |
|
<dd> |
|
<p>To specify where in the DOM to swap the returned HTML content</p> |
|
</dd> |
|
</dl> |
|
<p>Two of these attributes, <code>hx-swap</code> and |
|
<code>hx-trigger</code>, support a number of useful options for creating |
|
more advanced Hypermedia-Driven Applications.</p> |
|
<h3 id="hx-swap">hx-swap</h3> |
|
<p>We’ll start with the <code>hx-swap</code> attribute. This is often |
|
not included on elements that issue htmx-driven requests because its |
|
default behavior — <code>innerHTML</code>, which swaps the inner HTML of |
|
the element — tends to cover most use cases.</p> |
|
<p>We earlier saw situations where we wanted to override the default |
|
behavior and use <code>outerHTML</code>, for example. And, in Chapter 2, |
|
we discussed some other swap options beyond these two, |
|
<code>beforebegin</code>, <code>afterend</code>, etc.</p> |
|
<p>In Chapter 5, we also looked at the <code>swap</code> delay modifier |
|
for <code>hx-swap</code>, which allowed us to fade some content out |
|
before it was removed from the DOM.</p> |
|
<p>In addition to these, <code>hx-swap</code> offers further control |
|
with the following modifiers:</p> |
|
<dl> |
|
<dt><code>settle</code></dt> |
|
<dd> |
|
<p>Like <code>swap</code>, this allows you to apply a specific delay |
|
between when the content has been swapped into the DOM and when its |
|
attributes are “settled”, that is, updated from their old values (if |
|
any) to their new values. This can give you fine-grained control over |
|
CSS transitions.</p> |
|
</dd> |
|
<dt><code>show</code></dt> |
|
<dd> |
|
<p>Allows you to specify an element that should be shown — that is, |
|
scrolled into the viewport of the browser if necessary — when a request |
|
is completed.</p> |
|
</dd> |
|
<dt><code>scroll</code></dt> |
|
<dd> |
|
<p>Allows you to specify a scrollable element (that is, an element with |
|
scrollbars), that should be scrolled to the top or bottom when a request |
|
is completed.</p> |
|
</dd> |
|
<dt><code>focus-scroll</code></dt> |
|
<dd> |
|
<p>Allows you to specify that htmx should scroll to the focused element |
|
when a request completes. The default for this modifier is “false.”</p> |
|
</dd> |
|
</dl> |
|
<p>So, for example, if we had a button that issued a <code>GET</code> |
|
request, and we wished to scroll to the top of the <code>body</code> |
|
element when the request completed, we would write the following |
|
HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb1"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb1-1"><a aria-hidden="true" href="#cb1-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"#content-div"</span></span> |
|
<span id="cb1-2"><a aria-hidden="true" href="#cb1-2" tabindex="-1"></a><span class="ot"> hx-swap</span><span class="op">=</span><span class="st">"innerHTML show:body:top"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb1-3"><a aria-hidden="true" href="#cb1-3" tabindex="-1"></a> Get Contacts</span> |
|
<span id="cb1-4"><a aria-hidden="true" href="#cb1-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Scrolling to the top of the page</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This tells htmx to show the top of the body after the swap |
|
occurs.</p></li> |
|
</ol> |
|
<p>More details and examples can be found online in the |
|
<code>hx-swap</code> <a href="https://htmx.org/attributes/hx-swap/">documentation</a>.</p> |
|
<h3 id="hx-trigger">hx-trigger</h3> |
|
<p>Like <code>hx-swap</code>, <code>hx-trigger</code> can often be |
|
omitted when you are using htmx, because the default behavior is |
|
typically what you want. Recall the default triggering events are |
|
determined by an element’s type:</p> |
|
<ul> |
|
<li><p>Requests on <code>input</code>, <code>textarea</code> & |
|
<code>select</code> elements are triggered by the <code>change</code> |
|
event.</p></li> |
|
<li><p>Requests on <code>form</code> elements are triggered on the |
|
<code>submit</code> event.</p></li> |
|
<li><p>Requests on all other elements are triggered by the |
|
<code>click</code> event.</p></li> |
|
</ul> |
|
<p>There are times, however, when you want a more elaborate trigger |
|
specification. A classic example is the active search example we |
|
implemented in Contact.app:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb2"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb2-1"><a aria-hidden="true" href="#cb2-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span></span> |
|
<span id="cb2-2"><a aria-hidden="true" href="#cb2-2" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ request.args.get('q') or '' }}"</span></span> |
|
<span id="cb2-3"><a aria-hidden="true" href="#cb2-3" tabindex="-1"></a><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span></span> |
|
<span id="cb2-4"><a aria-hidden="true" href="#cb2-4" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"search, keyup delay:200ms changed"</span><span class="dt">/></span> <span class="er"><</span>1></span></code></pre></div> |
|
<figcaption><p>The active search input</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>An elaborate trigger specification.</p></li> |
|
</ol> |
|
<p>This example took advantage of two modifiers available for the |
|
<code>hx-trigger</code> attribute:</p> |
|
<dl> |
|
<dt><code>delay</code></dt> |
|
<dd> |
|
<p>Allows you to specify a delay to wait before a request is issued. If |
|
the event occurs again, the first event is discarded and the timer |
|
resets. This allows you to “debounce” requests.</p> |
|
</dd> |
|
<dt><code>changed</code></dt> |
|
<dd> |
|
<p>Allows you to specify that a request should only be issued when the |
|
<code>value</code> property of the given element has changed.</p> |
|
</dd> |
|
</dl> |
|
<p><code>hx-trigger</code> has several additional modifiers. This makes |
|
sense, because events are fairly complex and we want to be able to take |
|
advantage of all the power they offer. We will discuss events in more |
|
detail below.</p> |
|
<p>Here are the other modifiers available on |
|
<code>hx-trigger</code>:</p> |
|
<dl> |
|
<dt><code>once</code></dt> |
|
<dd> |
|
<p>The given event will only trigger a request once.</p> |
|
</dd> |
|
<dt><code>throttle</code></dt> |
|
<dd> |
|
<p>Allows you to throttle events, only issuing them once every certain |
|
interval. This is different than <code>delay</code> in that the first |
|
event will trigger immediately, but any following events will not |
|
trigger until the throttle time period has elapsed.</p> |
|
</dd> |
|
<dt><code>from</code></dt> |
|
<dd> |
|
<p>A CSS selector that allows you to pick another element to listen for |
|
events on. We will see an example of this used later in the chapter.</p> |
|
</dd> |
|
<dt><code>target</code></dt> |
|
<dd> |
|
<p>A CSS selector that allows you to filter events to only those that |
|
occur directly on a given element. In the DOM, events “bubble” to their |
|
ancestor elements, so a <code>click</code> event on a button will also |
|
trigger a <code>click</code> event on an enclosing <code>div</code>, all |
|
the way up to the <code>body</code> element. Sometimes you want to |
|
specify an event directly on a given element, and this attribute allows |
|
you to do that.</p> |
|
</dd> |
|
<dt><code>consume</code></dt> |
|
<dd> |
|
<p>If this option is set to <code>true</code>, the triggering event will |
|
be cancelled and not propagate to ancestor elements.</p> |
|
</dd> |
|
<dt><code>queue</code></dt> |
|
<dd> |
|
<p>This option allows you to specify how events are queued in htmx. By |
|
default, when htmx receives a triggering event, it will issue a request |
|
and start an event queue. If the request is still in flight when another |
|
event is received, it will queue the event and, when the request |
|
finishes, trigger a new request. By default, it only keeps the last |
|
event it receives, but you can modify that behavior using this option: |
|
for example, you can set it to <code>none</code> and ignore all |
|
triggering events that occur during a request.</p> |
|
</dd> |
|
</dl> |
|
<h4 id="trigger-filters">Trigger filters</h4> |
|
<p>The <code>hx-trigger</code> attribute also allows you to specify a |
|
<em class="test">filter</em> for events by using square brackets enclosing a |
|
JavaScript expression after the event name.</p> |
|
<p>Let’s say you have a complex situation where contacts should only be |
|
retrievable in certain situations. You have a JavaScript function, |
|
<code>contactRetrievalEnabled()</code> that returns a boolean, |
|
<code>true</code> if contacts can be retrieved and <code>false</code> |
|
otherwise. How could you use this function to place a gate on a button |
|
that issues a request to <code>/contacts</code>?</p> |
|
<p>To do this using an event filter in htmx, you would write the |
|
following HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">script</span><span class="dt">></span></span> |
|
<span id="cb3-2"><a aria-hidden="true" href="#cb3-2" tabindex="-1"></a> <span class="kw">function</span> <span class="fu">contactRetrievalEnabled</span>() {</span> |
|
<span id="cb3-3"><a aria-hidden="true" href="#cb3-3" tabindex="-1"></a> <span class="co">// code to test if contact retrieval is enabled</span></span> |
|
<span id="cb3-4"><a aria-hidden="true" href="#cb3-4" tabindex="-1"></a> <span class="op">...</span></span> |
|
<span id="cb3-5"><a aria-hidden="true" href="#cb3-5" tabindex="-1"></a> }</span> |
|
<span id="cb3-6"><a aria-hidden="true" href="#cb3-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">script</span><span class="dt">></span></span> |
|
<span id="cb3-7"><a aria-hidden="true" href="#cb3-7" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span></span> |
|
<span id="cb3-8"><a aria-hidden="true" href="#cb3-8" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"click[contactRetrievalEnabled()]"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb3-9"><a aria-hidden="true" href="#cb3-9" tabindex="-1"></a> Get Contacts</span> |
|
<span id="cb3-10"><a aria-hidden="true" href="#cb3-10" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The active search input</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A request is issued on click only when |
|
<code>contactRetrievalEnabled()</code> returns |
|
<code>true</code>.</p></li> |
|
</ol> |
|
<p>The button will not issue a request if |
|
<code>contactRetrievalEnabled()</code> returns false, allowing you to |
|
dynamically control when the request will be made. There are common |
|
situations that call for an event trigger, when you only want to issue a |
|
request under specific circumstances:</p> |
|
<ul> |
|
<li><p>if a certain element has focus</p></li> |
|
<li><p>if a given form is valid</p></li> |
|
<li><p>if a set of inputs have specific values</p></li> |
|
</ul> |
|
<p>Using event filters, you can use whatever logic you’d like to filter |
|
requests by htmx.</p> |
|
<h4 id="synthetic-events">Synthetic events</h4> |
|
<p>In addition to these modifiers, <code>hx-trigger</code> offers a few |
|
“synthetic” events, that is events that are not part of the regular DOM |
|
API. We have already seen <code>load</code> and <code>revealed</code> in |
|
our lazy loading and infinite scroll examples, but htmx also gives you |
|
an <code>intersect</code> event that triggers when an element intersects |
|
its viewport.</p> |
|
<p>This synthetic event uses the modern Intersection Observer API, which |
|
you can read more about at <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">MDN</a>.</p> |
|
<p>Intersection gives you fine-grained control over exactly when a |
|
request should be triggered. For example, you can set a threshold and |
|
specify that the request be issued only when an element is 50% |
|
visible.</p> |
|
<p>The <code>hx-trigger</code> attribute certainly is the most complex |
|
in htmx. More details and examples can be found in its <a href="https://htmx.org/attributes/hx-trigger/">documentation</a>.</p> |
|
<h3 id="_other_attributes">Other Attributes</h3> |
|
<p>Htmx offers many other less commonly used attributes for fine-tuning |
|
the behavior of your Hypermedia-Driven Application.</p> |
|
<p>Here are some of the most useful ones:</p> |
|
<dl> |
|
<dt>hx-push-url</dt> |
|
<dd> |
|
<p>“Pushes” the request URL (or some other value) into the navigation |
|
bar.</p> |
|
</dd> |
|
<dt>hx-preserve</dt> |
|
<dd> |
|
<p>Preserves a bit of the DOM between requests; the original content |
|
will be kept, regardless of what is returned.</p> |
|
</dd> |
|
<dt>hx-sync</dt> |
|
<dd> |
|
<p>Synchronized requests between two or more elements.</p> |
|
</dd> |
|
<dt>hx-disable</dt> |
|
<dd> |
|
<p>Disables htmx behavior on this element and any children. We will come |
|
back to this when we discuss the topic of security.</p> |
|
</dd> |
|
</dl> |
|
<p>Let’s take a look at <code>hx-sync</code>, which allows us to |
|
synchronize AJAX requests between two or more elements. Consider a |
|
simple case where we have two buttons that both target the same element |
|
on the screen:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb4"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb4-1"><a aria-hidden="true" href="#cb4-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="dt">></span></span> |
|
<span id="cb4-2"><a aria-hidden="true" href="#cb4-2" tabindex="-1"></a> Get Contacts</span> |
|
<span id="cb4-3"><a aria-hidden="true" href="#cb4-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb4-4"><a aria-hidden="true" href="#cb4-4" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/settings"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="dt">></span></span> |
|
<span id="cb4-5"><a aria-hidden="true" href="#cb4-5" tabindex="-1"></a> Get Settings</span> |
|
<span id="cb4-6"><a aria-hidden="true" href="#cb4-6" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Two competing buttons</p></figcaption> |
|
</figure> |
|
<p>This is fine and will work, but what if a user clicks the “Get |
|
Contacts” button and then the request takes a while to respond? And, in |
|
the meantime the user clicks the “Get Settings” button? In this case we |
|
would have two requests in flight at the same time.</p> |
|
<p>If the <code>/settings</code> request finished first and displayed |
|
the user’s setting information, they might be very surprised if they |
|
began making changes and then, suddenly, the <code>/contacts</code> |
|
request finished and replaced the entire body with the contacts |
|
instead!</p> |
|
<p>To deal with this situation, we might consider using an |
|
<code>hx-indicator</code> to alert the user that something is going on, |
|
making it less likely that they click the second button. But if we |
|
really want to guarantee that there is only one request at a time issued |
|
between these two buttons, the right thing to do is to use the |
|
<code>hx-sync</code> attribute. Let’s enclose both buttons in a |
|
<code>div</code> and eliminate the redundant <code>hx-target</code> |
|
specification by hoisting the attribute up to that <code>div</code>. We |
|
can then use <code>hx-sync</code> on that div to coordinate requests |
|
between the two buttons.</p> |
|
<p>Here is our updated code:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a> hx-sync="this"> <span class="er"><</span>2></span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="dt">></span></span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a> Get Contacts</span> |
|
<span id="cb5-5"><a aria-hidden="true" href="#cb5-5" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb5-6"><a aria-hidden="true" href="#cb5-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/settings"</span><span class="dt">></span></span> |
|
<span id="cb5-7"><a aria-hidden="true" href="#cb5-7" tabindex="-1"></a> Get Settings</span> |
|
<span id="cb5-8"><a aria-hidden="true" href="#cb5-8" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb5-9"><a aria-hidden="true" href="#cb5-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Syncing two buttons</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Hoist the duplicate <code>hx-target</code> attributes to the |
|
parent <code>div</code>.</p></li> |
|
<li><p>Synchronize on the parent <code>div</code>.</p></li> |
|
</ol> |
|
<p>By placing the <code>hx-sync</code> attribute on the <code>div</code> |
|
with the value <code>this</code>, we are saying “Synchronize all htmx |
|
requests that occur within this <code>div</code> element with one |
|
another.” This means that if one button already has a request in flight, |
|
other buttons within the <code>div</code> will not issue requests until |
|
that has finished.</p> |
|
<p>The <code>hx-sync</code> attribute supports a few different |
|
strategies that allow you to, for example, replace an existing request |
|
in flight, or queue requests with a particular queuing strategy. You can |
|
find complete documentation, as well as examples, at the htmx.org page |
|
for <a href="https://htmx.org/attributes/hx-sync/"><code>hx-sync</code></a>.</p> |
|
<p>As you can see, htmx offers a lot of attribute-driven functionality |
|
for more advanced Hypermedia-Driven Applications. A complete reference |
|
for all htmx attributes can be found <a href="https://htmx.org/reference/#attributes">on the htmx |
|
website</a>.</p> |
|
<h2 id="events">Events</h2> |
|
<p>Thus far we have worked with JavaScript events in htmx primarily via |
|
the <code>hx-trigger</code> attribute. This attribute has proven to be a |
|
powerful mechanism for driving our application using a declarative, |
|
HTML-friendly syntax.</p> |
|
<p>However, there is much more we can do with events. Events play a |
|
crucial role both in the extension of HTML as a hypermedia, and, as |
|
we’ll see, in hypermedia-friendly scripting. Events are the “glue” that |
|
brings the DOM, HTML, htmx and scripting together. You might think of |
|
the DOM as a sophisticated “event bus” for applications.</p> |
|
<p>We can’t emphasize enough: to build advanced Hypermedia-Driven |
|
Applications, it is worth the effort to learn about events <a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events">in |
|
depth</a>.</p> |
|
<h3 id="htmx-generated-events">Htmx-Generated Events</h3> |
|
<p>In addition to making it easy to <em class="test">respond</em> to events, htmx |
|
also <em class="test">emits</em> many useful events. You can use these events to add |
|
more functionality to your application, either via htmx itself, or by |
|
way of scripting.</p> |
|
<p>Here are some of the most commonly used events triggered by htmx:</p> |
|
<dl> |
|
<dt><code>htmx:load</code></dt> |
|
<dd> |
|
<p>Triggered when new content is loaded into the DOM by htmx.</p> |
|
</dd> |
|
<dt><code>htmx:configRequest</code></dt> |
|
<dd> |
|
<p>Triggered before a request is issued, allowing you to |
|
programmatically configure the request or cancel it entirely.</p> |
|
</dd> |
|
<dt><code>htmx:afterRequest</code></dt> |
|
<dd> |
|
<p>Triggered after a request has responded.</p> |
|
</dd> |
|
<dt><code>htmx:abort</code></dt> |
|
<dd> |
|
<p>A custom event that can be sent to an htmx-powered element to abort |
|
an open request.</p> |
|
</dd> |
|
</dl> |
|
<h3 id="using-the-htmx-configrequest-event">Using the htmx:configRequest Event</h3> |
|
<p>Let’s look at an example of how to work with htmx-emitted events. |
|
We’ll use the <code>htmx:configRequest</code> event to configure an HTTP |
|
request.</p> |
|
<p>Consider the following scenario: your server-side team has decided |
|
that they want you to include a server-generated token for extra |
|
security on every request. The token is going to be stored in |
|
<code>localStorage</code> in the browser, in the slot |
|
<code>special-token</code>.</p> |
|
<p>The token is being set via some JavaScript (don’t worry about the |
|
details yet) when the user first logs in:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb6"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb6-1"><a aria-hidden="true" href="#cb6-1" tabindex="-1"></a><span class="kw">let</span> response <span class="op">=</span> <span class="cf">await</span> <span class="fu">fetch</span>(<span class="st">"/token"</span>)<span class="op">;</span> <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb6-2"><a aria-hidden="true" href="#cb6-2" tabindex="-1"></a>localStorage[<span class="st">'special-token'</span>] <span class="op">=</span> <span class="cf">await</span> response<span class="op">.</span><span class="fu">text</span>()<span class="op">;</span></span></code></pre></div> |
|
<figcaption><p>Getting The Token in JavaScript</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Get the value of the token then set it into localStorage</p></li> |
|
</ol> |
|
<p>The server-side team wants you to include this special token on every |
|
request made by htmx, as the <code>X-SPECIAL-TOKEN</code> header. How |
|
could you achieve this? One way would be to catch the |
|
<code>htmx:configRequest</code> event and update the |
|
<code>detail.headers</code> object with this token from |
|
<code>localStorage</code>.</p> |
|
<p>In VanillaJS, it would look something like this, placed in a |
|
<code><script></code> tag in the <code><head></code> of our |
|
HTML document:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="bu">document</span><span class="op">.</span><span class="at">body</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">"htmx:configRequest"</span><span class="op">,</span> configEvent <span class="kw">=></span> {</span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a> configEvent<span class="op">.</span><span class="at">detail</span><span class="op">.</span><span class="at">headers</span>[<span class="st">'X-SPECIAL-TOKEN'</span>] <span class="op">=</span> <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a> localStorage[<span class="st">'special-token'</span>]<span class="op">;</span></span> |
|
<span id="cb7-4"><a aria-hidden="true" href="#cb7-4" tabindex="-1"></a>})</span></code></pre></div> |
|
<figcaption><p>Adding the <code>X-SPECIAL-TOKEN</code> |
|
header</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Retrieve the value from local storage and set it into a |
|
header.</p></li> |
|
</ol> |
|
<p>As you can see, we add a new value to the <code>headers</code> |
|
property of the event’s detail property. After the event handler |
|
executes, this <code>headers</code> property is read by htmx and used to |
|
construct the request headers for the AJAX request it makes.</p> |
|
<p>The <code>detail</code> property of the |
|
<code>htmx:configRequest</code> event contains a slew of useful |
|
properties that you can update to change the “shape” of the request, |
|
including:</p> |
|
<dl> |
|
<dt><code>detail.parameters</code></dt> |
|
<dd> |
|
<p>Allows you to add or remove request parameters</p> |
|
</dd> |
|
<dt><code>detail.target</code></dt> |
|
<dd> |
|
<p>Allows you to update the target of the request</p> |
|
</dd> |
|
<dt><code>detail.verb</code></dt> |
|
<dd> |
|
<p>Allows you to update HTTP “verb” of the request (e.g. |
|
<code>GET</code>)</p> |
|
</dd> |
|
</dl> |
|
<p>So, for example, if the server-side team decided they wanted the |
|
token included as a parameter, rather than as a request header, you |
|
could update your code to look like this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="bu">document</span><span class="op">.</span><span class="at">body</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">"htmx:configRequest"</span><span class="op">,</span> configEvent <span class="kw">=></span> {</span> |
|
<span id="cb8-2"><a aria-hidden="true" href="#cb8-2" tabindex="-1"></a> configEvent<span class="op">.</span><span class="at">detail</span><span class="op">.</span><span class="at">parameters</span>[<span class="st">'token'</span>] <span class="op">=</span> <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb8-3"><a aria-hidden="true" href="#cb8-3" tabindex="-1"></a> localStorage[<span class="st">'special-token'</span>]<span class="op">;</span></span> |
|
<span id="cb8-4"><a aria-hidden="true" href="#cb8-4" tabindex="-1"></a>})</span></code></pre></div> |
|
<figcaption><p>Adding the <code>token</code> parameter</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Retrieve the value from local storage and set it into a |
|
parameter.</p></li> |
|
</ol> |
|
<p>As you can see, this gives you a lot of flexibility in updating the |
|
AJAX request that htmx makes.</p> |
|
<p>The full documentation for the <code>htmx:configRequest</code> event |
|
(and other events you might be interested in) can be found <a href="https://htmx.org/events/#htmx:configRequest">on the htmx |
|
website</a>.</p> |
|
<h3 id="canceling-a-request-using-htmx-abort">Canceling a Request Using htmx:abort</h3> |
|
<p>We can listen for any of the many useful events from htmx, and we can |
|
respond to those events using <code>hx-trigger</code>. What else can we |
|
do with events?</p> |
|
<p>It turns out that htmx itself listens for one special event, |
|
<code>htmx:abort</code>. When htmx receives this event on an element |
|
that has a request in flight, it will abort the request.</p> |
|
<p>Consider a situation where we have a potentially long-running request |
|
to <code>/contacts</code>, and we want to offer a way for the users to |
|
cancel the request. What we want is a button that issues the request, |
|
driven by htmx, of course, and then another button that will send an |
|
<code>htmx:abort</code> event to the first one.</p> |
|
<p>Here is what the code might look like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> id</span><span class="op">=</span><span class="st">"contacts-btn"</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a> Get Contacts</span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb9-4"><a aria-hidden="true" href="#cb9-4" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span></span> |
|
<span id="cb9-5"><a aria-hidden="true" href="#cb9-5" tabindex="-1"></a><span class="ot"> onclick</span><span class="op">=</span><span class="st">"</span></span> |
|
<span id="cb9-6"><a aria-hidden="true" href="#cb9-6" tabindex="-1"></a><span class="st"> document.getElementById('contacts-btn')</span></span> |
|
<span id="cb9-7"><a aria-hidden="true" href="#cb9-7" tabindex="-1"></a><span class="st"> .dispatchEvent(new Event('htmx:abort')) </span><span class="er"><</span><span class="st">2></span></span> |
|
<span id="cb9-8"><a aria-hidden="true" href="#cb9-8" tabindex="-1"></a><span class="st"> "</span><span class="dt">></span></span> |
|
<span id="cb9-9"><a aria-hidden="true" href="#cb9-9" tabindex="-1"></a> Cancel</span> |
|
<span id="cb9-10"><a aria-hidden="true" href="#cb9-10" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A button with an abort</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A normal htmx-driven <code>GET</code> request to |
|
<code>/contacts</code></p></li> |
|
<li><p>JavaScript to look up the button and send it an |
|
<code>htmx:abort</code> event</p></li> |
|
</ol> |
|
<p>So now, if a user clicks on the “Get Contacts” button and the request |
|
takes a while, they can click on the “Cancel” button and end the |
|
request. Of course, in a more sophisticated user interface, you may want |
|
to disable the “Cancel” button unless an HTTP request is in flight, but |
|
that would be a pain to implement in pure JavaScript.</p> |
|
<p>Thankfully this isn’t too bad to implement in hyperscript, so let’s |
|
take a look at what that would look like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb10"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb10-1"><a aria-hidden="true" href="#cb10-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> id</span><span class="op">=</span><span class="st">"contacts-btn"</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="dt">></span></span> |
|
<span id="cb10-2"><a aria-hidden="true" href="#cb10-2" tabindex="-1"></a> Get Contacts</span> |
|
<span id="cb10-3"><a aria-hidden="true" href="#cb10-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb10-4"><a aria-hidden="true" href="#cb10-4" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span></span> |
|
<span id="cb10-5"><a aria-hidden="true" href="#cb10-5" tabindex="-1"></a><span class="ot"> _</span><span class="op">=</span><span class="st">"on click send htmx:abort to #contacts-btn</span></span> |
|
<span id="cb10-6"><a aria-hidden="true" href="#cb10-6" tabindex="-1"></a><span class="st"> on htmx:beforeRequest from #contacts-btn remove @disabled from me</span></span> |
|
<span id="cb10-7"><a aria-hidden="true" href="#cb10-7" tabindex="-1"></a><span class="st"> on htmx:afterRequest from #contacts-btn add @disabled to me"</span><span class="dt">></span></span> |
|
<span id="cb10-8"><a aria-hidden="true" href="#cb10-8" tabindex="-1"></a> Cancel</span> |
|
<span id="cb10-9"><a aria-hidden="true" href="#cb10-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A hyperscript-Powered Button With An |
|
Abort</p></figcaption> |
|
</figure> |
|
<p>Now we have a “Cancel” button that is enabled only when a request |
|
from the <code>contacts-btn</code> button is in flight. And we are |
|
taking advantage of htmx-generated and handled events, as well as the |
|
event-friendly syntax of hyperscript, to make it happen. Slick!</p> |
|
<h3 id="_server_generated_events">Server Generated Events</h3> |
|
<p>We are going to talk more about the various ways that htmx enhances |
|
regular HTTP requests and responses in the next section, but, since it |
|
involves events, we are going to discuss one HTTP Response header that |
|
htmx supports: <code>HX-Trigger</code>. We have discussed before how |
|
HTTP requests and responses support <em class="test">headers</em>, name-value pairs |
|
that contain metadata about a given request or response. We took |
|
advantage of the <code>HX-Trigger</code> request header, which includes |
|
the id of the element that triggered a given request.</p> |
|
<p>In addition to this <em class="test">request header</em>, htmx also supports a |
|
<em class="test">response header</em> also named <code>HX-Trigger</code>. This |
|
response header allows you to <em class="test">trigger an event</em> on the element |
|
that submitted an AJAX request. This turns out to be a powerful way to |
|
coordinate elements in the DOM in a decoupled manner.</p> |
|
<p>To see how this might work, let’s consider the following situation: |
|
we have a button that grabs new contacts from some remote system on the |
|
server. We will ignore the details of the server-side implementation, |
|
but we know that if we issue a <code>POST</code> to the |
|
<code>/sync</code> path, it will trigger a synchronization with the |
|
system.</p> |
|
<p>Now, this synchronization may or may not result in new contacts being |
|
created. In the case where new contacts <em class="test">are</em> created, we want to |
|
refresh our contacts table. In the case where no contacts are created, |
|
we don’t want to refresh the table.</p> |
|
<p>To implement this we could conditionally add an |
|
<code>HX-Trigger</code> response header with the value |
|
<code>contacts-updated</code>:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb11"><pre class="sourceCode py"><code class="sourceCode python"><span id="cb11-1"><a aria-hidden="true" href="#cb11-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">'/sync'</span>, methods<span class="op">=</span>[<span class="st">"POST"</span>])</span> |
|
<span id="cb11-2"><a aria-hidden="true" href="#cb11-2" tabindex="-1"></a><span class="kw">def</span> sync_with_server():</span> |
|
<span id="cb11-3"><a aria-hidden="true" href="#cb11-3" tabindex="-1"></a> contacts_updated <span class="op">=</span> RemoteServer.sync() <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb11-4"><a aria-hidden="true" href="#cb11-4" tabindex="-1"></a> resp <span class="op">=</span> make_response(render_template(<span class="st">'sync.html'</span>))</span> |
|
<span id="cb11-5"><a aria-hidden="true" href="#cb11-5" tabindex="-1"></a> <span class="cf">if</span> contacts_updated <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb11-6"><a aria-hidden="true" href="#cb11-6" tabindex="-1"></a> resp.headers[<span class="st">'HX-Trigger'</span>] <span class="op">=</span> <span class="st">'contacts-updated'</span></span> |
|
<span id="cb11-7"><a aria-hidden="true" href="#cb11-7" tabindex="-1"></a> <span class="cf">return</span> resp</span></code></pre></div> |
|
<figcaption><p>Conditionally Triggering a <code>contacts-updated</code> |
|
event</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>A call to the remote system that synchronized our contact |
|
database with it</p></li> |
|
<li><p>If any contacts were updated we conditionally trigger the |
|
<code>contacts-updated</code> event on the client</p></li> |
|
</ol> |
|
<p>This value would trigger the <code>contacts-updated</code> event on |
|
the button that made the AJAX request to <code>/sync</code>. We can then |
|
take advantage of the <code>from:</code> modifier of the |
|
<code>hx-trigger</code> attribute to listen for that event. With this |
|
pattern we can effectively trigger htmx requests from the server |
|
side.</p> |
|
<p>Here is what the client-side code might look like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb12"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb12-1"><a aria-hidden="true" href="#cb12-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-post</span><span class="op">=</span><span class="st">"/integrations/1"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb12-2"><a aria-hidden="true" href="#cb12-2" tabindex="-1"></a> Pull Contacts From Integration</span> |
|
<span id="cb12-3"><a aria-hidden="true" href="#cb12-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb12-4"><a aria-hidden="true" href="#cb12-4" tabindex="-1"></a></span> |
|
<span id="cb12-5"><a aria-hidden="true" href="#cb12-5" tabindex="-1"></a> ...</span> |
|
<span id="cb12-6"><a aria-hidden="true" href="#cb12-6" tabindex="-1"></a></span> |
|
<span id="cb12-7"><a aria-hidden="true" href="#cb12-7" tabindex="-1"></a><span class="dt"><</span><span class="kw">table</span><span class="ot"> hx-get</span><span class="op">=</span><span class="st">"/contacts/table"</span></span> |
|
<span id="cb12-8"><a aria-hidden="true" href="#cb12-8" tabindex="-1"></a><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"contacts-updated from:body"</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb12-9"><a aria-hidden="true" href="#cb12-9" tabindex="-1"></a> ...</span> |
|
<span id="cb12-10"><a aria-hidden="true" href="#cb12-10" tabindex="-1"></a><span class="dt"></</span><span class="kw">table</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The Contacts Table</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The response to this request may conditionally trigger the |
|
<code>contacts-updated</code> event</p></li> |
|
<li><p>This table listens for the event and refreshes when it |
|
occurs</p></li> |
|
</ol> |
|
<p>The table listens for the <code>contacts-updated</code> event, and it |
|
does so on the <code>body</code> element. It listens on the |
|
<code>body</code> element since the event will bubble up from the |
|
button, and this allows us to not couple the button and table together: |
|
we can move the button and table around as we like and, via events, the |
|
behavior we want will continue to work fine. Additionally, we may want |
|
<em class="test">other</em> elements or requests to trigger the |
|
<code>contacts-updated</code> event, so this provides a general |
|
mechanism for refreshing the contacts table in our application.</p> |
|
<h2 id="_http_requests_responses">HTTP Requests & Responses</h2> |
|
<p>We have just seen an advanced feature of HTTP responses supported by |
|
htmx, the <code>HX-Trigger</code> response header, but htmx supports |
|
quite a few more headers for both requests and responses. In Chapter 4 |
|
we discussed the headers present in HTTP Requests. Here are some of the |
|
more important headers you can use to change htmx behavior with HTTP |
|
responses:</p> |
|
<dl> |
|
<dt><code>HX-Location</code></dt> |
|
<dd> |
|
<p>Causes a client-side redirection to a new location</p> |
|
</dd> |
|
<dt><code>HX-Push-Url</code></dt> |
|
<dd> |
|
<p>Pushes a new URL into the location bar</p> |
|
</dd> |
|
<dt><code>HX-Refresh</code></dt> |
|
<dd> |
|
<p>Refreshes the current page</p> |
|
</dd> |
|
<dt><code>HX-Retarget</code></dt> |
|
<dd> |
|
<p>Allows you to specify a new target to swap the response content into |
|
on the client side</p> |
|
</dd> |
|
</dl> |
|
<p>You can find a reference for all requests and response headers in the |
|
<a href="https://htmx.org/reference/#headers">htmx |
|
documentation</a>.</p> |
|
<h3 id="http-response-codes">HTTP Response Codes</h3> |
|
<p>Even more important than response headers, in terms of information |
|
conveyed to the client, is the <em class="test">HTTP Response Code</em>. We discussed |
|
HTTP Response Codes in Chapter 3. By and large htmx handles various |
|
response codes in the manner that you would expect: it swaps content for |
|
all 200-level response codes and does nothing for others. There are, |
|
however, two “special” 200-level response codes:</p> |
|
<ul> |
|
<li><p><code>204 No Content</code> - When htmx receives this response |
|
code, it will <em class="test">not</em> swap any content into the DOM (even if the |
|
response has a body)</p></li> |
|
<li><p><code>286</code> - When htmx receives this response code to a |
|
request that is polling, it will stop the polling</p></li> |
|
</ul> |
|
<p>You can override the behavior of htmx with respect to response codes |
|
by, you guessed it, responding to an event! The |
|
<code>htmx:beforeSwap</code> event allows you to change the behavior of |
|
htmx with respect to various status codes.</p> |
|
<p>Let’s say that, rather than doing nothing when a <code>404</code> |
|
occurred, you wanted to alert the user that an error had occurred. To do |
|
so, you want to invoke a JavaScript method, |
|
<code>showNotFoundError()</code>. Let’s add some code to use the |
|
<code>htmx:beforeSwap</code> event to make this happen:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb13"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb13-1"><a aria-hidden="true" href="#cb13-1" tabindex="-1"></a><span class="bu">document</span><span class="op">.</span><span class="at">body</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">'htmx:beforeSwap'</span><span class="op">,</span> evt <span class="kw">=></span> { <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb13-2"><a aria-hidden="true" href="#cb13-2" tabindex="-1"></a> <span class="cf">if</span> (evt<span class="op">.</span><span class="at">detail</span><span class="op">.</span><span class="at">xhr</span><span class="op">.</span><span class="at">status</span> <span class="op">===</span> <span class="dv">404</span>) { <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb13-3"><a aria-hidden="true" href="#cb13-3" tabindex="-1"></a> <span class="fu">showNotFoundError</span>()<span class="op">;</span></span> |
|
<span id="cb13-4"><a aria-hidden="true" href="#cb13-4" tabindex="-1"></a> }</span> |
|
<span id="cb13-5"><a aria-hidden="true" href="#cb13-5" tabindex="-1"></a>})<span class="op">;</span></span></code></pre></div> |
|
<figcaption><p>Showing a 404 dialog</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Hook into the <code>htmx:beforeSwap</code> event.</p></li> |
|
<li><p>If the response code is a <code>404</code>, show the user a |
|
dialog.</p></li> |
|
</ol> |
|
<p>You can also use the <code>htmx:beforeSwap</code> event to configure |
|
if the response should be swapped into the DOM and what element the |
|
response should target. This gives you quite a bit of flexibility in |
|
choosing how you want to use HTTP Response codes in your application. |
|
Full documentation on the <code>htmx:beforeSwap</code> event can be |
|
found at <a href="https://htmx.org/events/#htmx:beforeSwap">htmx.org</a>.</p> |
|
<h2 id="_updating_other_content">Updating Other Content</h2> |
|
<p>Above we saw how to use a server-triggered event, via the |
|
<code>HX-Trigger</code> HTTP response header, to update a piece of the |
|
DOM based on the response to another part of the DOM. This technique |
|
addresses the general problem that comes up in Hypermedia-Driven |
|
Applications: “How do I update other content?” After all, in normal HTTP |
|
requests, there is only one “target”, the entire screen, and, similarly, |
|
in htmx-based requests, there is only one target: either the explicit or |
|
implicit target of the element.</p> |
|
<p>If you want to update other content in htmx, you have a few |
|
options:</p> |
|
<h3 id="_expanding_your_selection">Expanding Your Selection</h3> |
|
<p>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 be 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 layout. Regardless, we |
|
always recommend at least thinking about this approach first.</p> |
|
<h3 id="out-of-band-swaps">Out of Band Swaps</h3> |
|
<p>A second option, a bit more complex, is to take advantage of “Out Of |
|
Band” content support in htmx. When htmx receives a response, it will |
|
inspect it for top-level content that includes the |
|
<code>hx-swap-oob</code> attribute. That content will be removed from |
|
the response, so it will not be swapped into the DOM in the normal |
|
manner. Instead, it will be swapped in for the content that it matches |
|
by id.</p> |
|
<p>Let’s look at an example. Consider the situation we had earlier, |
|
where a contacts table needs to be updated if an integration pulls down |
|
any new contacts. Previously we solved this by using events and a |
|
server-triggered event via the <code>HX-Trigger</code> response |
|
header.</p> |
|
<p>This time, we’ll use the <code>hx-swap-oob</code> attribute in the |
|
response to the <code>POST</code> to <code>/integrations/1</code>. The |
|
new contacts table content will “piggyback” on the response.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb14"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb14-1"><a aria-hidden="true" href="#cb14-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> hx-post</span><span class="op">=</span><span class="st">"/integrations/1"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb14-2"><a aria-hidden="true" href="#cb14-2" tabindex="-1"></a> Pull Contacts From Integration</span> |
|
<span id="cb14-3"><a aria-hidden="true" href="#cb14-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb14-4"><a aria-hidden="true" href="#cb14-4" tabindex="-1"></a></span> |
|
<span id="cb14-5"><a aria-hidden="true" href="#cb14-5" tabindex="-1"></a> ...</span> |
|
<span id="cb14-6"><a aria-hidden="true" href="#cb14-6" tabindex="-1"></a></span> |
|
<span id="cb14-7"><a aria-hidden="true" href="#cb14-7" tabindex="-1"></a><span class="dt"><</span><span class="kw">table</span><span class="ot"> id</span><span class="op">=</span><span class="st">"contacts-table"</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb14-8"><a aria-hidden="true" href="#cb14-8" tabindex="-1"></a> ...</span> |
|
<span id="cb14-9"><a aria-hidden="true" href="#cb14-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">table</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>The updated contacts table</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The button still issues a <code>POST</code> to |
|
<code>/integrations/1</code>.</p></li> |
|
<li><p>The table no longer listens for an event, but it now has an |
|
id.</p></li> |
|
</ol> |
|
<p>Next, the response to the <code>POST</code> to |
|
<code>/integrations/1</code> will include the content that needs to be |
|
swapped into the button, per the usual htmx mechanism. But it will also |
|
include a new, updated version of the contacts table, which will be |
|
marked as <code>hx-swap-oob="true"</code>. This content will be removed |
|
from the response so that it is not inserted into the button. Instead, |
|
it is swapped into the DOM in place of the existing table since it has a |
|
matching id.</p> |
|
<figure> |
|
<pre><code>HTTP/1.1 200 OK |
|
Content-Type: text/html; charset=utf-8 |
|
... |
|
|
|
Pull Contacts From Integration <1> |
|
|
|
<table id="contacts-table" hx-swap-oob="true"> <2> |
|
... |
|
</table> |
|
</code></pre> |
|
<figcaption><p>A response with out-of-band content</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This content will be placed in the button.</p></li> |
|
<li><p>This content will be removed from the response and swapped by |
|
id.</p></li> |
|
</ol> |
|
<p>Using this piggybacking technique, you can update content wherever |
|
needed on a page. The <code>hx-swap-oob</code> attribute supports other |
|
additional features, all of which are <a href="https://htmx.org/attributes/hx-swap-oob/">documented</a>.</p> |
|
<p>Depending on how exactly your server-side templating technology |
|
works, and what level of interactivity your application requires, out of |
|
band swapping can be a powerful mechanism for content updates.</p> |
|
<h3 id="events-1">Events</h3> |
|
<p>Finally, the most complex mechanism for updating content is the one |
|
we saw back in the events section: using server-triggered events to |
|
update elements. This approach can be very clean, but also requires a |
|
deeper conceptual knowledge of HTML and events, and a commitment to the |
|
event-driven approach. While we like this style of development, it isn’t |
|
for everyone. We typically recommend this pattern only if the htmx |
|
philosophy of event-driven hypermedia really speaks to you.</p> |
|
<p>If it <em class="test">does</em> speak to you, however, we say: go for it. We’ve |
|
created some very complex and flexible user interfaces using this |
|
approach, and we are quite fond of it.</p> |
|
<h3 id="being-pragmatic">Being Pragmatic</h3> |
|
<p>All of these approaches to the “Updating Other Content” problem will |
|
work, and will often work well. However, there may come a point where it |
|
would just be simpler to use a different approach for your UI, like the |
|
reactive one. As much as we like the hypermedia approach, the reality is |
|
that there are some UX patterns that simply cannot be implemented easily |
|
using it. The canonical example of this sort of pattern, which we have |
|
mentioned before, is something like a live online spreadsheet: it is |
|
simply too complex a user interface, with too many interdependencies, to |
|
be done well via exchanges of hypermedia with a server.</p> |
|
<p>In cases like this, and any time you feel like an htmx-based solution |
|
is proving to be more complex than another approach might be, we |
|
recommend that you consider a different technology. Be pragmatic, and |
|
use the right tool for the job. You can always use htmx for the parts of |
|
your application that aren’t as complex and don’t need the full |
|
complexity of a reactive framework, and save that complexity budget for |
|
the parts that do.</p> |
|
<p>We encourage you to learn many different web technologies, with an |
|
eye to the strengths and weaknesses of each one. This will give you a |
|
deep tool chest to reach into when problems present themselves. Our |
|
experience is that, with htmx, hypermedia is a tool you can reach for |
|
frequently.</p> |
|
<h2 id="debugging">Debugging</h2> |
|
<p>We are not ashamed to admit: we are big fans of events. They are the |
|
underlying technology of almost any interesting user interface, and are |
|
particularly useful in the DOM once they have been unlocked for general |
|
use in HTML. They let you build nicely decoupled software while often |
|
preserving the locality of behavior we like so much.</p> |
|
<p>However, events are not perfect. One area where events can be |
|
particularly tricky to deal with is <em class="test">debugging</em>: you often want |
|
to know why an event <em class="test">isn’t</em> happening. But where can you set a |
|
break point for something that <em class="test">isn’t</em> happening? The answer, as |
|
of right now, is: you can’t.</p> |
|
<p>There are two techniques that can help in this regard, one provided |
|
by htmx, the other provided by Chrome, the browser by Google.</p> |
|
<h3 id="_logging_htmx_events">Logging Htmx Events</h3> |
|
<p>The first technique, provided by htmx itself, is to call the |
|
<code>htmx.logAll()</code> method. When you do this, htmx will log all |
|
the internal events that occur as it goes about its business, loading up |
|
content, responding to events and so forth.</p> |
|
<p>This can be overwhelming, but with judicious filtering can help you |
|
zero in on a problem. Here are what (a bit of) the logs look like when |
|
clicking on the “docs” link on <a href="https://htmx.org">https://htmx.org</a>, with <code>logAll()</code> |
|
enabled:</p> |
|
<figure> |
|
<pre class="text"><code>htmx:configRequest |
|
<a href="/docs/"> |
|
Object { parameters: {}, unfilteredParameters: {}, headers: {…}, target: body, verb: "get", errors: [], withCredentials: false, timeout: 0, path: "/docs/", triggeringEvent: a |
|
, … } |
|
htmx.js:439:29 |
|
htmx:beforeRequest |
|
<a href="/docs/"> |
|
Object { xhr: XMLHttpRequest, target: body, requestConfig: {…}, etc: {}, pathInfo: {…}, elt: a |
|
} |
|
htmx.js:439:29 |
|
htmx:beforeSend |
|
<a class="htmx-request" href="/docs/"> |
|
Object { xhr: XMLHttpRequest, target: body, requestConfig: {…}, etc: {}, pathInfo: {…}, elt: a.htmx-request |
|
} |
|
htmx.js:439:29 |
|
htmx:xhr:loadstart |
|
<a class="htmx-request" href="/docs/"> |
|
Object { lengthComputable: false, loaded: 0, total: 0, elt: a.htmx-request |
|
} |
|
htmx.js:439:29 |
|
htmx:xhr:progress |
|
<a class="htmx-request" href="/docs/"> |
|
Object { lengthComputable: true, loaded: 4096, total: 19915, elt: a.htmx-request |
|
} |
|
htmx.js:439:29 |
|
htmx:xhr:progress |
|
<a class="htmx-request" href="/docs/"> |
|
Object { lengthComputable: true, loaded: 19915, total: 19915, elt: a.htmx-request |
|
} |
|
htmx.js:439:29 |
|
htmx:beforeOnLoad |
|
<a class="htmx-request" href="/docs/"> |
|
Object { xhr: XMLHttpRequest, target: body, requestConfig: {…}, etc: {}, pathInfo: {…}, elt: a.htmx-request |
|
} |
|
htmx.js:439:29 |
|
htmx:beforeSwap |
|
<body hx-ext="class-tools, preload"> |
|
</code></pre> |
|
<figcaption><p>Htmx logs</p></figcaption> |
|
</figure> |
|
<p>Not exactly easy on the eyes, is it?</p> |
|
<p>But, if you take a deep breath and squint, you can see that it isn’t |
|
<em class="test">that</em> bad: a series of htmx events, some of which we have seen |
|
before (there’s <code>htmx:configRequest</code>!), get logged to the |
|
console, along with the element they are triggered on.</p> |
|
<p>After a bit of reading and filtering, you will be able to make sense |
|
of the event stream, and it can help you debug htmx-related issues.</p> |
|
<h3 id="_monitoring_events_in_chrome">Monitoring Events in Chrome</h3> |
|
<p>The preceding technique is useful if the problem is occurring |
|
somewhere <em class="test">within</em> htmx, but what if htmx is never getting |
|
triggered at all? This comes up some times, like when, for example, you |
|
have accidentally typed an event name incorrectly somewhere.</p> |
|
<p>In cases like this you will need recourse to a tool available in the |
|
browser itself. Fortunately, the Chrome browser by Google provides a |
|
very useful function, <code>monitorEvents()</code>, that allows you to |
|
monitor <em class="test">all</em> events that are triggered on an element.</p> |
|
<p>This feature is available <em class="test">only</em> in the console, so you can’t |
|
use it in code on your page. But, if you are working with htmx in |
|
Chrome, and are curious why an event isn’t triggering on an element, you |
|
can open the developers console and type the following:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb17"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb17-1"><a aria-hidden="true" href="#cb17-1" tabindex="-1"></a><span class="fu">monitorEvents</span>(<span class="bu">document</span><span class="op">.</span><span class="fu">getElementById</span>(<span class="st">"some-element"</span>))<span class="op">;</span></span></code></pre></div> |
|
<figcaption><p>Htmx logs</p></figcaption> |
|
</figure> |
|
<p>This will then print <em class="test">all</em> the events that are triggered on |
|
the element with the id <code>some-element</code> to the console. This |
|
can be very useful for understanding exactly which events you want to |
|
respond to with htmx, or troubleshooting why an expected event isn’t |
|
occurring.</p> |
|
<p>Using these two techniques will help you as you (infrequently, we |
|
hope) troubleshoot event-related issues when developing with htmx.</p> |
|
<h2 id="security-considerations">Security Considerations</h2> |
|
<p>In general, htmx and hypermedia tends to be more secure than |
|
JavaScript heavy approaches to building web applications. This is |
|
because, by moving much of the processing to the back end, the |
|
hypermedia approach tends not to expose as much surface area of your |
|
system to end users for manipulation and shenanigans.</p> |
|
<p>However, even with hypermedia, there are still situations that |
|
require care when doing development. Of particular concern are |
|
situations where user-generated content is shown to other users: a |
|
clever user might try to insert htmx code that tricks the other users |
|
into clicking on content that triggers actions they don’t want to |
|
take.</p> |
|
<p>In general, all user-generated content should be escaped on the |
|
server-side, and most server-side rendering frameworks provide |
|
functionality for handling this situation. But there is always a risk |
|
that something slips through the cracks.</p> |
|
<p>In order to help you sleep better at night, htmx provides the |
|
<code>hx-disable</code> attribute. When this attribute is placed on an |
|
element, all htmx attributes within that element will be ignored.</p> |
|
<h3 id="content-security-policies---htmx">Content Security Policies & Htmx</h3> |
|
<p>A Content Security Policy (CSP) is a browser technology that allows |
|
you to detect and prevent certain types of content injection-based |
|
attacks. A full discussion of CSPs is beyond the scope of this book, but |
|
we refer you to the <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">Mozilla |
|
Developer Network article</a> on the topic for more information.</p> |
|
<p>A common feature to disable using a CSP is the <code>eval()</code> |
|
feature of JavaScript, which allows you to evaluate arbitrary JavaScript |
|
code from a string. This has proven to be a security issue and many |
|
teams have decided that it is not worth the risk to keep it enabled in |
|
their web applications.</p> |
|
<p>Htmx does not make heavy use of <code>eval()</code> and, thus, a CSP |
|
with this restriction in place will be fine. The one feature that does |
|
rely on <code>eval()</code> is event filters, discussed above. If you |
|
decide to disable <code>eval()</code> for your web application, you will |
|
not be able to use the event filtering syntax.</p> |
|
<h2 id="configuring">Configuring</h2> |
|
<p>There are a large number of configuration options available for htmx. |
|
Some examples of things you can configure are:</p> |
|
<ul> |
|
<li><p>The default swap style</p></li> |
|
<li><p>The default swap delay</p></li> |
|
<li><p>The default timeout of AJAX requests</p></li> |
|
</ul> |
|
<p>A full list of configuration options can be found in the config |
|
section of the <a href="https://htmx.org/docs/#config">main htmx |
|
documentation</a>.</p> |
|
<p>Htmx is typically configured via a <code>meta</code> tag, found in |
|
the header of a page. The name of the meta tag should be |
|
<code>htmx-config</code>, and the content attribute should contain the |
|
configuration overrides, formatted as JSON. Here is an example:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb18"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb18-1"><a aria-hidden="true" href="#cb18-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">meta</span><span class="ot"> name</span><span class="op">=</span><span class="st">"htmx-config"</span><span class="ot"> content</span><span class="op">=</span><span class="st">'{"defaultSwapStyle":"outerHTML"}'</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>An htmx configuration via <code>meta</code> |
|
tag</p></figcaption> |
|
</figure> |
|
<p>In this case, we are overriding the default swap style from the usual |
|
<code>innerHTML</code> to <code>outerHTML</code>. This might be useful |
|
if you find yourself using <code>outerHTML</code> more frequently than |
|
<code>innerHTML</code> and want to avoid having to explicitly set that |
|
swap value throughout your application.</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: Semantic HTML</h2> |
|
<p>Telling people to “use semantic HTML” instead of “read the spec” has |
|
led to a lot of people guessing at the meaning of tags — “looks pretty |
|
semantic to me!” — instead of engaging with the spec.</p> |
|
<blockquote> |
|
<p>I think being asked to write <em class="test">meaningful</em> HTML better lights |
|
the path to realizing that it isn’t about what the text means to |
|
humans—it’s about using tags for the purpose outlined in the specs to |
|
meet the needs of software like browsers, assistive technologies, and |
|
search engines.</p> |
|
</blockquote><p class="quote-attribution"> <a href="https://t-ravis.com/post/doc/semantic_the_8_letter_s-word/">https://t-ravis.com/post/doc/semantic_the_8_letter_s-word/</a></p> |
|
<p>We recommend talking about, and writing, <em class="test">conformant</em> HTML. |
|
(We can always bikeshed further). Use the elements to the full extent |
|
provided by the HTML specification, and let the software take from it |
|
whatever meaning they can.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Client Side Scripting</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_is_scripting_allowed">Is Scripting Allowed?</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#scripting-for-hypermedia">Scripting for Hypermedia</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_scripting_tools_for_the_web">Scripting Tools for the Web</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#vanilla-javascript">Vanilla JavaScript</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_a_simple_counter">A Simple Counter</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_an_inline_implementation">An inline implementation</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_separating_our_scripting_out">Separating our scripting out</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#locality-of-behavior">Locality of Behavior</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#what-to-do-with-our-counter-">What to do with our counter?</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#rsjs">RSJS</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_vanillajs_in_action_an_overflow_menu">VanillaJS in Action: An |
|
Overflow Menu</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#alpine-js">Alpine.js</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#-x-on-click--vs---onclick-">“x-on:click” vs. “onclick”</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#reactivity-and-templating">Reactivity and Templating</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_alpine_js_in_action_a_bulk_action_toolbar">Alpine.js in Action: |
|
A Bulk Action Toolbar</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_implementing_actions">Implementing actions</a> |
|
</li></ul> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#-hyperscript">_hyperscript</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_hyperscript_in_action_a_keyboard_shortcut">_hyperscript in |
|
Action: A Keyboard Shortcut</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_why_a_new_programming_language">Why a New Programming |
|
Language?</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_using_off_the_shelf_components">Using Off-the-Shelf |
|
Components</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_integration_options">Integration Options</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_integrating_using_callbacks">Integrating using callbacks</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_integrating_using_events">Integrating using events</a> |
|
</li></ul> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#_pragmatic_scripting">Pragmatic Scripting</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/client-side-scripting/#html-note-title">HTML Notes: HTML is for Applications</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<blockquote> |
|
<p>REST allows client functionality to be extended by downloading and |
|
executing code in the form of applets or scripts. This simplifies |
|
clients by reducing the number of features required to be |
|
pre-implemented.</p> |
|
</blockquote><p class="quote-attribution"> Roy Fielding, Architectural Styles and the Design of Network-based |
|
Software Architectures</p> |
|
<p>Thus far we have (mostly) avoided writing any JavaScript (or |
|
_hyperscript) in Contact.app, mainly because the functionality we |
|
implemented has not required it. In this chapter we are going to look at |
|
scripting and, in particular, hypermedia-friendly scripting within the |
|
context of a Hypermedia-Driven Application.</p> |
|
<h2 id="_is_scripting_allowed">Is Scripting Allowed?</h2> |
|
<p>A common criticism of the web is that it’s being misused. There is a |
|
narrative that WWW was created as a delivery system for “documents”, and |
|
only came to be used for “applications” by way of an accident or bizarre |
|
circumstances.</p> |
|
<p>However, the concept of hypermedia challenges the split of document |
|
and application. Hypermedia systems like HyperCard, which preceded the |
|
web, featured rich capabilities for active and interactive experiences, |
|
including scripting.</p> |
|
<p>HTML, as specified and implemented, does lack affordances needed to |
|
build highly interactive applications. This doesn’t mean, however, that |
|
hypermedia’s <em class="test">purpose</em> is “documents” over “applications.”</p> |
|
<p>Rather, while the theoretical foundation is there, the implementation |
|
is underdeveloped. With JavaScript being the only extension point and |
|
hypermedia controls not being well integrated to JavaScript (why can’t |
|
one click a link without halting the program?), developers have not |
|
internalized hypermedia and have instead used the web as a dumb pipe for |
|
apps that imitate “native” ones.</p> |
|
<p>A goal of this book is to show that it is possible to build |
|
sophisticated web applications using the original technology of the web, |
|
hypermedia, without the application developer needing to reach for the |
|
abstractions provided by the large, popular JavaScript frameworks.</p> |
|
<p>Htmx itself is, of course, written in JavaScript, and one of its |
|
advantages is that hypermedia interactions that go through htmx expose a |
|
rich interface to JavaScript code with configuration, events, and htmx’s |
|
own extension support.</p> |
|
<p>Htmx expands the expressiveness of HTML enough that it removes the |
|
need for scripting in many situations. This makes htmx attractive to |
|
people who don’t want to write JavaScript, and there are many of those |
|
sorts of developers, wary of the complexity of Single Page Application |
|
frameworks.</p> |
|
<p>However, dunking on JavaScript is not the aim of the htmx project. |
|
The goal of htmx is not less JavaScript, but less code, more readable |
|
and hypermedia-friendly code.</p> |
|
<p>Scripting has been a massive force multiplier for the web. Using |
|
scripting, web application developers are not only able to enhance their |
|
HTML websites, but also create full-fledged client-side applications |
|
that can often compete with native, thick client applications.</p> |
|
<p>This JavaScript-centric approach to building web applications is a |
|
testament to the power of the web and to the sophistication of web |
|
browsers in particular. It has its place in web development: there are |
|
situations where the hypermedia approach simply can’t provide the level |
|
of interaction that an SPA can.</p> |
|
<p>However, in addition to this more JavaScript-centric style, we want |
|
to develop a style of scripting more compatible and consistent with |
|
Hypermedia-Driven Applications.</p> |
|
<h2 id="scripting-for-hypermedia">Scripting for Hypermedia</h2> |
|
<p>Borrowing from Roy Fielding’s notion of “constraints” defining REST, |
|
we offer two constraints of hypermedia-friendly scripting. You are |
|
scripting in an HDA-compatible manner if the following two constraints |
|
are adhered to:</p> |
|
<ul> |
|
<li><p>The main data format exchanged between server and client must be |
|
hypermedia, the same as it would be without scripting.</p></li> |
|
<li><p>Client-side state, outside the DOM itself, is kept to a |
|
minimum.</p></li> |
|
</ul> |
|
<p>The goal of these constraints is to confine scripting to where it |
|
shines best and where nothing else comes close: <em class="test">interaction |
|
design</em>. Business logic and presentation logic are the |
|
responsibility of the server, where we can pick whichever languages or |
|
tools are appropriate for our business domain.</p> |
|
<div> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>The Server</strong></p> |
|
</div> |
|
<div> |
|
<p>Keeping business logic and presentation logic both “on the server” |
|
does not mean these two “concerns” are mixed or coupled. They can be |
|
modularized on the server. In fact, they <em class="test">should</em> be modularized |
|
on the server, along with all the other concerns of our application.</p> |
|
<p>Note also that, especially in web development parlance, the humble |
|
“server” is usually a whole fleet of racks, virtual machines, containers |
|
and more. Even a worldwide network of datacenters is reduced to “the |
|
server” when discussing the server-side of a Hypermedia-Driven |
|
Application.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<p>Satisfying these two constraints sometimes requires us to diverge |
|
from what is typically considered best practice for JavaScript. Keep in |
|
mind that the cultural wisdom of JavaScript was largely developed in |
|
JavaScript-centric SPA applications.</p> |
|
<p>The Hypermedia-Driven Application cannot as comfortably fall back on |
|
this tradition. This chapter is our contribution to the development of a |
|
new style and best practices for what we are calling Hypermedia-Driven |
|
Applications.</p> |
|
<p>Unfortunately, simply listing “best practices” is rarely convincing |
|
or edifying. To be honest, it’s boring.</p> |
|
<p>Instead, we will demonstrate these best practices by implementing |
|
client-side features in Contact.app. To cover different aspects of |
|
hypermedia-friendly scripting, we will implement three different |
|
features:</p> |
|
<ul> |
|
<li><p>An overflow menu to hold the <em class="test">Edit</em>, <em class="test">View</em> and |
|
<em class="test">Delete</em> actions, to clean up visual clutter in our list of |
|
contacts.</p></li> |
|
<li><p>An improved interface for bulk deletion.</p></li> |
|
<li><p>A keyboard shortcut for focusing the search box.</p></li> |
|
</ul> |
|
<p>The important takeaway in the implementation of each of these |
|
features is that, while they are implemented entirely on the client-side |
|
using scripting, they <em class="test">don’t exchange information with the |
|
server</em> via a non-hypermedia format, such as JSON, and that they |
|
don’t store a significant amount of state outside of the DOM itself.</p> |
|
<h2 id="_scripting_tools_for_the_web">Scripting Tools for the Web</h2> |
|
<p>The primary scripting language for the web is, of course, JavaScript, |
|
which is ubiquitous in web development today.</p> |
|
<p>A bit of interesting internet lore, however, is that JavaScript was |
|
not always the only built-in option. As the quote from Roy Fielding at |
|
the start of this chapter hints, “applets” written in other languages |
|
such as Java were considered to be part of the scripting infrastructure |
|
of the web. In addition, there was a time period when Internet Explorer |
|
supported VBScript, a scripting language based on Visual Basic.</p> |
|
<p>Today, we have a variety of <em class="test">transcompilers</em> (often shortened |
|
to <em class="test">transpilers</em>) that convert many languages to JavaScript, such |
|
as TypeScript, Dart, Kotlin, ClojureScript, F# and more. There is also |
|
the WebAssembly (WASM) bytecode format, which is supported as a |
|
compilation target for C, Rust, and the WASM-first language |
|
AssemblyScript.</p> |
|
<p>However, most of these options are not geared towards a |
|
hypermedia-friendly style of scripting. Compile-to-JS languages are |
|
often paired with SPA-oriented libraries (Dart and AngularDart, |
|
ClojureScript and Reagent, F# and Elm), and WASM is currently mainly |
|
geared toward linking to C/C++ libraries from JavaScript.</p> |
|
<p>We will instead focus on three client-side scripting technologies |
|
that <em class="test">are</em> hypermedia-friendly:</p> |
|
<ul> |
|
<li><p>VanillaJS, that is, using JavaScript without depending on any |
|
framework.</p></li> |
|
<li><p>Alpine.js, a JavaScript library for adding behavior directly in |
|
HTML.</p></li> |
|
<li><p>_hyperscript, a non-JavaScript scripting language created |
|
alongside htmx. Like AlpineJS, _hyperscript is usually embedded in |
|
HTML.</p></li> |
|
</ul> |
|
<p>Let’s take a quick look at each of these scripting options, so we |
|
know what we are dealing with.</p> |
|
<p>Note that, as with CSS, we are going to show you just enough of each |
|
of these options to give a flavor of how they work and, we hope, spark |
|
your interest in looking into any of them more extensively.</p> |
|
<h2 id="vanilla-javascript">Vanilla JavaScript</h2> |
|
<blockquote> |
|
<p>No code is faster than no code.</p> |
|
</blockquote><p class="quote-attribution"> Merb (Ruby web framework), motto</p> |
|
<p>Vanilla JavaScript is simply using plain JavaScript in your |
|
application, without any intermediate layers. The term “Vanilla” entered |
|
frontend web dev parlance as it became assumed that any sufficiently |
|
“advanced” web app would use some library with a name ending in “.js”. |
|
As JavaScript matured as a scripting language, however, standardized |
|
across browsers and provided more and more functionality, these |
|
frameworks and libraries became less important.</p> |
|
<p>Somewhat ironically though, as JavaScript became more powerful and |
|
removed the need for the first generation of JavaScript libraries such |
|
as jQuery, it also enabled people to build complex SPA libraries. These |
|
SPA libraries are often even more elaborate than the original first |
|
generation of JavaScript libraries.</p> |
|
<p>A quote from the website <a href="http://vanilla-js.com">http://vanilla-js.com</a>, which is well |
|
worth visiting even though it’s slightly out of date, captures the |
|
situation well:</p> |
|
<blockquote> |
|
<p>VanillaJS is the lowest-overhead, most comprehensive framework I’ve |
|
ever used.</p> |
|
</blockquote><p class="quote-attribution"> http://vanilla-js.com</p> |
|
<p>With JavaScript having matured as a scripting language, this is |
|
certainly the case for many applications. It is especially true in the |
|
case of HDAs, since, by using hypermedia, your application will not need |
|
many of the features typically provided by more elaborate Single Page |
|
Application JavaScript frameworks:</p> |
|
<ul> |
|
<li><p>Client-side routing</p></li> |
|
<li><p>An abstraction over DOM manipulation (i.e., templates that |
|
automatically update when referenced variables change)</p></li> |
|
<li><p>Server side rendering <a class="footnote-ref" href="#fn1" id="fnref1" role="doc-noteref"><sup>1</sup></a></p></li> |
|
<li><p>Attaching dynamic behavior to server-rendered tags on load (i.e., |
|
“hydration”)</p></li> |
|
<li><p>Network requests</p></li> |
|
</ul> |
|
<p>Without all this complexity being handled in JavaScript, your |
|
framework needs are dramatically reduced.</p> |
|
<p>One of the best things about VanillaJS is how you install it: you |
|
don’t have to!</p> |
|
<p>You can just start writing JavaScript in your web application, and it |
|
will simply work.</p> |
|
<p>That’s the good news. The bad news is that, despite improvements over |
|
the last decade, JavaScript has some significant limitations as a |
|
scripting language that can make it less than ideal as a stand-alone |
|
scripting technology for Hypermedia-Driven Applications:</p> |
|
<ul> |
|
<li><p>Being as established as it is, it has accreted a lot of features |
|
and warts.</p></li> |
|
<li><p>It has a complicated and confusing set of features for working |
|
with asynchronous code.</p></li> |
|
<li><p>Working with events is surprisingly difficult.</p></li> |
|
<li><p>DOM APIs (a large portion of which were originally designed for |
|
Java, yes <em class="test">Java</em>) are verbose and don’t have a habit of making |
|
common functionality easy to use.</p></li> |
|
</ul> |
|
<p>None of these limitations are deal-breakers, of course. Many of them |
|
are gradually being fixed and many people prefer the “close to the |
|
metal” (for lack of a better term) nature of vanilla JavaScript over |
|
more elaborate client-side scripting approaches.</p> |
|
<h3 id="_a_simple_counter">A Simple Counter</h3> |
|
<p>To dive into vanilla JavaScript as a front end scripting option, |
|
let’s create a simple counter widget.</p> |
|
<p>Counter widgets are a common “Hello World” example for JavaScript |
|
frameworks, so looking at how it can be done in vanilla JavaScript (as |
|
well as the other options we are going to look at) will be |
|
instructive.</p> |
|
<p>Our counter widget will be very simple: it will have a number, shown |
|
as text, and a button that increments the number.</p> |
|
<p>One problem with tackling this problem in vanilla JavaScript is that |
|
it lacks one thing that most JavaScript frameworks provide: a default |
|
code and architectural style.</p> |
|
<p>With vanilla JavaScript, there are no rules!</p> |
|
<p>This isn’t all bad. It presents a great opportunity to take a small |
|
journey through various styles that people have developed for writing |
|
their JavaScript.</p> |
|
<h4 id="_an_inline_implementation">An inline implementation</h4> |
|
<p>To begin, let’s start with the simplest thing imaginable: all of our |
|
JavaScript will be written inline, directly in the HTML. When the button |
|
is clicked, we will look up the <code>output</code> element holding the |
|
number, and increment the number contained within it.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb1"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb1-1"><a aria-hidden="true" href="#cb1-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">section</span><span class="ot"> class</span><span class="op">=</span><span class="st">"counter"</span><span class="dt">></span></span> |
|
<span id="cb1-2"><a aria-hidden="true" href="#cb1-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">output</span><span class="ot"> id</span><span class="op">=</span><span class="st">"my-output"</span><span class="dt">></span>0<span class="dt"></</span><span class="kw">output</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb1-3"><a aria-hidden="true" href="#cb1-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span></span> |
|
<span id="cb1-4"><a aria-hidden="true" href="#cb1-4" tabindex="-1"></a><span class="ot"> onclick</span><span class="op">=</span><span class="st">" </span><span class="er"><</span><span class="st">2></span></span> |
|
<span id="cb1-5"><a aria-hidden="true" href="#cb1-5" tabindex="-1"></a><span class="st"> document.querySelector('#my-output') </span><span class="er"><</span><span class="st">3></span></span> |
|
<span id="cb1-6"><a aria-hidden="true" href="#cb1-6" tabindex="-1"></a><span class="st"> .textContent++ </span><span class="er"><</span><span class="st">4></span></span> |
|
<span id="cb1-7"><a aria-hidden="true" href="#cb1-7" tabindex="-1"></a><span class="st"> "</span></span> |
|
<span id="cb1-8"><a aria-hidden="true" href="#cb1-8" tabindex="-1"></a><span class="ot"> </span><span class="dt">></span>Increment<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb1-9"><a aria-hidden="true" href="#cb1-9" tabindex="-1"></a><span class="dt"></</span><span class="kw">section</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Counter in vanilla JavaScript, inline |
|
version</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Our output element has an ID to help us find it.</p></li> |
|
<li><p>We use the <code>onclick</code> attribute to add an event |
|
listener.</p></li> |
|
<li><p>Find the output via a querySelector() call.</p></li> |
|
<li><p>JavaScript allows us use the <code>++</code> operator on |
|
strings.</p></li> |
|
</ol> |
|
<p>Not too bad.</p> |
|
<p>It’s not the most beautiful code, and can be irritating especially if |
|
you aren’t used to the DOM APIs.</p> |
|
<p>It’s a little annoying that we needed to add an <code>id</code> to |
|
the <code>output</code> element. The |
|
<code>document.querySelector()</code> function is a bit verbose compared |
|
with, say, the <code>$</code> function, as provided by jQuery.</p> |
|
<p>But it works. It’s also easy enough to understand, and crucially it |
|
doesn’t require any other JavaScript libraries.</p> |
|
<p>So that’s the simple, inline approach with VanillaJS.</p> |
|
<h4 id="_separating_our_scripting_out">Separating our scripting out</h4> |
|
<p>While the inline implementation is simple in some sense, a more |
|
standard way to write this would be to move the code into a separate |
|
JavaScript file. This JavaScript file would then either be linked to via |
|
a <code><script src></code> tag or placed into an inline |
|
<code><script></code> tag by a build process.</p> |
|
<p>Here we see the HTML and JavaScript <em class="test">separated out</em> from one |
|
another, in different files. The HTML is now “cleaner” in that there is |
|
no JavaScript in it.</p> |
|
<p>The JavaScript is a bit more complex than in our inline version: we |
|
need to look up the button using a query selector and add an <em class="test">event |
|
listener</em> to handle the click event and increment the counter.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb2"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb2-1"><a aria-hidden="true" href="#cb2-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">section</span><span class="ot"> class</span><span class="op">=</span><span class="st">"counter"</span><span class="dt">></span></span> |
|
<span id="cb2-2"><a aria-hidden="true" href="#cb2-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">output</span><span class="ot"> id</span><span class="op">=</span><span class="st">"my-output"</span><span class="dt">></span>0<span class="dt"></</span><span class="kw">output</span><span class="dt">></span></span> |
|
<span id="cb2-3"><a aria-hidden="true" href="#cb2-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> class</span><span class="op">=</span><span class="st">"increment-btn"</span><span class="dt">></span>Increment<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb2-4"><a aria-hidden="true" href="#cb2-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">section</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Counter HTML</p></figcaption> |
|
</figure> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a><span class="kw">const</span> counterOutput <span class="op">=</span> <span class="bu">document</span><span class="op">.</span><span class="fu">querySelector</span>(<span class="st">"#my-output"</span>)<span class="op">,</span> <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb3-2"><a aria-hidden="true" href="#cb3-2" tabindex="-1"></a> incrementBtn <span class="op">=</span> <span class="bu">document</span><span class="op">.</span><span class="fu">querySelector</span>(<span class="st">".counter .increment-btn"</span>) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb3-3"><a aria-hidden="true" href="#cb3-3" tabindex="-1"></a></span> |
|
<span id="cb3-4"><a aria-hidden="true" href="#cb3-4" tabindex="-1"></a>incrementBtn<span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">"click"</span><span class="op">,</span> e <span class="kw">=></span> { <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb3-5"><a aria-hidden="true" href="#cb3-5" tabindex="-1"></a> counterOutput<span class="op">.</span><span class="at">innerHTML</span><span class="op">++</span> <span class="op"><</span><span class="dv">4</span><span class="op">></span></span> |
|
<span id="cb3-6"><a aria-hidden="true" href="#cb3-6" tabindex="-1"></a>})</span></code></pre></div> |
|
<figcaption><p>Counter JavaScript</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Find the output element.</p></li> |
|
<li><p>Find the button.</p></li> |
|
<li><p>We use <code>addEventListener</code>, which is preferable to |
|
<code>onclick</code> for many reasons.</p></li> |
|
<li><p>The logic stays the same, only the structure around it |
|
changes.</p></li> |
|
</ol> |
|
<p>In moving the JavaScript out to another file, we are following a |
|
software design principle known as <em class="test">Separation of Concerns |
|
(SoC).</em></p> |
|
<p>Separation of Concerns posits that the various “concerns” (or |
|
aspects) of a software project should be divided up into multiple files, |
|
so that they don’t “pollute” one another. JavaScript isn’t markup, so it |
|
shouldn’t be in your HTML, it should be <em class="test">elsewhere</em>. Styling |
|
information, similarly, isn’t markup, and so it belongs in a separate |
|
file as well (A CSS file, for example.)</p> |
|
<p>For quite some time, this Separation of Concerns was considered the |
|
“orthodox” way to build web applications.</p> |
|
<p>A stated goal of Separation of Concerns is that we should be able to |
|
modify and evolve each concern independently, with confidence that we |
|
won’t break any of the other concerns.</p> |
|
<p>However, let’s look at exactly how this principle has worked out in |
|
our simple counter example. If you look closely at the new HTML, it |
|
turns out that we’ve had to add a class to the button. We added this |
|
class so that we could look the button up in JavaScript and add in an |
|
event handler for the “click” event.</p> |
|
<p>Now, in both the HTML and the JavaScript, this class name is just a |
|
string and there isn’t any process to <em class="test">verify</em> that the button |
|
has the right classes on it or its ancestors to ensure that the event |
|
handler is actually added to the right element.</p> |
|
<p>Unfortunately, it has turned out that the careless use of CSS |
|
selectors in JavaScript can cause what is known as <em class="test">jQuery soup</em>. |
|
jQuery soup is a situation where:</p> |
|
<ul> |
|
<li><p>The JavaScript that attaches a given behavior to a given element |
|
is difficult to find.</p></li> |
|
<li><p>Code reuse is difficult.</p></li> |
|
<li><p>The code ends up wildly disorganized and “flat”, with lots of |
|
unrelated event handlers mixed together.</p></li> |
|
</ul> |
|
<p>The name “jQuery soup” comes from the fact that most JavaScript-heavy |
|
applications used to be built in jQuery (many still are), which, perhaps |
|
inadvertently, tended to encourage this style of JavaScript.</p> |
|
<p>So, you can see that the notion of Separation of Concerns doesn’t |
|
always work as well as promised: our concerns end up intertwined or |
|
coupled pretty deeply, even when we separate them into different |
|
files.</p> |
|
<figure> |
|
<div> |
|
<div data-align="start"> |
|
<pre><code> EXPECTATION REALITY |
|
|
|
HTML CSS JS HTML CSS JS |
|
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ |
|
│ │ │ │ │ │ │ │ │ │ │ │ |
|
│ C │ │ C │ │ c │ │ CO │ │ NC │ │ern │ |
|
│ O │ │ o │ │ o │ │ │ │ │ │ │ |
|
│ N │ │ n │ │ n │ │ Co │ │ nc │ │ERN │ |
|
│ C │ │ c │ │ c │ │ │ │ │ │ │ |
|
│ E │ │ e │ │ e │ │ co │ │ NC │ │ERN │ |
|
│ R │ │ r │ │ r │ │ │ │ │ │ │ |
|
│ N │ │ n │ │ n │ │ CO │ │ Nc │ │ern │ |
|
│ │ │ │ │ │ │ │ │ │ │ │ |
|
└────┘ └────┘ └────┘ └────┘ └────┘ └────┘ |
|
</code></pre> |
|
</div> |
|
</div> |
|
<figcaption><p>What concerns?</p></figcaption> |
|
</figure> |
|
<p>To show that it isn’t just naming between concerns that can get you |
|
into trouble, consider another small change to our HTML that |
|
demonstrates the problems with our separation of concerns: imagine that |
|
we decide to change the number field from an <code><output></code> |
|
tag to an <code><input type="number"></code>.</p> |
|
<p>This small change to our HTML will break our JavaScript, despite the |
|
fact we have “separated” our concerns.</p> |
|
<p>The fix for this issue is simple enough (we would need to change the |
|
<code>.textContent</code> property to <code>.value</code> property), but |
|
it demonstrates the burden of synchronizing markup changes and code |
|
changes across multiple files. Keeping everything in sync can become |
|
increasingly difficult as your application size increases.</p> |
|
<p>The fact that small changes to our HTML can break our scripting |
|
indicates that the two are <em class="test">tightly coupled</em>, despite being |
|
broken up into multiple files. This tight coupling suggests that |
|
separation between HTML and JavaScript (and CSS) is often an illusory |
|
separation of concerns: the concerns are sufficiently related to one |
|
another that they aren’t easily separated.</p> |
|
<p>In Contact.app we are not <em class="test">concerned</em> with “structure,” |
|
“styling” or “behavior”; we are concerned with collecting contact info |
|
and presenting it to users. SoC, in the way it’s formulated in web |
|
development orthodoxy, is not really an inviolate architectural |
|
guideline, but rather a stylistic choice that, as we can see, can even |
|
become a hindrance.</p> |
|
<h4 id="locality-of-behavior">Locality of Behavior</h4> |
|
<p>It turns out that there is a burgeoning reaction <em class="test">against</em> the |
|
Separation of Concerns design principle. Consider the following web |
|
technologies and techniques:</p> |
|
<ul> |
|
<li><p>JSX</p></li> |
|
<li><p>LitHTML</p></li> |
|
<li><p>CSS-in-JS</p></li> |
|
<li><p>Single-File Components</p></li> |
|
<li><p>Filesystem based routing</p></li> |
|
</ul> |
|
<p>Each of these technologies <em class="test">colocate</em> code in various |
|
languages that address a single <em class="test">feature</em> (typically a UI |
|
widget).</p> |
|
<p>All of them mix <em class="test">implementation</em> concerns together in order to |
|
present a unified abstraction to the end-user. Separating technical |
|
detail concerns just isn’t as much of an, ahem, concern.</p> |
|
<p>Locality of Behavior (LoB) is an alternative software design |
|
principle that we coined, in opposition to Separation of Concerns. It |
|
describes the following characteristic of a piece of software:</p> |
|
<blockquote> |
|
<p>The behavior of a unit of code should be as obvious as possible by |
|
looking only at that unit of code.</p> |
|
</blockquote><p class="quote-attribution"> https://htmx.org/essays/locality-of-behaviour/</p> |
|
<p>In simple terms: you should be able to tell what a button does by |
|
simply looking at the code or markup that creates that button. This does |
|
not mean you need to inline the entire implementation, but that you |
|
shouldn’t need to hunt for it or require prior knowledge of the codebase |
|
to find it.</p> |
|
<p>We will demonstrate Locality of Behavior in all of our examples, both |
|
the counter demos and the features we add to Contact.app. Locality of |
|
behavior is an explicit design goal of both _hyperscript and Alpine.js |
|
(which we will cover later) as well as htmx.</p> |
|
<p>All of these tools achieve Locality of Behavior by having you embed |
|
attributes directly within your HTML, as opposed to having code look up |
|
elements in a document through CSS selectors in order to add event |
|
listeners onto them.</p> |
|
<p>In a Hypermedia-Driven Application, we feel that the Locality of |
|
Behavior design principle is often more important than the more |
|
traditional Separation of Concerns design principle.</p> |
|
<h4 id="what-to-do-with-our-counter-">What to do with our counter?</h4> |
|
<p>So, should we go back to the <code>onclick</code> attribute way of |
|
doing things? That approach certainly wins in Locality of Behavior, and |
|
has the additional benefit that it is baked into HTML.</p> |
|
<p>Unfortunately, however, the <code>on*</code> JavaScript attributes |
|
also come with some drawbacks:</p> |
|
<ul> |
|
<li><p>They don’t support custom events.</p></li> |
|
<li><p>There is no good mechanism for associating long-lasting variables |
|
with an element — all variables are discarded when an event listener |
|
completes executing.</p></li> |
|
<li><p>If you have multiple instances of an element, you will need to |
|
repeat the listener code on each, or use something more clever like |
|
event delegation.</p></li> |
|
<li><p>JavaScript code that directly manipulates the DOM gets verbose, |
|
and clutters the markup.</p></li> |
|
<li><p>An element cannot listen for events on another element.</p></li> |
|
</ul> |
|
<p>Consider this common situation: you have a popup, and you want it to |
|
be dismissed when a user clicks outside of it. The listener will need to |
|
be on the body element in this situation, far away from the actual popup |
|
markup. This means that the body element would need to have listeners |
|
attached to it that deal with many unrelated components. Some of these |
|
components may not even be on the page when it was first rendered, if |
|
they are added dynamically after the initial HTML page is rendered.</p> |
|
<p>So vanilla JavaScript and Locality of Behavior don’t seem to mesh |
|
<em class="test">quite</em> as well as we would like them to.</p> |
|
<p>The situation is not hopeless, however: it’s important to understand |
|
that LoB does not require behavior to be <em class="test">implemented</em> at a use |
|
site, but merely <em class="test">invoked</em> there. That is, we don’t need to write |
|
all our code on a given element, we just need to make it clear that a |
|
given element is <em class="test">invoking</em> some code, which can be located |
|
elsewhere.</p> |
|
<p>Keeping this in mind, it <em class="test">is</em> possible to improve LoB while |
|
writing JavaScript in a separate file, provided we have a reasonable |
|
system for structuring our JavaScript.</p> |
|
<h3 id="rsjs">RSJS</h3> |
|
<p>(the “Reasonable System for JavaScript Structure,” <a href="https://ricostacruz.com/rsjs/">https://ricostacruz.com/rsjs/</a>) |
|
is a set of guidelines for JavaScript architecture targeted at “a |
|
typical non-SPA website.” RSJS provides a solution to the lack of a |
|
standard code style for vanilla JavaScript that we mentioned |
|
earlier.</p> |
|
<p>Here are the RSJS guidelines most relevant for our counter |
|
widget:</p> |
|
<ul> |
|
<li><p>“Use <code>data-</code> attributes” in HTML: invoking behavior |
|
via adding data attributes makes it obvious there is JavaScript |
|
happening, as opposed to using random classes or IDs that may be |
|
mistakenly removed or changed.</p></li> |
|
<li><p>“One component per file”: the name of the file should match the |
|
data attribute so that it can be found easily, a win for LoB.</p></li> |
|
</ul> |
|
<p>To follow the RSJS guidelines, let’s restructure our current HTML and |
|
JavaScript files. First, we will use <em class="test">data attributes</em>, that is, |
|
HTML attributes that begin with <code>data-</code>, a standard feature |
|
of HTML, to indicate that our HTML is a counter component. We will then |
|
update our JavaScript to use an attribute selector that looks for the |
|
<code>data-counter</code> attribute as the root element in our counter |
|
component and wires in the appropriate event handlers and logic. |
|
Additionally, let’s rework the code to use |
|
<code>querySelectorAll()</code> and add the counter functionality to |
|
<em class="test">all</em> counter components found on the page. (You never know how |
|
many counters you might want!)</p> |
|
<p>Here is what our code looks like now:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">section</span><span class="ot"> class</span><span class="op">=</span><span class="st">"counter"</span><span class="ot"> data-counter</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">output</span><span class="ot"> id</span><span class="op">=</span><span class="st">"my-output"</span><span class="ot"> data-counter-output</span><span class="dt">></span>0<span class="dt"></</span><span class="kw">output</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> class</span><span class="op">=</span><span class="st">"increment-btn"</span><span class="ot"> data-counter-increment</span><span class="dt">></span>Increment<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">section</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Counter in vanilla JavaScript, with RSJS</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Invoke a JavaScript behavior with a data attribute.</p></li> |
|
<li><p>Mark relevant descendant elements.</p></li> |
|
</ol> |
|
<figure> |
|
<div class="sourceCode" id="cb6"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb6-1"><a aria-hidden="true" href="#cb6-1" tabindex="-1"></a><span class="co">// counter.js <1></span></span> |
|
<span id="cb6-2"><a aria-hidden="true" href="#cb6-2" tabindex="-1"></a><span class="bu">document</span><span class="op">.</span><span class="fu">querySelectorAll</span>(<span class="st">"[data-counter]"</span>) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb6-3"><a aria-hidden="true" href="#cb6-3" tabindex="-1"></a> <span class="op">.</span><span class="fu">forEach</span>(el <span class="kw">=></span> {</span> |
|
<span id="cb6-4"><a aria-hidden="true" href="#cb6-4" tabindex="-1"></a> <span class="kw">const</span></span> |
|
<span id="cb6-5"><a aria-hidden="true" href="#cb6-5" tabindex="-1"></a> output <span class="op">=</span> el<span class="op">.</span><span class="fu">querySelector</span>(<span class="st">"[data-counter-output]"</span>)<span class="op">,</span></span> |
|
<span id="cb6-6"><a aria-hidden="true" href="#cb6-6" tabindex="-1"></a> increment <span class="op">=</span> el<span class="op">.</span><span class="fu">querySelector</span>(<span class="st">"[data-counter-increment]"</span>)<span class="op">;</span> <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb6-7"><a aria-hidden="true" href="#cb6-7" tabindex="-1"></a></span> |
|
<span id="cb6-8"><a aria-hidden="true" href="#cb6-8" tabindex="-1"></a> increment<span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">"click"</span><span class="op">,</span> e <span class="kw">=></span> output<span class="op">.</span><span class="at">textContent</span><span class="op">++</span>)<span class="op">;</span> <span class="op"><</span><span class="dv">4</span><span class="op">></span></span> |
|
<span id="cb6-9"><a aria-hidden="true" href="#cb6-9" tabindex="-1"></a> })<span class="op">;</span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>File should have the same name as the data attribute, so that we |
|
can locate it easily.</p></li> |
|
<li><p>Get all elements that invoke this behavior.</p></li> |
|
<li><p>Get any child elements we need.</p></li> |
|
<li><p>Register event handlers.</p></li> |
|
</ol> |
|
<p>Using RSJS solves, or at least alleviates, many of the problems we |
|
pointed out with our first, unstructured example of VanillaJS being |
|
split out to a separate file:</p> |
|
<ul> |
|
<li><p>The JS that attaches behavior to a given element is |
|
<em class="test">clear</em> (though only through naming conventions).</p></li> |
|
<li><p>Reuse is <em class="test">easy</em> — you can create another counter component |
|
on the page and it will just work.</p></li> |
|
<li><p>The code is <em class="test">well-organized</em> — one behavior per |
|
file.</p></li> |
|
</ul> |
|
<p>All in all, RSJS is a good way to structure your vanilla JavaScript |
|
in a Hypermedia-Driven Application. So long as the JavaScript isn’t |
|
communicating with a server via a plain data JSON API, or holding a |
|
bunch of internal state outside of the DOM, this is perfectly compatible |
|
with the HDA approach.</p> |
|
<p>Let’s implement a feature in Contact.app using the RSJS/vanilla |
|
JavaScript approach.</p> |
|
<h3 id="_vanillajs_in_action_an_overflow_menu">VanillaJS in Action: An |
|
Overflow Menu</h3> |
|
<p>Our homepage has “Edit”, “View” and “Delete” links for every contact |
|
in our table. This uses a lot of space and creates visual clutter. Let’s |
|
fix that by placing these actions inside a drop-down menu with a button |
|
to open it.</p> |
|
<p>If you’re less familiar with JavaScript and the code here starts to |
|
feel too complicated, don’t worry; the Alpine.js and _hyperscript |
|
examples — which we’ll look at next — are easier to follow.</p> |
|
<p>Let’s begin by sketching the markup we want for our dropdown menu. |
|
First, we need an element, we’ll use a <code><div></code>, to |
|
enclose the entire widget and mark it as a menu component. Within this |
|
div, we will have a standard <code><button></code> that will |
|
function as the mechanism that shows and hides our menu items. Finally, |
|
we’ll have another <code><div></code> that holds the menu items |
|
that we are going to show.</p> |
|
<p>These menu items will be simple anchor tags, as they are in the |
|
current contacts table.</p> |
|
<p>Here is what our updated, RSJS-structured HTML looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> data-overflow-menu</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> type</span><span class="op">=</span><span class="st">"button"</span><span class="ot"> aria-haspopup</span><span class="op">=</span><span class="st">"menu"</span></span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a><span class="ot"> aria-controls</span><span class="op">=</span><span class="st">"contact-menu-{{ contact.id }}"</span></span> |
|
<span id="cb7-4"><a aria-hidden="true" href="#cb7-4" tabindex="-1"></a><span class="ot"> </span><span class="dt">></span>Options<span class="dt"></</span><span class="kw">button</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb7-5"><a aria-hidden="true" href="#cb7-5" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> role</span><span class="op">=</span><span class="st">"menu"</span><span class="ot"> hidden id</span><span class="op">=</span><span class="st">"contact-menu-{{ contact.id }}"</span><span class="dt">></span> <span class="er"><</span>3></span> |
|
<span id="cb7-6"><a aria-hidden="true" href="#cb7-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> role</span><span class="op">=</span><span class="st">"menuitem"</span></span> |
|
<span id="cb7-7"><a aria-hidden="true" href="#cb7-7" tabindex="-1"></a><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}/edit"</span><span class="dt">></span>Edit<span class="dt"></</span><span class="kw">a</span><span class="dt">></span> <span class="er"><</span>4></span> |
|
<span id="cb7-8"><a aria-hidden="true" href="#cb7-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">a</span><span class="ot"> role</span><span class="op">=</span><span class="st">"menuitem"</span><span class="ot"> href</span><span class="op">=</span><span class="st">"/contacts/{{ contact.id }}"</span><span class="dt">></span>View<span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span> |
|
<span id="cb7-9"><a aria-hidden="true" href="#cb7-9" tabindex="-1"></a> <span class="co"><!-- ... --></span></span> |
|
<span id="cb7-10"><a aria-hidden="true" href="#cb7-10" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb7-11"><a aria-hidden="true" href="#cb7-11" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>Mark the root element of the menu component</p></li> |
|
<li><p>This button will open and close our menu</p></li> |
|
<li><p>A container for our menu items</p></li> |
|
<li><p>Menu items</p></li> |
|
</ol> |
|
<p>The roles and ARIA attributes are based on the Menu and Menu Button |
|
patterns from the ARIA Authoring Practices Guide.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>What is ARIA?</strong></p> |
|
</div> |
|
<div> |
|
<p>As we web developers create more interactive, app-like websites, |
|
HTML’s repertoire of elements won’t have all we need. As we have seen, |
|
using CSS and JavaScript, we can endow existing elements with extended |
|
behavior and appearances, rivaling those of native controls.</p> |
|
<p>However, there was one thing web apps couldn’t replicate. While these |
|
widgets may <em class="test">look</em> similar enough to the real deal, assistive |
|
technology (e.g., screen readers) could only deal with the underlying |
|
HTML elements.</p> |
|
<p>Even if you take the time to get all the keyboard interactions right, |
|
some users often are unable to work with these custom elements |
|
easily.</p> |
|
<p>ARIA was created by W3C’s Web Accessibility Initiative (WAI) in 2008 |
|
to address this problem. At a surface level, it is a set of attributes |
|
you can add to HTML to make it meaningful to assistive software such as |
|
a screen reader.</p> |
|
<p>ARIA has two main components that interact with one another:</p> |
|
<p>The first is the <code>role</code> attribute. This attribute has a |
|
predefined set of possible values: <code>menu, dialog, radiogroup</code> |
|
etc. The <code>role</code> attribute <em class="test">does not add any behavior</em> |
|
to HTML elements. Rather, it is a promise you make to the user. When you |
|
annotate an element as <code>role='menu'</code>, you are saying: <em class="test">I |
|
will make this element work like a menu.</em></p> |
|
<p>If you add a <code>role</code> to an element but you <em class="test">don’t</em> |
|
uphold the promise, the experience for many users will be <em class="test">worse</em> |
|
than if the element had no <code>role</code> at all. Thus, it is |
|
written:</p> |
|
<blockquote> |
|
<p>No ARIA is better than Bad ARIA.</p> |
|
</blockquote><p class="quote-attribution"> W3C, Read Me First | APG, |
|
https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/</p> |
|
<p>The second component of ARIA is the <em class="test">states and properties</em>, |
|
all sharing the <code>aria-</code> prefix: |
|
<code>aria-expanded, aria-controls, aria-label</code> etc. These |
|
attributes can specify various things such as the state of a widget, the |
|
relationships between components, or additional semantics. Once again, |
|
these attributes are <em class="test">promises</em>, not implementations.</p> |
|
<p>Rather than learn all the roles and attributes and try to combine |
|
them into a usable widget, the best course of action for most developers |
|
is to rely on the ARIA Authoring Practices Guide (APG), a web resource |
|
with practical information aimed directly at web developers.</p> |
|
<p>If you’re new to ARIA, check out the following W3C resources:</p> |
|
<ul> |
|
<li><p>ARIA: Read Me First: <a href="https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/">https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/</a></p></li> |
|
<li><p>ARIA UI patterns: <a href="https://www.w3.org/WAI/ARIA/apg/patterns/">https://www.w3.org/WAI/ARIA/apg/patterns/</a></p></li> |
|
<li><p>ARIA Good Practices: <a href="https://www.w3.org/WAI/ARIA/apg/practices/">https://www.w3.org/WAI/ARIA/apg/practices/</a></p></li> |
|
</ul> |
|
<p>Always remember to <strong>test</strong> your website for |
|
accessibility to ensure all users can interact with it easily and |
|
effectively.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>On the JS side of our implementation, we’ll begin with the RSJS |
|
boilerplate: query for all elements with some data attribute, iterate |
|
over them, get any relevant descendants.</p> |
|
<p>Note that, below, we’ve modified the RSJS boilerplate a bit to |
|
integrate with htmx; we load the overflow menu when htmx loads new |
|
content.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="kw">function</span> <span class="fu">overflowMenu</span>(tree <span class="op">=</span> <span class="bu">document</span>) {</span> |
|
<span id="cb8-2"><a aria-hidden="true" href="#cb8-2" tabindex="-1"></a> tree<span class="op">.</span><span class="fu">querySelectorAll</span>(<span class="st">"[data-overflow-menu]"</span>)<span class="op">.</span><span class="fu">forEach</span>(menuRoot <span class="kw">=></span> { <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb8-3"><a aria-hidden="true" href="#cb8-3" tabindex="-1"></a> <span class="kw">const</span></span> |
|
<span id="cb8-4"><a aria-hidden="true" href="#cb8-4" tabindex="-1"></a> button <span class="op">=</span> menuRoot<span class="op">.</span><span class="fu">querySelector</span>(<span class="st">"[aria-haspopup]"</span>)<span class="op">,</span> <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb8-5"><a aria-hidden="true" href="#cb8-5" tabindex="-1"></a> menu <span class="op">=</span> menuRoot<span class="op">.</span><span class="fu">querySelector</span>(<span class="st">"[role=menu]"</span>)<span class="op">,</span> <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb8-6"><a aria-hidden="true" href="#cb8-6" tabindex="-1"></a> items <span class="op">=</span> [<span class="op">...</span>menu<span class="op">.</span><span class="fu">querySelectorAll</span>(<span class="st">"[role=menuitem]"</span>)]<span class="op">;</span></span> |
|
<span id="cb8-7"><a aria-hidden="true" href="#cb8-7" tabindex="-1"></a> })<span class="op">;</span></span> |
|
<span id="cb8-8"><a aria-hidden="true" href="#cb8-8" tabindex="-1"></a>}</span> |
|
<span id="cb8-9"><a aria-hidden="true" href="#cb8-9" tabindex="-1"></a></span> |
|
<span id="cb8-10"><a aria-hidden="true" href="#cb8-10" tabindex="-1"></a><span class="fu">addEventListener</span>(<span class="st">"htmx:load"</span><span class="op">,</span> e <span class="kw">=></span> <span class="fu">overflowMenu</span>(e<span class="op">.</span><span class="at">target</span>))<span class="op">;</span> <span class="op"><</span><span class="dv">4</span><span class="op">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>With RSJS, you’ll be writing |
|
<code>document.querySelectorAll(…).forEach</code> a lot.</p></li> |
|
<li><p>To keep the HTML clean, we use ARIA attributes rather than custom |
|
data attributes here.</p></li> |
|
<li><p>Use the spread operator to convert a <code>NodeList</code> into a |
|
normal <code>Array</code>.</p></li> |
|
<li><p>Initialize all overflow menus when the page is loaded or content |
|
is inserted by htmx.</p></li> |
|
</ol> |
|
<p>Conventionally, we would keep track of whether the menu is open using |
|
a JavaScript variable or a property in a JavaScript state object. This |
|
approach is common in large, JavaScript-heavy web applications.</p> |
|
<p>However, this approach has some drawback:</p> |
|
<ul> |
|
<li><p>We would need to keep the DOM in sync with the state (harder |
|
without a framework).</p></li> |
|
<li><p>We would lose the ability to serialize the HTML (as this open |
|
state isn’t stored in the DOM, but rather in JavaScript).</p></li> |
|
</ul> |
|
<p>Instead of taking this approach, we will use the DOM to store our |
|
state. We’ll lean on the <code>hidden</code> attribute on the menu |
|
element to tell us it’s closed. If the HTML of the page is snapshotted |
|
and restored, the menu can be restored as well by simply re-running the |
|
JS.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a>items <span class="op">=</span> [<span class="op">...</span>menu<span class="op">.</span><span class="fu">querySelectorAll</span>(<span class="st">"[role=menuitem]"</span>)]<span class="op">;</span> <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a></span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a><span class="kw">const</span> isOpen <span class="op">=</span> () <span class="kw">=></span> <span class="op">!</span>menu<span class="op">.</span><span class="at">hidden</span><span class="op">;</span> <span class="op"><</span><span class="dv">2</span><span class="op">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>We get the list of menu items at the start. This implementation |
|
will not support dynamically adding or removing menu items.</p></li> |
|
<li><p>The <code>hidden</code> attribute is helpfully reflected as a |
|
<code>hidden</code> <em class="test">property</em>, so we don’t need to use |
|
<code>getAttribute</code>.</p></li> |
|
</ol> |
|
<p>We’ll also make the menu items non-tabbable, so we can manage their |
|
focus ourselves.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb10"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb10-1"><a aria-hidden="true" href="#cb10-1" tabindex="-1"></a>items<span class="op">.</span><span class="fu">forEach</span>(item <span class="kw">=></span> item<span class="op">.</span><span class="fu">setAttribute</span>(<span class="st">"tabindex"</span><span class="op">,</span> <span class="st">"-1"</span>))<span class="op">;</span></span></code></pre></div> |
|
</figure> |
|
<p>Now let’s implement toggling the menu in JavaScript:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb11"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb11-1"><a aria-hidden="true" href="#cb11-1" tabindex="-1"></a><span class="kw">function</span> <span class="fu">toggleMenu</span>(open <span class="op">=</span> <span class="op">!</span><span class="fu">isOpen</span>()) { <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb11-2"><a aria-hidden="true" href="#cb11-2" tabindex="-1"></a> <span class="cf">if</span> (open) {</span> |
|
<span id="cb11-3"><a aria-hidden="true" href="#cb11-3" tabindex="-1"></a> menu<span class="op">.</span><span class="at">hidden</span> <span class="op">=</span> <span class="kw">false</span><span class="op">;</span></span> |
|
<span id="cb11-4"><a aria-hidden="true" href="#cb11-4" tabindex="-1"></a> button<span class="op">.</span><span class="fu">setAttribute</span>(<span class="st">"aria-expanded"</span><span class="op">,</span> <span class="st">"true"</span>)<span class="op">;</span></span> |
|
<span id="cb11-5"><a aria-hidden="true" href="#cb11-5" tabindex="-1"></a> items[<span class="dv">0</span>]<span class="op">.</span><span class="fu">focus</span>()<span class="op">;</span> <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb11-6"><a aria-hidden="true" href="#cb11-6" tabindex="-1"></a> } <span class="cf">else</span> {</span> |
|
<span id="cb11-7"><a aria-hidden="true" href="#cb11-7" tabindex="-1"></a> menu<span class="op">.</span><span class="at">hidden</span> <span class="op">=</span> <span class="kw">true</span><span class="op">;</span></span> |
|
<span id="cb11-8"><a aria-hidden="true" href="#cb11-8" tabindex="-1"></a> button<span class="op">.</span><span class="fu">setAttribute</span>(<span class="st">"aria-expanded"</span><span class="op">,</span> <span class="st">"false"</span>)<span class="op">;</span></span> |
|
<span id="cb11-9"><a aria-hidden="true" href="#cb11-9" tabindex="-1"></a> }</span> |
|
<span id="cb11-10"><a aria-hidden="true" href="#cb11-10" tabindex="-1"></a>}</span> |
|
<span id="cb11-11"><a aria-hidden="true" href="#cb11-11" tabindex="-1"></a></span> |
|
<span id="cb11-12"><a aria-hidden="true" href="#cb11-12" tabindex="-1"></a><span class="fu">toggleMenu</span>(<span class="fu">isOpen</span>())<span class="op">;</span> <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb11-13"><a aria-hidden="true" href="#cb11-13" tabindex="-1"></a>button<span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">"click"</span><span class="op">,</span> () <span class="kw">=></span> <span class="fu">toggleMenu</span>())<span class="op">;</span> <span class="op"><</span><span class="dv">4</span><span class="op">></span></span> |
|
<span id="cb11-14"><a aria-hidden="true" href="#cb11-14" tabindex="-1"></a>menuRoot<span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">"blur"</span><span class="op">,</span> e <span class="kw">=></span> <span class="fu">toggleMenu</span>(<span class="kw">false</span>))<span class="op">;</span> <span class="op"><</span><span class="dv">5</span><span class="op">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>Optional parameter to specify desired state. This allows us to |
|
use one function to open, close, or toggle the menu.</p></li> |
|
<li><p>Focus first item of menu when opened.</p></li> |
|
<li><p>Call <code>toggleMenu</code> with current state, to initialize |
|
element attributes.</p></li> |
|
<li><p>Toggle menu when button is clicked.</p></li> |
|
<li><p>Close menu when focus moves away.</p></li> |
|
</ol> |
|
<p>Let’s also make the menu close when we click outside it, a nice |
|
behavior that mimics how native drop-down menus work. This will require |
|
an event listener on the whole window.</p> |
|
<p>Note that we need to be careful with this kind of listener: you may |
|
find that listeners accumulate as components add listeners and fail to |
|
remove them when the component is removed from the DOM. This, |
|
unfortunately, leads to difficult to track down memory leaks.</p> |
|
<p>There is not an easy way in JavaScript to execute logic when an |
|
element is removed. The best option is what is known as the |
|
<code>MutationObserver</code> API. A <code>MutationObserver</code> is |
|
very useful, but the API is quite heavy and a bit arcane, so we won’t be |
|
using it for our example.</p> |
|
<p>Instead, we will use a simple pattern to avoid leaking event |
|
listeners: when our event listener runs, we will check if the attaching |
|
component is still in the DOM, and, if the element is no longer in the |
|
DOM, we will remove the listener and exit.</p> |
|
<p>This is a somewhat hacky, manual form of <em class="test">garbage collection</em>. |
|
As is (usually) the case with other garbage collection algorithms, our |
|
strategy removes listeners in a nondeterministic amount of time after |
|
they are no longer needed. Fortunately for us, With a frequent event |
|
like “the user clicks anywhere in the page” driving the collection, it |
|
should work well enough for our system.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb12"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb12-1"><a aria-hidden="true" href="#cb12-1" tabindex="-1"></a><span class="bu">window</span><span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">"click"</span><span class="op">,</span> <span class="kw">function</span> <span class="fu">clickAway</span>(<span class="bu">event</span>) {</span> |
|
<span id="cb12-2"><a aria-hidden="true" href="#cb12-2" tabindex="-1"></a> <span class="cf">if</span> (<span class="op">!</span>menuRoot<span class="op">.</span><span class="at">isConnected</span>)</span> |
|
<span id="cb12-3"><a aria-hidden="true" href="#cb12-3" tabindex="-1"></a> <span class="bu">window</span><span class="op">.</span><span class="fu">removeEventListener</span>(<span class="st">"click"</span><span class="op">,</span> clickAway)<span class="op">;</span> <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb12-4"><a aria-hidden="true" href="#cb12-4" tabindex="-1"></a> <span class="cf">if</span> (<span class="op">!</span>menuRoot<span class="op">.</span><span class="fu">contains</span>(<span class="bu">event</span><span class="op">.</span><span class="at">target</span>)) <span class="fu">toggleMenu</span>(<span class="kw">false</span>)<span class="op">;</span> <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb12-5"><a aria-hidden="true" href="#cb12-5" tabindex="-1"></a>})<span class="op">;</span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>This line is the garbage collection.</p></li> |
|
<li><p>If the click is outside the menu, close the menu.</p></li> |
|
</ol> |
|
<p>Now, let’s move on to the keyboard interactions for our dropdown |
|
menu. The keyboard handlers turn out to all be pretty similar to one |
|
another and not particularly intricate, so let’s knock them all out in |
|
one go:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb13"><pre class="sourceCode js"><code class="sourceCode javascript"><span id="cb13-1"><a aria-hidden="true" href="#cb13-1" tabindex="-1"></a><span class="kw">const</span> currentIndex <span class="op">=</span> () <span class="kw">=></span> { <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb13-2"><a aria-hidden="true" href="#cb13-2" tabindex="-1"></a> <span class="kw">const</span> idx <span class="op">=</span> items<span class="op">.</span><span class="fu">indexOf</span>(<span class="bu">document</span><span class="op">.</span><span class="at">activeElement</span>)<span class="op">;</span></span> |
|
<span id="cb13-3"><a aria-hidden="true" href="#cb13-3" tabindex="-1"></a> <span class="cf">if</span> (idx <span class="op">===</span> <span class="op">-</span><span class="dv">1</span>) <span class="cf">return</span> <span class="dv">0</span><span class="op">;</span></span> |
|
<span id="cb13-4"><a aria-hidden="true" href="#cb13-4" tabindex="-1"></a> <span class="cf">return</span> idx<span class="op">;</span></span> |
|
<span id="cb13-5"><a aria-hidden="true" href="#cb13-5" tabindex="-1"></a>}</span> |
|
<span id="cb13-6"><a aria-hidden="true" href="#cb13-6" tabindex="-1"></a></span> |
|
<span id="cb13-7"><a aria-hidden="true" href="#cb13-7" tabindex="-1"></a>menu<span class="op">.</span><span class="fu">addEventListener</span>(<span class="st">"keydown"</span><span class="op">,</span> e <span class="kw">=></span> {</span> |
|
<span id="cb13-8"><a aria-hidden="true" href="#cb13-8" tabindex="-1"></a> <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">"ArrowUp"</span>) {</span> |
|
<span id="cb13-9"><a aria-hidden="true" href="#cb13-9" tabindex="-1"></a> items[<span class="fu">currentIndex</span>() <span class="op">-</span> <span class="dv">1</span>]<span class="op">?.</span><span class="fu">focus</span>()<span class="op">;</span> <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb13-10"><a aria-hidden="true" href="#cb13-10" tabindex="-1"></a></span> |
|
<span id="cb13-11"><a aria-hidden="true" href="#cb13-11" tabindex="-1"></a> } <span class="cf">else</span> <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">"ArrowDown"</span>) {</span> |
|
<span id="cb13-12"><a aria-hidden="true" href="#cb13-12" tabindex="-1"></a> items[<span class="fu">currentIndex</span>() <span class="op">+</span> <span class="dv">1</span>]<span class="op">?.</span><span class="fu">focus</span>()<span class="op">;</span> <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb13-13"><a aria-hidden="true" href="#cb13-13" tabindex="-1"></a></span> |
|
<span id="cb13-14"><a aria-hidden="true" href="#cb13-14" tabindex="-1"></a> } <span class="cf">else</span> <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">"Space"</span>) {</span> |
|
<span id="cb13-15"><a aria-hidden="true" href="#cb13-15" tabindex="-1"></a> items[<span class="fu">currentIndex</span>()]<span class="op">.</span><span class="fu">click</span>()<span class="op">;</span> <span class="op"><</span><span class="dv">4</span><span class="op">></span></span> |
|
<span id="cb13-16"><a aria-hidden="true" href="#cb13-16" tabindex="-1"></a></span> |
|
<span id="cb13-17"><a aria-hidden="true" href="#cb13-17" tabindex="-1"></a> } <span class="cf">else</span> <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">"Home"</span>) {</span> |
|
<span id="cb13-18"><a aria-hidden="true" href="#cb13-18" tabindex="-1"></a> items[<span class="dv">0</span>]<span class="op">.</span><span class="fu">focus</span>()<span class="op">;</span> <span class="op"><</span><span class="dv">5</span><span class="op">></span></span> |
|
<span id="cb13-19"><a aria-hidden="true" href="#cb13-19" tabindex="-1"></a></span> |
|
<span id="cb13-20"><a aria-hidden="true" href="#cb13-20" tabindex="-1"></a> } <span class="cf">else</span> <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">"End"</span>) {</span> |
|
<span id="cb13-21"><a aria-hidden="true" href="#cb13-21" tabindex="-1"></a> items[items<span class="op">.</span><span class="at">length</span> <span class="op">-</span> <span class="dv">1</span>]<span class="op">.</span><span class="fu">focus</span>()<span class="op">;</span> <span class="op"><</span><span class="dv">6</span><span class="op">></span></span> |
|
<span id="cb13-22"><a aria-hidden="true" href="#cb13-22" tabindex="-1"></a></span> |
|
<span id="cb13-23"><a aria-hidden="true" href="#cb13-23" tabindex="-1"></a> } <span class="cf">else</span> <span class="cf">if</span> (e<span class="op">.</span><span class="at">key</span> <span class="op">===</span> <span class="st">"Escape"</span>) {</span> |
|
<span id="cb13-24"><a aria-hidden="true" href="#cb13-24" tabindex="-1"></a> <span class="fu">toggleMenu</span>(<span class="kw">false</span>)<span class="op">;</span> <span class="op"><</span><span class="dv">7</span><span class="op">></span></span> |
|
<span id="cb13-25"><a aria-hidden="true" href="#cb13-25" tabindex="-1"></a> button<span class="op">.</span><span class="fu">focus</span>()<span class="op">;</span> <span class="op"><</span><span class="dv">8</span><span class="op">></span></span> |
|
<span id="cb13-26"><a aria-hidden="true" href="#cb13-26" tabindex="-1"></a> }</span> |
|
<span id="cb13-27"><a aria-hidden="true" href="#cb13-27" tabindex="-1"></a>})<span class="op">;</span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>Helper: Get the index in the items array of the currently focused |
|
menu item (0 if none).</p></li> |
|
<li><p>Move focus to the previous menu item when the up arrow key is |
|
pressed.</p></li> |
|
<li><p>Move focus to the next menu item when the down arrow key is |
|
pressed.</p></li> |
|
<li><p>Activate the currently focused element when the space key is |
|
pressed.</p></li> |
|
<li><p>Move focus to the first menu item when Home is pressed.</p></li> |
|
<li><p>Move focus to the last menu item when End is pressed.</p></li> |
|
<li><p>Close menu when Escape is pressed.</p></li> |
|
<li><p>Return focus to menu button when closing menu.</p></li> |
|
</ol> |
|
<p>That should cover all our bases, and we’ll admit that’s a lot of |
|
code. But, in fairness, it’s code that encodes a lot of behavior.</p> |
|
<p>Now, our drop-down menu isn’t perfect, and it doesn’t handle a lot of |
|
things. For example, we don’t support submenus, or menu items being |
|
added or removed dynamically to the menu. If we needed more menu |
|
features like this, it might make more sense to use an off-the-shelf |
|
library, such as GitHub’s <a href="https://github.com/github/details-menu-element"><code>details-menu-element</code></a>.</p> |
|
<p>But, for our relatively simple use case, vanilla JavaScript does a |
|
fine job, and we got to explore ARIA and RSJS while implementing it.</p> |
|
<h2 id="alpine-js">Alpine.js</h2> |
|
<p>OK, so that’s an in-depth look at how to structure plain |
|
VanillaJS-style JavaScript. Let’s turn our attention to an actual |
|
JavaScript framework that enables a different approach for adding |
|
dynamic behavior to your application, <a href="https://alpinejs.dev">Alpine.js</a>.</p> |
|
<p>Alpine is a relatively new JavaScript library that allows developers |
|
to embed JavaScript code directly in HTML, akin to the <code>on*</code> |
|
attributes available in plain HTML and JavaScript. However, Alpine takes |
|
this concept of embedded scripting much further than <code>on*</code> |
|
attributes.</p> |
|
<p>Alpine bills itself as a modern replacement for jQuery, the widely |
|
used, older JavaScript library. As you will see, it definitely lives up |
|
to this promise.</p> |
|
<p>Installing Alpine is very easy: it is a single file and is |
|
dependency-free, so you can simply include it via a CDN:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb14"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb14-1"><a aria-hidden="true" href="#cb14-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">script</span><span class="ot"> src</span><span class="op">=</span><span class="st">"https://unpkg.com/alpinejs"</span><span class="dt">></</span><span class="kw">script</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Installing Alpine</p></figcaption> |
|
</figure> |
|
<p>You can also install it via a package manager such as NPM, or vendor |
|
it from your own server.</p> |
|
<p>Alpine provides a set of HTML attributes, all of which begin with the |
|
<code>x-</code> prefix, the main one of which is <code>x-data</code>. |
|
The content of <code>x-data</code> is a JavaScript expression which |
|
evaluates to an object. The properties of this object can, then, be |
|
accessed within the element that the <code>x-data</code> attribute is |
|
located.</p> |
|
<p>To get a flavor of AlpineJS, let’s look at how to implement our |
|
counter example using it.</p> |
|
<p>For the counter, the only state we need to keep track of is the |
|
current number, so let’s declare a JavaScript object with one property, |
|
<code>count</code>, in an <code>x-data</code> attribute on the div for |
|
our counter:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb15"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb15-1"><a aria-hidden="true" href="#cb15-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"counter"</span><span class="ot"> x-data</span><span class="op">=</span><span class="st">"{ count: 0 }"</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Counter with Alpine, line 1</p></figcaption> |
|
</figure> |
|
<p>This defines our state, that is, the data we are going to be using to |
|
drive dynamic updates to the DOM. With the state declared like this, we |
|
can now use it <em class="test">within</em> the div element it is declared on. Let’s |
|
add an <code>output</code> element with an <code>x-text</code> |
|
attribute.</p> |
|
<p>Next, we will <em class="test">bind</em> the <code>x-text</code> attribute to the |
|
<code>count</code> attribute we declared in the <code>x-data</code> |
|
attribute on the parent <code>div</code> element. This will have the |
|
effect of setting the text of the <code>output</code> element to |
|
whatever the value of <code>count</code> is: if <code>count</code> is |
|
updated, so will the text of the <code>output</code>. This is “reactive” |
|
programming, in that the DOM will “react” to changes to the backing |
|
data.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb16"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb16-1"><a aria-hidden="true" href="#cb16-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> x-data</span><span class="op">=</span><span class="st">"{ count: 0 }"</span><span class="dt">></span></span> |
|
<span id="cb16-2"><a aria-hidden="true" href="#cb16-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">output</span><span class="ot"> x-text</span><span class="op">=</span><span class="st">"count"</span><span class="dt">></</span><span class="kw">output</span><span class="dt">></span> <span class="er"><</span>1></span></code></pre></div> |
|
<figcaption><p>Counter with Alpine, lines 1-2</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The <code>x-text</code> attribute.</p></li> |
|
</ol> |
|
<p>Next, we need to update the count, using a button. Alpine allows you |
|
to attach event listeners with the <code>x-on</code> attribute.</p> |
|
<p>To specify the event to listen for, you add a colon and then the |
|
event name after the <code>x-on</code> attribute name. Then, the value |
|
of the attribute is the JavaScript you wish to execute. This is similar |
|
to the plain <code>on*</code> attributes we discussed earlier, but it |
|
turns out to be much more flexible.</p> |
|
<p>We want to listen for a <code>click</code> event, and we want to |
|
increment <code>count</code> when a click occurs, so here is what the |
|
Alpine code will look like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb17"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb17-1"><a aria-hidden="true" href="#cb17-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> x-data</span><span class="op">=</span><span class="st">"{ count: 0 }"</span><span class="dt">></span></span> |
|
<span id="cb17-2"><a aria-hidden="true" href="#cb17-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">output</span><span class="ot"> x-text</span><span class="op">=</span><span class="st">"count"</span><span class="dt">></</span><span class="kw">output</span><span class="dt">></span></span> |
|
<span id="cb17-3"><a aria-hidden="true" href="#cb17-3" tabindex="-1"></a></span> |
|
<span id="cb17-4"><a aria-hidden="true" href="#cb17-4" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> x-on:click</span><span class="op">=</span><span class="st">"count++"</span><span class="dt">></span>Increment<span class="dt"></</span><span class="kw">button</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb17-5"><a aria-hidden="true" href="#cb17-5" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Counter with Alpine, the full thing</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>With <code>x-on</code>, we specify the event in the attribute |
|
<em class="test">name</em>.</p></li> |
|
</ol> |
|
<p>And that’s all it takes. A simple component like a counter should be |
|
simple to code, and Alpine delivers.</p> |
|
<h3 id="-x-on-click--vs---onclick-">“x-on:click” vs. “onclick”</h3> |
|
<p>As we said, the Alpine <code>x-on:click</code> attribute (or its |
|
shorthand, the <code>@click</code> attribute) is similar to the built-in |
|
<code>onclick</code> attribute. However, it has additional features that |
|
make it significantly more useful:</p> |
|
<ul> |
|
<li><p>You can listen for events from other elements. For example, the |
|
<code>.outside</code> modifier lets you listen to any click event that |
|
is <em class="test">not</em> within the element.</p></li> |
|
<li><p>You can use other modifiers to:</p> |
|
<ul> |
|
<li><p>throttle or debounce event listeners</p></li> |
|
<li><p>ignore events that are bubbled up from descendant |
|
elements</p></li> |
|
<li><p>attach passive listeners</p></li> |
|
</ul></li> |
|
<li><p>You can listen to custom events. For example, if you wanted to |
|
listen for the <code>htmx:after-request</code> event you could write |
|
<code>x-on:htmx:after-request="doSomething()"</code>.</p></li> |
|
</ul> |
|
<h3 id="reactivity-and-templating">Reactivity and Templating</h3> |
|
<p>We hope you’ll agree that the AlpineJS version of the counter widget |
|
is better, in general, than the VanillaJS implementation, which was |
|
either somewhat hacky or spread out over multiple files.</p> |
|
<p>A big part of the power of AlpineJS is that it supports a notion of |
|
“reactive” variables, allowing you to bind the count of the |
|
<code>div</code> element to a variable that both the <code>output</code> |
|
and the <code>button</code> can reference, and properly updating all the |
|
dependencies when a mutation occurs. Alpine allows for much more |
|
elaborate data bindings than we have demonstrated here, and it is an |
|
excellent general purpose client-side scripting library.</p> |
|
<h3 id="_alpine_js_in_action_a_bulk_action_toolbar">Alpine.js in Action: |
|
A Bulk Action Toolbar</h3> |
|
<p>Let’s implement a feature in Contact.app with Alpine. As it stands |
|
currently, Contact.app has a “Delete Selected Contacts” button at the |
|
very bottom of the page. This button has a long name, is not easy to |
|
find and takes up a lot of room. If we wanted to add additional “bulk” |
|
actions, this wouldn’t scale well visually.</p> |
|
<p>In this section, we’ll replace this single button with a toolbar. |
|
Furthermore, the toolbar will only appear when the user starts selecting |
|
contacts. Finally, it will show how many contacts are selected and let |
|
you select all contacts in one go.</p> |
|
<p>The first thing we will need to add is an <code>x-data</code> |
|
attribute, to hold the state that we will use to determine if the |
|
toolbar is visible or not. We will need to place this on an ancestor |
|
element of both the toolbar that we are going to add, as well as the |
|
checkboxes, which will be updating the state when they are checked and |
|
unchecked. The best option given our current HTML is to place the |
|
attribute on the <code>form</code> element that surrounds the contacts |
|
table. We will declare a property, <code>selected</code>, which will be |
|
an array that holds the selected contact ids, based on the checkboxes |
|
that are selected.</p> |
|
<p>Here is what our form tag will look like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb18"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb18-1"><a aria-hidden="true" href="#cb18-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">form</span><span class="ot"> x-data</span><span class="op">=</span><span class="st">"{ selected: [] }"</span><span class="dt">></span> <span class="er"><</span>1></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>This form wraps around the contacts table.</p></li> |
|
</ol> |
|
<p>Next, at the top of the contacts table, we are going to add a |
|
<code>template</code> tag. A template tag is <em class="test">not</em> rendered by a |
|
browser, by default, so you might be surprised that we are using it. |
|
However, by adding an Alpine <code>x-if</code> attribute, we can tell |
|
Alpine: if a condition is true, show the HTML within this template.</p> |
|
<p>Recall that we want to show the toolbar if and only if one or more |
|
contacts are selected. But we know that we will have the ids of the |
|
selected contacts in the <code>selected</code> property. Therefore, we |
|
can check the <em class="test">length</em> of that array to see if there are any |
|
selected contacts, quite easily:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb19"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb19-1"><a aria-hidden="true" href="#cb19-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">template</span><span class="ot"> x-if</span><span class="op">=</span><span class="st">"selected.length > 0"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb19-2"><a aria-hidden="true" href="#cb19-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"box info tool-bar"</span><span class="dt">></span></span> |
|
<span id="cb19-3"><a aria-hidden="true" href="#cb19-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">slot</span><span class="ot"> x-text</span><span class="op">=</span><span class="st">"selected.length"</span><span class="dt">></</span><span class="kw">slot</span><span class="dt">></span></span> |
|
<span id="cb19-4"><a aria-hidden="true" href="#cb19-4" tabindex="-1"></a> contacts selected</span> |
|
<span id="cb19-5"><a aria-hidden="true" href="#cb19-5" tabindex="-1"></a></span> |
|
<span id="cb19-6"><a aria-hidden="true" href="#cb19-6" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> type</span><span class="op">=</span><span class="st">"button"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"bad bg color border"</span><span class="dt">></span>Delete<span class="dt"></</span><span class="kw">button</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb19-7"><a aria-hidden="true" href="#cb19-7" tabindex="-1"></a> <span class="dt"><</span><span class="kw">hr</span><span class="ot"> aria-orientation</span><span class="op">=</span><span class="st">"vertical"</span><span class="dt">></span></span> |
|
<span id="cb19-8"><a aria-hidden="true" href="#cb19-8" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> type</span><span class="op">=</span><span class="st">"button"</span><span class="dt">></span>Cancel<span class="dt"></</span><span class="kw">button</span><span class="dt">></span> <span class="er"><</span>2></span> |
|
<span id="cb19-9"><a aria-hidden="true" href="#cb19-9" tabindex="-1"></a> <span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span> |
|
<span id="cb19-10"><a aria-hidden="true" href="#cb19-10" tabindex="-1"></a><span class="dt"></</span><span class="kw">template</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>Show this HTML if there are 1 or more selected contacts.</p></li> |
|
<li><p>We will implement these buttons in just a moment.</p></li> |
|
</ol> |
|
<p>The next step is to ensure that toggling a checkbox for a given |
|
contact adds (or removes) a given contact’s id from the |
|
<code>selected</code> property. To do this, we will need to use a new |
|
Alpine attribute, <code>x-model</code>. The <code>x-model</code> |
|
attribute allows you to <em class="test">bind</em> a given element to some underlying |
|
data, or its “model.”</p> |
|
<p>In this case, we want to bind the value of the checkbox inputs to the |
|
<code>selected</code> property. This is how we do this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb20"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb20-1"><a aria-hidden="true" href="#cb20-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">td</span><span class="dt">></span></span> |
|
<span id="cb20-2"><a aria-hidden="true" href="#cb20-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">input</span><span class="ot"> type</span><span class="op">=</span><span class="st">"checkbox"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"selected_contact_ids"</span></span> |
|
<span id="cb20-3"><a aria-hidden="true" href="#cb20-3" tabindex="-1"></a><span class="ot"> value</span><span class="op">=</span><span class="st">"{{ contact.id }}"</span><span class="ot"> x-model</span><span class="op">=</span><span class="st">"selected"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb20-4"><a aria-hidden="true" href="#cb20-4" tabindex="-1"></a><span class="dt"></</span><span class="kw">td</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>The <code>x-model</code> attribute binds the <code>value</code> |
|
of this input to the <code>selected</code> property</p></li> |
|
</ol> |
|
<p>Now, when a checkbox is checked or unchecked, the |
|
<code>selected</code> array will be updated with the given row’s contact |
|
id. Furthermore, mutations we make to the <code>selected</code> array |
|
will similarly be reflected in the checkboxes” state. This is known as a |
|
<em class="test">two-way</em> binding.</p> |
|
<p>With this code written, we can make the toolbar appear and disappear, |
|
based on whether contact checkboxes are selected.</p> |
|
<p>Very slick.</p> |
|
<p>Before we move on, you may have noticed our code here includes some |
|
“class=” references. These are for css styling, and are not part of |
|
Alpine.js. We’ve included them only as a reminder that the menu bar |
|
we’re building will require css to work well. The classes in the code |
|
above refer to a minimal css library called Missing.css. If you use |
|
other css libraries, such as Bootstrap, Tailwind, Bulma, Pico.css, etc., |
|
your styling code will be different.</p> |
|
<h4 id="_implementing_actions">Implementing actions</h4> |
|
<p>Now that we have the mechanics of showing and hiding the toolbar, |
|
let’s look at how to implement the buttons within the toolbar.</p> |
|
<p>Let’s first implement the “Clear” button, because it is quite easy. |
|
All we need to do is, when the button is clicked, clear out the |
|
<code>selected</code> array. Because of the two-way binding that Alpine |
|
provides, this will uncheck all the selected contacts (and then hide the |
|
toolbar)!</p> |
|
<p>For the <em class="test">Cancel</em> button, our job is simple:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb21"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb21-1"><a aria-hidden="true" href="#cb21-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> type</span><span class="op">=</span><span class="st">"button"</span><span class="ot"> </span><span class="er">@</span><span class="ot">click</span><span class="op">=</span><span class="st">"selected = []"</span><span class="dt">></span>Cancel<span class="dt"></</span><span class="kw">button</span><span class="dt">></span> <span class="er"><</span>1></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>Reset the <code>selected</code> array.</p></li> |
|
</ol> |
|
<p>Once again, AlpineJS makes this very easy.</p> |
|
<p>The “Delete” button, however, will be a bit more complicated. It will |
|
need to do two things: first it will confirm if the user indeed intends |
|
to delete the contacts selected. Then, if the user confirms the action, |
|
it will use the htmx JavaScript API to issue a <code>DELETE</code> |
|
request.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb22"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb22-1"><a aria-hidden="true" href="#cb22-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> type</span><span class="op">=</span><span class="st">"button"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"bad bg color border"</span></span> |
|
<span id="cb22-2"><a aria-hidden="true" href="#cb22-2" tabindex="-1"></a><span class="ot"> </span><span class="er">@</span><span class="ot">click</span><span class="op">=</span><span class="st">"</span></span> |
|
<span id="cb22-3"><a aria-hidden="true" href="#cb22-3" tabindex="-1"></a><span class="st"> confirm(`Delete ${selected.length} contacts?`) </span><span class="er">&&</span><span class="st"> </span><span class="er"><</span><span class="st">1></span></span> |
|
<span id="cb22-4"><a aria-hidden="true" href="#cb22-4" tabindex="-1"></a><span class="st"> htmx.ajax('DELETE', '/contacts',</span></span> |
|
<span id="cb22-5"><a aria-hidden="true" href="#cb22-5" tabindex="-1"></a><span class="st"> { source: $root, target: document.body }) </span><span class="er"><</span><span class="st">2></span></span> |
|
<span id="cb22-6"><a aria-hidden="true" href="#cb22-6" tabindex="-1"></a><span class="st"> "</span><span class="dt">></span></span> |
|
<span id="cb22-7"><a aria-hidden="true" href="#cb22-7" tabindex="-1"></a> Delete</span> |
|
<span id="cb22-8"><a aria-hidden="true" href="#cb22-8" tabindex="-1"></a><span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>Confirm the user wishes to delete the selected number of |
|
contacts.</p></li> |
|
<li><p>Issue a <code>DELETE</code> using the htmx JavaScript |
|
API.</p></li> |
|
</ol> |
|
<p>Note that we are using the short-circuiting behavior of the |
|
<code>&&</code> operator in JavaScript to avoid the call to |
|
<code>htmx.ajax()</code> if the <code>confirm()</code> call returns |
|
false.</p> |
|
<p>The <code>htmx.ajax()</code> function is just a way to access the |
|
normal, HTML-driven hypermedia exchange that htmx’s HTML attributes give |
|
you directly from JavaScript.</p> |
|
<p>Looking at how we call <code>htmx.ajax</code>, we first pass in that |
|
we want to issue a <code>DELETE</code> to <code>/contacts</code>. We |
|
then pass in two additional pieces of information: <code>source</code> |
|
and <code>target</code>. The <code>source</code> property is the element |
|
from which htmx will collect data to include in the request. We set this |
|
to <code>$root</code>, which is a special symbol in Alpine that will be |
|
the element that has the <code>x-data</code> attribute declared on it. |
|
In this case, it will be the form containing all of our contacts. The |
|
<code>target</code>, or where the response HTML will be placed, is just |
|
the entire document’s body, since the <code>DELETE</code> handler |
|
returns a whole page when it completes.</p> |
|
<p>Note that we are using Alpine here in a Hypermedia-Driven Application |
|
compatible manner. We <em class="test">could</em> have issued an AJAX request |
|
directly from Alpine and perhaps updated an <code>x-data</code> property |
|
depending on the results of that request. But, instead, we delegated to |
|
htmx’s JavaScript API, which made a <em class="test">hypermedia exchange</em> with |
|
the server.</p> |
|
<p>This is the key to scripting in a hypermedia-friendly manner within a |
|
Hypermedia-Driven Application.</p> |
|
<p>So, with all of this in place, we now have a much improved experience |
|
for performing bulk actions on contacts: less visual clutter and the |
|
toolbar can be extended with more options without creating bloat in the |
|
main interface of our app.</p> |
|
<h2 id="-hyperscript">_hyperscript</h2> |
|
<p>The final scripting technology we are going to look at is a bit |
|
further afield: <a href="https://hyperscript.org">_hyperscript</a>. The |
|
authors of this book initially created _hyperscript as a sibling project |
|
to htmx. We felt that JavaScript wasn’t event-oriented enough, which |
|
made adding small scripting enhancements to htmx applications |
|
cumbersome.</p> |
|
<p>While the previous two examples are JavaScript-oriented, _hyperscript |
|
has a completely different syntax than JavaScript, based on an older |
|
language called HyperTalk. HyperTalk was the scripting language for a |
|
technology called HyperCard, an old hypermedia system available on early |
|
Macintosh Computers.</p> |
|
<p>The most noticeable thing about _hyperscript is that it resembles |
|
English prose more than it resembles other programming languages.</p> |
|
<p>Like Alpine, _hyperscript is a modern jQuery replacement. Also like |
|
Alpine, _hyperscript allows you to write your scripting inline, in |
|
HTML.</p> |
|
<p>Unlike Alpine, however, _hyperscript is <em class="test">not</em> reactive. It |
|
instead focuses on making DOM manipulations in response to events easy |
|
to write and easy to read. It has built-in language constructs for many |
|
DOM operations, preventing you from needing to navigate the |
|
sometimes-verbose JavaScript DOM APIs.</p> |
|
<p>We will give a small taste of what scripting in the _hyperscript |
|
language is like, so you can pursue the language in more depth later if |
|
you find it interesting.</p> |
|
<p>Like htmx and AlpineJS, _hyperscript can be installed via a CDN or |
|
from npm (package name <code>hyperscript.org</code>):</p> |
|
<figure> |
|
<div class="sourceCode" id="cb23"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb23-1"><a aria-hidden="true" href="#cb23-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">script</span><span class="ot"> src</span><span class="op">=</span><span class="st">"//unpkg.com/hyperscript.org"</span><span class="dt">></</span><span class="kw">script</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>Installing _hyperscript via CDN</p></figcaption> |
|
</figure> |
|
<p>_hyperscript uses the <code>_</code> (underscore) attribute for |
|
putting scripting on DOM elements. You may also use the |
|
<code>script</code> or <code>data-script</code> attributes, depending on |
|
your HTML validation needs.</p> |
|
<p>Let’s look at how to implement the simple counter component we have |
|
been looking at using _hyperscript. We will place an <code>output</code> |
|
element and a <code>button</code> inside of a <code>div</code>. To |
|
implement the counter, we will need to add a small bit of _hyperscript |
|
to the button. On a click, the button should increment the text of the |
|
previous <code>output</code> tag.</p> |
|
<p>As you’ll see, that last sentence is close to the actual _hyperscript |
|
code:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb24"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb24-1"><a aria-hidden="true" href="#cb24-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">"counter"</span><span class="dt">></span></span> |
|
<span id="cb24-2"><a aria-hidden="true" href="#cb24-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">output</span><span class="dt">></span>0<span class="dt"></</span><span class="kw">output</span><span class="dt">></span></span> |
|
<span id="cb24-3"><a aria-hidden="true" href="#cb24-3" tabindex="-1"></a> <span class="dt"><</span><span class="kw">button</span><span class="ot"> _</span><span class="op">=</span><span class="st">"on click</span></span> |
|
<span id="cb24-4"><a aria-hidden="true" href="#cb24-4" tabindex="-1"></a><span class="st"> increment the textContent of the previous </span><span class="er"><</span><span class="st">output/>"</span><span class="dt">></span> <span class="er"><</span>1></span> |
|
<span id="cb24-5"><a aria-hidden="true" href="#cb24-5" tabindex="-1"></a> Increment</span> |
|
<span id="cb24-6"><a aria-hidden="true" href="#cb24-6" tabindex="-1"></a> <span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span> |
|
<span id="cb24-7"><a aria-hidden="true" href="#cb24-7" tabindex="-1"></a><span class="dt"></</span><span class="kw">div</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<ol> |
|
<li><p>The _hyperscript code added inline to the button.</p></li> |
|
</ol> |
|
<p>Let’s go through each component of this script:</p> |
|
<ul> |
|
<li><p><code>on click</code> is an event listener, telling the button to |
|
listen for a <code>click</code> event and then executing the remaining |
|
code.</p></li> |
|
<li><p><code>increment</code> is a “command” in _hyperscript that |
|
“increments” things, similar to the <code>++</code> operator in |
|
JavaScript.</p></li> |
|
<li><p><code>the</code> doesn’t have any semantic meaning in |
|
_hyperscript, but can be used to make scripts more readable.</p></li> |
|
<li><p><code>textContent of</code> is one form of <em class="test">property |
|
access</em> in _hyperscript. You are probably familiar with the |
|
JavaScript syntax <code>a.b</code>, meaning “Get the property |
|
<code>b</code> on object <code>a</code>. _hyperscript supports this |
|
syntax, but also supports the forms <code>b of a</code> and |
|
<code>a’s b</code>. Which one you use should depend on which one is most |
|
readable.</p></li> |
|
<li><p><code>previous</code> is an expression in _hyperscript that finds |
|
the previous element in the DOM that matches some condition.</p></li> |
|
<li><p><code><output /></code> is a <em class="test">query literal</em>, which |
|
is a CSS selector wrapped between <code><</code> and |
|
<code>/></code>.</p></li> |
|
</ul> |
|
<p>In this code, the <code>previous</code> keyword (and the accompanying |
|
<code>next</code> keyword) is an example of how _hyperscript makes DOM |
|
operations easier: there is no such native functionality to be found in |
|
the standard DOM API, and implementing this in VanillaJS is trickier |
|
than you might think!</p> |
|
<p>So, you can see, _hyperscript is very expressive, particularly when |
|
it comes to DOM manipulations. This makes it easier to embed scripts |
|
directly in HTML: since the scripting language is more powerful, scripts |
|
written in it tend to be shorter and easier to read.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Natural Language Programming?</strong></p> |
|
</div> |
|
<div> |
|
<p>Seasoned programmers may be suspicious of _hyperscript: There have |
|
been many “natural language programming” (NLP) projects that target |
|
non-programmers and beginner programmers, assuming that being able to |
|
read code in their “natural language” will give them the ability to |
|
write it as well. This has led to some badly written and structured code |
|
and has failed to live up to the (often over the top) hype.</p> |
|
<p>_hyperscript is <em class="test">not</em> an NLP programming language. Yes, its |
|
syntax is inspired in many places by the speech patterns of web |
|
developers. But _hyperscript’s readability is achieved not through |
|
complex heuristics or fuzzy NLP processing, but rather through judicious |
|
use of common parsing tricks, coupled with a culture of readability.</p> |
|
<p>As you can see in the above example, with the use of a <em class="test">query |
|
reference</em>, <code><output/></code>, _hyperscript does not shy |
|
away from using DOM-specific, non-natural language when appropriate.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_hyperscript_in_action_a_keyboard_shortcut">_hyperscript in |
|
Action: A Keyboard Shortcut</h3> |
|
<p>While the counter demo is a good way to compare various approaches to |
|
scripting, the rubber meets the road when you try to actually implement |
|
a useful feature with an approach. For _hyperscript, let’s add a |
|
keyboard shortcut to Contact.app: when a user hits Alt+S in our app, we |
|
will focus the search field.</p> |
|
<p>Since our keyboard shortcut focuses the search input, let’s put the |
|
code for it on that search input, satisfying locality.</p> |
|
<p>Here is the original HTML for the search input:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb25"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb25-1"><a aria-hidden="true" href="#cb25-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Search Contacts"</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<p>We will add an event listener using the <code>on keydown</code> |
|
syntax, which will fire whenever a keydown occurs. Further, we can use |
|
an <em class="test">event filter</em> syntax in _hyperscript using square brackets |
|
after the event. In the square brackets we can place a <em class="test">filter |
|
expression</em> that will filter out <code>keydown</code> events we |
|
aren’t interested in. In our case, we only want to consider events where |
|
the Alt key is held down and where the “S” key is being pressed. We can |
|
create a boolean expression that inspects the <code>altKey</code> |
|
property (to see if it is <code>true</code>) and the <code>code</code> |
|
property (to see if it is <code>"KeyS"</code>) of the event to achieve |
|
this.</p> |
|
<p>So far our _hyperscript looks like this:</p> |
|
<figure> |
|
<pre class="hyperscript"><code>on keydown[altKey and code is 'KeyS'] ... |
|
</code></pre> |
|
<figcaption><p>A start on our keyboard shortcut</p></figcaption> |
|
</figure> |
|
<p>Now, by default, _hyperscript will listen for a given event <em class="test">on |
|
the element where it is declared</em>. So, with the script we have, we |
|
would only get <code>keydown</code> events if the search box is already |
|
focused. That’s not what we want! We want to have this key work |
|
<em class="test">globally</em>, no matter which element has focus.</p> |
|
<p>Not a problem! We can listen for the <code>keyDown</code> event |
|
elsewhere by using a <code>from</code> clause in our event handler. In |
|
this case we want to listen for the <code>keyDown</code> from the |
|
window, and our code ends up looking, naturally, like this:</p> |
|
<figure> |
|
<pre class="hyperscript"><code>on keydown[altKey and code is 'KeyS'] from window ... |
|
</code></pre> |
|
<figcaption><p>Listening globally</p></figcaption> |
|
</figure> |
|
<p>Using the <code>from</code> clause, we can attach the listener to the |
|
window while, at the same time, keeping the code on the element it |
|
logically relates to.</p> |
|
<p>Now that we’ve picked out the event we want to use to focus the |
|
search box, let’s implement the actual focusing by calling the standard |
|
<code>.focus()</code> method.</p> |
|
<p>Here is the entire script, embedded in HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb28"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb28-1"><a aria-hidden="true" href="#cb28-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">input</span><span class="ot"> id</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> name</span><span class="op">=</span><span class="st">"q"</span><span class="ot"> type</span><span class="op">=</span><span class="st">"search"</span><span class="ot"> placeholder</span><span class="op">=</span><span class="st">"Search Contacts"</span></span> |
|
<span id="cb28-2"><a aria-hidden="true" href="#cb28-2" tabindex="-1"></a><span class="ot"> _</span><span class="op">=</span><span class="st">"on keydown[altKey and code is 'KeyS'] from the window</span></span> |
|
<span id="cb28-3"><a aria-hidden="true" href="#cb28-3" tabindex="-1"></a><span class="st"> focus() me"</span><span class="dt">></span> <span class="er"><</span>1></span></code></pre></div> |
|
<figcaption><p>Our final script</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>“me” refers to the element that the script is written |
|
on.</p></li> |
|
</ol> |
|
<p>Given all the functionality, this is surprisingly terse, and, as an |
|
English-like programming language, pretty easy to read.</p> |
|
<h3 id="_why_a_new_programming_language">Why a New Programming |
|
Language?</h3> |
|
<p>This is all well and good, but you may be thinking “An entirely new |
|
scripting language? That seems excessive.” And, at some level, you are |
|
right: JavaScript is a decent scripting language, is very well optimized |
|
and is widely understood in web development. On the other hand, by |
|
creating an entirely new front end scripting language, we had the |
|
freedom to address some problems that we saw generating ugly and verbose |
|
code in JavaScript:</p> |
|
<dl> |
|
<dt>Async transparency</dt> |
|
<dd> |
|
<p>In _hyperscript, asynchronous functions (i.e., functions that return |
|
<code>Promise</code> instances) can be invoked <em class="test">as if they were |
|
synchronous</em>. Changing a function from sync to async does not break |
|
any _hyperscript code that calls it. This is achieved by checking for a |
|
Promise when evaluating any expression, and suspending the running |
|
script if one exists (only the current event handler is suspended and |
|
the main thread is not blocked). JavaScript, instead, requires either |
|
the explicit use of callbacks <em class="test">or</em> the use of explicit |
|
<code>async</code> annotations (which can’t be mixed with synchronous |
|
code).</p> |
|
</dd> |
|
<dt>Array property access</dt> |
|
<dd> |
|
<p>In _hyperscript, accessing a property on an array (other than |
|
<code>length</code> or a number) will return an array of the values of |
|
property on each member of that array, making array property access act |
|
like a flat-map operation. jQuery has a similar feature, but only for |
|
its own data structure.</p> |
|
</dd> |
|
<dt>Native CSS Syntax</dt> |
|
<dd> |
|
<p>In _hyperscript, you can use things like CSS class and ID literals, |
|
or CSS query literals, directly in the language, rather than needing to |
|
call out to a wordy DOM API, as you do in JavaScript.</p> |
|
</dd> |
|
<dt>Deep Event Support</dt> |
|
<dd> |
|
<p>Working with events in _hyperscript is far more pleasant than working |
|
with them in JavaScript, with native support for responding to and |
|
sending events, as well as for common event-handling patterns such as |
|
“debouncing” or rate limiting events. _hyperscript also provides |
|
declarative mechanisms for synchronizing events within a given element |
|
and across multiple elements.</p> |
|
</dd> |
|
</dl> |
|
<p>Again we wish to stress that, in this example, we are not stepping |
|
outside the lines of a Hypermedia-Driven Application: we are only adding |
|
frontend, client-side functionality with our scripting. We are not |
|
creating and managing a large amount of state outside of the DOM itself, |
|
or communicating with the server in a non-hypermedia exchange.</p> |
|
<p>Additionally, since _hyperscript embeds so well in HTML, it keeps the |
|
focus <em class="test">on the hypermedia</em>, rather than on the scripting |
|
logic.</p> |
|
<p>It may not fit all scripting styles or needs, but _hyperscript can |
|
provide an excellent scripting experience for Hypermedia-Driven |
|
Applications. It is a small and obscure programming language worth a |
|
look to understand what it is trying to achieve.</p> |
|
<h2 id="_using_off_the_shelf_components">Using Off-the-Shelf |
|
Components</h2> |
|
<p>That concludes our look at three different options for <em class="test">your</em> |
|
scripting infrastructure, that is, the code that <em class="test">you</em> write to |
|
enhance your Hypermedia-Driven Application. However, there is another |
|
major area to consider when discussing client side scripting: “off the |
|
shelf” components. That is, JavaScript libraries that other people have |
|
created that offer some sort of functionality, such as showing modal |
|
dialogs.</p> |
|
<p>Components have become very popular in the web development world, |
|
with libraries like <a href="https://datatables.net/">DataTables</a> |
|
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 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!</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>Web Components</strong></p> |
|
</div> |
|
<div> |
|
<p>Web Components is the collective name of a few standards; Custom |
|
Elements and Shadow DOM, and <code><template></code> and |
|
<code><slot></code>.</p> |
|
<p>All of these standards bring useful capabilities to the table. |
|
<code><template></code> elements remove their contents from the |
|
document, while still parsing them as HTML (unlike comments) and making |
|
them accessible to JavaScript. Custom Elements let us initialize and |
|
tear down behaviors when elements are added or removed, which would |
|
previously require manual work or MutationObservers. Shadow DOM lets us |
|
encapsulate elements, leaving the “light” (non-shadow) DOM clean.</p> |
|
<p>However, trying to reap these benefits is often frustrating. Some |
|
difficulties are simply growing pains of new standards (like the |
|
accessibility problems of Shadow DOM) that are actively being worked on. |
|
Others are the result of Web Components trying to be too many things at |
|
the same time:</p> |
|
<ul> |
|
<li><p>An extension mechanism for HTML. To this end, each custom element |
|
is a tag we add to the language.</p></li> |
|
<li><p>A lifecycle mechanism for behaviors. Methods like |
|
<code>createdCallback</code>, <code>connectedCallback</code>, etc. allow |
|
behavior to be added to elements without needing to be manually invoked |
|
when those elements are added.</p></li> |
|
<li><p>A unit of encapsulation. Shadow DOM insulates elements from their |
|
surroundings.</p></li> |
|
</ul> |
|
<p>The result is that if you want any one of these things, the others |
|
come along for the ride. If you want to attach some behaviors to some |
|
elements using lifecycle callbacks, you need to create a new tag, which |
|
means you can’t have multiple behaviors on one element, and you isolate |
|
elements you add from elements already in the page, which is a problem |
|
if they need to have ARIA relationships.</p> |
|
<p>When should we use Web Components? A good rule of thumb is to ask |
|
yourself: “Could this reasonably be a built-in HTML element?” For |
|
example, a code editor is a good candidate, since HTML already has |
|
<code><textarea></code> and <code>contenteditable</code> elements. |
|
In addition, a fully-featured code editor will have many child elements |
|
that won’t provide much information anyway. We can use features like <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM">Shadow |
|
DOM</a> to encapsulate these elements<a class="footnote-ref" href="#fn2" id="fnref2" role="doc-noteref"><sup>2</sup></a>. We can create a <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements">custom |
|
element</a>, <code><code-area></code>, that we can drop into our |
|
page whenever we want.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_integration_options">Integration Options</h3> |
|
<p>The best JavaScript libraries to work with when you are building a |
|
Hypermedia-Driven Application are ones that:</p> |
|
<ul> |
|
<li><p>Mutate the DOM but don’t communicate with a server over |
|
JSON</p></li> |
|
<li><p>Respect HTML norms (e.g., using <code>input</code> elements to |
|
store values)</p></li> |
|
<li><p>Trigger many custom events as the library updates things</p></li> |
|
</ul> |
|
<p>The last point, triggering many custom events (over the alternative |
|
of using lots of methods and callbacks) is especially important, as |
|
these custom events can be dispatched or listened to without additional |
|
glue code written in a scripting language.</p> |
|
<p>Let’s take a look at two different approaches to scripting, one using |
|
JavaScript call backs, and one using events.</p> |
|
<p>To make things concrete, let’s implement a better confirmation dialog |
|
for the <code>DELETE</code> button we created in Alpine in the previous |
|
section. In the original example we used the <code>confirm()</code> |
|
function built in to JavaScript, which shows a pretty bare-bones system |
|
confirmation dialog. We will replace this function with a popular |
|
JavaScript library, SweetAlert2, that shows a much nicer looking |
|
confirmation dialog. Unlike the <code>confirm()</code> function, which |
|
blocks and returns a boolean (<code>true</code> if the user confirmed, |
|
<code>false</code> otherwise), SweetAlert2 returns a |
|
<code>Promise</code> object, which is a JavaScript mechanism for hooking |
|
in a callback once an asynchronous action (such as waiting for a user to |
|
confirm or deny an action) completes.</p> |
|
<h4 id="_integrating_using_callbacks">Integrating using callbacks</h4> |
|
<p>With SweetAlert2 installed as a library, you have access to the |
|
<code>Swal</code> object, which has a <code>fire()</code> function on it |
|
to trigger showing an alert. You can pass in arguments to the |
|
<code>fire()</code> method to configure exactly what the buttons on the |
|
confirmation dialog look like, what the title of the dialog is, and so |
|
forth. We won’t get into these details too much, but you will see what a |
|
dialog looks like in a bit.</p> |
|
<p>So, given we have installed the SweetAlert2 library, we can swap it |
|
in place of the <code>confirm()</code> function call. We then need to |
|
restructure the code to pass a <em class="test">callback</em> to the |
|
<code>then()</code> method on the <code>Promise</code> that |
|
<code>Swal.fire()</code> returns. A deep dive into Promises is beyond |
|
the scope of this chapter, but suffice to say that this callback will be |
|
called when a user confirms or denies the action. If the user confirmed |
|
the action, then the <code>result.isConfirmed</code> property will be |
|
<code>true</code>.</p> |
|
<p>Given all that, our updated code will look like this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb29"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb29-1"><a aria-hidden="true" href="#cb29-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> type</span><span class="op">=</span><span class="st">"button"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"bad bg color border"</span></span> |
|
<span id="cb29-2"><a aria-hidden="true" href="#cb29-2" tabindex="-1"></a><span class="ot"> </span><span class="er">@</span><span class="ot">click</span><span class="op">=</span><span class="st">"Swal.fire({ </span><span class="er"><</span><span class="st">1></span></span> |
|
<span id="cb29-3"><a aria-hidden="true" href="#cb29-3" tabindex="-1"></a><span class="st"> title: 'Delete these contacts?', </span><span class="er"><</span><span class="st">2></span></span> |
|
<span id="cb29-4"><a aria-hidden="true" href="#cb29-4" tabindex="-1"></a><span class="st"> showCancelButton: true,</span></span> |
|
<span id="cb29-5"><a aria-hidden="true" href="#cb29-5" tabindex="-1"></a><span class="st"> confirmButtonText: 'Delete'</span></span> |
|
<span id="cb29-6"><a aria-hidden="true" href="#cb29-6" tabindex="-1"></a><span class="st"> }).then((result) => { </span><span class="er"><</span><span class="st">3></span></span> |
|
<span id="cb29-7"><a aria-hidden="true" href="#cb29-7" tabindex="-1"></a><span class="st"> if (result.isConfirmed) htmx.ajax('DELETE', '/contacts',</span></span> |
|
<span id="cb29-8"><a aria-hidden="true" href="#cb29-8" tabindex="-1"></a><span class="st"> { source: $root, target: document.body })</span></span> |
|
<span id="cb29-9"><a aria-hidden="true" href="#cb29-9" tabindex="-1"></a><span class="st"> });"</span></span> |
|
<span id="cb29-10"><a aria-hidden="true" href="#cb29-10" tabindex="-1"></a><span class="dt">></span>Delete<span class="dt"></</span><span class="kw">button</span><span class="dt">></span></span></code></pre></div> |
|
<figcaption><p>A callback-based confirmation dialog</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Invoke the <code>Swal.fire()</code> function</p></li> |
|
<li><p>Configure the dialog</p></li> |
|
<li><p>Handle the result of the user’s selection</p></li> |
|
</ol> |
|
<p>And now, when this button is clicked, we get a nice looking dialog in |
|
our web application (<a class="ref" href="#fig-swal-screenshot">[fig-swal-screenshot]</a>) — much nicer than the system |
|
confirmation dialog. Still, this feels a little wrong. This is a lot of |
|
code to write just to trigger a slightly nicer <code>confirm()</code>, |
|
isn’t it? And the htmx JavaScript code we are using here feels awkward. |
|
It would be more natural to move the htmx out to attributes on the |
|
button, as we have been doing, and then trigger the request via |
|
events.</p> |
|
<figure id="fig-swal-screenshot"> |
|
<p><img src="https://hypermedia.systems/images/screenshot_sweet_alert.png"/></p> |
|
<figcaption><p>A SweetAlert dialog box</p></figcaption> |
|
</figure> |
|
<p>So let’s take a different approach and see how that looks.</p> |
|
<h4 id="_integrating_using_events">Integrating using events</h4> |
|
<p>To clean this code up, we will pull the <code>Swal.fire()</code> code |
|
out to a custom JavaScript function we will create called |
|
<code>sweetConfirm()</code>. <code>sweetConfirm()</code> will take the |
|
dialog options that are passed into the <code>fire()</code> method, as |
|
well as the element that is confirming an action. The big difference |
|
here is that the new <code>sweetConfirm()</code> function, rather than |
|
calling some htmx directly, will instead trigger a |
|
<code>confirmed</code> event on the button when the user confirms they |
|
wish to delete.</p> |
|
<p>Here is what our JavaScript function looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb30"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span id="cb30-1"><a aria-hidden="true" href="#cb30-1" tabindex="-1"></a><span class="kw">function</span> <span class="fu">sweetConfirm</span>(elt<span class="op">,</span> config) {</span> |
|
<span id="cb30-2"><a aria-hidden="true" href="#cb30-2" tabindex="-1"></a> Swal<span class="op">.</span><span class="fu">fire</span>(config) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb30-3"><a aria-hidden="true" href="#cb30-3" tabindex="-1"></a> <span class="op">.</span><span class="fu">then</span>((result) <span class="kw">=></span> {</span> |
|
<span id="cb30-4"><a aria-hidden="true" href="#cb30-4" tabindex="-1"></a> <span class="cf">if</span> (result<span class="op">.</span><span class="at">isConfirmed</span>) {</span> |
|
<span id="cb30-5"><a aria-hidden="true" href="#cb30-5" tabindex="-1"></a> elt<span class="op">.</span><span class="fu">dispatchEvent</span>(<span class="kw">new</span> <span class="bu">Event</span>(<span class="st">'confirmed'</span>))<span class="op">;</span> <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb30-6"><a aria-hidden="true" href="#cb30-6" tabindex="-1"></a> }</span> |
|
<span id="cb30-7"><a aria-hidden="true" href="#cb30-7" tabindex="-1"></a> })<span class="op">;</span></span> |
|
<span id="cb30-8"><a aria-hidden="true" href="#cb30-8" tabindex="-1"></a>}</span></code></pre></div> |
|
<figcaption><p>An event-based confirmation dialog</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Pass the config through to the <code>fire()</code> |
|
function.</p></li> |
|
<li><p>If the user confirmed the action, trigger a |
|
<code>confirmed</code> event.</p></li> |
|
</ol> |
|
<p>With this method available, we can now tighten up our delete button |
|
quite a bit. We can remove all the SweetAlert2 code that we had in the |
|
<code>@click</code> Alpine attribute, and simply call this new |
|
<code>sweetConfirm()</code> method, passing in the arguments |
|
<code>$el</code>, which is the Alpine syntax for getting “the current |
|
element” that the script is on, and then the exact configuration we want |
|
for our dialog.</p> |
|
<p>If the user confirms the action, a <code>confirmed</code> event will |
|
be triggered on the button. This means that we can go back to using our |
|
trusty htmx attributes! Namely, we can move <code>DELETE</code> to an |
|
<code>hx-delete</code> attribute, and we can use <code>hx-target</code> |
|
to target the body. And then, and here is the crucial step, we can use |
|
the <code>confirmed</code> event that is triggered in the |
|
<code>sweetConfirm()</code> function, to trigger the request, but adding |
|
an <code>hx-trigger</code> for it.</p> |
|
<p>Here is what our code looks like:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb31"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb31-1"><a aria-hidden="true" href="#cb31-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">button</span><span class="ot"> type</span><span class="op">=</span><span class="st">"button"</span><span class="ot"> class</span><span class="op">=</span><span class="st">"bad bg color border"</span></span> |
|
<span id="cb31-2"><a aria-hidden="true" href="#cb31-2" tabindex="-1"></a><span class="ot"> hx-delete</span><span class="op">=</span><span class="st">"/contacts"</span><span class="ot"> hx-target</span><span class="op">=</span><span class="st">"body"</span><span class="ot"> hx-trigger</span><span class="op">=</span><span class="st">"confirmed"</span><span class="ot"> </span><span class="er"><1</span><span class="dt">></span></span> |
|
<span id="cb31-3"><a aria-hidden="true" href="#cb31-3" tabindex="-1"></a> @click="sweetConfirm($el, { <span class="er"><</span>2></span> |
|
<span id="cb31-4"><a aria-hidden="true" href="#cb31-4" tabindex="-1"></a> title: 'Delete these contacts?', <span class="er"><</span>3></span> |
|
<span id="cb31-5"><a aria-hidden="true" href="#cb31-5" tabindex="-1"></a> showCancelButton: true,</span> |
|
<span id="cb31-6"><a aria-hidden="true" href="#cb31-6" tabindex="-1"></a> confirmButtonText: 'Delete'</span> |
|
<span id="cb31-7"><a aria-hidden="true" href="#cb31-7" tabindex="-1"></a> })"></span></code></pre></div> |
|
<figcaption><p>An Event-based Confirmation Dialog</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Our htmx attributes are back.</p></li> |
|
<li><p>We pass the button in to the function, so an event can be |
|
triggered on it.</p></li> |
|
<li><p>We pass through the SweetAlert2 configuration |
|
information.</p></li> |
|
</ol> |
|
<p>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 |
|
<code>sweetConfirm()</code> function fires an event that htmx is able to |
|
listen for.</p> |
|
<p>This is why a rich event model is important to look for when choosing |
|
a library to work with, both with htmx and with Hypermedia-Driven |
|
Applications in general.</p> |
|
<p>Unfortunately, due to the prevalence and dominance of the |
|
JavaScript-first mindset today, many libraries are like SweetAlert2: |
|
they expect you to pass a callback in the first style. In these cases |
|
you can use the technique we have demonstrated here, wrapping the |
|
library in a function that triggers events in a callback, to make the |
|
library more hypermedia and htmx-friendly.</p> |
|
<h2 id="_pragmatic_scripting">Pragmatic Scripting</h2> |
|
<blockquote> |
|
<p>In case of conflict, consider users over authors over implementors |
|
over specifiers over theoretical purity.</p> |
|
</blockquote><p class="quote-attribution"> W3C, HTML Design Principles § 3.2 Priority of Constituencies</p> |
|
<p>We have looked at several tools and techniques for scripting in a |
|
Hypermedia-Driven Application. How should you pick between them? The sad |
|
truth is that there will never be a single, always correct answer to |
|
this question.</p> |
|
<p>Are you committed to vanilla JavaScript-only, perhaps due to company |
|
policy? Well, you can use vanilla JavaScript effectively to script your |
|
Hypermedia-Driven Application.</p> |
|
<p>Do you have more leeway and like the look of Alpine.js? That’s a |
|
great way to add more structured, localized JavaScript to your |
|
application, and offers some nice reactive features as well.</p> |
|
<p>Are you a bit more bold in your technical choices? Maybe _hyperscript |
|
is worth a look. (We certainly think so.)</p> |
|
<p>Sometimes you might even consider picking two (or more) of these |
|
approaches within an application. Each has its own strengths and |
|
weaknesses, and all of them are relatively small and self-contained, so |
|
picking the right tool for the job at hand might be the best |
|
approach.</p> |
|
<p>In general, we encourage a <em class="test">pragmatic</em> approach to scripting: |
|
whatever feels right is probably right (or, at least, right |
|
<em class="test">enough</em>) for you. Rather than being concerned about which |
|
particular approach is taken for your scripting, we would focus on these |
|
more general concerns:</p> |
|
<ul> |
|
<li><p>Avoid communicating with the server via JSON data APIs.</p></li> |
|
<li><p>Avoid storing large amounts of state outside of the DOM.</p></li> |
|
<li><p>Favor using events, rather than hard-coded callbacks or method |
|
calls.</p></li> |
|
</ul> |
|
<p>And even on these topics, sometimes a web developer has to do what a |
|
web developer has to do. If the perfect widget for your application |
|
exists but uses a JSON data API? That’s OK.</p> |
|
<p>Just don’t make it a habit.</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: HTML is for Applications</h2> |
|
<p>A prevalent meme among developers suggests that HTML was designed for |
|
“documents” and is unsuitable for “applications.” In reality, hypermedia |
|
is not only a sophisticated, modern architecture for applications, but |
|
it can allow us to do away with this artificial app/document split for |
|
good.</p> |
|
<blockquote> |
|
<p>When I say Hypertext, I mean the simultaneous presentation of |
|
information and controls such that the information becomes the |
|
affordance through which the user obtains choices and selects |
|
actions.</p> |
|
</blockquote><p class="quote-attribution"> Roy Fielding, <a href="https://www.slideshare.net/royfielding/a-little-rest-and-relaxation">A |
|
little REST and Relaxation</a></p> |
|
<p>HTML allows documents to contain rich multimedia including images, |
|
audio, video, JavaScript programs, vector graphics and (with some help) |
|
3D environments. More importantly, however, it allows interactive |
|
controls to be embedded within these documents, allowing the information |
|
itself to be the app through which it is accessed.</p> |
|
<p>Consider: Is it not mind-boggling that a single application — which |
|
works on all types of computers and OSs — can let you read news, place |
|
video calls, compose documents, enter virtual worlds, and do almost any |
|
other everyday computing task?</p> |
|
<p>Unfortunately, it is the interactive capabilities of HTML that is its |
|
least developed aspect. For reasons unknown to us, while HTML made it to |
|
version 5 and became a Living Standard, accreting many game-changing |
|
features on the way, the data interactions in it are still mainly |
|
restricted to links and forms. It’s up to developers to extend HTML, and |
|
we want to do so in a way that doesn’t abstract over its simplicity with |
|
an imitation of classical “native” toolkits.</p> |
|
<blockquote> |
|
<ul> |
|
<li><p><span class="smallcaps">Software was not supposed to use native |
|
toolkits</span></p></li> |
|
<li><p><span class="smallcaps">Years of windows UI libraries</span> yet |
|
<span class="smallcaps">no real-world use found</span> for going lower |
|
level than <span class="smallcaps">the Web</span></p></li> |
|
<li><p>Wanted a window anyway for a laugh? We had a tool for that: It |
|
was called “<span class="smallcaps">Electron</span>”</p></li> |
|
<li><p>“yes I would love to write 4 <span class="smallcaps">different</span> copies of the same UI” - Statements |
|
dreamed up by the Utterly Deranged</p></li> |
|
</ul> |
|
</blockquote><p class="quote-attribution"> Leah Clark, @leah@tilde.zone</p> |
|
</div> |
|
</div> |
|
<section class="footnotes footnotes-end-of-document" id="footnotes" role="doc-endnotes"> |
|
<hr/> |
|
<ol> |
|
<li id="fn1"><p>Rendering here refers to HTML generation. Framework |
|
support for server-side rendering is not needed in a HDA because |
|
generating HTML on the server is the default.<a class="footnote-back" href="#fnref1" role="doc-backlink">↩︎</a></p></li> |
|
<li id="fn2"><p>Beware that Shadow DOM is a newer web platform feature |
|
that’s still in development at the time of writing. In particular, there |
|
are some accessibility bugs that may occur when elements inside and |
|
outside the shadow root interact.<a class="footnote-back" href="#fnref2" role="doc-backlink">↩︎</a></p></li> |
|
</ol> |
|
</section> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">JSON Data APIs</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/json-data-apis/#_hypermedia_apis_json_data_apis">Hypermedia APIs & JSON Data |
|
APIs</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/json-data-apis/#_differences_between_hypermedia_apis_data_apis">Differences |
|
Between Hypermedia APIs & Data APIs</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/json-data-apis/#_adding_a_json_data_api_to_contact_app">Adding a JSON Data API |
|
To Contact.app</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/json-data-apis/#_picking_a_root_url_for_our_api">Picking a Root URL For Our |
|
API</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/json-data-apis/#our-first-json-endpoint--listing-all-contacts">Our First JSON Endpoint: Listing All Contacts</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/json-data-apis/#adding-contacts">Adding Contacts</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/json-data-apis/#_viewing_contact_details">Viewing Contact Details</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/json-data-apis/#_updating_deleting_contacts">Updating & Deleting |
|
Contacts</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/json-data-apis/#_additional_data_api_considerations">Additional Data API |
|
Considerations</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/json-data-apis/#authentication-in-web-applications">Authentication in web applications</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/json-data-apis/#_the_shape_of_our_two_apis">The “Shape” of Our Two APIs</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/json-data-apis/#_the_model_view_controller_mvc_paradigm">The Model View |
|
Controller (MVC) Paradigm</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/json-data-apis/#html-note-title">HTML Notes: Microformats</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>So far we have been focusing on using hypermedia to build |
|
Hypermedia-Driven Applications. In doing so we are following and taking |
|
advantage of the native network architecture of the web, and building a |
|
RESTful system, in the original sense of that term.</p> |
|
<p>However, today, we should acknowledge that many web applications are |
|
often <em class="test">not</em> built using this approach. Instead, they use a Single |
|
Page Application front end library such as React to build their |
|
application, and they interact with the server via a JSON API. This JSON |
|
API almost never uses hypermedia concepts. Rather JSON APIs tend to be |
|
<em class="test">Data APIs</em>, that is, an API that simply returns structured |
|
domain data to the client without any hypermedia control information. |
|
The client itself must know how to interpret the JSON Data: what end |
|
points are associated with the data, how certain fields should be |
|
interpreted, and so on.</p> |
|
<p>Now, believe it or not, we <em class="test">have</em> been creating an API for |
|
Contact.app.</p> |
|
<p>This may sound confusing to you: an API? We have just been creating a |
|
web application, with handlers that just return HTML.</p> |
|
<p>How is that an API?</p> |
|
<p>It turns out that Contact.app is, indeed, providing an API. It just |
|
happens to be a <em class="test">hypermedia</em> API that a <em class="test">hypermedia |
|
client</em>, that is, a browser, understands. We are building an API for |
|
the browser to interact with over HTTP, and, thanks to the magic of HTML |
|
and hypermedia, the browser doesn’t need to know anything about our |
|
hypermedia API beyond an entry point URL: all the actions and display |
|
information comes, self-contained, within the HTML responses.</p> |
|
<p>Building RESTful web applications like this is so natural and simple |
|
that you might not think of it as an API at all, but we assure you, it |
|
is.</p> |
|
<h2 id="_hypermedia_apis_json_data_apis">Hypermedia APIs & JSON Data |
|
APIs</h2> |
|
<p>So, we have a hypermedia API for Contact.app. Should we include a |
|
Data API for Contact.app as well?</p> |
|
<p>Sure! The existence of a hypermedia API <em class="test">in no way means</em> that |
|
you can’t <em class="test">also</em> have a Data API. In fact, this is a common |
|
situation in traditional web applications: there is the “web |
|
application” that is entered through that entry point URL, say |
|
<code>https://mywebapp.example.com/</code>. And there is also a |
|
<em class="test">separate</em> JSON API that is accessible through another URL, |
|
perhaps <code>https://api.mywebapp.example.com/v1</code>.</p> |
|
<p>This is a perfectly reasonable way to split up the hypermedia |
|
interface to your application and the Data API you provide to other, |
|
non-hypermedia clients.</p> |
|
<p>Why would you want to include a Data API along with a hypermedia API? |
|
Well, because <em class="test">non-hypermedia clients</em> might also want to |
|
interact with your application as well.</p> |
|
<p>For example:</p> |
|
<ul> |
|
<li><p>Perhaps you have a mobile application that isn’t built using |
|
Hyperview. That application will need to interact with your server |
|
somehow, and using the existing HTML API would almost certainly be a |
|
poor fit! You want programmatic access to your system via a Data API, |
|
and JSON is a natural choice for this.</p></li> |
|
<li><p>Perhaps you have an automated script that needs to interact with |
|
the system on a regular basis. For example, maybe we have a bulk-import |
|
job that runs nightly, and needs to import/sync thousands of contacts. |
|
While it would be possible to script this against the HTML API, it would |
|
also be annoying: parsing HTML in scripts is error prone and tedious. It |
|
would be better to have a simple JSON API for this use case.</p></li> |
|
<li><p>Perhaps there are 3rd party clients who wish to integrate with |
|
your system’s data in some way. Maybe a partner wants to synchronize |
|
data nightly. As with the bulk-import example, this isn’t a great use |
|
case for an HTML-based API, and it would make more sense to provide |
|
something more amenable to scripting.</p></li> |
|
</ul> |
|
<p>For all of these use cases, a JSON Data API makes sense: in each case |
|
the API is not being consumed by a hypermedia client, so presenting an |
|
HTML-based hypermedia API would be inefficient and complicated for the |
|
client to deal with. A simple JSON Data API fits the bill for what we |
|
want and, as always, we recommend using the right tool for the job.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>“What!?! You Want Me To Parse HTML!?!”</strong></p> |
|
</div> |
|
<div> |
|
<p>A confusion we often run into in online discussions when we advocate |
|
a hypermedia approach to building web applications is that people think |
|
we mean that they should parse the HTML responses from the server, and |
|
then dump the data into their SPA framework or mobile applications.</p> |
|
<p>This is, of course, silly.</p> |
|
<p>What we mean, instead, is that you should consider using a hypermedia |
|
API <em class="test">with a hypermedia client</em>, like the browser, interpreting |
|
the hypermedia response itself and presenting it to the user. A |
|
hypermedia API can’t simply be welded on top of an existing SPA |
|
approach. It requires a sophisticated hypermedia client such as the |
|
browser to be consumed effectively.</p> |
|
<p>If you are writing code to tease apart your hypermedia only to then |
|
treat as data to feed into a client-side model, you are probably doing |
|
it wrong.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h3 id="_differences_between_hypermedia_apis_data_apis">Differences |
|
Between Hypermedia APIs & Data APIs</h3> |
|
<p>Let’s accept for a moment that we <em class="test">are</em> going to have a Data |
|
API for our application, in addition to our hypermedia API. At this |
|
point, some developers may be wondering: why have both? Why not have a |
|
single API, the JSON Data API, and have multiple clients use this one |
|
API to communicate with it?</p> |
|
<p>Isn’t it redundant to have both types of APIs for our |
|
application?</p> |
|
<p>This is a reasonable point: we do advocate having multiple APIs to |
|
your web application if necessary and, yes, this may lead to some |
|
redundancy in code. However, there are distinct advantages to both sorts |
|
of APIs and, even more so, distinct requirements for both sorts of |
|
APIs.</p> |
|
<p>By supporting both of these types of APIs separately you can get the |
|
strengths of both, while keeping their varying styles of code and |
|
infrastructure needs cleanly split out.</p> |
|
<p>Let’s contrast the needs of JSON APIs with Hypermedia APIs:</p> |
|
<div data-align="center"> |
|
<table> |
|
<tbody> |
|
<tr class="odd"> |
|
<td><p>JSON API Needs</p></td> |
|
<td><p>Hypermedia API</p></td> |
|
</tr> |
|
<tr class="even"> |
|
<td><p>It must remain stable over time: you cannot change the API |
|
willy-nilly or you risk breaking clients that use the API and expect |
|
certain end points to behave in certain ways.</p></td> |
|
<td><p>There is no need to remain stable over time: all URLs are |
|
discovered via HTML responses, so you can be much more aggressive in |
|
changing the shape of a hypermedia API.</p></td> |
|
</tr> |
|
<tr class="odd"> |
|
<td><p>It must be versioned: related to the first point, when you do |
|
make a major change, you need to version the API so that clients that |
|
are using the old API continue to work.</p></td> |
|
<td><p>Versioning is not an issue, another strength of the hypermedia |
|
approach.</p></td> |
|
</tr> |
|
<tr class="even"> |
|
<td><p>It should be rate limited: since data APIs are often used by |
|
other clients, not just your own internal web application, requests |
|
should be rate limited, often by user, in order to avoid a single client |
|
overloading the system.</p></td> |
|
<td><p>Rate limiting probably isn’t as important beyond the prevention |
|
of Distributed Denial of Service (DDoS) attacks.</p></td> |
|
</tr> |
|
<tr class="odd"> |
|
<td><p>It should be a general API: since the API is for <em class="test">all</em> |
|
clients, not just for your web application, you should avoid specialized |
|
end points that are driven by your own application needs. Instead, the |
|
API should be general and expressive enough to satisfy as many potential |
|
client needs as possible.</p></td> |
|
<td><p>The API can be <em class="test">very specific</em> to your application needs: |
|
since it is designed only for your particular web application, and since |
|
the API is discovered through hypermedia, you can add and remove highly |
|
tuned end points for specific features or optimization needs in your |
|
application.</p></td> |
|
</tr> |
|
<tr class="even"> |
|
<td><p>Authentication for these sorts of API is typically token based, |
|
which we will discuss in more detail later.</p></td> |
|
<td><p>Authentication is typically managed through a session cookie |
|
established by a login page.</p></td> |
|
</tr> |
|
</tbody> |
|
</table> |
|
</div> |
|
<p>These two different types of APIs have different strengths and needs, |
|
so it makes sense to use both. The hypermedia approach can be used for |
|
your web application, allowing you to specialize the API for the “shape” |
|
of your application. The Data API approach can be used for other, |
|
non-hypermedia clients like mobile, integration partners, etc.</p> |
|
<p>Note that by splitting these two APIs apart, you reduce the pressure |
|
to constantly change a general Data API to address application needs. |
|
Your Data API can focus on remaining stable and reliable, rather than |
|
requiring a new version with every added feature.</p> |
|
<p>This is the key advantage of splitting your Data API from your |
|
Hypermedia API.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>JSON Data APIs vs JSON “REST” APIs</strong></p> |
|
</div> |
|
<div> |
|
<p>Unfortunately, today, for historical reasons, what we are calling |
|
JSON Data APIs are often referred to as “REST APIs” in the industry. |
|
This is ironic, because, by any reasonable reading of Roy Fielding’s |
|
work defining what REST means, the vast majority of JSON APIs are |
|
<em class="test">not</em> RESTful. Not even close.</p> |
|
<blockquote> |
|
<p>I am getting frustrated by the number of people calling any |
|
HTTP-based interface a REST API. Today’s example is the SocialSite REST |
|
API. That is RPC. It screams RPC. There is so much coupling on display |
|
that it should be given an X rating.</p> |
|
<p>What needs to be done to make the REST architectural style clear on |
|
the notion that hypertext is a constraint? In other words, if the engine |
|
of application state (and hence the API) is not being driven by |
|
hypertext, then it cannot be RESTful and cannot be a REST API. Period. |
|
Is there some broken manual somewhere that needs to be fixed?</p> |
|
</blockquote><p class="quote-attribution"> Roy Fielding, |
|
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven</p> |
|
<p>The story of how “REST API” came to mean “JSON APIs” in the industry |
|
is a long and sordid one, and beyond the scope of this book. However, if |
|
you are interested, you can refer to an essay by one of the authors of |
|
this book entitled “How Did REST Come To Mean The Opposite of REST?” on |
|
the <a href="https://htmx.org/essays/how-did-rest-come-to-mean-the-opposite-of-rest/">htmx |
|
website</a>.</p> |
|
<p>In this book we use the term “Data API” to describe these JSON APIs, |
|
while acknowledging that many people in the industry will continue to |
|
call them “REST APIs” for the foreseeable future.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h2 id="_adding_a_json_data_api_to_contact_app">Adding a JSON Data API |
|
To Contact.app</h2> |
|
<p>Alright, so how are we going to add a JSON Data API to our |
|
application? One approach, popularized by the Ruby on Rails web |
|
framework, is to use the same URL endpoints as your hypermedia |
|
application, but use the HTTP <code>Accept</code> header to determine if |
|
the client wants a JSON representation or an HTML representation. The |
|
HTTP <code>Accept</code> header allows a client to specify what sort of |
|
Multipurpose Internet Mail Extensions (MIME) types, that is file types, |
|
it wants back from the server: JSON, HTML, text and so on.</p> |
|
<p>So, if the client wanted a JSON representation of all contacts, they |
|
might issue a <code>GET</code> request that looks like this:</p> |
|
<figure> |
|
<pre class="http"><code>Accept: application/json |
|
|
|
GET /contacts |
|
</code></pre> |
|
<figcaption><p>A request for a JSON representation of all |
|
contacts</p></figcaption> |
|
</figure> |
|
<p>If we adopted this pattern then our request handler for |
|
<code>/contacts/</code> would need to be updated to inspect this header |
|
and, depending on the value, return a JSON rather than HTML |
|
representation for the contacts. Ruby on Rails has support for this |
|
pattern baked into the framework, making it very easy to switch on the |
|
requested MIME type.</p> |
|
<p>Unfortunately, our experience with this pattern has not been great, |
|
for reasons that should be clear given the differences we outlined |
|
between Data and hypermedia APIs: they have different needs and often |
|
take on very different “shapes”, and trying to pound them into the same |
|
set of URLs ends up creating a lot of tension in the application |
|
code.</p> |
|
<p>Given the different needs of the two APIs and our experience managing |
|
multiple APIs like this, we think separating the two, and, therefore, |
|
breaking the JSON Data API out to its own set of URLs is the right |
|
choice. This will allow us to evolve the two APIs separately from one |
|
another, and give us room to improve each independently, in a manner |
|
consistent with their own individual strengths.</p> |
|
<h3 id="_picking_a_root_url_for_our_api">Picking a Root URL For Our |
|
API</h3> |
|
<p>Given that we are going to split our JSON Data API routes out from |
|
our regular hypermedia routes, where should we place them? One important |
|
consideration here is that we want to make sure that we can version our |
|
API cleanly in some way, regardless of the pattern we choose.</p> |
|
<p>Looking around, a lot of places use a subdomain for their APIs, |
|
something like <code>https://api.mywebapp.example.com</code> and, in |
|
fact, often encode versioning in the subdomain: |
|
<code>https://v1.api.mywebapp.example.com</code>.</p> |
|
<p>While this makes sense for large companies, it seems like a bit of |
|
overkill for our modest little Contact.app. Rather than using |
|
subdomains, which are a pain for local development, we will use |
|
sub-paths within the existing application:</p> |
|
<ul> |
|
<li><p>We will use <code>/api</code> as the root for our Data API |
|
functionality</p></li> |
|
<li><p>We will use <code>/api/v1</code> as the entry point for version 1 |
|
of our Data API</p></li> |
|
</ul> |
|
<p>If and when we decide to bump the API version, we can move to |
|
<code>/api/v2</code> and so on.</p> |
|
<p>This approach isn’t perfect, of course, but it will work for our |
|
simple application and can be adapted to a subdomain approach or various |
|
other methods at a later point, when our Contact.app has taken over the |
|
internet and we can afford a large team of API developers. :)</p> |
|
<h3 id="our-first-json-endpoint--listing-all-contacts">Our First JSON Endpoint: Listing All Contacts</h3> |
|
<p>Let’s add our first Data API endpoint. It will handle an HTTP |
|
<code>GET</code> request to <code>/api/v1/contacts</code>, and return a |
|
JSON list of all contacts in the system. In some ways it will look quite |
|
a bit like our initial code for the hypermedia route |
|
<code>/contacts</code>: we will load all the contacts from the contacts |
|
database and then render some text as a response.</p> |
|
<p>We are also going to take advantage of a nice feature of Flask: if |
|
you simply return an object from a handler, it will serialize (that is, |
|
convert) that object into a JSON response. This makes it very easy to |
|
build simple JSON APIs in flask!</p> |
|
<figure> |
|
<div class="sourceCode" id="cb2"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb2-1"><a aria-hidden="true" href="#cb2-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/api/v1/contacts"</span>, methods<span class="op">=</span>[<span class="st">"GET"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb2-2"><a aria-hidden="true" href="#cb2-2" tabindex="-1"></a><span class="kw">def</span> json_contacts():</span> |
|
<span id="cb2-3"><a aria-hidden="true" href="#cb2-3" tabindex="-1"></a> contacts_set <span class="op">=</span> Contact.<span class="bu">all</span>()</span> |
|
<span id="cb2-4"><a aria-hidden="true" href="#cb2-4" tabindex="-1"></a> contacts_dicts <span class="op">=</span> [c.__dict__ <span class="cf">for</span> c <span class="kw">in</span> contacts_set] <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb2-5"><a aria-hidden="true" href="#cb2-5" tabindex="-1"></a> <span class="cf">return</span> {<span class="st">"contacts"</span>: contacts_dicts} <span class="op"><</span><span class="dv">3</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>A JSON data API to return all contacts</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>JSON API gets its own path, starting with |
|
<code>/api</code>.</p></li> |
|
<li><p>Convert the contacts array into an array of simple dictionary |
|
(map) objects.</p></li> |
|
<li><p>Return a dictionary that contains a <code>contacts</code> |
|
property of all the contacts.</p></li> |
|
</ol> |
|
<p>This Python code might look a little foreign to you if you are not a |
|
Python developer, but all we are doing is converting our contacts into |
|
an array of simple name/value pairs and returning that array in an |
|
enclosing object as the <code>contacts</code> property. This object will |
|
be serialized into a JSON response automatically by Flask.</p> |
|
<p>With this in place, if we make an HTTP <code>GET</code> request to |
|
<code>/api/v1/contacts</code>, we will see a response that looks |
|
something like this:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb3"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb3-1"><a aria-hidden="true" href="#cb3-1" tabindex="-1"></a><span class="fu">{</span></span> |
|
<span id="cb3-2"><a aria-hidden="true" href="#cb3-2" tabindex="-1"></a> <span class="dt">"contacts"</span><span class="fu">:</span> <span class="ot">[</span></span> |
|
<span id="cb3-3"><a aria-hidden="true" href="#cb3-3" tabindex="-1"></a> <span class="fu">{</span></span> |
|
<span id="cb3-4"><a aria-hidden="true" href="#cb3-4" tabindex="-1"></a> <span class="dt">"email"</span><span class="fu">:</span> <span class="st">"carson@example.com"</span><span class="fu">,</span></span> |
|
<span id="cb3-5"><a aria-hidden="true" href="#cb3-5" tabindex="-1"></a> <span class="dt">"errors"</span><span class="fu">:</span> <span class="fu">{},</span></span> |
|
<span id="cb3-6"><a aria-hidden="true" href="#cb3-6" tabindex="-1"></a> <span class="dt">"first"</span><span class="fu">:</span> <span class="st">"Carson"</span><span class="fu">,</span></span> |
|
<span id="cb3-7"><a aria-hidden="true" href="#cb3-7" tabindex="-1"></a> <span class="dt">"id"</span><span class="fu">:</span> <span class="dv">2</span><span class="fu">,</span></span> |
|
<span id="cb3-8"><a aria-hidden="true" href="#cb3-8" tabindex="-1"></a> <span class="dt">"last"</span><span class="fu">:</span> <span class="st">"Gross"</span><span class="fu">,</span></span> |
|
<span id="cb3-9"><a aria-hidden="true" href="#cb3-9" tabindex="-1"></a> <span class="dt">"phone"</span><span class="fu">:</span> <span class="st">"123-456-7890"</span></span> |
|
<span id="cb3-10"><a aria-hidden="true" href="#cb3-10" tabindex="-1"></a> <span class="fu">}</span><span class="ot">,</span></span> |
|
<span id="cb3-11"><a aria-hidden="true" href="#cb3-11" tabindex="-1"></a> <span class="fu">{</span></span> |
|
<span id="cb3-12"><a aria-hidden="true" href="#cb3-12" tabindex="-1"></a> <span class="dt">"email"</span><span class="fu">:</span> <span class="st">"joe@example2.com"</span><span class="fu">,</span></span> |
|
<span id="cb3-13"><a aria-hidden="true" href="#cb3-13" tabindex="-1"></a> <span class="dt">"errors"</span><span class="fu">:</span> <span class="fu">{},</span></span> |
|
<span id="cb3-14"><a aria-hidden="true" href="#cb3-14" tabindex="-1"></a> <span class="dt">"first"</span><span class="fu">:</span> <span class="st">""</span><span class="fu">,</span></span> |
|
<span id="cb3-15"><a aria-hidden="true" href="#cb3-15" tabindex="-1"></a> <span class="dt">"id"</span><span class="fu">:</span> <span class="dv">3</span><span class="fu">,</span></span> |
|
<span id="cb3-16"><a aria-hidden="true" href="#cb3-16" tabindex="-1"></a> <span class="dt">"last"</span><span class="fu">:</span> <span class="st">""</span><span class="fu">,</span></span> |
|
<span id="cb3-17"><a aria-hidden="true" href="#cb3-17" tabindex="-1"></a> <span class="dt">"phone"</span><span class="fu">:</span> <span class="st">""</span></span> |
|
<span id="cb3-18"><a aria-hidden="true" href="#cb3-18" tabindex="-1"></a> <span class="fu">}</span><span class="ot">,</span></span> |
|
<span id="cb3-19"><a aria-hidden="true" href="#cb3-19" tabindex="-1"></a> <span class="er">...</span></span> |
|
<span id="cb3-20"><a aria-hidden="true" href="#cb3-20" tabindex="-1"></a> <span class="ot">]</span></span> |
|
<span id="cb3-21"><a aria-hidden="true" href="#cb3-21" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div> |
|
<figcaption><p>Some sample data from our API</p></figcaption> |
|
</figure> |
|
<p>So, you can see, we now have a way to get a relatively simple JSON |
|
representation of our contacts via an HTTP request. Not perfect, but |
|
it’s a good start. It’s certainly good enough to write some basic |
|
automated scripts against. For example, you could use this Data API |
|
to:</p> |
|
<ul> |
|
<li><p>Move your contacts to another system on a nightly basis</p></li> |
|
<li><p>Back your contacts up to a local file</p></li> |
|
<li><p>Automate an email blast to your contacts</p></li> |
|
</ul> |
|
<p>Having this small JSON Data API opens up a lot of automation |
|
possibilities that would be messier to achieve with our existing |
|
hypermedia API.</p> |
|
<h3 id="adding-contacts">Adding Contacts</h3> |
|
<p>Let’s move on to the next piece of functionality: the ability to add |
|
a new contact. Once again, our code is going to look similar in some |
|
ways to the code that we wrote for our normal web application. However, |
|
here we are also going to see the JSON API and the hypermedia API for |
|
our web application begin to obviously diverge.</p> |
|
<p>In the web application, we needed a separate path, |
|
<code>/contacts/new</code> to host the HTML form for creating a new |
|
contact. In the web application we made the decision to issue a |
|
<code>POST</code> to that same path to keep things consistent.</p> |
|
<p>In the case of the JSON API, there is no such path needed: the JSON |
|
API “just is”: it doesn’t need to provide any hypermedia representation |
|
for creating a new contact. You simply know where to issue a |
|
<code>POST</code> to create a contact — likely through some |
|
documentation provided about the API — and that’s it.</p> |
|
<p>Because of that fact, we can put the “create” handler on the same |
|
path as the “list” handler: <code>/api/v1/contacts</code>, but have it |
|
respond only to HTTP <code>POST</code> requests.</p> |
|
<p>The code here is relatively straightforward: populate a new contact |
|
with the information from the <code>POST</code> request, attempt to save |
|
it, and — if it is not successful — show error messages. Here is the |
|
code:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb4"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb4-1"><a aria-hidden="true" href="#cb4-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/api/v1/contacts"</span>, methods<span class="op">=</span>[<span class="st">"POST"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb4-2"><a aria-hidden="true" href="#cb4-2" tabindex="-1"></a><span class="kw">def</span> json_contacts_new():</span> |
|
<span id="cb4-3"><a aria-hidden="true" href="#cb4-3" tabindex="-1"></a> c <span class="op">=</span> Contact(<span class="va">None</span>,</span> |
|
<span id="cb4-4"><a aria-hidden="true" href="#cb4-4" tabindex="-1"></a> request.form.get(<span class="st">'first_name'</span>),</span> |
|
<span id="cb4-5"><a aria-hidden="true" href="#cb4-5" tabindex="-1"></a> request.form.get(<span class="st">'last_name'</span>),</span> |
|
<span id="cb4-6"><a aria-hidden="true" href="#cb4-6" tabindex="-1"></a> request.form.get(<span class="st">'phone'</span>),</span> |
|
<span id="cb4-7"><a aria-hidden="true" href="#cb4-7" tabindex="-1"></a> request.form.get(<span class="st">'email'</span>)) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb4-8"><a aria-hidden="true" href="#cb4-8" tabindex="-1"></a> <span class="cf">if</span> c.save(): <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb4-9"><a aria-hidden="true" href="#cb4-9" tabindex="-1"></a> <span class="cf">return</span> c.__dict__</span> |
|
<span id="cb4-10"><a aria-hidden="true" href="#cb4-10" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb4-11"><a aria-hidden="true" href="#cb4-11" tabindex="-1"></a> <span class="cf">return</span> {<span class="st">"errors"</span>: c.errors}, <span class="dv">400</span> <span class="op"><</span><span class="dv">4</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Adding contacts with our JSON API</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>This handler is on the same path as the first one for our JSON |
|
API, but handles <code>POST</code> requests.</p></li> |
|
<li><p>We create a new Contact based on values submitted with the |
|
request.</p></li> |
|
<li><p>We attempt to save the contact and, if successful, render it as a |
|
JSON object.</p></li> |
|
<li><p>If the save is not successful, we render an object showing the |
|
errors, with a response code of <code>400 (Bad Request)</code>.</p></li> |
|
</ol> |
|
<p>In some ways this is similar to our <code>contacts_new()</code> |
|
handler from our web application; we are creating the contact and |
|
attempting to save it. In other ways it is very different:</p> |
|
<ul> |
|
<li><p>There is no redirection happening here on a successful creation, |
|
because we are not dealing with a hypermedia client like the |
|
browser.</p></li> |
|
<li><p>In the case of a bad request, we simply return an error response |
|
code, <code>400 (Bad Request)</code>. This is in contrast with the web |
|
application, where we re-render the form with error messages in |
|
it.</p></li> |
|
</ul> |
|
<p>These sorts of differences, over time, build up and make the idea of |
|
keeping your JSON and hypermedia APIs on the same set of URLs less and |
|
less appealing.</p> |
|
<h3 id="_viewing_contact_details">Viewing Contact Details</h3> |
|
<p>Next, let’s make it possible for a JSON API client to download the |
|
details for a single contact. We will naturally use an HTTP |
|
<code>GET</code> for this functionality and will follow the convention |
|
we established for our regular web application, and put the path at |
|
<code>/api/v1/contacts/<contact id></code>. So, for example, if |
|
you want to see the details of the contact with the id <code>42</code>, |
|
you would issue an HTTP <code>GET</code> to |
|
<code>/api/v1/contacts/42</code>.</p> |
|
<p>This code is quite simple:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/api/v1/contacts/<contact_id>"</span>, methods<span class="op">=</span>[<span class="st">"GET"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a><span class="kw">def</span> json_contacts_view(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a> <span class="cf">return</span> contact.__dict__ <span class="op"><</span><span class="dv">3</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Getting the details of a contact in JSON</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Add a new <code>GET</code> route at the path we want to use for |
|
viewing contact details</p></li> |
|
<li><p>Look the contact up via the id passed in through the |
|
path</p></li> |
|
<li><p>Convert the contact to a dictionary, so it can be rendered as |
|
JSON response</p></li> |
|
</ol> |
|
<p>Nothing too complicated: we look the contact up by the ID provided in |
|
the path to the controller. We then render it as JSON. You have to |
|
appreciate the simplicity of this code!</p> |
|
<p>Next, let’s add updating and deleting a contact as well.</p> |
|
<h3 id="_updating_deleting_contacts">Updating & Deleting |
|
Contacts</h3> |
|
<p>As with the create contact API endpoint, because there is no HTML UI |
|
to produce for them, we can reuse the |
|
<code>/api/v1/contacts/<contact id></code> path. We will use the |
|
<code>PUT</code> HTTP method for updating a contact and the |
|
<code>DELETE</code> method for deleting one.</p> |
|
<p>Our update code is going to look nearly identical to the create |
|
handler, except that, rather than creating a new contact, we will look |
|
up the contact by ID and update its fields. In this sense we are just |
|
combining the code of the create handler and the detail view |
|
handler.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb6"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb6-1"><a aria-hidden="true" href="#cb6-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/api/v1/contacts/<contact_id>"</span>, methods<span class="op">=</span>[<span class="st">"PUT"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb6-2"><a aria-hidden="true" href="#cb6-2" tabindex="-1"></a><span class="kw">def</span> json_contacts_edit(contact_id):</span> |
|
<span id="cb6-3"><a aria-hidden="true" href="#cb6-3" tabindex="-1"></a> c <span class="op">=</span> Contact.find(contact_id) <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb6-4"><a aria-hidden="true" href="#cb6-4" tabindex="-1"></a> c.update(</span> |
|
<span id="cb6-5"><a aria-hidden="true" href="#cb6-5" tabindex="-1"></a> request.form[<span class="st">'first_name'</span>],</span> |
|
<span id="cb6-6"><a aria-hidden="true" href="#cb6-6" tabindex="-1"></a> request.form[<span class="st">'last_name'</span>],</span> |
|
<span id="cb6-7"><a aria-hidden="true" href="#cb6-7" tabindex="-1"></a> request.form[<span class="st">'phone'</span>],</span> |
|
<span id="cb6-8"><a aria-hidden="true" href="#cb6-8" tabindex="-1"></a> request.form[<span class="st">'email'</span>]) <span class="op"><</span><span class="dv">3</span><span class="op">></span></span> |
|
<span id="cb6-9"><a aria-hidden="true" href="#cb6-9" tabindex="-1"></a> <span class="cf">if</span> c.save(): <span class="op"><</span><span class="dv">4</span><span class="op">></span></span> |
|
<span id="cb6-10"><a aria-hidden="true" href="#cb6-10" tabindex="-1"></a> <span class="cf">return</span> c.__dict__</span> |
|
<span id="cb6-11"><a aria-hidden="true" href="#cb6-11" tabindex="-1"></a> <span class="cf">else</span>:</span> |
|
<span id="cb6-12"><a aria-hidden="true" href="#cb6-12" tabindex="-1"></a> <span class="cf">return</span> {<span class="st">"errors"</span>: c.errors}, <span class="dv">400</span></span></code></pre></div> |
|
<figcaption><p>Updating a contact with our JSON API</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We handle <code>PUT</code> requests to the URL for a given |
|
contact.</p></li> |
|
<li><p>Look the contact up via the id passed in through the |
|
path.</p></li> |
|
<li><p>We update the contact’s data from the values included in the |
|
request.</p></li> |
|
<li><p>From here on the logic is identical to the |
|
<code>json_contacts_create()</code> handler.</p></li> |
|
</ol> |
|
<p>Once again, thanks to the built-in functionality in Flask, simple to |
|
implement.</p> |
|
<p>Let’s look at deleting a contact now. This turns out to be even |
|
simpler: as with the update handler we are going to look up the contact |
|
by id, and then, well, delete it. At that point we can return a simple |
|
JSON object indicating success.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><span class="at">@app.route</span>(<span class="st">"/api/v1/contacts/<contact_id>"</span>, methods<span class="op">=</span>[<span class="st">"DELETE"</span>]) <span class="op"><</span><span class="dv">1</span><span class="op">></span></span> |
|
<span id="cb7-2"><a aria-hidden="true" href="#cb7-2" tabindex="-1"></a><span class="kw">def</span> json_contacts_delete(contact_id<span class="op">=</span><span class="dv">0</span>):</span> |
|
<span id="cb7-3"><a aria-hidden="true" href="#cb7-3" tabindex="-1"></a> contact <span class="op">=</span> Contact.find(contact_id)</span> |
|
<span id="cb7-4"><a aria-hidden="true" href="#cb7-4" tabindex="-1"></a> contact.delete() <span class="op"><</span><span class="dv">2</span><span class="op">></span></span> |
|
<span id="cb7-5"><a aria-hidden="true" href="#cb7-5" tabindex="-1"></a> <span class="cf">return</span> jsonify({<span class="st">"success"</span>: <span class="va">True</span>}) <span class="op"><</span><span class="dv">3</span><span class="op">></span></span></code></pre></div> |
|
<figcaption><p>Deleting a contact with our JSON API</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>We handle <code>DELETE</code> requests to the URL for a given |
|
contact.</p></li> |
|
<li><p>Look the contact up and invoke the <code>delete()</code> method |
|
on it.</p></li> |
|
<li><p>Return a simple JSON object indicating that the contact was |
|
successfully deleted.</p></li> |
|
</ol> |
|
<p>And, with that, we have our simple little JSON Data API to live |
|
alongside our regular web application, nicely separated out from the |
|
main web application, so it can evolve separately as needed.</p> |
|
<h3 id="_additional_data_api_considerations">Additional Data API |
|
Considerations</h3> |
|
<p>Now, we would have a lot more to do if we wanted to make this a |
|
production ready JSON API. At minimum we would need to add:</p> |
|
<ul> |
|
<li><p>Rate limiting, important for any public-facing Data API to avoid |
|
abusive clients.</p></li> |
|
<li><p>An authentication mechanism. (We don’t have one for our web |
|
application either!)</p></li> |
|
<li><p>Support for pagination of our contact data.</p></li> |
|
<li><p>Several small items, such as rendering a proper |
|
<code>404 (Not Found)</code> response if someone makes a request with a |
|
contact id that doesn’t exist.</p></li> |
|
</ul> |
|
<p>These topics are beyond the scope of this book, but we’d like to |
|
focus on one in particular, authentication, in order to show the |
|
difference between our hypermedia and JSON API. In order to secure our |
|
application we need to add <em class="test">authentication</em>, some mechanism for |
|
determining who a request is coming from, and also |
|
<em class="test">authorization</em>, determining if they have the right to perform |
|
the request.</p> |
|
<p>We will set authorization aside for now and consider only |
|
authentication.</p> |
|
<h4 id="authentication-in-web-applications">Authentication in web applications</h4> |
|
<p>In the HTML web application world, authentication has traditionally |
|
been done via a login page that asks a user for their username (often |
|
their email) and a password. This password is then checked against a |
|
database of (hashed) passwords to establish that the user is who they |
|
say they are. If the password is correct, then a <em class="test">session cookie</em> |
|
is established, indicating who the user is. This cookie is then sent |
|
with every request that the user makes to the web application, allowing |
|
the application to know which user is making a given request.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>HTTP Cookies</strong></p> |
|
</div> |
|
<div> |
|
<p>HTTP Cookies are kind of a strange feature of HTTP. In some ways they |
|
violate the goal of remaining stateless, a major component of the |
|
RESTful architecture: a server will often use a session cookie as an |
|
index into state kept on the server “on the side”, such as a cache of |
|
the last action performed by the user.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<p>Nonetheless, cookies have proven extremely useful and so people tend |
|
not to complain about this aspect of them too much (We are not sure what |
|
our other options would be here!) An interesting example of pragmatism |
|
gone (relatively) right in web development.</p> |
|
<p>In comparison with the standard web application approach to |
|
authentication, a JSON API will typically use some sort of <em class="test">token |
|
based</em> authentication: an authentication token will be established |
|
via a mechanism like OAuth, and that authentication token will then be |
|
passed, often as an HTTP Header, with every request that a client |
|
makes.</p> |
|
<p>At a high level this is similar to what happens in normal web |
|
application authentication: a token is established somehow and then that |
|
token is part of every request. However, in practice, the mechanics tend |
|
to be wildly different:</p> |
|
<ul> |
|
<li><p>Cookies are part of the HTTP specification and can be easily |
|
<em class="test">set</em> by an HTTP Server.</p></li> |
|
<li><p>JSON Authentication tokens, in contrast, often require elaborate |
|
exchange mechanics like OAuth to be established.</p></li> |
|
</ul> |
|
<p>These differing mechanics for establishing authentication are yet |
|
another good reason for splitting up our JSON and hypermedia APIs.</p> |
|
<h3 id="_the_shape_of_our_two_apis">The “Shape” of Our Two APIs</h3> |
|
<p>When we were building out our API, we noted that in many cases the |
|
JSON API didn’t require as many end points as our hypermedia API did: we |
|
didn’t need a <code>/contacts/new</code> handler, for example, to |
|
provide a hypermedia representation for creating contacts.</p> |
|
<p>Another aspect of our hypermedia API to consider was the performance |
|
improvement we made: we pulled the total contact count out to a separate |
|
endpoint and implemented the “Lazy Load” pattern, to improve the |
|
perceived performance of our application.</p> |
|
<p>Now, if we had both our hypermedia and JSON API sharing the same |
|
paths, would we want to publish this API as a JSON endpoint as well?</p> |
|
<p>Maybe, but maybe not. This was a pretty specific need for our web |
|
application, and, absent a request from a user of our JSON API, it |
|
doesn’t make sense to include it for JSON consumers.</p> |
|
<p>And what if, by some miracle, the performance issues with |
|
<code>Contact.count()</code> that we were addressing with the Lazy Load |
|
pattern goes away? Well, in our Hypermedia-Driven Application we can |
|
simply revert to the old code and include the count directly in the |
|
request to <code>/contacts</code>. We can remove the |
|
<code>contacts/count</code> endpoint and all the logic associated with |
|
it. Because of the uniform interface of hypermedia, the system will |
|
continue to work just fine.</p> |
|
<p>But what if we had tied our JSON API and hypermedia API together, and |
|
published <code>/contacts/count</code> as a supported end point for our |
|
JSON API? In that case we couldn’t simply remove the endpoint: a |
|
(non-hypermedia) client might be relying on it.</p> |
|
<p>Once again you can see the flexibility of the hypermedia approach and |
|
why separating your JSON API out from your hypermedia API lets you take |
|
maximum advantage of that flexibility.</p> |
|
<h3 id="_the_model_view_controller_mvc_paradigm">The Model View |
|
Controller (MVC) Paradigm</h3> |
|
<p>One thing you may have noticed about the handlers for our JSON API is |
|
that they are relatively simple and regular. Most of the hard work of |
|
updating data and so forth is done within the contact model itself: the |
|
handlers act as simple connectors that provide a go-between the HTTP |
|
requests and the model.</p> |
|
<p>This is the ideal controller of the Model-View-Controller (MVC) |
|
paradigm that was so popular in the early web: a controller should be |
|
“thin”, with the model containing the majority of the logic in the |
|
system.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>The Model View Controller pattern</strong></p> |
|
</div> |
|
<div> |
|
<p>The Model View Controller design pattern is a classic architectural |
|
pattern in software development, and was a major influence in early web |
|
development. It is no longer emphasized as heavily, as web development |
|
has split into frontend and backend camps, but most web developers are |
|
still familiar with the idea.</p> |
|
<p>Traditionally, the MVC pattern mapped into web development like |
|
so:</p> |
|
<ul> |
|
<li><p>Model - A collection of “domain” classes that implement all the |
|
logic and rules for the particular domain your application is designed |
|
for. The model typically provides “resources” that are then presented to |
|
clients as HTML “representations.”</p></li> |
|
<li><p>View - Typically views would be some sort of client-side |
|
templating system, and would render the aforementioned HTML |
|
representation for a given Model instance.</p></li> |
|
<li><p>Controller - The controller’s job is to take HTTP requests, |
|
convert them into sensible requests to the Model and forward those |
|
requests on to the appropriate Model objects. It then passes the HTML |
|
representation back to the client as an HTTP response.</p></li> |
|
</ul> |
|
</div> |
|
</div> |
|
</div> |
|
<p>Thin controllers make it easy to split your JSON and hypermedia APIs |
|
out, because all the important logic lives in the domain model that is |
|
shared by both. This allows you to evolve both separately, while still |
|
keeping logic in sync with one another.</p> |
|
<p>With properly built “thin” controllers and “fat” models, keeping two |
|
separate APIs both in sync and yet still evolving separately is not as |
|
difficult or as crazy as it might sound.</p> |
|
<div id="html-note"> |
|
<div> |
|
<h2 id="html-note-title">HTML Notes: Microformats</h2> |
|
<p><a href="https://microformats.org/">Microformats</a> is a standard |
|
for embedding machine-readable structured data in HTML. It uses classes |
|
to mark certain elements as containing information to be extracted, with |
|
conventions for extracting common properties like name, URL and photo |
|
without classes. By adding these classes into the HTML representation of |
|
an object, we allow the properties of the object to be recovered from |
|
the HTML. For example, this simple HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><span class="dt"><</span><span class="kw">a</span><span class="ot"> class</span><span class="op">=</span><span class="st">"h-card"</span><span class="ot"> href</span><span class="op">=</span><span class="st">"https://john.example"</span><span class="dt">></span></span> |
|
<span id="cb8-2"><a aria-hidden="true" href="#cb8-2" tabindex="-1"></a> <span class="dt"><</span><span class="kw">img</span><span class="ot"> src</span><span class="op">=</span><span class="st">"john.jpg"</span><span class="ot"> alt</span><span class="op">=</span><span class="st">""</span><span class="dt">></span> John Doe</span> |
|
<span id="cb8-3"><a aria-hidden="true" href="#cb8-3" tabindex="-1"></a><span class="dt"></</span><span class="kw">a</span><span class="dt">></span></span></code></pre></div> |
|
</figure> |
|
<p>can be parsed into this JSON-like structure by a microformats |
|
parser:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb9"><pre class="sourceCode json"><code class="sourceCode json"><span id="cb9-1"><a aria-hidden="true" href="#cb9-1" tabindex="-1"></a><span class="fu">{</span></span> |
|
<span id="cb9-2"><a aria-hidden="true" href="#cb9-2" tabindex="-1"></a> <span class="dt">"type"</span><span class="fu">:</span> <span class="ot">[</span><span class="st">"h-card"</span><span class="ot">]</span><span class="fu">,</span></span> |
|
<span id="cb9-3"><a aria-hidden="true" href="#cb9-3" tabindex="-1"></a> <span class="dt">"properties"</span><span class="fu">:</span> <span class="fu">{</span></span> |
|
<span id="cb9-4"><a aria-hidden="true" href="#cb9-4" tabindex="-1"></a> <span class="dt">"name"</span><span class="fu">:</span> <span class="ot">[</span><span class="st">"John Doe"</span><span class="ot">]</span><span class="fu">,</span></span> |
|
<span id="cb9-5"><a aria-hidden="true" href="#cb9-5" tabindex="-1"></a> <span class="dt">"photo"</span><span class="fu">:</span> <span class="ot">[</span><span class="st">"john.jpg"</span><span class="ot">]</span><span class="fu">,</span></span> |
|
<span id="cb9-6"><a aria-hidden="true" href="#cb9-6" tabindex="-1"></a> <span class="dt">"url"</span><span class="fu">:</span> <span class="ot">[</span><span class="st">"https://john.example"</span><span class="ot">]</span></span> |
|
<span id="cb9-7"><a aria-hidden="true" href="#cb9-7" tabindex="-1"></a> <span class="fu">}</span></span> |
|
<span id="cb9-8"><a aria-hidden="true" href="#cb9-8" tabindex="-1"></a><span class="fu">}</span></span></code></pre></div> |
|
</figure> |
|
<p>Using a variety of properties and nested objects, we could mark up |
|
every bit of information about a contact, for example, in a |
|
machine-readable way.</p> |
|
<p>As explained in the above chapter, trying to use the same mechanism |
|
for human and machine interaction is not a good idea. Your human-facing |
|
and machine-facing interfaces may end up being limited by each other. If |
|
you want to expose domain-specific data and actions to users and |
|
developers, a JSON API is a great option.</p> |
|
<p>However, microformats are way easier to adopt. A protocol or standard |
|
that requires websites to implement a JSON API has a high technical |
|
barrier. In comparison, any website can be augmented with microformats |
|
simply by adding a few classes. Other HTML-embedded data formats like |
|
microdata, Open Graph are similarly easy to adopt. This makes |
|
microformats useful for cross-website (dare we say <em class="test">web-scale</em>) |
|
systems like the <a href="https://indieweb.org">IndieWeb</a>, which uses |
|
it pervasively.</p> |
|
</div> |
|
</div> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<div class="chapter"> |
|
<h2 class="chapter-title">Hyperview: A Mobile Hypermedia</h2> |
|
<main> |
|
<details class="division-toc"><summary>Contents</summary> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_the_state_of_mobile_app_development">The State of Mobile App |
|
Development</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#hypermedia-for-mobile-apps">Hypermedia for Mobile Apps</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_web_views">Web Views</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#hyperview">Hyperview</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#the-format">The format</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#the-client">The client</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_extensibility">Extensibility</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_which_hypermedia_architecture_should_you_use">Which Hypermedia |
|
Architecture Should You Use?</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#introduction-to-hxml">Introduction to HXML</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#hello-world-">Hello World!</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#ui-elements">UI Elements</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#lists">Lists</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#images">Images</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#inputs">Inputs</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#styling">Styling</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#custom-elements">Custom elements</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_behaviors">Behaviors</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#actions">Actions</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#navigation-actions">Navigation actions</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#update-actions">Update actions</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#system-actions">System actions</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#custom-actions">Custom actions</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#triggers">Triggers</a> |
|
<ul> |
|
<li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_long_press">Long-press</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_load">Load</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_visible">Visible</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_refresh">Refresh</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_focus_blur_and_change">Focus, blur, and change</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#using-multiple-behaviors">Using multiple behaviors</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_summary">Summary</a> |
|
</li></ul> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#_hypermedia_for_mobile">Hypermedia, for Mobile</a> |
|
</li><li> |
|
<a href="https://hypermedia.systems/hyperview-a-mobile-hypermedia/#html-note-title">Hypermedia Notes: Maximize Your Server-Side |
|
Strengths</a> |
|
</li></ul> |
|
</details> |
|
<div class="division-content"> |
|
<p>You may be forgiven for thinking the hypermedia architecture is |
|
synonymous with the web, web browsers, and HTML. No doubt, the web is |
|
the largest hypermedia system, and web browsers are the most popular |
|
hypermedia client. The dominance of the web in discussions about |
|
hypermedia make it easy to forget that hypermedia is a general concept, |
|
and can be applied to all types of platforms and applications. In this |
|
chapter, we will see the hypermedia architecture applied to a non-web |
|
platform: native mobile applications.</p> |
|
<p>Mobile as a platform has different constraints than the web. It |
|
requires different trade-offs and design decisions. Nonetheless, the |
|
concepts of hypermedia, HATEOAS, and REST can be directly applied to |
|
build delightful mobile applications.</p> |
|
<p>In this chapter we will cover shortcomings with the current state of |
|
mobile app development, and how a hypermedia architecture can address |
|
these problems. We will then look at a path toward hypermedia on mobile: |
|
Hyperview, a mobile app framework that uses the hypermedia architecture. |
|
We’ll conclude with an overview of HXML, the hypermedia format used by |
|
Hyperview.</p> |
|
<h2 id="_the_state_of_mobile_app_development">The State of Mobile App |
|
Development</h2> |
|
<p>Before we can discuss how to apply hypermedia to mobile platforms, we |
|
need to understand how native mobile apps are commonly built. I’m using |
|
the word “native” to refer to code written against an SDK provided by |
|
the phone’s operating system (typically Android or iOS). This code is |
|
packaged into an executable binary, and uploaded & approved through |
|
app stores controlled by Google and Apple. When users install or update |
|
an app, they’re downloading this executable and running the code |
|
directly on their device’s OS. In this way, mobile apps have a lot in |
|
common with old-school desktop apps for Mac, Windows, or Linux. There is |
|
one important difference between PC desktop apps of yesteryear and |
|
today’s mobile apps. These days, almost all mobile apps are “networked”. |
|
By networked, we mean the app needs to read and write data over the |
|
Internet to deliver its core functionality. In other words, a networked |
|
mobile app needs to implement the client-server architecture.</p> |
|
<p>When implementing the client-server architecture, the developer needs |
|
to make a decision: Should the app be designed as a thin client or thick |
|
client? The current mobile ecosystems strongly push developers towards a |
|
thick-client approach. Why? Remember, Android and iOS require that a |
|
native mobile app be packaged and distributed as an executable binary. |
|
There’s no way around it. Since the developer needs to write code to |
|
package into an executable, it seems logical to implement some of the |
|
app’s logic in that code. The code may as well initiate HTTP calls to |
|
the server to retrieve data, and then render that data using the |
|
platform’s UI libraries. Thus, developers are naturally led into a |
|
thick-client pattern that looks something like this:</p> |
|
<ul> |
|
<li><p>The client contains code to make API requests to the server, and |
|
code to translate those responses to UI updates</p></li> |
|
<li><p>The server implements an HTTP API that speaks JSON, and knows |
|
little about the state of the client</p></li> |
|
</ul> |
|
<p>Just like with SPAs on the web, this architecture has a big downside: |
|
the app’s logic gets spread across the client and server. Sometimes, |
|
this means that logic gets duplicated (like to validate form data). |
|
Other times, the client and server each implement disjoint parts of the |
|
app’s overall logic. To understand what the app does, a developer needs |
|
to trace interactions between two very different codebases.</p> |
|
<p>There’s another downside that affects mobile apps more than SPAs: API |
|
churn. Remember, the app stores control how your app gets distributed |
|
and updated. Users can even control if and when they get updated |
|
versions of your app. As a mobile developer, you can’t assume that every |
|
user will be on the latest version of your app. Your frontend code gets |
|
fragmented across many versions, and now your backend needs to support |
|
all of them.</p> |
|
<h2 id="hypermedia-for-mobile-apps">Hypermedia for Mobile Apps</h2> |
|
<p>We’ve seen that the hypermedia architecture can address the |
|
shortcomings of SPAs on the web. But can hypermedia work for mobile apps |
|
as well? The answer is yes!</p> |
|
<p>Just like on the web, we can use hypermedia formats on mobile and let |
|
it serve as the engine of application state. All of the logic is |
|
controlled from the backend, rather than being spread between two |
|
codebases. Hypermedia architecture also solves the annoying problem of |
|
API churn on mobile apps. Since the backend serves a hypermedia response |
|
containing both data and actions, there’s no way for the data and UI to |
|
get out of sync. No more worries about backwards compatibility or |
|
maintaining multiple API versions.</p> |
|
<p>So how can you use hypermedia for your mobile app? There are two |
|
approaches employing hypermedia to build & ship native mobile apps |
|
today:</p> |
|
<ul> |
|
<li><p>Web views, which wraps the trusty web platform in a mobile app |
|
shell</p></li> |
|
<li><p>Hyperview, a new hypermedia system we designed specifically for |
|
mobile apps</p></li> |
|
</ul> |
|
<h3 id="_web_views">Web Views</h3> |
|
<p>The simplest way to use hypermedia architecture on mobile is by |
|
leveraging web technologies. Both Android and iOS SDKs provide “web |
|
views”: chromeless web browsers that can be embedded in native apps. |
|
Tools like Apache Cordova make it easy to take the URL of a website, and |
|
spit out native iOS and Android apps based on web views. If you already |
|
have a responsive web app, you can get a “native” mobile HDA for free. |
|
Sounds too good to be true, right?</p> |
|
<p>Of course, there is a fundamental limitation with this approach. The |
|
web platform and mobile platforms have different capabilities and UX |
|
conventions. HTML doesn’t natively support common UI patterns of mobile |
|
apps. One of the biggest differences is around how each platform handles |
|
navigation. On the web, navigation is page-based, with one page |
|
replacing another and the browser providing back/forward buttons to |
|
navigate the page history. On mobile, navigation is more complex, and |
|
tuned for the physicality of gesture-based interactions.</p> |
|
<ul> |
|
<li><p>To drill down, screens slide on top of each other, forming stacks |
|
of screens.</p></li> |
|
<li><p>Tab bars at the top or bottom of the app allow switching between |
|
various stacks of screens.</p></li> |
|
<li><p>Modals slide up from the bottom of the app, covering the other |
|
stacks and tab bar.</p></li> |
|
<li><p>Unlike with web pages, all of these screens are still present in |
|
memory, rendered and updating based on app state.</p></li> |
|
</ul> |
|
<p>The navigation architecture is a major difference between how mobile |
|
and web apps function. But it’s not the only one. Many other UX patterns |
|
are present in mobile apps, but are not natively supported on the |
|
web:</p> |
|
<ul> |
|
<li><p>pull-to-refresh to refresh content in a screen</p></li> |
|
<li><p>horizontal swipe on UI elements to reveal actions</p></li> |
|
<li><p>sectioned lists with sticky headers</p></li> |
|
</ul> |
|
<p>While these interactions are not natively supported by web browsers, |
|
they can be simulated with JS libraries. Of course, these libraries will |
|
never have the same feel and performance as native gestures. And using |
|
them usually requires embracing a JS-heavy SPA architecture like React. |
|
This puts us back at square 1! To avoid using the typical thick-client |
|
architecture of native mobile apps, we turned to a web view. The web |
|
view allows us to use good-old hypermedia-based HTML. But to get the |
|
desired look & feel of a mobile app, we end up building a SPA in JS, |
|
losing the benefits of Hypermedia in the process.</p> |
|
<p>To build a mobile HDA that acts and feels like a native app, HTML |
|
isn’t going to cut it. We need a format designed to represent the |
|
interactions and patterns of native mobile apps. That’s exactly what |
|
Hyperview does.</p> |
|
<h3 id="hyperview">Hyperview</h3> |
|
<p>Hyperview is an open-source hypermedia system that provides:</p> |
|
<ul> |
|
<li><p>A hypermedia format for defining mobile apps called HXML</p></li> |
|
<li><p>A hypermedia client for HXML that works on iOS and |
|
Android</p></li> |
|
<li><p>Extension points in HXML and the client to customize the |
|
framework for a given app</p></li> |
|
</ul> |
|
<h4 id="the-format">The format</h4> |
|
<p>HXML was designed to feel familiar to web developers, used to working |
|
with HTML. Thus the choice of XML for the base format. In addition to |
|
familiar ergonomics, XML is compatible with server-side rendering |
|
libraries. For example, Jinja2 is perfectly suited as a templating |
|
library to render HXML. The familiarity of XML and the ease of |
|
integration on the backend make it simple to adopt in both new and |
|
existing codebases. Take a look at a “Hello World” app written in HXML. |
|
The syntax should be familiar to anyone who’s worked with HTML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb1"><pre class="sourceCode xml"><code class="sourceCode xml"><span id="cb1-1"><a aria-hidden="true" href="#cb1-1" tabindex="-1"></a><<span class="kw">doc</span><span class="ot"> xmlns=</span><span class="st">"https://hyperview.org/hyperview"</span>></span> |
|
<span id="cb1-2"><a aria-hidden="true" href="#cb1-2" tabindex="-1"></a> <<span class="kw">screen</span>></span> |
|
<span id="cb1-3"><a aria-hidden="true" href="#cb1-3" tabindex="-1"></a> <<span class="kw">styles</span> /></span> |
|
<span id="cb1-4"><a aria-hidden="true" href="#cb1-4" tabindex="-1"></a> <<span class="kw">body</span>></span> |
|
<span id="cb1-5"><a aria-hidden="true" href="#cb1-5" tabindex="-1"></a> <<span class="kw">header</span>></span> |
|
<span id="cb1-6"><a aria-hidden="true" href="#cb1-6" tabindex="-1"></a> <<span class="kw">text</span>>My first app</<span class="kw">text</span>></span> |
|
<span id="cb1-7"><a aria-hidden="true" href="#cb1-7" tabindex="-1"></a> </<span class="kw">header</span>></span> |
|
<span id="cb1-8"><a aria-hidden="true" href="#cb1-8" tabindex="-1"></a> <<span class="kw">view</span>></span> |
|
<span id="cb1-9"><a aria-hidden="true" href="#cb1-9" tabindex="-1"></a> <<span class="kw">text</span>>Hello World!</<span class="kw">text</span>></span> |
|
<span id="cb1-10"><a aria-hidden="true" href="#cb1-10" tabindex="-1"></a> </<span class="kw">view</span>></span> |
|
<span id="cb1-11"><a aria-hidden="true" href="#cb1-11" tabindex="-1"></a> </<span class="kw">body</span>></span> |
|
<span id="cb1-12"><a aria-hidden="true" href="#cb1-12" tabindex="-1"></a> </<span class="kw">screen</span>></span> |
|
<span id="cb1-13"><a aria-hidden="true" href="#cb1-13" tabindex="-1"></a></<span class="kw">doc</span>></span></code></pre></div> |
|
<figcaption><p>Hello World</p></figcaption> |
|
</figure> |
|
<p>But HXML is not just a straight port of HTML with differently named |
|
tags. In previous chapters, we’ve seen how htmx enhances HTML with a |
|
handful of new attributes. These additions maintain the declarative |
|
nature of HTML, while giving developers the power to create rich web |
|
apps. In HXML, the concepts of htmx are built into the spec. |
|
Specifically, HXML is not limited to “click a link” and “submit a form” |
|
interactions like basic HTML. It supports a range of triggers and |
|
actions for modifying the content on a screen. These interactions are |
|
bundled together in a powerful concept of “behaviors.” Developers can |
|
even define new behavior actions to add new capabilities to their app, |
|
without the need for scripting. We will learn more about behaviors later |
|
in this chapter.</p> |
|
<h4 id="the-client">The client</h4> |
|
<p>Hyperview provides an open-source HXML client library written in |
|
React Native. With a little bit of configuration and a few steps on the |
|
command line, this library compiles into native app binaries for iOS or |
|
Android. Users install the app on their device via an app store. On |
|
launch, the app makes an HTTP request to the configured URL, and renders |
|
the HXML response as the first screen.</p> |
|
<p>It may seem a little strange that developing a HDA using Hyperview |
|
requires a single-purpose client binary. After all, we don’t ask users |
|
to first download and install a binary to view a web app. No, users just |
|
enter a URL in the address bar of a general-purpose web browser. A |
|
single HTML client renders apps from any HTML server (<a class="ref" href="#fig-1clientmanyserver">[fig-1clientmanyserver]</a>).</p> |
|
<figure id="fig-1clientmanyserver"> |
|
<div> |
|
<div data-align="start"> |
|
<pre><code> ┌────────────┐ |
|
│ │ |
|
┌──────────┬─┐ │ SERVER │ |
|
├──────────┴─┤ │ │ |
|
│ │ └──▲─────────┘ |
|
│ │ │ |
|
│ │ │ |
|
│ ├───────┘ ┌────────────┐ |
|
│ │ │ │ |
|
│ CLIENT ├──────────▶ SERVER │ |
|
│ │ │ │ |
|
│ ├─────┐ └────────────┘ |
|
│ │ │ |
|
│ │ ┌────────────┐ |
|
│ │ │ │ |
|
└────────────┘ │ SERVER │ |
|
│ │ |
|
└────────────┘ |
|
</code></pre> |
|
</div> |
|
</div> |
|
<figcaption><p>One HTML client, multiple HTML servers</p></figcaption> |
|
</figure> |
|
<p>It is theoretically possible to build an equivalent general-purpose |
|
“Hyperview browser.” This HXML client would render apps from any HXML |
|
server, and users would enter a URL to specify the app they want to use. |
|
But iOS and Android are built around the concept of single-purpose apps. |
|
Users expect to find and install apps from an app store, and launch them |
|
from the home screen of their device. Hyperview embraces this |
|
app-centric paradigm of today’s popular mobile platforms. That means |
|
that the HXML client (app binary) renders its UI from a single |
|
pre-configured HXML server (<a class="ref" href="#fig-1client1server">[fig-1client1server]</a>).</p> |
|
<figure id="fig-1client1server"> |
|
<div> |
|
<div data-align="start"> |
|
<pre><code>┌────────────┐ |
|
│ │ ┌────────────┐ |
|
│ │ │┌──────────┐│ |
|
│ SERVER │ ││ ││ |
|
│ │ ││┌───┐┌───┐││ |
|
│ ◀─────────┤ ││ │││ |
|
│ │ ││└───┘└───┘││ |
|
│ │ ││ App App ││ |
|
│ │ ││ ││ |
|
│ │ ││┌───┐┌───┐││ |
|
│ │ │└┴───┴┴───┴┘│ |
|
│ │ │ CLIENT │ |
|
│ │ └────────────┘ |
|
└────────────┘ |
|
</code></pre> |
|
</div> |
|
</div> |
|
<figcaption><p>One HXML client, one HXML server</p></figcaption> |
|
</figure> |
|
<p>Luckily, developers do not need to write a HXML client from scratch; |
|
the open-source client library does 99% of the work. And as we will see |
|
in the next section, there are major benefits to controlling both the |
|
client and server in a HDA.</p> |
|
<h4 id="_extensibility">Extensibility</h4> |
|
<p>To understand the benefits of Hyperview’s architecture, we need to |
|
first discuss the drawbacks of the web architecture. On the web, any web |
|
browser can render HTML from any web server. This level of compatibility |
|
can only happen with well-defined standards such as HTML5. But defining |
|
and evolving standards is a laborious process. For example, the W3C took |
|
over 7 years to go from first draft to recommendation on the HTML5 spec. |
|
It’s not surprising, given the level of thoughtfulness that needs to go |
|
into a change that impacts so many people. But it means that progress |
|
happens slowly. As a web developer, you may need to wait years for |
|
browsers to gain widespread support for the feature you need.</p> |
|
<p>So what are the benefits of Hyperview’s architecture? In a Hyperview |
|
app, <em class="test">your</em> mobile app only renders HXML from <em class="test">your</em> |
|
server. You don’t need to worry about compatibility between your server |
|
and other mobile apps, or between your mobile app and other servers. |
|
There is no standards body to consult. If you want to add a blink |
|
feature to your mobile app, go ahead and implement a |
|
<code><blink></code> element in the client, and start returning |
|
<code><blink></code> elements in the HXML responses from your |
|
server. In fact, the Hyperview client library was built with this type |
|
of extensibility in mind. There are extension points for custom UI |
|
elements and custom behavior actions. We expect and encourage developers |
|
to use these extensions to make HXML more expressive and customized to |
|
their app’s functionality.</p> |
|
<p>And by extending the HXML format and client itself, there’s no need |
|
for Hyperview to include a scripting layer in HXML. Features that |
|
require client-side logic get “built-in” to the client binary. HXML |
|
responses remain pure, with UI and interactions represented in |
|
declarative XML.</p> |
|
<h3 id="_which_hypermedia_architecture_should_you_use">Which Hypermedia |
|
Architecture Should You Use?</h3> |
|
<p>We’ve discussed two approaches for creating mobile apps using |
|
hypermedia systems:</p> |
|
<ul> |
|
<li><p>create a backend that returns HTML, and serve it in a mobile app |
|
through a web view</p></li> |
|
<li><p>create a backend that returns HXML, and serve it in a mobile app |
|
with the Hyperview client</p></li> |
|
</ul> |
|
<p>I purposefully described the two approaches in a way to highlight |
|
their similarities. After all, they are both based on hypermedia |
|
systems, just with different formats and clients. Both approaches solve |
|
the fundamental issues with traditional, SPA-like mobile app |
|
development:</p> |
|
<ul> |
|
<li><p>The backend controls the full state of the app.</p></li> |
|
<li><p>Our app’s logic is all in one place.</p></li> |
|
<li><p>The app always runs the latest version, there’s no API churn to |
|
worry about.</p></li> |
|
</ul> |
|
<p>So which approach should you use for a mobile HDA? Based on our |
|
experience building both types of apps, we believe the Hyperview |
|
approach results in a better user experience. The web-view will always |
|
feel out-of-place on iOS and Android; there’s just no good way to |
|
replicate the patterns of navigation and interaction that mobile users |
|
expect. Hyperview was created specifically to address the limitations of |
|
thick-client and web view approaches. After the initial investment to |
|
learn Hyperview, you’ll get all of the benefits of the Hypermedia |
|
architecture, without the downsides of a degraded user experience.</p> |
|
<p>Of course, if you already have a simple, mobile-friendly web app, |
|
then using a web-view approach is sensible. You will certainly save time |
|
from not having to serve your app as HXML in addition to HTML. But as we |
|
will show at the end of this chapter, it doesn’t take a lot of work to |
|
convert an existing Hypermedia-driven web app into a Hyperview mobile |
|
app. But before we get there, we need to introduce the concepts of |
|
elements and behaviors in Hyperview. Then, we’ll re-build our contacts |
|
app in Hyperview.</p> |
|
<div id="sidebar"> |
|
<div> |
|
<div> |
|
<p><strong>When Shouldn’t You Use Hypermedia to Build a Mobile |
|
App?</strong></p> |
|
</div> |
|
<div> |
|
<p>Hypermedia is not always the right choice to build a mobile app. Just |
|
like on the web, apps that require highly dynamic UIs (such as a |
|
spreadsheet application) are better implemented with client-side code. |
|
Additionally, some apps need to function while fully offline. Since HDAs |
|
require a server to render UI, offline-first mobile apps are not a good |
|
fit for this architecture. However, just like on the web, developers can |
|
use a hybrid approach to build their mobile app. The highly dynamic |
|
screens can be built with complex client-side logic, while the less |
|
dynamic screens can be built with web views or Hyperview. In this way, |
|
developers can spend their <em class="test">complexity budget</em> on the core of the |
|
application, and keep the simple screens simple.</p> |
|
</div> |
|
</div> |
|
</div> |
|
<h2 id="introduction-to-hxml">Introduction to HXML</h2> |
|
<h3 id="hello-world-">Hello World!</h3> |
|
<p>HXML was designed to feel natural to web developers coming from HTML. |
|
Let’s take a closer look at the “Hello World” app defined in HXML:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb4"><pre class="sourceCode xml"><code class="sourceCode xml"><span id="cb4-1"><a aria-hidden="true" href="#cb4-1" tabindex="-1"></a><<span class="kw">doc</span><span class="ot"> xmlns=</span><span class="st">"https://hyperview.org/hyperview"</span>> <span class="er"><</span>1></span> |
|
<span id="cb4-2"><a aria-hidden="true" href="#cb4-2" tabindex="-1"></a> <<span class="kw">screen</span>> <span class="er"><</span>2></span> |
|
<span id="cb4-3"><a aria-hidden="true" href="#cb4-3" tabindex="-1"></a> <<span class="kw">styles</span> /></span> |
|
<span id="cb4-4"><a aria-hidden="true" href="#cb4-4" tabindex="-1"></a> <<span class="kw">body</span>> <span class="er"><</span>3></span> |
|
<span id="cb4-5"><a aria-hidden="true" href="#cb4-5" tabindex="-1"></a> <<span class="kw">header</span>> <span class="er"><</span>4></span> |
|
<span id="cb4-6"><a aria-hidden="true" href="#cb4-6" tabindex="-1"></a> <<span class="kw">text</span>>My first app</<span class="kw">text</span>></span> |
|
<span id="cb4-7"><a aria-hidden="true" href="#cb4-7" tabindex="-1"></a> </<span class="kw">header</span>></span> |
|
<span id="cb4-8"><a aria-hidden="true" href="#cb4-8" tabindex="-1"></a> <<span class="kw">view</span>> <span class="er"><</span>5></span> |
|
<span id="cb4-9"><a aria-hidden="true" href="#cb4-9" tabindex="-1"></a> <<span class="kw">text</span>>Hello World!</<span class="kw">text</span>> <span class="er"><</span>6></span> |
|
<span id="cb4-10"><a aria-hidden="true" href="#cb4-10" tabindex="-1"></a> </<span class="kw">view</span>></span> |
|
<span id="cb4-11"><a aria-hidden="true" href="#cb4-11" tabindex="-1"></a> </<span class="kw">body</span>></span> |
|
<span id="cb4-12"><a aria-hidden="true" href="#cb4-12" tabindex="-1"></a> </<span class="kw">screen</span>></span> |
|
<span id="cb4-13"><a aria-hidden="true" href="#cb4-13" tabindex="-1"></a></<span class="kw">doc</span>></span></code></pre></div> |
|
<figcaption><p>Hello World, revisited</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>The root element of the HXML app</p></li> |
|
<li><p>The element representing a screen of the app</p></li> |
|
<li><p>The element representing the UI of the screen</p></li> |
|
<li><p>The element representing the top header of the screen</p></li> |
|
<li><p>A wrapper element around the content shown on the screen</p></li> |
|
<li><p>The text content shown on the screen</p></li> |
|
</ol> |
|
<p>Nothing too strange here, right? Just like HTML, the syntax defines a |
|
tree of elements using start tags (<code><screen></code>) and end |
|
tags (<code></screen></code>). Elements can contain other elements |
|
(<code><view></code>) or text (<code>Hello World!</code>). |
|
Elements can also be empty, represented with an empty tag |
|
(<code><styles /></code>). However, you’ll notice that the names |
|
of the HXML element are different from those in HTML. Let’s take a |
|
closer look at each of those elements to understand what they do.</p> |
|
<p><code><doc></code> is the root of the HXML app. Think of it as |
|
equivalent to the <code><html></code> element in HTML. Note that |
|
the <code><doc></code> element contains an attribute |
|
<code>xmlns="https://hyperview.org/hyperview"</code>. This defines the |
|
default namespace for the doc. Namespaces are a feature of XML that |
|
allow one doc to contain elements defined by different developers. To |
|
prevent conflicts when two developers use the same name for their |
|
element, each developer defines a unique namespace. We will talk more |
|
about namespaces when we discuss custom elements & behaviors later |
|
in this chapter. For now, it’s enough to know that elements in a HXML |
|
doc without an explicit namespace are considered to be part of the |
|
<code>https://hyperview.org/hyperview</code> namespace.</p> |
|
<p><code><screen></code> represents the UI that gets rendered on a |
|
single screen of a mobile app. It’s possible for one |
|
<code><doc></code> to contain multiple <code><screen></code> |
|
elements, but we won’t get into that now. Typically, a |
|
<code><screen></code> element will contain elements that define |
|
the content and styling of the screen.</p> |
|
<p><code><styles></code> defines the styles of the UI on the |
|
screen. We won’t get too much into styling in Hyperview in this chapter. |
|
Suffice it to say, unlike HTML, Hyperview does not use a separate |
|
language (CSS) to define styles. Instead, styling rules such as colors, |
|
spacing, layout, and fonts are defined in HXML. These rules are then |
|
explicitly referenced by UI elements, much like using classes in |
|
CSS.</p> |
|
<p><code><body></code> defines the actual UI of the screen. The |
|
body includes all text, images, buttons, forms, etc that will be shown |
|
to the user. This is equivalent to the <code><body></code> element |
|
in HTML.</p> |
|
<p><code><header></code> defines the header of the screen. |
|
Typically in mobile apps, the header includes some navigation (like a |
|
back button), and the title of the screen. It’s useful to define the |
|
header separately from the rest of the body. Some mobile OSes will use a |
|
different transition for the header than the rest of the screen |
|
content.</p> |
|
<p><code><view></code> is the basic building block for layouts and |
|
structure within the screen’s body. Think of it like a |
|
<code><div></code> in HTML. Note that unlike HTML |
|
<code><div></code> elements, a <code><view></code> cannot |
|
directly contain text.</p> |
|
<p><code><text></code> elements are the only way to render text in |
|
the UI. In this example, “Hello World” is contained within a |
|
<code><text></code> element.</p> |
|
<p>That’s all there is to define a basic “Hello World” app in HXML. Of |
|
course, this isn’t very exciting. Let’s cover some other built-in |
|
display elements.</p> |
|
<h3 id="ui-elements">UI Elements</h3> |
|
<h4 id="lists">Lists</h4> |
|
<p>A very common pattern in mobile apps is to scroll through a list of |
|
items. The physical properties of a phone screen (long & vertical) |
|
and the intuitive gesture of swiping a thumb up & down makes this a |
|
good choice for many screens.</p> |
|
<p>HXML has dedicated elements for representing lists and items.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb5"><pre class="sourceCode xml"><code class="sourceCode xml"><span id="cb5-1"><a aria-hidden="true" href="#cb5-1" tabindex="-1"></a><<span class="kw">list</span>> <span class="er"><</span>1></span> |
|
<span id="cb5-2"><a aria-hidden="true" href="#cb5-2" tabindex="-1"></a> <<span class="kw">item</span><span class="ot"> key=</span><span class="st">"item1"</span>> <span class="er"><</span>2></span> |
|
<span id="cb5-3"><a aria-hidden="true" href="#cb5-3" tabindex="-1"></a> <<span class="kw">text</span>>My first item</<span class="kw">text</span>> <span class="er"><</span>3></span> |
|
<span id="cb5-4"><a aria-hidden="true" href="#cb5-4" tabindex="-1"></a> </<span class="kw">item</span>></span> |
|
<span id="cb5-5"><a aria-hidden="true" href="#cb5-5" tabindex="-1"></a> <<span class="kw">item</span><span class="ot"> key=</span><span class="st">"item2"</span>></span> |
|
<span id="cb5-6"><a aria-hidden="true" href="#cb5-6" tabindex="-1"></a> <<span class="kw">text</span>>My second item</<span class="kw">text</span>></span> |
|
<span id="cb5-7"><a aria-hidden="true" href="#cb5-7" tabindex="-1"></a> </<span class="kw">item</span>></span> |
|
<span id="cb5-8"><a aria-hidden="true" href="#cb5-8" tabindex="-1"></a></<span class="kw">list</span>></span></code></pre></div> |
|
<figcaption><p>List element</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Element representing a list</p></li> |
|
<li><p>Element representing an item in the list, with a unique |
|
key</p></li> |
|
<li><p>The content of the item in the list.</p></li> |
|
</ol> |
|
<p>Lists are represented with two new elements. The |
|
<code><list></code> wraps all of the items in the list. It can be |
|
styled like a generic <code><view></code> (width, height, etc). A |
|
<code><list></code> element only contains |
|
<code><item></code> elements. Of course, these represent each |
|
unique item in the list. Note that <code><item></code> is required |
|
to have a <code>key</code> attribute, which is unique among all items in |
|
the list.</p> |
|
<p>You might be asking, “Why do we need a custom syntax for lists of |
|
items? Can’t we just use a bunch of <code><view></code> |
|
elements?”. Yes, for lists with a small number of items, using nested |
|
<code><views></code> will work quite well. However, often the |
|
number of items in a list can be long enough to require optimizations to |
|
support smooth scrolling interactions. Consider browsing a feed of posts |
|
in a social media app. As you keep scrolling through the feed, it’s not |
|
unusual for the app to show hundreds if not thousands of posts. At any |
|
time, you can flick your finger to scroll to almost any part of the |
|
feed. Mobile devices tend to be memory-constrained. Keeping the |
|
fully-rendered list of items in memory could consume more resources than |
|
available. That’s why both iOS and Android provide APIs for optimized |
|
list UIs. These APIs know which part of the list is currently on-screen. |
|
To save memory, they clear out the non-visible list items, and recycle |
|
the item UI objects to conserve memory. By using explicit |
|
<code><list></code> and <code><item></code> elements in |
|
HXML, the Hyperview client knows to use these optimized list APIs to |
|
make your app more performant.</p> |
|
<p>It’s also worth mentioning that HXML supports section lists. Section |
|
lists are useful for building list-based UIs, where the items in the |
|
list can be grouped for the user’s convenience. For example, a UI |
|
showing a restaurant menu could group the offerings by dish type:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb6"><pre class="sourceCode xml"><code class="sourceCode xml"><span id="cb6-1"><a aria-hidden="true" href="#cb6-1" tabindex="-1"></a><<span class="kw">section-list</span>> <span class="er"><</span>1></span> |
|
<span id="cb6-2"><a aria-hidden="true" href="#cb6-2" tabindex="-1"></a> <<span class="kw">section</span>> <span class="er"><</span>2></span> |
|
<span id="cb6-3"><a aria-hidden="true" href="#cb6-3" tabindex="-1"></a> <<span class="kw">section-title</span>> <span class="er"><</span>3></span> |
|
<span id="cb6-4"><a aria-hidden="true" href="#cb6-4" tabindex="-1"></a> <<span class="kw">text</span>>Appetizers</<span class="kw">text</span>></span> |
|
<span id="cb6-5"><a aria-hidden="true" href="#cb6-5" tabindex="-1"></a> </<span class="kw">section-title</span>></span> |
|
<span id="cb6-6"><a aria-hidden="true" href="#cb6-6" tabindex="-1"></a> <<span class="kw">item</span><span class="ot"> key=</span><span class="st">"1"</span>> <span class="er"><</span>4></span> |
|
<span id="cb6-7"><a aria-hidden="true" href="#cb6-7" tabindex="-1"></a> <<span class="kw">text</span>>French Fries</<span class="kw">text</span>></span> |
|
<span id="cb6-8"><a aria-hidden="true" href="#cb6-8" tabindex="-1"></a> </<span class="kw">item</span>></span> |
|
<span id="cb6-9"><a aria-hidden="true" href="#cb6-9" tabindex="-1"></a> <<span class="kw">item</span><span class="ot"> key=</span><span class="st">"2"</span>></span> |
|
<span id="cb6-10"><a aria-hidden="true" href="#cb6-10" tabindex="-1"></a> <<span class="kw">text</span>>Onion Rings</<span class="kw">text</span>></span> |
|
<span id="cb6-11"><a aria-hidden="true" href="#cb6-11" tabindex="-1"></a> </<span class="kw">item</span>></span> |
|
<span id="cb6-12"><a aria-hidden="true" href="#cb6-12" tabindex="-1"></a> </<span class="kw">section</span>></span> |
|
<span id="cb6-13"><a aria-hidden="true" href="#cb6-13" tabindex="-1"></a></span> |
|
<span id="cb6-14"><a aria-hidden="true" href="#cb6-14" tabindex="-1"></a> <<span class="kw">section</span>> <span class="er"><</span>5></span> |
|
<span id="cb6-15"><a aria-hidden="true" href="#cb6-15" tabindex="-1"></a> <<span class="kw">section-title</span>></span> |
|
<span id="cb6-16"><a aria-hidden="true" href="#cb6-16" tabindex="-1"></a> <<span class="kw">text</span>>Entrees</<span class="kw">text</span>></span> |
|
<span id="cb6-17"><a aria-hidden="true" href="#cb6-17" tabindex="-1"></a> </<span class="kw">section-title</span>></span> |
|
<span id="cb6-18"><a aria-hidden="true" href="#cb6-18" tabindex="-1"></a> <<span class="kw">item</span><span class="ot"> key=</span><span class="st">"3"</span>></span> |
|
<span id="cb6-19"><a aria-hidden="true" href="#cb6-19" tabindex="-1"></a> <<span class="kw">text</span>>Burger</<span class="kw">text</span>></span> |
|
<span id="cb6-20"><a aria-hidden="true" href="#cb6-20" tabindex="-1"></a> </<span class="kw">item</span>></span> |
|
<span id="cb6-21"><a aria-hidden="true" href="#cb6-21" tabindex="-1"></a> </<span class="kw">section</span>></span> |
|
<span id="cb6-22"><a aria-hidden="true" href="#cb6-22" tabindex="-1"></a></<span class="kw">section-list</span>></span></code></pre></div> |
|
<figcaption><p>Section list element</p></figcaption> |
|
</figure> |
|
<ol> |
|
<li><p>Element representing a list with sections</p></li> |
|
<li><p>The first section of appetizer offerings</p></li> |
|
<li><p>Element for the title of the section, rendering the text |
|
“Appetizers”</p></li> |
|
<li><p>An item representing an appetizer</p></li> |
|
<li><p>A section for entree offerings</p></li> |
|
</ol> |
|
<p>You’ll notice a couple of differences between |
|
<code><list></code> and <code><section-list></code>. The |
|
section list element only contains <code><section></code> |
|
elements, representing a group of items. A section can contain a |
|
<code><section-title></code> element. This is used to render some |
|
UI that acts as the header of the section. This header is “sticky”, |
|
meaning it stays on screen while scrolling through items that belong to |
|
the corresponding section. Finally, <code><item></code> elements |
|
act the same as in the regular list, but can only appear within a |
|
<code><section></code>.</p> |
|
<h4 id="images">Images</h4> |
|
<p>Showing images in Hyperview is pretty similar to HTML, but there are |
|
a few differences.</p> |
|
<figure> |
|
<div class="sourceCode" id="cb7"><pre class="sourceCode xml"><code class="sourceCode xml"><span id="cb7-1"><a aria-hidden="true" href="#cb7-1" tabindex="-1"></a><<span class="kw">image</span><span class="ot"> source=</span><span class="st">"/profiles/1.jpg"</span><span class="ot"> style=</span><span class="st">"avatar"</span> /></span></code></pre></div> |
|
<figcaption><p>Image element</p></figcaption> |
|
</figure> |
|
<p>The <code>source</code> attribute specifies how to load the image. |
|
Like in HTML, the source can be an absolute or relative URL. |
|
Additionally, the source can be an encoded data URI, for example |
|
<code>data:image/png;base64,iVBORw</code>. However, the source can also |
|
be a “local” URL, referring to an image that is bundled as an asset in |
|
the mobile app. The local URL is prefixed with <code>./</code>:</p> |
|
<figure> |
|
<div class="sourceCode" id="cb8"><pre class="sourceCode xml"><code class="sourceCode xml"><span id="cb8-1"><a aria-hidden="true" href="#cb8-1" tabindex="-1"></a><<span class="kw">image</span><span class="ot"> source=</span><span class="st">"./logo.png"</span><span class="ot"> style=</span><span class="st">"logo"</span> /></span></code></pre></div> |
|
<figcaption><p>Image element, pointing to local source</p></figcaption> |
|
</figure> |
|
<p>Using Local URLs is an optimization. Since the images are on the |
|
mobile device, they don’t require a network request and will appear |
|
quickly. However, bundling the image with the mobile app binary |
|
increases the binary size. Using local images is a good trade-off for |
|
images that are frequently accessed but rarely change. Good examples |
|
include the app logo, or common button icons.</p> |
|
<p>The other thing to note is the presence of the <code>style</code> |
|
attribute on the <code><image></code> element. In HXML, images are |
|
required to have a style that has rules for the i |