mirror of
https://github.com/bigskysoftware/hypermedia-systems.git
synced 2025-12-06 00:02:36 -05:00
edits, ch7, and fix broken diagram link ch10
This commit is contained in:
parent
b4123280ad
commit
2a32302d99
@ -22,12 +22,12 @@ when Google adopted it for search results, and many applications now implement i
|
||||
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 front end code will look fairly similar, a testament to how general the "`issue
|
||||
a request on an event and replace something on the screen`" approach that htmx takes really is.
|
||||
will, of course, be very different, but the front-end code will look fairly similar due to htmx's general approach of "`issue
|
||||
a request on an event and replace something on the screen.`"
|
||||
|
||||
=== Our Current Search UI
|
||||
|
||||
Let's recall what the current search field in our application currently looks like:
|
||||
Let's recall what the search field in our application currently looks like:
|
||||
|
||||
.Our search form
|
||||
[source,html]
|
||||
@ -47,24 +47,23 @@ As it stands right now, the user must hit enter when the search input is focused
|
||||
of these events will trigger a `submit` event on the form, causing it to issue an HTTP `GET` and re-rendering the whole
|
||||
page.
|
||||
|
||||
Currently, thanks to `hx-boost`, the form will use an AJAX request for this `GET`, but we currently don't get that nice
|
||||
search-as-you-type behavior that we want.
|
||||
Currently, thanks to `hx-boost`, the form will use an AJAX request for this `GET`, but we don't yet get that nice
|
||||
search-as-you-type behavior we want.
|
||||
|
||||
=== Adding Active Search
|
||||
|
||||
To add active search behavior, we will need to add a few htmx attributes to the search input. We will leave the current
|
||||
form as it is, with an `action` and `method`, so that, in case a user does not have JavaScript enabled, the normal
|
||||
search behavior continues to work. This will make our "`Active Search`" improvement a nice "`progressive enhancement`".
|
||||
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 `action` and `method`, 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.`"
|
||||
|
||||
So, in addition to the regular form behavior, we _also_ want to issue an HTTP `GET` 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.
|
||||
|
||||
As we said, this functionality is very similar to what we needed for email validation isn't it? We can, in fact copy
|
||||
As we said, this functionality is very similar to what we needed for email validation. We can, in fact copy
|
||||
the `hx-trigger` attribute directly from our email validation example, with its small 200-millisecond delay, to allow a
|
||||
user to stop typing before a request is triggered.
|
||||
|
||||
Again, a great example of how common patterns come up again and again when you are using htmx.
|
||||
This is another example of how common patterns come up again and again when using htmx.
|
||||
|
||||
.Adding active search behavior
|
||||
[source,html]
|
||||
@ -81,15 +80,15 @@ Again, a great example of how common patterns come up again and again when you a
|
||||
<2> Issue a `GET` to the same URL as the form.
|
||||
<3> Nearly the same `hx-trigger` specification as for the email input validation.
|
||||
|
||||
We did make a small change to the `hx-trigger` attribute: we switched out the `change` event for the `search` event.
|
||||
We made a small change to the `hx-trigger` attribute: we switched out the `change` event for the `search` event.
|
||||
The `search` 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 `keyup`
|
||||
which, as with the email example, is delayed with the `delay:200ms` modifier to "`debounce`" the input requests and
|
||||
it doesn't hurt to include here. The main functionality of the feature is provided by the second triggering event, the `keyup`.
|
||||
As in the email example, this trigger is delayed with the `delay:200ms` modifier to "`debounce`" the input requests and
|
||||
avoid hammering our server with requests on every keyup.
|
||||
|
||||
=== Targeting The Correct Element
|
||||
|
||||
What we have is already pretty close to what we want, but we need to set up the correct target. Recall that the default
|
||||
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 `GET` request will be issued to the `/contacts` path,
|
||||
which will, as of now, return an entire HTML document of search results, and then this whole document will be inserted
|
||||
into the _inner_ HTML of the search input.
|
||||
@ -113,7 +112,7 @@ the table of contacts:
|
||||
<label for="search">Search Term</label>
|
||||
<input id="search" type="search" name="q" value="{{ request.args.get('q') or '' }}"
|
||||
hx-get="/contacts"
|
||||
hx-trigger="change, keyup delay:200ms changed"
|
||||
hx-trigger="search, keyup delay:200ms changed"
|
||||
hx-target="tbody"/> <1>
|
||||
<input type="submit" value="Search"/>
|
||||
</form>
|
||||
@ -133,8 +132,7 @@ Now if you try typing something into the search box, we'll see some results: a r
|
||||
into the document within the `tbody`. Unfortunately, the content that is coming back is still an entire HTML document.
|
||||
|
||||
Here we end up with a "`double render`" situation, where an entire document has been inserted _inside_ 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
|
||||
silly mis-targeting issues we mentioned earlier.
|
||||
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.
|
||||
|
||||
Thankfully, it is pretty easy to fix.
|
||||
|
||||
@ -167,12 +165,12 @@ bit_ of HTML, rather than a full document. Currently, we are letting the server
|
||||
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.
|
||||
|
||||
In our application, however, since we are doing "`Full Stack`" development (that is: we control both the front end _and_ the back end
|
||||
In our application, however, since we are doing "`Full Stack`" development (that is: we control both front-end _and_ back-end
|
||||
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.
|
||||
|
||||
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 take this opportunity to explore returning
|
||||
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.
|
||||
|
||||
Here's a look again at the current server-side code for our search logic:
|
||||
@ -229,9 +227,8 @@ Sec-GPC: 1
|
||||
TE: trailers
|
||||
----
|
||||
|
||||
|
||||
htmx takes advantage of this feature of HTTP and adds additional headers and, therefore, additional _context_ to the
|
||||
HTTP requests that it makes. This allows you to inspect those headers and make smarter decisions with respect to exactly
|
||||
Htmx takes advantage of this feature of HTTP and adds additional headers and, therefore, additional _context_ 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.
|
||||
|
||||
Here is a table of the HTTP headers that htmx includes in HTTP requests:
|
||||
@ -281,16 +278,16 @@ def contacts():
|
||||
contacts_set = Contact.all()
|
||||
return render_template("index.html", contacts=contacts_set) <2>
|
||||
----
|
||||
<1> If the request header `HX-Trigger` is equal to "`search`", we want to do something different.
|
||||
<1> If the request header `HX-Trigger` is equal to "`search`" we want to do something different.
|
||||
<2> We need to learn how to render just the table rows.
|
||||
|
||||
OK, so how do we render only the result rows?
|
||||
|
||||
=== Factoring Your Templates
|
||||
|
||||
Now we come to what is a common pattern in htmx: we want to _factor_ our server-side templates. This means that we want to
|
||||
Now we come to a common pattern in htmx: we want to _factor_ 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 this new template `rows.html` and we will include it from
|
||||
the results table out to a separate template we will call `rows.html`. We will include it from
|
||||
the original `index.html` 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.
|
||||
|
||||
@ -377,10 +374,10 @@ The last step in factoring our templates is to modify our web controller to take
|
||||
file when it responds to an active search request.
|
||||
|
||||
Since `rows.html` is just another template, just like `index.html`, all we need to do is call the `render_template`
|
||||
function with `rows.html` rather than `index.html`, and we will render _only_ the row content rather than the entire
|
||||
function with `rows.html` rather than `index.html`. This will render _only_ the row content rather than the entire
|
||||
page:
|
||||
|
||||
.Updating our server side search
|
||||
.Updating our server-side search
|
||||
[source,python]
|
||||
----
|
||||
@app.route("/contacts")
|
||||
@ -398,9 +395,9 @@ def contacts():
|
||||
|
||||
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 `tbody` on
|
||||
the index page, without any need for an `hx-select` or any other client-side processing.
|
||||
the index page, without any need for `hx-select` or other client-side processing.
|
||||
|
||||
And, as a bonus, the old form-based search still works as well, thanks to the fact that we conditionally render the rows
|
||||
And, as a bonus, the old form-based search _still works_. We conditionally render the rows
|
||||
only when the `search` input issues the HTTP request via htmx. Again, this is a progressive enhancement to our
|
||||
application.
|
||||
|
||||
@ -435,18 +432,17 @@ the browser's notion of history: if you click the back button it will take you t
|
||||
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.
|
||||
|
||||
As it stands right now, during our Active Search, we are not updating the browser's navigation bar, so users aren't getting
|
||||
nice copy-and-pasteable links and you aren't getting history entries either, so no back button support. Fortunately, htmx
|
||||
provides a way for fixing this that we've already seen: the `hx-push-url` attribute.
|
||||
As it stands right now, during our Active Search, we are not updating the browser's navigation bar. So, users aren't getting
|
||||
nice copy-and-pasteable links 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 `hx-push-url` attribute.
|
||||
|
||||
The `hx-push-url` attribute lets you tell htmx "`Please push the URL of this request into the browser's navigation bar`".
|
||||
The `hx-push-url` 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.
|
||||
|
||||
So, to get proper history support for our Active Search, all we need to do is to set the `hx-push-url` attribute to
|
||||
`true`. Let's update our search input:
|
||||
`true`.
|
||||
|
||||
.Updating the URL during active search
|
||||
[source, html]
|
||||
@ -462,27 +458,26 @@ So, to get proper history support for our Active Search, all we need to do is to
|
||||
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.
|
||||
|
||||
Now, you might not _want_ this behavior. You might feel it would be confusing to users to see the navigation bar updated
|
||||
You might not _want_ 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 `hx-push-url`
|
||||
attribute and it will go back to the behavior you want. Htmx tries to be flexible enough that you can achieve the UX
|
||||
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 _you_ want, while staying within the declarative HTML model.
|
||||
|
||||
=== Adding A Request Indicator
|
||||
|
||||
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 to know that the active search functionality is doing a request implicitly and,
|
||||
if the search takes a bit, may end up thinking that the feature isn't working. By adding a request indicator we let
|
||||
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.
|
||||
|
||||
htmx provides support for request indicators via the `hx-indicator` attribute. This attribute takes, you guessed it,
|
||||
Htmx provides support for request indicators via the `hx-indicator` 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`".
|
||||
is happening.`"
|
||||
|
||||
Let's add a spinner after our search input:
|
||||
|
||||
.Updating the URL during active search
|
||||
.Adding a request indicator to search
|
||||
[source, html]
|
||||
----
|
||||
<input id="search" type="search" name="q" value="{{ request.args.get('q') or '' }}"
|
||||
@ -499,15 +494,14 @@ Let's add a spinner after our search input:
|
||||
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.
|
||||
|
||||
Note that the indicator `img` tag has the `htmx-indicator` class on it. `htmx-indicator` is a CSS class that is
|
||||
It just works, but how does htmx make the spinner appear and disappear? Note that the indicator `img` tag has the `htmx-indicator` class on it. `htmx-indicator` is a CSS class that is
|
||||
automatically injected into the page by htmx. This class sets the default `opacity` of an element to `0`, which hides
|
||||
the element from view, while at the same time not disrupting the layout of the page.
|
||||
|
||||
When an htmx request is triggered that points to this indicator, another class, `htmx-request` 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,
|
||||
and then, when a request is in flight, will be shown. This is all done via standard CSS classes, allowing you to control
|
||||
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 `display` rather than
|
||||
`opacity`). Htmx is flexible in this regard.
|
||||
`opacity`).
|
||||
|
||||
.Use Request Indicators!
|
||||
****
|
||||
@ -515,28 +509,26 @@ Request indicators are an important UX aspect of any distributed application. I
|
||||
de-emphasized their native request indicators over time, and it is doubly unfortunate that request indicators are not
|
||||
part of the JavaScript ajax APIs.
|
||||
|
||||
Be sure not to neglect this significant aspect of your application. Even though requests might seem instant when you are
|
||||
working on your application locally, in the real world they can take quite a bit longer due to network latency. It's
|
||||
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.
|
||||
****
|
||||
|
||||
With this request indicator, we now have a pretty sophisticated user experience built out when compared with plain HTML, but
|
||||
we've built it all as a hypermedia-driven feature. No JSON or JavaScript to be seen. And this particular implementation also
|
||||
has the benefit of being a progressive enhancement, so this aspect of our application will continue to work for clients
|
||||
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.
|
||||
|
||||
== Lazy Loading
|
||||
|
||||
With Active Search behind us, let's move on to a very different sort of problem: lazy loading. Lazy loading is
|
||||
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.
|
||||
|
||||
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 easy it is to add lazy loading to our
|
||||
application with htmx.
|
||||
give us a potentially expensive operation that we can use to demonstrate how to add lazy loading with htmx.
|
||||
|
||||
First let's update our server code in the `/contacts` 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.
|
||||
@ -560,11 +552,10 @@ def contacts():
|
||||
<1> Get the total count of contacts from the Contact model.
|
||||
<2> Pass the count out to the `index.html` template to use when rendering.
|
||||
|
||||
As with the rest of the application, in the interest of staying focused on the _hypermedia_ part of Contact.app, we are
|
||||
not going to look into the details of how `Contact.count()` works. We just need to know that:
|
||||
As with the rest of the application, in the interest of staying focused on the _hypermedia_ part of Contact.app, we'll skip over the details of how `Contact.count()` works. We just need to know that:
|
||||
|
||||
* It returns the total count of contacts in the contact database
|
||||
* It may potentially be slow
|
||||
* It returns the total count of contacts in the contact database.
|
||||
* It may be slow (for the sake of our example).
|
||||
|
||||
Next lets add some HTML to our `index.html` 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:
|
||||
@ -590,7 +581,7 @@ image::screenshot_total_contacts.png[(22 total Contacts)]
|
||||
Beautiful.
|
||||
|
||||
Of course, as you probably suspected, all is not perfect. Unfortunately, upon shipping this feature to production, we
|
||||
start getting some complaints from the users that the application "`feels slow.`" Like all good developers faced with
|
||||
start getting 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.
|
||||
|
||||
@ -608,8 +599,7 @@ using htmx instead.
|
||||
|
||||
=== Pulling Out The Expensive Code
|
||||
|
||||
The first step in implementing the Lazy Load pattern is to pull the expensive code, that is, the call to `Contacts.count()`
|
||||
out of the request handler for the `/contacts` endpoint.
|
||||
The first step in implementing the Lazy Load pattern is to pull the expensive code -- that is, the call to `Contacts.count()` -- out of the request handler for the `/contacts` endpoint.
|
||||
|
||||
Let's put this function call into its own HTTP request handler as a new HTTP endpoint that we will put at `/contacts/count`.
|
||||
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
|
||||
@ -676,7 +666,7 @@ to populate it instead.
|
||||
|
||||
And, check it out, our `/contacts` 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 much more quickly.
|
||||
expensive calculation to a secondary request, allowing the initial request to finish loading faster.
|
||||
|
||||
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
|
||||
@ -690,7 +680,7 @@ Yes, the total time to get all the information on the screen takes just as long.
|
||||
we now need two HTTP requests to get all the information for the page. But the _perceived performance_ for the end user will
|
||||
be much better: they can do what they want nearly immediately, even if some information isn't available instantaneously.
|
||||
|
||||
Lazy Loading is a great tool to have in your tool belt when optimizing your web application performance.
|
||||
Lazy Loading is a great tool to have in your belt when optimizing web application performance.
|
||||
|
||||
=== Adding An Indicator
|
||||
|
||||
@ -721,12 +711,11 @@ So let's add that spinner from the active search example as the initial content
|
||||
----
|
||||
<1> Yep, that's it.
|
||||
|
||||
Now when the user loads the page, rather than having the total contact count sprung on them like a surprise,
|
||||
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.
|
||||
|
||||
Note that all we had to do was copy and paste our indicator from the active search example into the `span`. Once again
|
||||
we see a great demonstration of how htmx provides flexible, composable features and building blocks for you to
|
||||
work with: implementing a new feature is often just copy-and-paste, maybe a tweak or two, and you are done.
|
||||
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.
|
||||
|
||||
=== But That's Not Lazy!
|
||||
|
||||
@ -864,7 +853,7 @@ we did in our "`Click To Load`" and "`Infinite Scroll`" features:
|
||||
|
||||
=== Updating The Server Side
|
||||
|
||||
Now we need to update the server side as well. We want to keep the "`Delete Contact`" button working as well, and in
|
||||
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 `DELETE` requests that are
|
||||
triggered by the button and `DELETE` requests that come from this anchor.
|
||||
|
||||
@ -886,7 +875,7 @@ change to the existing HTML:
|
||||
<1> An `id` attribute has been added to the button.
|
||||
|
||||
By giving this button an id attribute, we now have a mechanism for differentiating between the delete button in the
|
||||
`edit.html` template and the delete links in the `rows.html` template. When this button issues a request, it will now
|
||||
`edit.html` template and the delete links in the `rows.html` template. When this button issues a request, it will
|
||||
look something like this:
|
||||
|
||||
[source, http]
|
||||
@ -927,13 +916,13 @@ def contacts_delete(contact_id=0):
|
||||
|
||||
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 very powerful in this manner.
|
||||
dramatically different behavior. Hypermedia is powerful in this manner.
|
||||
|
||||
=== The Htmx Swapping Model
|
||||
|
||||
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 nice if, rather than just instantly deleting the row, we faded it out before we removed
|
||||
it. That easement makes it more obvious that the row is being removed, giving the user some nice visual feedback on the
|
||||
it. The fade would make it clear that the row is being removed, giving the user some nice visual feedback on the
|
||||
deletion.
|
||||
|
||||
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.
|
||||
@ -961,17 +950,17 @@ CSS transitions are a technology that allow you to animate a transition from one
|
||||
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.
|
||||
****
|
||||
|
||||
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
|
||||
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!
|
||||
****
|
||||
|
||||
=== Taking Advantage of "`htmx-swapping`"
|
||||
|
||||
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 `tr` element is removed, it will have the
|
||||
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 `tr` element is removed, it will have the
|
||||
`htmx-swapping` 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:
|
||||
|
||||
@ -992,7 +981,7 @@ above makes sense to you, even if this is the first time you've seen CSS transit
|
||||
|
||||
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 `htmx-swapping` 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 removing the row.
|
||||
fading the row out. Then the new (empty) content will be swapped in, which will effectively remove the row.
|
||||
|
||||
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
|
||||
@ -1021,7 +1010,7 @@ With this modification, the existing row will stay in the DOM for an additional
|
||||
on it. This will give the row time to transition to an opacity of zero, giving the fade out effect we want.
|
||||
|
||||
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
|
||||
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.)
|
||||
|
||||
@ -1125,14 +1114,14 @@ Now, when the button issues a `DELETE`, it will include all the contact ids that
|
||||
|
||||
=== The Server Side for Delete Selected Contacts
|
||||
|
||||
The server-side implementation is going to look an awful lot like our original server-side code for deleting a contact.
|
||||
In fact, once again, we can just copy and paste, and fix a bit of stuff up:
|
||||
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:
|
||||
|
||||
* We want to change the URL to `/contacts`
|
||||
* We want to change the URL to `/contacts`.
|
||||
* We want the handler to get _all_ the ids submitted as `selected_contact_ids` and iterate over each one, deleting the
|
||||
given contact
|
||||
given contact.
|
||||
|
||||
Those are really the only changes we need to make! Here is what the server-side code looks like:
|
||||
Those are the only changes we need to make! Here is what the server-side code looks like:
|
||||
|
||||
.The "`delete selected contacts`" button
|
||||
[source, python]
|
||||
@ -1153,10 +1142,9 @@ def contacts_delete_all():
|
||||
<4> Delete the given contact with each id.
|
||||
<5> Beyond that, it's the same code as our original delete handler: flash a message and render the `index.html` template.
|
||||
|
||||
So, as you can see, we just took the original delete logic and slightly modified it to deal with an array of ids, rather
|
||||
than a single id.
|
||||
So, we took the original delete logic and slightly modified it to deal with an array of ids, rather than a single id.
|
||||
|
||||
Readers with sharp eyes might notice one other small change: we did away with the redirect that was in the original
|
||||
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.
|
||||
|
||||
@ -299,7 +299,7 @@ which, perhaps inadvertently, tended to encourage this style of JavaScript.
|
||||
So, you can see that the notion of Separation of Concerns doesn't always work out as well as promised: our concerns
|
||||
end up intertwined or coupled pretty deeply, even when we separate them into different files.
|
||||
|
||||
image::images/diagram/separation-of-concerns.svg["Expectation: HTML concern, CSS concern, JS concern. Reality: HTML Co co co CSS nc nc nc JS ern ern ern"]
|
||||
image::diagram/separation-of-concerns.svg["Expectation: HTML concern, CSS concern, JS concern. Reality: HTML Co co co CSS nc nc nc JS ern ern ern"]
|
||||
|
||||
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
|
||||
@ -1288,7 +1288,7 @@ ____
|
||||
In case of conflict, consider users over authors over implementors over specifiers over theoretical purity.
|
||||
____
|
||||
|
||||
We have shown you quite a few tools and techniques for scripting in a Hypermedia-Driven Application. How should you
|
||||
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.
|
||||
|
||||
Are you committed to vanilla JavaScript-only, perhaps due to company policy? Well, you can use vanilla JavaScript effectively
|
||||
@ -1307,9 +1307,9 @@ In general, we encourage a _pragmatic_ approach to scripting: whatever feels rig
|
||||
right _enough_) for you. Rather than being concerned about which particular approach is taken for your scripting,
|
||||
we would focus on these more general concerns:
|
||||
|
||||
* Avoid communicating with the server via JSON data APIs
|
||||
* Avoid storing large amounts of state outside of the DOM
|
||||
* Favor using events, rather than hard-coded callbacks or method calls
|
||||
* Avoid communicating with the server via JSON data APIs.
|
||||
* Avoid storing large amounts of state outside of the DOM.
|
||||
* Favor using events, rather than hard-coded callbacks or method calls.
|
||||
|
||||
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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user