edits, ch7, and fix broken diagram link ch10

This commit is contained in:
Bill Talcott 2023-03-14 21:39:30 -04:00
parent b4123280ad
commit 2a32302d99
2 changed files with 76 additions and 88 deletions

View File

@ -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.

View File

@ -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.