CH10 and some small updates elsewhere

This commit is contained in:
Carson Gross 2022-08-14 16:13:38 -06:00
parent 0e4ec41d5a
commit eb0271b6dd
15 changed files with 9224 additions and 98572 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -120,7 +120,7 @@ Here is a visual representation of these two hypermedia interactions:
[#figure-1-1, reftext="Figure {chapter}.{counter:figure}"]
.HTTP Requests In Action
image::../images/figure_1-1_http_mental_model.png[]
image::../images/figure_1-1_http_mental_model.svg[]
As someone interested in web development, the above diagram should look very familiar to you, perhaps even boring. But,
despite its familiarity, consider the fact that the two above mechanisms are really the _only_ native ways for a user
@ -311,7 +311,7 @@ These _hypermedia-oriented_ libraries re-center the hypermedia approach as a via
In the web development world today there is a debate going on between the SPAs approach and what are now being called
"Multi-Page Applications" or MPAs. MPAs are usually just the old, traditional way of building web applications with
links and forms across multiple web pages and are thus, by their nature, hypermedia oriented. They are clunky, but,
despite this clunkiness, some web developers have become so exasperated at the complexity of SPA applications they have
despite this clunkiness, some web developers have become so exasperated at the complexity of SPA applications they `have`
decided to go back to this older way of building things and just accept the limitations of plain HTML.
Some thought leaders in web development, such as Rich Harris, creator of svelte.js, a popular SPA library, propose a mix
@ -320,7 +320,7 @@ it attempts to mix both the older MPA approach and the newer SPA approach into a
like the "transitional" trend in architecture, which blends traditional and modern architectural styles. It's a
good term and a reasonable compromise between the two approaches to building web applications.
But it still feels a bit unsatisfactory. Why have two very different architectual models _by default_? Recall that the
But it still feels a bit unsatisfactory. Why have two very different architectural models _by default_? Recall that the
crux of the tradeoffs between SPAs and MPAs is the _user experience_ or interactivity of the application.
This is typically the driving decision when choosing one approach versus the other for an application or, in the case
of Transitional Web Applications, for a particular feature.

View File

@ -1,10 +1,10 @@
= Hypermedia In Action
:chapter: 8
:chapter: 9
:sectnums:
:figure-caption: Figure {chapter}.
:listing-caption: Listing {chapter}.
:table-caption: Table {chapter}.
:sectnumoffset: 7
:sectnumoffset: 8
// line above: :sectnumoffset: 5 (chapter# minus 1)
:leveloffset: 1
:sourcedir: ../code/src
@ -245,7 +245,6 @@ like this:
"phone": ""
},
...
// TODO how to indicate code ommitted
----
So, you can see, a relatively simple JSON representation of our contacts. Not perfect, but good enough for the purposes
@ -484,5 +483,9 @@ still evolving separately is not as difficult or as crazy as it might sound at f
== Summary
* Having a Hypermedia Driven API is not mutually exclusive with having a JSON Data API
applications
* In this chapter we saw that a Hypermedia Driven Application can have a JSON Data API as well
* Hypermedia APIs and JSON Data APIs have different needs and shapes
* By properly factoring your back end code, much of the logic can be shared between the two APIs
* By splitting your APIs into both a Hypermedia API and a JSON Data API, you can evolve both without interferring with
one another, allowing you to, for example, change your Hypermedia API dramatically without breaking your JSON API
clients

View File

@ -0,0 +1,589 @@
= Hypermedia In Action
:chapter: 8
:sectnums:
:figure-caption: Figure {chapter}.
:listing-caption: Listing {chapter}.
:table-caption: Table {chapter}.
:sectnumoffset: 7
// line above: :sectnumoffset: 5 (chapter# minus 1)
:leveloffset: 1
:sourcedir: ../code/src
:source-language:
= Creating A Dynamic Archive UI
This chapter covers
* Creating a dynamically updated download UI using hypermedia
* Adding smooth animations to a progress bar
* Triggering a file download with a response header
[partintro]
== A Dynamic Archive UI
We've come a long way from our plain old traditional web application at this point: we've added active search, bulk
delete (with some nice animations) and a slew of other features, to say nothing of the hyperview-based mobile application
we have built. I hope you'll agree that we have reached a level of interactivity that most people would assume requires
some sort JavaScript framework, but we've done nearly all of it with good old hypermedia and a bit of scripting on the
side.
However, despite the wonderful reception of Contact.app in the world, one feature keeps coming up again and again: users
would like to be able to download all their contacts, preferably in an easy-to-use JSON format.
This is a reasonable request and another team has been working on the back-end support for doing exactly this. There is
one problem though: the archive takes a bit of time to prepare and export, typically on the order of five to 10 seconds,
but sometimes longer.
This is a classic problem in web app development. When faced with a long-running process we have two options:
* When the user triggers the action, block until it is complete and then respond with the result
* Start the action and return immediately
Just blocking and waiting for the action to complete is certainly the easy way to handle it, but it is a pretty terrible
user experience. 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 results of this choice.
The second option, starting the action in a separate, asynchronous manner (say, by starting a thread, or submitting it
to a job runner system) is much nicer: you can respond immediately and the user doesn't need to sit there wondering what's
going on. But the question is, what do you respond with?
I have seen a few different "simple" approaches in this scenario:
* 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
* Let the user know that the process has started and recommend that they manually (!!!) refresh the page to see the
status of the process
* Let the user know that the process has started and, using some JavaScript, automatically refresh the page every few
seconds
All of these work, but they sure aren't great user experiences, are they?
What we'd like in this scenario is something more like what you see when, for example, you download a file via the
browser: a nice progress bar indicating where in the process you are and then an option to click a link immediately
to view the result of the process.
Now, at this point, surely we are beyond what can be achieved using only hypermedia, right? Well, we wouldn't have a
whole chapter on this topic if that were the case, would we? We'll need to push htmx pretty hard to make this all work,
but when it is done it won't be _that_ much code, and it will give us the user interface that we want.
=== UI Requirements
Before we dive into the implementation, let's discuss in broad terms what our new UI should look like: we want a button
in the application labeled "Download Contact Archive". When a user clicks on that button, we want to replace that
button with a progress bar instead. As the archive job progresses, we want to move the progress bar along. When the
archive job is done, we want to show a link to the user to download the archive file.
As I mentioned earlier, thankfully another team has been working on the actual archive process, and they have given us
a class that we can work with, `Archiver`, that implements all the functionality that we need. In particular,
it gives us the following methods:
* `status()` - A string representing the status of the download, either `Waiting`, `Running` or `Complete`
* `progress()` - A number between 0 and 1, indicating how much progress the archive job has made
* `run()` - Starts A new archive job (if the current status is `Waiting`)
* `reset()` - Cancels the current archive job, if any, and resets to the "Waiting" state
* `archive_file()` - The path to the archive file that has been created on the server, so we can send it to the client
* `get()` - A class method that lets us get the Archiver for the current user
Not a terribly complicated API, the only somewhat tricky aspect to it is that the `run()` method is non-blocking: it
starts a background job to do the actual archiving and returns immediately.
=== Beginning Our Implementation
Now we 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.
So, in getting down to building the UI, the first thing I want to note is that the UI is largely self-contained: we
want to replace the button with the download progress bar, and then the link to download the results of the archive
process. Everything will all be in one place in the UI, which is a strong hint that we want to create a new template
to handle this little sub-section of the application. Let's call this template `archive_ui.html`.
Another thing that jumps out at me is that we are going to want to replace the entire download UI in multiple cases.
Since we want to do that, it makes sense to wrap the entire UI in a `div` tag, and then use that `div` as the target
for all our operations. So let's get our new template going with the following content:
.Our Initial Archive UI Template
[source, html]
----
<div id="archive-ui" hx-target="this"<1> hx-swap="outerHTML"<2>>
</div>
----
<1> This div will be the target for all elements inside of it
<2> Replace the entire div every time using `outerHTML`
Next, lets add that "Download Contact Archive" button to the `div`, which will kick off the archive-then-download
process. Let's use a `POST` to the path `/contacts/archive` to trigger the start of the process:
.Adding The Button
[source, html]
----
<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
<button hx-post="/contacts/archive"> <1>
Download Contact Archive
</button>
</div>
----
<1> This button will issue a `POST` to `/contacts/archive`
Finally, let's include this template in our main `index.html` template, above the contacts table:
.Our Initial Archive UI Template
[source, html]
----
{% block content %}
{% include 'archive_ui.html' %} <1>
<form action="/contacts" method="get" class="tool-bar">
----
<1> This template will now be included in the main template
With that done, we now have a button showing up in our web application to get the download going. Since the enclosing
`div` has an `hx-target="this"` on it, the button will inherit that target and replace the `div` with whatever HTML
comes back from the `POST` to `/contacts/archive`.
=== Adding the POST End Point
Our next step is to handle the `POST` that the button is making. What we are going to want to do is to get the
`Archiver` for the current user and invoke the `run()` method on it. This will start the archive process running. Then
we will want to render some new content indicating that the process is running.
To do that, what we want to do is reuse the `archive_ui` template to handle rendering the archive UI for both states,
when the archiver is "Waiting" and when it is "Running". (We will also handle the "Complete" state in a bit.)
This is a very common pattern: we put all the different UIs for a given conceptual "chuck" of the user interface into
a single template, and conditionally render the appropriate interface. This keeps everything together and makes it
very easy to understand how the UIs interact with one another.
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. So, again: we need to invoke `run()` and then pass the archiver out to the
template for conditional rendering. Here is what the code looks like:
.Server Side Code To Start The Archive Process
[source, python]
----
@app.route("/contacts/archive", methods=["POST"]) <1>
def start_archive():
archiver = Archiver.get() <2>
archiver.run() <3>
return render_template("archive_ui.html", archiver=archiver) <4>
----
<1> Handle `POST` to `/contacts/archive`
<2> Look up the Archiver
<3> Invoke the non-blocking `run()` method on it
<4> Render the `archive_ui.html` template, passing in the archiver
=== Conditionally Rendering A Progress UI
Now let's turn our attention to updating `archive_ui.html` to conditionally. We are passing the archiver through
as a variable to the template, and recall that the archiver has a `status()` method that we can consult to see what
the status of the archive process.
We want to render the "Download Contact Archive" button if the archiver has the status `Waiting`, and we want to render
some sort of message indicating that progress is happening if the status is `Running`. Let's update our template code
to do just that:
.Adding Conditional Rendering
[source, html]
----
<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
{% if archiver.status() == "Waiting" %} <1>
<button hx-post="/contacts/archive">
Download Contact Archive
</button>
{% elif archiver.status() == "Running" %}<2>
Running...<3>
{% end %}
</div>
----
<1> Only render button if the status is "Waiting"
<2> Render different content when status is "Running"
<3> For now, just some text saying things are Running
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...
Ouch:
.Something Went Wrong
----
UndefinedError
jinja2.exceptions.UndefinedError: 'archiver' is undefined
----
We get an error message right out of the box. Why? Ah, of course, we are including the `archive_ui.html` in the
`index.html` template, but now the `archive_ui.html` template expects the archiver to be passed through to it, so
it can conditionally render the correct UI. Well, that's an easy fix: we just need to pass the archiver through
when we render the `index.html` template as well:
.Including The Archiver When We Render index.html
[source, python]
----
@app.route("/contacts")
def contacts():
search = request.args.get("q")
if search:
contacts_set = Contact.search(search)
if request.headers.get('HX-Trigger') == 'search':
return render_template("rows.html", contacts=contacts_set)
else:
contacts_set = Contact.all()
return render_template("index.html", contacts=contacts_set, archiver=Archiver.get())<1>
----
<1> Pass through archiver to the main template
Now with that done, we can load up the page. And, sure enough, we can see the "Download Contact Archive" button now!
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.
== Polling
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!
What we want to do is have 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?
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. Polling has a bit of a bad rap, and it isn't the sexiest technique in the world: today
developers might look at a more advanced technique like WebSockets or Server Sent Events (SSE) to address this situation.
But, say what one will, polling _works_ and it is drop-dead simple. You need to be careful to make sure you don't overwhelm
you system with polling requests, but, with a bit of care, you can create a reliable, passively updated component in
your UI.
htmx offers two types of polling. The first is "fixed rate polling", which uses a special `hx-trigger` syntax to indicate
that something should be polled on a fixed interval. Here is an example:
.Fixed Interval Polling
[source, html]
----
<div hx-get="/messages" hx-trigger="every 3s"> <1>
</div>
----
<1> trigger a `GET` to `/messages` every three seconds
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.
In our case, we have a definite process with an ending to it. So, in our case, it will be better to use the other polling
technique, known as "load polling". In load polling, you take advantage of the fact that htmx triggers a `load` event
when content is loaded into the DOM. So you can create a trigger on the `load` event, but then add a bit of a delay so that
the request doesn't trigger immediately.
If you do this, then you can conditionally render the `hx-trigger` on every request: when a process has completed you
can simply not include the trigger and the load polling stops. A nice and simple way to poll for until a definite
process finishes.
=== Using Polling To Update The Archive UI
So, let's use load polling now 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 `progress()` method which returns a number between 0 and 1 indicating
how close the archive process is to completion. Here is the snippet of HTML we will use:
.A CSS-based Progress Bar
[source, html]
----
<div class="progress" >
<div class="progress-bar" style="width:{{ archiver.progress() * 100 }}%"></div> <1>
</div>
----
<1> The width of the inner element corresponds to the progress
This CSS-based progress bar has two components: an outer `div` that provides the wire frame for the progress bar, and
and inner `div` 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 `progress()` result by 100 to get a percentage) and that will make the progress
indicator the appropriate width within the parent div.
As I have mentioned before, this is not a book on CSS, but, for completeness, here is the CSS for this progress bar:
.The CSS For Our Progress Bar
[source, css]
----
.progress {
height: 20px;
margin-bottom: 20px;
overflow: hidden;
background-color: #f5f5f5;
border-radius: 4px;
box-shadow: inset 0 1px 2px rgba(0,0,0,.1);
}
.progress-bar {
float: left;
width: 0%;
height: 100%;
font-size: 12px;
line-height: 20px;
color: #fff;
text-align: center;
background-color: #337ab7;
box-shadow: inset 0 -1px 0 rgba(0,0,0,.15);
transition: width .6s ease;
}
----
Which ends up rendering like this:
[#figure-8-1, reftext="Figure {chapter}.{counter:figure}"]
.Our CSS-Based Progress Bar
image::../images/screenshot_progress_bar.png[]
So let's add the code for our progress bar into our `archive_ui.html` template for the case when the archiver is
running, and let's update the copy to say "Creating Archive...":
.Adding The Progress Bar
[source, html]
----
<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
{% if archiver.status() == "Waiting" %}
<button hx-post="/contacts/archive">
Download Contact Archive
</button>
{% elif archiver.status() == "Running" %}
<div>
Creating Archive...
<div class="progress" > <1>
<div class="progress-bar" style="width:{{ archiver.progress() * 100 }}%"></div>
</div>
</div>
{% endif %}
</div>
----
<1> Our shiny new progress bar
Sweet, 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.
To get the UI we want, we'll need to implement load polling using `hx-trigger`. 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 `div` that is
wrapping around the "Creating Archive..." text and the progress bar. Finally, let's make it poll by issuing a
`GET` to the same path that the `POST` was issued too: `/contacts/archive`. (As you have probably notices, this is a
common pattern in RESTful systems: reusing the same path with different actions.)
.Implementing Load Polling
[source, html]
----
<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
{% if archiver.status() == "Waiting" %}
<button hx-post="/contacts/archive">
Download Contact Archive
</button>
{% elif archiver.status() == "Running" %}
<div hx-get="/contacts/archive" hx-trigger="load delay:500ms"> <1>
Creating Archive...
<div class="progress" >
<div class="progress-bar" style="width:{{ archiver.progress() * 100 }}%"></div>
</div>
</div>
{% endif %}
</div>
----
<1> Issue a `GET` to `/contacts/archive` 500 milliseconds after the content loads
Again, it is important to realize that, when this `GET` is issued to `/contacts/archive`, it is going to replace
the `div` with the id `archive-ui`, not just itself. The `hx-target` attribute is _inherited_ by all child elements
within the `archive-ui` `div`, so, unless it is explicitly overriden by a child, the childrean will all target that
outermost `div` in the `archive_ui.html` file.
OK, now we need to handle the `GET` to `/contacts/archive` on the server. Thankfully, this is quite easy: all we
want to do is re-render `archive_ui.html` with the archiver:
.Handling Progress Updates
[source, python]
----
@app.route("/contacts/archive", methods=["GET"]) <1>
def archive_status():
archiver = Archiver.get()
return render_template("archive_ui.html", archiver=archiver) <2>
----
<1> handle `GET` to the `/contacts/archive` path
<2> just re-render the `archive_ui.html` template
Simple, like so much else with hypermedia!
And now, when we click the "Download Contact Archive", sure enough, we get a progress bar that updates every 500
milliseconds! And, as the result of the call to `archiver.progress()` incrementally updates from 0 to 1, the
progress bar moves across the screen for us, very cool!
=== Downloading The Result
OK, we have one more state to handle, the case when `achiver.status()` 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 `archive_file()` call.
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, `/contacts/archive/file`, which will respond with the archived JSON file. Here is
the new code:
.Rendering A Download Link When Archiving Completes
[source, html]
----
<div id="archive-ui" hx-target="this" hx-swap="outerHTML">
{% if archiver.status() == "Waiting" %}
<button hx-post="/contacts/archive">
Download Contact Archive
</button>
{% elif archiver.status() == "Running" %}
<div hx-get="/contacts/archive" hx-trigger="load delay:500ms">
Creating Archive...
<div class="progress" >
<div class="progress-bar" style="width:{{ archiver.progress() * 100 }}%"></div>
</div>
</div>
{% elif archiver.status() == "Complete" %} <1>
<a hx-boost="false" href="/contacts/archive/file">Archive Ready! Click here to download. &downarrow;</a> <2>
{% endif %}
</div>
----
<1> If the status is "Complete", render a download link
<2> The link will issue a `GET` to `/contacts/archive/file`
Note that the link has a `hx-boost` set to `false`. 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.
The final step is to handle the `GET` request to `/contacts/archive/file`. We want to send the file that the
archiver created down to the client. We are in luck: flask has a very simple mechanism for sending a file as
a downloaded response: the `send_file()` method. We can pass this method 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", which will
set the appropriate HTTP response headers to trigger the browsers downloading behavior.
.Sending A File To The Client
[source, python]
----
@app.route("/contacts/archive/file", methods=["GET"])
def archive_content():
manager = Archiver.get()
return send_file(manager.archive_file(), "archive.json", as_attachment=True) <1>
----
<1> send the file to the client
Perfect! Now we have an archive UI that is pretty darned slick: You can click the 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!
== Smoothing Things Out: More On The htmx Swap Model
As cool as this UI is, there is one minor annoyance with it: as the progress bar updates it "jumps" from one position
to the next. This looks jerky and is reminiscent of the feel of a full page refresh in web 1.0 style applications. It
turns out that there is a native HTML technology for smoothing out changes on an element from one state to another
that we discussed in Chapter 5: the CSS Transitions API.
Using CSS Transitions, you can smoothly animate an element between different styling by using the `transition` property.
If you look back at our CSS definition of the `.progress-bar` class, you will see the following transition definition
in it: `transition: width .6s ease;`. 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).
That's great and all, but in our example, htmx is _replacing_ the content with new content. It isn't updating the width
of the _existing_ element, which would trigger a transition. Rather, it is simply replacing it with a new element. So
no transition will occur, which is, indeed, what we are seeing: the progress bar jumps from spot to spot as it moves
towards completion.
=== Settling
When we discussed the htmx swap model in Chapter 5, we focused on the classes that htmx adds and removes, but we skipped
over the idea of "settling". What is "settling" in htmx terms? Settling is the following process: when htmx is
about to replace a chunk of content, it looks through the new content and finds all elements with an `id` on it. It then
looks in the _existing_ content for elements with the same `id`. If there is one, it does the following shuffle:
* The _new_ content gets the attributes of the _old_ content temporarily
* The new content is inserted
* After a small delay, the new content has its attributes reverted to their actual values
So, what is this strange little dance supposed to achieve? Well, what this ends up meaning is that, if an element
has a stable id between swaps, you _can_ write CSS transitions between various states. Since the new content briefly
has the _old_ attributes, the normal CSS mechanism will kick in when the actual values are restored.
So, in our case, all we need to do is to add a stable ID to our `progress-bar` element, and, rather than jumping
on every update, it the progress bar should smoothly move across the screen as it is updating, using the CSS transition
defined in our style sheet:
.Smoothing Things Out
[source, html]
----
<div class="progress" >
<div id="archive-progress" class="progress-bar" style="width:{{ archiver.progress() * 100 }}%"></div> <1>
</div>
----
<1> The progress bar div now has a stable id across requests
All we had to do was add a simple `id` attribute and viola, a much smoother user experience!
== Dismissing The Download UI
Next, let's make it possible for the user to dismiss the download link and return to the original export UI state. To
do this, we'll add a button that issues a `DELETE` to the path `/contacts/archive`, indicating that the current archive
can be removed or cleaned up.
We'll add it after the download link, like so:
.Clearing The Download
[source, html]
----
<a hx-boost="false" href="/contacts/archive/file" _="on load click() me">Archive Ready! Click here to download. &downarrow;</a>
<button hx-delete="/contacts/archive">Clear Download</button> <1>
----
<1> A simple button that issues a `DELETE` to `/contacts/archive`
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, that is straight fowards: we simply create a new handler for the `DELETE` HTTP Action,
invoke the `reset()` method on the archiver, and re-render the `archive_ui.html` template. Since this button is
picking up the same `hx-target` and `hx-swap` configuration as everything else, it "just works".
Here is the server side code:
.Resetting The Download
[source, python]
----
@app.route("/contacts/archive", methods=["DELETE"])
def reset_archive():
archiver = Archiver.get()
archiver.reset() <1>
return render_template("archive_ui.html", archiver=archiver)
----
<1> Call `reset()` on the archiver
Looks pretty similar to our other methods, doesn't it? That's the idea!
== Auto-Download
One pattern that I see sometimes on the web is "auto-downloading" where a file is created and then, when it is ready,
the system automatically downloads the file. We can add that functionality quite easily to our application with a bit
of hyperscript.
What we want to do is, when the download link renders, automatically click on the link for the user. The hyperscript
will read basically just like that:
.Auto-Downloading
[source, html]
----
<a hx-boost="false" href="/contacts/archive/file"
_="on load click() me"> <1>
Archive Downloading! Click here if the download does not start.
</a>
----
<1> a bit of hyperscript to make the file auto-download
Note that the scripting here is simply _enhancing_ the existing hypermedia, rather than replacing it with a non-hypermedia
request. This is hypermedia-friendly scripting!
So, despite our initial trepidation that it could be done, we've managed to create a very dynamic UI for our 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. And it only took about 16 lines of front end code and 16 lines
of backend code to build the whole thing, showing once again that HTML, with the help of htmx, can, in fact, be very
expressive.
== Summary
* In this chapter we built a sophisticated user interface to interact with a non-blocking, asynchornous back end process:
creating an archive of all contacts in our application
* We saw a few different ways to do polling in htmx, and settled on using "load polling" for our situation
* We saw how the htmx swap mechanism enables CSS transitions when an element has a stable ID in new pieces of content,
and we used that to smooth out the progress bar in our application
* We used a bit of hypermedia-friendly scripting to trigger an auto-download when the archive progress completes

View File

@ -82,16 +82,23 @@
.. 9.3 Adding a JSON Data API To Contact.app
.. 9.4 Summary
. 10 Advanced htmx
. 10 Creating A Dynamic Download UI
[none]
.. 10.1 Configuration
.. 10.2 Animations
.. 10.3 Debugging
.. 10.4 Security Concerns
.. 10.5 Understanding the htmx event model
.. 10.6 Understanding the extension model
.. 10.1 A Dynamic Archive UI
.. 10.2 Polling
.. 10.3 Smoothing Things Out: More On The htmx Swap Model
.. 10.4 Dismissing The Download UI
.. 10.5 Auto-Download
.. 10.6 Summary
. 11 Other Hypermedia-Oriented Javascript Libraries
. 11 Developing With htmx
.. 11.1 Additional Attributes
.. 11.2 Debugging
.. 11.3 Security Concerns
.. 11.4 Understanding the htmx event model
.. 11.5 Understanding the extension model
. 12 Other Hypermedia-Oriented Javascript Libraries
[none]
.. 11.1 Unpoly
.. 11.2 Hotwire

View File

@ -4,5 +4,6 @@ asciidoctor-pdf -a pdf-theme=manning -r /opt/homebrew/Cellar/asciidoctor/2.0.17/
asciidoctor-pdf -a pdf-theme=manning -r /opt/homebrew/Cellar/asciidoctor/2.0.17/libexec/gems/asciidoctor-pdf-1.6.2/lib/sectnumoffset-treeprocessor.rb -o pdfs/generated/CH04_gross_Hypermedia_In_Action.pdf CH04_improving_our_hypermedia_application.adoc
asciidoctor-pdf -a pdf-theme=manning -r /opt/homebrew/Cellar/asciidoctor/2.0.17/libexec/gems/asciidoctor-pdf-1.6.2/lib/sectnumoffset-treeprocessor.rb -o pdfs/generated/CH05_gross_Hypermedia_In_Action.pdf CH05_advanced_hypermedia_patterns.adoc
asciidoctor-pdf -a pdf-theme=manning -r /opt/homebrew/Cellar/asciidoctor/2.0.17/libexec/gems/asciidoctor-pdf-1.6.2/lib/sectnumoffset-treeprocessor.rb -o pdfs/generated/CH09_gross_Hypermedia_In_Action.pdf CH09_gross_json_data_apis.adoc
asciidoctor-pdf -a pdf-theme=manning -r /opt/homebrew/Cellar/asciidoctor/2.0.17/libexec/gems/asciidoctor-pdf-1.6.2/lib/sectnumoffset-treeprocessor.rb -o pdfs/generated/CH10_gross_Hypermedia_In_Action.pdf CH10_gross_download_ui.adoc
asciidoctor-pdf -a pdf-theme=manning -r /opt/homebrew/Cellar/asciidoctor/2.0.17/libexec/gems/asciidoctor-pdf-1.6.2/lib/sectnumoffset-treeprocessor.rb -o pdfs/generated/Hypermedia_In_Action.pdf index.adoc
asciidoctor-pdf -a pdf-theme=manning -r /opt/homebrew/Cellar/asciidoctor/2.0.17/libexec/gems/asciidoctor-pdf-1.6.2/lib/sectnumoffset-treeprocessor.rb -o pdfs/generated/TOC_gross_Hypermedia_In_Action.pdf TOC.adoc
asciidoctor-pdf -a pdf-theme=manning -r /opt/homebrew/Cellar/asciidoctor/2.0.17/libexec/gems/asciidoctor-pdf-1.6.2/lib/sectnumoffset-treeprocessor.rb -o pdfs/generated/TOC_gross_Hypermedia_In_Action.pdf TOC.adoc

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@
/Creator (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/Producer (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/ModDate (D:20220805162949-06'00')
/CreationDate (D:20220806092936-06'00')
/CreationDate (D:20220814160143-06'00')
>>
endobj
2 0 obj

View File

@ -5,7 +5,7 @@
/Creator (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/Producer (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/ModDate (D:20220805162953-06'00')
/CreationDate (D:20220806092937-06'00')
/CreationDate (D:20220814160144-06'00')
>>
endobj
2 0 obj

View File

@ -5,7 +5,7 @@
/Creator (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/Producer (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/ModDate (D:20220805164632-06'00')
/CreationDate (D:20220806092938-06'00')
/CreationDate (D:20220814160145-06'00')
>>
endobj
2 0 obj

View File

@ -5,7 +5,7 @@
/Creator (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/Producer (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/ModDate (D:20220805164543-06'00')
/CreationDate (D:20220806092939-06'00')
/CreationDate (D:20220814160146-06'00')
>>
endobj
2 0 obj

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,8 @@
<< /Title (Untitled)
/Creator (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/Producer (Asciidoctor PDF 1.6.2, based on Prawn 2.4.0)
/ModDate (D:20220805165302-06'00')
/CreationDate (D:20220806092950-06'00')
/ModDate (D:20220814151839-06'00')
/CreationDate (D:20220814160153-06'00')
>>
endobj
2 0 obj
@ -886,7 +886,7 @@ endobj
>>
endobj
16 0 obj
<< /Length 5386
<< /Length 6259
>>
stream
q
@ -1038,7 +1038,7 @@ ET
BT
66.24 419.896 Td
/F1.0 14 Tf
[<313020> 55.1758 <416476616e6365642068746d78>] TJ
[<3130204372656174696e6720> 55.1758 <41> 55.1758 <2044796e616d696320446f776e6c6f6164205549>] TJ
ET
0.0 0.0 0.0 SCN
@ -1057,7 +1057,7 @@ ET
BT
84.24 394.098 Td
/F1.0 14 Tf
<31302e3120436f6e66696775726174696f6e> Tj
[<31302e3120> 55.1758 <41> 55.1758 <2044796e616d696320> 55.1758 <41726368697665205549>] TJ
ET
0.0 0.0 0.0 SCN
@ -1068,7 +1068,7 @@ ET
BT
84.24 368.3 Td
/F1.0 14 Tf
[<31302e3220> 55.1758 <416e696d6174696f6e73>] TJ
<31302e3220506f6c6c696e67> Tj
ET
0.0 0.0 0.0 SCN
@ -1079,7 +1079,7 @@ ET
BT
84.24 342.502 Td
/F1.0 14 Tf
<31302e3320446562756767696e67> Tj
[<31302e3320536d6f6f7468696e6720> 18.0664 <5468696e6773204f75743a204d6f7265204f6e20> 18.0664 <5468652068746d782053776170204d6f64656c>] TJ
ET
0.0 0.0 0.0 SCN
@ -1090,7 +1090,7 @@ ET
BT
84.24 316.704 Td
/F1.0 14 Tf
<31302e3420536563757269747920436f6e6365726e73> Tj
[<31302e34204469736d697373696e6720> 18.0664 <54686520446f776e6c6f6164205549>] TJ
ET
0.0 0.0 0.0 SCN
@ -1101,7 +1101,7 @@ ET
BT
84.24 290.906 Td
/F1.0 14 Tf
<31302e3520556e6465727374616e64696e67207468652068746d78206576656e74206d6f64656c> Tj
[<31302e3520> 55.1758 <4175746f2d446f776e6c6f6164>] TJ
ET
0.0 0.0 0.0 SCN
@ -1112,7 +1112,7 @@ ET
BT
84.24 265.108 Td
/F1.0 14 Tf
<31302e3620556e6465727374616e64696e672074686520657874656e73696f6e206d6f64656c> Tj
<31302e362053756d6d617279> Tj
ET
0.0 0.0 0.0 SCN
@ -1123,7 +1123,7 @@ ET
BT
66.24 239.31 Td
/F1.0 14 Tf
[<31> 37.1094 <31204f746865722048797065726d656469612d4f7269656e746564204a617661736372697074204c6962726172696573>] TJ
[<31> 37.1094 <3120446576656c6f70696e6720> 18.0664 <57> 40.0391 <6974682068746d78>] TJ
ET
0.0 0.0 0.0 SCN
@ -1134,6 +1134,17 @@ ET
0.0 Tc
-0.5 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
68.538 213.512 Td
/F1.0 14 Tf
<612e> Tj
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.0 Tc
0.2 0.2 0.2 scn
@ -1142,51 +1153,127 @@ ET
BT
84.24 213.512 Td
/F1.0 14 Tf
[<31> 37.1094 <312e3120556e706f6c79>] TJ
[<31> 37.1094 <312e3120> 55.1758 <4164646974696f6e616c20> 55.1758 <41747472696275746573>] TJ
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
-0.5 Tc
0.0 Tc
-0.5 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
67.74 187.714 Td
/F1.0 14 Tf
<622e> Tj
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.0 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
84.24 187.714 Td
/F1.0 14 Tf
[<31> 37.1094 <312e3220486f7477697265>] TJ
[<31> 37.1094 <312e3220446562756767696e67>] TJ
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
-0.5 Tc
0.0 Tc
-0.5 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
68.538 161.916 Td
/F1.0 14 Tf
<632e> Tj
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.0 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
84.24 161.916 Td
/F1.0 14 Tf
[<31> 37.1094 <312e33206a5175657279>] TJ
[<31> 37.1094 <312e3320536563757269747920436f6e6365726e73>] TJ
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
-0.5 Tc
0.0 Tc
-0.5 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
67.74 136.118 Td
/F1.0 14 Tf
<642e> Tj
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.0 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
84.24 136.118 Td
/F1.0 14 Tf
[<31> 37.1094 <312e3420> 18.0664 <56> 110.8398 <616e696c6c614a53>] TJ
[<31> 37.1094 <312e3420556e6465727374616e64696e67207468652068746d78206576656e74206d6f64656c>] TJ
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
-0.5 Tc
0.0 Tc
-0.5 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
68.538 110.32 Td
/F1.0 14 Tf
<652e> Tj
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.0 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
84.24 110.32 Td
/F1.0 14 Tf
[<31> 37.1094 <312e352053756d6d617279>] TJ
[<31> 37.1094 <312e3520556e6465727374616e64696e672074686520657874656e73696f6e206d6f64656c>] TJ
ET
0.0 0.0 0.0 SCN
@ -1197,7 +1284,7 @@ ET
BT
66.24 84.522 Td
/F1.0 14 Tf
[<31322048797065726d656469613a20> 55.1758 <41> 55.1758 <2052657475726e20> 18.0664 <54> 69.8242 <6f20> 18.0664 <54686520> 18.0664 <57> 80.0781 <6562d5> 55.1758 <7320526f6f7473>] TJ
<3132204f746865722048797065726d656469612d4f7269656e746564204a617661736372697074204c6962726172696573> Tj
ET
0.0 0.0 0.0 SCN
@ -1274,7 +1361,7 @@ endobj
>>
endobj
18 0 obj
<< /Length 1402
<< /Length 2458
>>
stream
q
@ -1294,7 +1381,7 @@ q
BT
84.24 729.472 Td
/F1.0 14 Tf
[<31322e3120> 18.0664 <54> 35.1562 <72656e647320696e20536f66747761726520446576656c6f706d656e74>] TJ
[<31> 37.1094 <312e3120556e706f6c79>] TJ
ET
0.0 0.0 0.0 SCN
@ -1305,7 +1392,7 @@ ET
BT
84.24 703.674 Td
/F1.0 14 Tf
<31322e3220436f6d706c65786974792053656c6c732c2053696d706c696369747920456e6475726573> Tj
[<31> 37.1094 <312e3220486f7477697265>] TJ
ET
0.0 0.0 0.0 SCN
@ -1316,6 +1403,80 @@ ET
BT
84.24 677.876 Td
/F1.0 14 Tf
[<31> 37.1094 <312e33206a5175657279>] TJ
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
84.24 652.078 Td
/F1.0 14 Tf
[<31> 37.1094 <312e3420> 18.0664 <56> 110.8398 <616e696c6c614a53>] TJ
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
84.24 626.28 Td
/F1.0 14 Tf
[<31> 37.1094 <312e352053756d6d617279>] TJ
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
66.24 600.482 Td
/F1.0 14 Tf
[<31322048797065726d656469613a20> 55.1758 <41> 55.1758 <2052657475726e20> 18.0664 <54> 69.8242 <6f20> 18.0664 <54686520> 18.0664 <57> 80.0781 <6562d5> 55.1758 <7320526f6f7473>] TJ
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
-0.5 Tc
0.0 Tc
-0.5 Tc
0.0 Tc
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
84.24 574.684 Td
/F1.0 14 Tf
[<31322e3120> 18.0664 <54> 35.1562 <72656e647320696e20536f66747761726520446576656c6f706d656e74>] TJ
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
84.24 548.886 Td
/F1.0 14 Tf
<31322e3220436f6d706c65786974792053656c6c732c2053696d706c696369747920456e6475726573> Tj
ET
0.0 0.0 0.0 SCN
0.0 0.0 0.0 scn
0.2 0.2 0.2 scn
0.2 0.2 0.2 SCN
BT
84.24 523.088 Td
/F1.0 14 Tf
<31322e332053756d6d617279> Tj
ET
@ -1325,7 +1486,7 @@ ET
0.2 0.2 0.2 SCN
BT
66.24 652.078 Td
66.24 497.29 Td
/F1.0 14 Tf
[<417070656e64697820313a20> 55.1758 <41> 55.1758 <20526576696577206f6620436861707465722035206f6620526f79204669656c64696e67d5> 55.1758 <7320446973736572746174696f6e204f6e20> 18.0664 <54686520> 18.0664 <57> 80.0781 <6562>] TJ
ET
@ -1903,35 +2064,35 @@ xref
0000006973 00000 n
0000011896 00000 n
0000012223 00000 n
0000017662 00000 n
0000017989 00000 n
0000019444 00000 n
0000019771 00000 n
0000019845 00000 n
0000019980 00000 n
0000020111 00000 n
0000020201 00000 n
0000020467 00000 n
0000020733 00000 n
0000020913 00000 n
0000021177 00000 n
0000021441 00000 n
0000032302 00000 n
0000032525 00000 n
0000033879 00000 n
0000034793 00000 n
0000076282 00000 n
0000076500 00000 n
0000077854 00000 n
0000078768 00000 n
0000089708 00000 n
0000089940 00000 n
0000091294 00000 n
0000018535 00000 n
0000018862 00000 n
0000021373 00000 n
0000021700 00000 n
0000021774 00000 n
0000021909 00000 n
0000022040 00000 n
0000022130 00000 n
0000022396 00000 n
0000022662 00000 n
0000022842 00000 n
0000023106 00000 n
0000023370 00000 n
0000034231 00000 n
0000034454 00000 n
0000035808 00000 n
0000036722 00000 n
0000078211 00000 n
0000078429 00000 n
0000079783 00000 n
0000080697 00000 n
0000091637 00000 n
0000091869 00000 n
0000093223 00000 n
trailer
<< /Size 41
/Root 2 0 R
/Info 1 0 R
>>
startxref
92208
94137
%%EOF