edits ch6

This commit is contained in:
Bill Talcott 2023-03-12 22:32:17 -04:00
parent 8a19791c26
commit b4123280ad

View File

@ -46,16 +46,14 @@ Believe it or not, that's it! This simple script tag will make htmx's functiona
== AJAX-ifying Our Application == AJAX-ifying Our Application
To get our feet wet with htmx, the first feature we are going to take advantage of, is what is known as "`boosting.`" This is To get our feet wet with htmx, the first feature we are going to take advantage of is known as "`boosting.`" This is
a bit of a "`cheater`" feature of htmx in that we don't need to do much beyond adding a single attribute, `hx-boost`, to the a bit of a "`magic`" feature in that we don't need to do much beyond adding a single attribute, `hx-boost`, to the
application. This `hx-boost` attribute is unlike most other attributes in htmx: whereas other htmx attributes tend to be application.
very focused on one aspect of improving HTML (e.g. `hx-trigger` focuses on the events that trigger a request, `hx-swap` focuses on how responses
are swapped into the DOM, etc.) the `hx-boost` attribute, in contrast, is a high-level attribute.
When you put `hx-boost` on a given element with the value `true`, it will "`boost`" all anchor and form elements within that When you put `hx-boost` on a given element with the value `true`, 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 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 into AJAX-powered hypermedia controls. Rather than issuing "`normal`" HTTP requests that replace the whole page, the links
and forms will issue AJAX requests and then htmx will swap the inner content of the `<body>` tag in the response to these and forms will issue AJAX requests. Htmx then swaps the inner content of the `<body>` tag in the response to these
requests into the existing pages `<body>` tag. requests into the existing pages `<body>` tag.
This makes navigation feel faster because the browser will not be re-interpreting most of the tags in the response This makes navigation feel faster because the browser will not be re-interpreting most of the tags in the response
@ -65,7 +63,7 @@ This makes navigation feel faster because the browser will not be re-interpretin
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. 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 `hx-boost="true"` on it, htmx will halt the normal link behavior of issuing a request to the `/settings` path and replacing Because it has `hx-boost="true"` on it, htmx will halt the normal link behavior of issuing a request to the `/settings` path and replacing
the entire page with the response. Instead, htmx will issue an AJAX request to `/settings`, taking the result and replacing the entire page with the response. Instead, htmx will issue an AJAX request to `/settings`, take the result and replace
the `body` element with the new content. the `body` element with the new content.
.A boosted link .A boosted link
@ -100,9 +98,9 @@ Links will act pretty much like "`normal`", they will just be faster.
=== Boosted Forms === Boosted Forms
Boosted form tags work in a similar way to boosted anchor tags: a boosted form will use an AJAX request rather than the 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: usual browser-issued request, and will replace the entire body with the response.
Here is an example of a form that posts messages to the `/messages` end point using an HTTP `POST` request. By adding Here is an example of a form that posts messages to the `/messages` endpoint using an HTTP `POST` request. By adding
`hx-boost` to it, those requests will be done in AJAX, rather than the normal browser behavior. `hx-boost` to it, those requests will be done in AJAX, rather than the normal browser behavior.
[#listing-4-2, reftext={chapter}.{counter:listing}] [#listing-4-2, reftext={chapter}.{counter:listing}]
@ -129,8 +127,8 @@ unstyled content. This can make a "`boosted`" application feel both smoother an
=== Attribute Inheritance === Attribute Inheritance
Let's expand on our previous example of a boosted link, and add a few more boosted links alongside it. We add links Let's expand on our previous example of a boosted link, and add a few more boosted links alongside it. We'll add links
such that we have one to the `/contacts` page, the one to the `/settings` page, and one to the `/help` page. All these so that we have one to the `/contacts` page, the `/settings` page, and the `/help` page. All these
links are boosted and will behave in the manner that we have described above. links are boosted and will behave in the manner that we have described above.
This feels a little redundant, doesn't it? It seems silly to annotate all three links with the `hx-boost="true"` attribute This feels a little redundant, doesn't it? It seems silly to annotate all three links with the `hx-boost="true"` attribute
@ -174,8 +172,8 @@ it? A good example of this situation is when a link is to a resource to be down
file can't be handled well by an AJAX request, so you probably want that link to behave "`normally`", issuing a full 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. page request for the PDF, which the browser will then offer to save as a file on the user's local system.
To handle this situation, you would simply override the parent `hx-boost` value with `hx-boost="false"` on the To handle this situation, you simply override the parent `hx-boost` value with `hx-boost="false"` on the
anchor tag that you didn't want to be boosted: anchor tag that you don't want to boost:
.Disabling boosting .Disabling boosting
[source,html] [source,html]
@ -203,16 +201,14 @@ to as many users as possible, while delivering a better experience to users with
Consider the links in the example above. What would happen if someone did not have JavaScript enabled? Consider the links in the example above. What would happen if someone did not have JavaScript enabled?
Nothing much! 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
The application would continue to work, but it would issue regular HTTP requests, rather than AJAX-based browsers (or users who have not turned off JavaScript) can take advantage of the benefits of the AJAX-style navigation
HTTP requests. This means that your web application will work for the maximum number of users, with users of more modern that htmx offers, and others can still use the app just fine.
browsers (or users who have not turned off JavaScript) able to take advantage of the benefits of the AJAX-style navigation
that htmx offers, but other people will still able to use the app just fine.
Compare the behavior of htmx's `hx-boost` attribute with a JavaScript heavy Single Page Application: such an application Compare the behavior of htmx's `hx-boost` attribute with a JavaScript heavy Single Page Application: such an application
often won't function _at all_ without JavaScript enabled. It is often very difficult to adopt a progressive enhancement often won't function _at all_ without JavaScript enabled. It is often very difficult to adopt a progressive enhancement
approach when you adopt an SPA framework. approach when you use an SPA framework.
This is _not_ to say that every htmx feature offers progressive enhancement. It is certainly possible to build features that This is _not_ 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 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
@ -229,7 +225,7 @@ Right? Why not?
How could we accomplish that? How could we accomplish that?
Well, it's pretty darned easy (and pretty common in htmx-powered web applications): we can just add `hx-boost` on the Well, it's easy (and pretty common in htmx-powered web applications): we can just add `hx-boost` on the
`body` tag of our `layout.html` template, and we are done. `body` tag of our `layout.html` template, and we are done.
.Boosting the entire contact.app .Boosting the entire contact.app
@ -259,8 +255,8 @@ support and so on. And, if JavaScript isn't enabled, it will fall back to the n
All this with one htmx attribute. All this with one htmx attribute.
`hx-boost` is more "`magic`" than other attributes in htmx, which generally are lower level and require a bit more explicit The `hx-boost` attribute is more "`magic`" than others. Htmx attributes generally are lower level and require more explicit
annotation work, in order to specify exactly what you want htmx to do. In general, this is the design philosophy of htmx: annotation in order to specify exactly what you want htmx to do. In general, this is the design philosophy of htmx:
prefer explicit to implicit and obvious to "`magic.`" However, the `hx-boost` attribute is too useful to allow dogma to prefer explicit to implicit and obvious to "`magic.`" However, the `hx-boost` attribute is too useful to allow dogma to
override practicality, and so it is included as a feature in the library. override practicality, and so it is included as a feature in the library.
@ -311,7 +307,7 @@ A couple of things to notice:
Note that we have done something pretty magical here: we have turned this button into a _hypermedia control_. It is no Note that we have done something pretty magical here: we have turned this button into a _hypermedia control_. It is no
longer necessary that this button be placed within a larger `form` tag in order to trigger an HTTP request: it is a longer necessary that this button be placed within a larger `form` tag in order to trigger an HTTP request: it is a
stand-alone, and fully featured hypermedia control on its own. This is the crux of htmx, allowing any element to become 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 the Hypermedia-Driven Application. a hypermedia control and fully participate in the Hypermedia-Driven Application.
We should note that, unlike with the `hx-boost` examples above, this solution will _not_ degrade gracefully. To make We should note that, unlike with the `hx-boost` examples above, this solution will _not_ degrade gracefully. To make
@ -320,14 +316,13 @@ side as well.
In the interest of keeping our application simple, we are going to omit that more elaborate solution. In the interest of keeping our application simple, we are going to omit that more elaborate solution.
=== Updating The Server Side === Updating The Server-Side Code
We have updated the client-side code (if HTML can be considered code) so it now issues a `DELETE` request to the appropriate We have updated the client-side code (if HTML can be considered code) so it now issues a `DELETE` 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 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. need to update the server-side implementation as well to handle this new HTTP Request.
Here is the original code for deleting a contact on the server side: .The original server-side code for deleting a contact
[source, python] [source, python]
---- ----
@app.route("/contacts/<contact_id>/delete", methods=["POST"]) @app.route("/contacts/<contact_id>/delete", methods=["POST"])
@ -338,10 +333,10 @@ def contacts_delete(contact_id=0):
return redirect("/contacts") return redirect("/contacts")
---- ----
We are going to have to make two changes to our handler: first we need to update the route for our handler to the new We'll need to make two changes to our handler: update the route, and update the HTTP method we are using to delete contacts.
location and then, secondly, we need to update the HTTP method we are using to delete contacts:
[source, python] [source, python]
.Updated handler with new route and method
---- ----
@app.route("/contacts/<contact_id>", methods=["DELETE"]) <1> @app.route("/contacts/<contact_id>", methods=["DELETE"]) <1>
def contacts_delete(contact_id=0): def contacts_delete(contact_id=0):
@ -350,7 +345,7 @@ def contacts_delete(contact_id=0):
flash("Deleted Contact!") flash("Deleted Contact!")
return redirect("/contacts") return redirect("/contacts")
---- ----
<1> An update path and method for the handler. <1> An updated path and method for the handler.
Pretty simple, and much cleaner. Pretty simple, and much cleaner.
@ -373,33 +368,32 @@ Fortunately, there is a different response code, https://developer.mozilla.org/e
that does what we want: when a browser receives a `303 See Other` redirect response, it will issue a `GET` to the new that does what we want: when a browser receives a `303 See Other` redirect response, it will issue a `GET` to the new
location. location.
So we want to update our code to use the `303` response code in controller. So we want to update our code to use the `303` response code in the controller.
Thankfully, this is very easy: there is a second parameter to `redirect()` that takes the numeric response code you wish Thankfully, this is very easy: there is a second parameter to `redirect()` that takes the numeric response code you wish
to send. to send.
Here is the updated code:
[source, python] [source, python]
.Updated handler with `303` redirect response
---- ----
@app.route("/contacts/<contact_id>", methods=["DELETE"]) <1> @app.route("/contacts/<contact_id>", methods=["DELETE"])
def contacts_delete(contact_id=0): def contacts_delete(contact_id=0):
contact = Contact.find(contact_id) contact = Contact.find(contact_id)
contact.delete() contact.delete()
flash("Deleted Contact!") flash("Deleted Contact!")
return redirect("/contacts", 303) <2> return redirect("/contacts", 303) <1>
---- ----
<1> A slightly different path and method for the handler. <1> The response code is now a 303.
<2> The response code is now a 303.
Now, when you want to remove a given contact, you can simply issue a `DELETE` to the same URL as you used to access the Now, when you want to remove a given contact, you can simply issue a `DELETE` to the same URL as you used to access the
contact in the first place. contact in the first place.
This is a much more natural HTTP-based approach to deleting a resource! This is a natural HTTP-based approach to deleting a resource.
=== Targeting The Right Element === Targeting The Right Element
We aren't quite finished with our updated delete button yet, however. Recall that, by default, htmx "`targets`" the element 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`" 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. button is targeting itself.
@ -418,11 +412,12 @@ The fix for this is easy: add an explicit target to the button, and target the `
Delete Contact Delete Contact
</button> </button>
---- ----
<1> We have added an explicit target to the button now <1> An explicit target added to the button.
Now our button behaves as expected: clicking on the button will issue an HTTP `DELETE` to the server against the URL for Now our button behaves as expected: clicking on the button will issue an HTTP `DELETE` 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. We've the current contact, delete the contact and redirect back to the contact list page, with a nice flash message.
got everything working smoothly now.
Is everything working smoothly now?
=== Updating The Location Bar URL Properly === Updating The Location Bar URL Properly
@ -432,8 +427,8 @@ If you click on the button you will notice that, despite the redirect, the URL i
not correct. It still points to `/contacts/{{ contact.id }}`. That's because we haven't told htmx to update not correct. It still points to `/contacts/{{ contact.id }}`. That's because we haven't told htmx to update
the URL: it just issues the `DELETE` request and then updates the DOM with the response. the URL: it just issues the `DELETE` request and then updates the DOM with the response.
As we mentioned, boosting will naturally update the location bar for you, mimicking normal anchors and forms, but in As we mentioned, boosting via `hx-boost` 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 because we want to issue a `DELETE`. So, in this case, we this case we are building a custom button hypermedia control to issue a `DELETE`. We
need to let htmx know that we want the resulting URL from this request "`pushed`" into the location bar. need to let htmx know that we want the resulting URL from this request "`pushed`" into the location bar.
We can achieve this by adding the `hx-push-url` attribute with the value `true` to our button: We can achieve this by adding the `hx-push-url` attribute with the value `true` to our button:
@ -453,10 +448,10 @@ _Now_ we are done.
We have a button that, all by itself, is able to issue a properly formatted HTTP `DELETE` request to We have a button that, all by itself, is able to issue a properly formatted HTTP `DELETE` request to
the correct URL, and the UI and location bar are all updated correctly. This was accomplished with three declarative 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 `hx-delete`, `hx-target` and `hx-push-url`. attributes placed directly on the button: `hx-delete`, `hx-target` and `hx-push-url`.
This is definitely more work than the `hx-boost` change was, but it is explicit and easy to see what the button is doing This required more work than the `hx-boost` change, but the explicit code makes it easy to see what the button is doing
as a custom hypermedia control. And the resulting solution feels a lot cleaner as a total solution, taking advantage of 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. the built-in features of the web as a hypermedia system without any URL hacks.
=== One More Thing... === One More Thing...
@ -506,7 +501,7 @@ other JavaScript framework, for improving your web applications.
== Next Steps: Validating Contact Emails == Next Steps: Validating Contact Emails
Let's move on to another improvement in our application: a big part of any web app is validating the data that is 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 submitted to the server: ensuring emails are correctly formatted and unique, numeric values are valid, dates are
acceptable, and so forth. acceptable, and so forth.
@ -532,7 +527,7 @@ def contacts_edit_post(contact_id=0):
<2> If the save does not succeed we re-render the form to display error messages. <2> If the save does not succeed we re-render the form to display error messages.
So we attempt to save the contact, and, if the `save()` method returns true, we redirect to the contact's detail page. So we attempt to save the contact, and, if the `save()` method returns true, we redirect to the contact's detail page.
If the `save()` method does not return true, that indicates that there was a validation error and so, instead of redirecting If the `save()` 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 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. alongside the inputs.
@ -560,9 +555,7 @@ the same email address, and adds an error to the contact model if so, since we d
database. This is a very common validation example: emails are usually unique and adding two contacts with the same email 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. is almost certainly a user error.
Again, we are not going to go into the details of how validation works in our models, in the interest of staying focused 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
on hypermedia, but whatever server-side framework you are using almost certainly has some sort of infrastructure available
for validating data and collecting errors to display to the user. This sort of infrastructure is very common in
Web 1.0 server-side frameworks. Web 1.0 server-side frameworks.
**** ****
@ -573,12 +566,12 @@ image::screenshot_validation_error.png[Red text next to email input in form: Ema
All of this is done using plain HTML and using Web 1.0 techniques, and it works well. All of this is done using plain HTML and using Web 1.0 techniques, and it works well.
However, as the application currently stands, there are two annoyances: However, as the application currently stands, there are two annoyances.
* First, there is no email format validation: you can enter whatever characters you'd like as an email and, * 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 as long as they are unique, the system will allow it.
* Second, if a user has entered a duplicate email, they will not find this fact out until they have filled in * 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 because we only check the email's uniqueness when all the data is submitted. This could be 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 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. before being made aware of this fact.
@ -597,7 +590,7 @@ enforce that the value entered properly matches the email format:
<span class="error">{{ contact.errors['email'] }}</span> <span class="error">{{ contact.errors['email'] }}</span>
</p> </p>
---- ----
<1> A simple change of the `type` attribute to `email` ensures that values entered are valid emails. <1> A change of the `type` attribute to `email` ensures that values entered are valid emails.
With this change, when the user enters a value that isn't a valid email, the browser will display an 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. error message asking for a properly formed email in that field.
@ -663,10 +656,12 @@ Let's make those changes to our HTML:
<1> Issue an HTTP `GET` to the `email` endpoint for the contact. <1> Issue an HTTP `GET` to the `email` endpoint for the contact.
<2> Target the next element with the class `error` on it. <2> Target the next element with the class `error` on it.
Note that in the `hx-target` attribute we are using a _relative positional_ selector. This is a feature of htmx and Note that in the `hx-target` attribute we are using a _relative positional_ selector, `next`. This is a feature of htmx and
an extension to normal CSS. htmx supports prefixes that will find targets _relative_ to the current element. Here an extension to normal CSS. Htmx supports prefixes that will find targets _relative_ to the current element.
is a table of relative positional expressions available:
.Relative Positional Expressions in Htmx
****
`next`:: `next`::
Scan forward in the DOM for the next matching element, e.g. `next .error` Scan forward in the DOM for the next matching element, e.g. `next .error`
@ -681,13 +676,13 @@ Scan the children of this element for matching element, e.g. `find span`
`this`:: `this`::
the current element is the target (default) the current element is the target (default)
****
By using relative positional expressions we can avoid adding explicit ids to elements and take advantage of the local By using relative positional expressions we can avoid adding explicit ids to elements and take advantage of the local
structure of HTML. structure of HTML.
So, with these two attributes in place, whenever someone changes the value of the input (remember, `change` is the So, in our example with added `hx-get` and `hx-target` attributes, whenever someone changes the value of the input (remember, `change` is the
_default_ trigger for inputs in htmx) an HTTP `GET` request will be issued to the given URL and, if there are any errors, they _default_ trigger for inputs in htmx) an HTTP `GET` request will be issued to the given URL. If there are any errors, they will be loaded into the error span.
will be loaded into the error span.
=== Validating Emails Server-Side === Validating Emails Server-Side
@ -699,10 +694,8 @@ we only want to update the email of the contact, and we obviously don't want to
That method will validate the email is unique and so forth. At that point we can return any errors associated with the 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. email directly, or the empty string if none exist.
Here is the code:
[source, python] [source, python]
.Our email validation endpoint .Code for our email validation endpoint
---- ----
@app.route("/contacts/<contact_id>/email", methods=["GET"]) @app.route("/contacts/<contact_id>/email", methods=["GET"])
def contacts_email_get(contact_id=0): def contacts_email_get(contact_id=0):
@ -729,7 +722,7 @@ simplifying aspect of Hypermedia-Driven Applications: since validations are done
the data you might need to do any sort of validation you'd like. the data you might need to do any sort of validation you'd like.
Here again we want to stress that this interaction is done entirely within the hypermedia model: we are using declarative 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 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. to improve our user experience dramatically.
=== Taking The User Experience Further === Taking The User Experience Further
@ -760,17 +753,17 @@ In fact, all we need to do is to change our trigger. Currently, we are using th
<span class="error">{{ contact.errors['email'] }}</span> <span class="error">{{ contact.errors['email'] }}</span>
</p> </p>
---- ----
<1> An explicit trigger has been declared, and it triggers on both the `change` and `keyup` events. <1> An explicit `keyup` trigger has been added along with `change`.
With this tiny change, every time a user types a character we will issue a request and validate the email. Simple. With this tiny change, every time a user types a character we will issue a request and validate the email. Simple.
=== Debouncing Our Validation Requests === Debouncing Our Validation Requests
Simple as, yes, but probably not what we want: issuing a new request on every key up event would be very wasteful 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 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`". a small amount of time. This is called "`debouncing`" the input, where requests are delayed until things have "`settled down`".
htmx supports a `delay` modifier for triggers that allows you to debounce a request by adding a delay before the request Htmx supports a `delay` 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 is sent. If another event of the same kind appears within that interval, htmx will not issue the request and will reset
the timer. the timer.
@ -848,7 +841,7 @@ shown, with the ability to navigate around the pages in the data set.
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 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. than 10 contacts in the contact database.
The first change we will need to make is to add a simple paging widget to our `index.html` template. The first change we will make is to add a simple paging widget to our `index.html` template.
We will conditionally include two links: We will conditionally include two links:
@ -859,7 +852,7 @@ This isn't a perfect paging widget: ideally we'd show the number of pages and of
specific page navigation, and there is the possibility that the next page might have 0 results in it since 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. we aren't checking the total results count, but it will do for now for our simple application.
Let's look at the jinja template code for this in `index.html` Let's look at the jinja template code for this in `index.html`.
[source, html] [source, html]
.Adding paging widgets to our list of contacts .Adding paging widgets to our list of contacts
@ -883,7 +876,7 @@ Note that here we are using a special jinja filter syntax `contacts|length` to c
list. The details of this filter syntax is beyond the scope of this book, but in this case you can think of it as 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 `contacts.length` property and then comparing that with `10`. invoking the `contacts.length` property and then comparing that with `10`.
Now that we have these links in place to support paging, let's address the server-side implementation of paging. Now that we have these links in place, let's address the server-side implementation of paging.
We are using the `page` request parameter to encode the paging state of the UI. So, in our handler, we need to look for We are using the `page` request parameter to encode the paging state of the UI. So, in our handler, we need to look for
that `page` parameter and pass that through to our model, as an integer, so the model knows which page of contacts to return: that `page` parameter and pass that through to our model, as an integer, so the model knows which page of contacts to return:
@ -999,8 +992,8 @@ as the last item of a list or table of elements is scrolled into view, more elem
or table. or table.
Now, this behavior makes more sense in situations where a user is exploring a category or series of social media posts, rather 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 off what you can do with than in the context of a contact application. However, for completeness, and to just show what you can do with
htmx, we will show how to implement this pattern as well. htmx, we will implement this pattern as well.
It turns out that we can repurpose the "`Click To Load`" code to implement this new pattern quite easily: if you think 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 about it for a moment, infinite scroll is really just the "`Click To Load`" logic, but rather than loading when a click
@ -1035,7 +1028,7 @@ a span and then add the `revealed` event trigger.
The fact that switching to infinite scroll was so easy shows how well htmx generalizes HTML: just a few attributes allow 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. us to dramatically expand what we can achieve in the hypermedia.
And, again, we note that we are doing all this within the original, RESTful model of the web: despite all this new And, again, we are doing all this within the original, RESTful model of the web. Despite all this new
behavior, we are still exchanging hypermedia with the server, no JSON API response to be seen. behavior, we are still exchanging hypermedia with the server, with no JSON API response to be seen.
As the web was designed. As the web was designed.