Ditch heavy JS and 💋 your frontend again with HTML over-the-wire
Ditch heavy JS and 💋 your frontend again with HTML over-the-wire
- The History
- The Why?
- The Rundown
- HTMX
- Turbo
- Unpoly
- The Roundup
- When You Need More
History
Web 1.0: What Web 2.0 isn't...but kind of is...
Static vs. interactive content
History
History
<tr>
<td align="middle" valign="top">
<br>
<br>
<a href="cmp/lineup/lineupframes.html">
<img src="img/p-lineup.gif" height="52" width="63" alt="The Lineup" border="0">
</a>
</td>
<td colspan="3" rowspan="2" align="right" valign="middle"></td>
<td align="right" valign="bottom"></td>
</tr>
History
History
History
History
History
The Why?
The Why?
The Why?
But what about the full page reload, Alex? I see a flash and lose my scroll position every dang time.
The Why?
The Why?
The reason client rendering is still popular is that it is so much simpler than SSR. SSR can be a huge pain — dealing with hydration, environment differences, effects not running on the server, etc. For some apps, the DX of CSR outweighs the marginal UX benefits of SSR.
The Why?
client side rendering is only simpler than server side rendering w/a JavaScript-first mindset
as soon as you let go of that & re-embrace hypermedia, SSR is *dramatically* simpler
i still have no idea what hydration means & absolutely refuse to learn on general principle
The Why?
What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?"
–Roy Fielding, Creator of the term REST
The Why?
The Rundown
htmx | Turbo | Unpoly | Categories |
---|---|---|---|
Installation | |||
Documentation | |||
Heartbeat | |||
Boost | |||
Form Validations | |||
Fragments | |||
Animations | |||
Streams | |||
Bonus | |||
Total |
The Rundown
Installation
- How easy is it to install
- Asset weight
The Rundown
Documentation
- How easy is it to install
- How easy is it to maintain
- Are there examples readily available for copy/paste
- https://diataxis.fr/
The Rundown
Heartbeat
- Important to gauge the long-term success
- Can go by GitHub stars, number of downloads, issue queue management
- At the end of the day, it's a feeling...based on my opinion
The Rundown
Boost
- Allows for navigation without a full refresh
- Updates the URL History
- Can have a cache of previously visited pages
- Includes forms as well as links
The Rundown
Instant Form Validations
- Nice to let the user know something is wrong before submitting the whole form
- SPA-JS usually has an advantage with more clientside validation
- Built-in methods and examples for form field validations shows pragmaticism and forethought
The Rundown
Fragments
- Breaks a page down into pieces
- Targeted by an HTML attribute
- Can have different ways to swap elements
- An easy way to lazy load parts of an app/page
- Use different triggers to update fragments
The Rundown
Animations
- FOUC is a major pain in Web 1.0 (although View Transitions API)
- Need a way to make replacing content look decent
- SPA-JS frameworks have animation support baked-in
The Rundown
Streams
- Many apps now have real-time features: collaboration, broadcasting
- Using WebSockets or Server Sent Events
- Nice to declare the actions and relationships in HTML attributes
The Rundown
Bonus
- Each framework has its quirks and superpowers
- Highlight the biggest bonus I see for adopting each framework
The Rundown
Don't @ Me
- Accessibility - much in docs about accessibility but too much to cover in this talk
- Complex Examples - Not appropriate when doing comparisons
- Real-world usage - I can't use these at my day job, but maybe you could...
htmx
javascript fatigue:
longing for a hypertext
already in hand
htmx gives you access to AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext.
htmx
Installation
<head>
<script src="https://unpkg.com/htmx.org@1.9.0"></script>
</head>
// npm install htmx.org
// import 'htmx.org';
// window.htmx = require('htmx.org');
- Can use CDN but should install locally
- 13.8 kb gzipped
htmx
Documentation
- The overview is concise but thorough
- The examples are great demos
htmx
Heartbeat
💓
- 31,165 npm downloads weekly
- 11,710 GitHub stars
- 162 issues, 52 pull requests
- last commit: today
htmx
Boost
<body hx-boost="true">
<a href="/about" hx-boost="false">About</a>
<a href="/contact">Contact Us</a>
<a href="https://foo.bar">Foo</a>
</body>
- Use [hx-boost] to set up boosting
- Updates the URL History without reloading the whole page
- The standard method is a GET for links and POST for forms
- Sends an
HX-Boosted
header in the request
htmx
Forms
<body hx-boost="true">
<form hx-include="#foo">
<fieldset>
<label for="name">Enter your name: </label>
<input type="text" name="name" id="name" required>
</fieldset>
<fieldset>
<label for="email">Enter your email: </label>
<input type="email" name="email" id="email" required>
</fieldset>
<button type="submit" hx-post="/contact-us">Submit</button>
</form>
<input id="foo" type="hidden" value="bar" />
</body>
- If a non-
GET
request, the inputs of the nearest enclosing form will be included. - Can use hx-include for more targeting
htmx
Instant Form Validations
<form hx-post="/sign-up">
<div hx-target="this" hx-swap="outerHTML">
<label>Email Address</label>
<input name="email" hx-post="/contact/email" hx-indicator="#ind">
<img id="ind" src="/img/bars.svg" class="htmx-indicator"/>
</div>
<!-- ... -->
</form>
<!-- Server response with error -->
<div hx-target="this" hx-swap="outerHTML" class="error">
<label>Email Address</label>
<input name="email" hx-post="/contact/email" hx-indicator="#ind" value="test@foo.com">
<img id="ind" src="/img/bars.svg" class="htmx-indicator"/>
<div class='error-message'>That email is already taken. Try another one.</div>
</div>
- The input will
POST
to/contact/email
for validation, when thechanged
event occurs - The response has div with the
error
class and includes an error message element
htmx
Fragments
<input type="text" name="q"
hx-get="/search-results"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-results"
placeholder="Search...">
<div id="search-results">...</div>
- By default, AJAX requests are triggered by the “natural” event of an element
- You can use the hx-trigger attribute to specify which event will cause the request.
htmx
Animations
<style>
.smooth {transition: all 1s ease-in;}
.fade-me-out.htmx-swapping {
opacity: 0;
transition: opacity 1s ease-out;}
</style>
<div id="color-demo" class="smooth" style="color:red"
hx-get="/colors" hx-swap="outerHTML" hx-trigger="every 1s">
Color Swap Demo
</div>
<button class="fade-me-out"
hx-delete="/thing/42"
hx-confirm="Are you sure?"
hx-swap="outerHTML swap:1s">
Delete
</button>
- CSS Transitions work if the id is preserved
- CSS classes ".htmx-swapping" help with targeting
- You can extend the swap/settle phase
htmx
Streams
<div hx-ws="connect:wss:/chatroom">
<div id="chat_room">
message 1,message 2
</div>
<form hx-ws="send:submit">
<input name="chat_message">
</form>
</div>
<script>
// Backend websocket server...
client.send(
`<div id="chat_room" hx-swap-oob="beforeend">,` +
the_msg.chat_message +
`</div>`);
</script>
<div hx-get="/news" hx-trigger="every 2s"></div>
- Moving to extensions in htmx 2.0
- Can use "hx-ws" or "hx-sse" attributes
- Can also dirty poll
htmx
Triggers + Essays
<div hx-post="/mouse_entered" hx-trigger="mouseenter once">
[Here Mouse, Mouse!]
</div>
<div hx-get="/clicked" hx-trigger="click[ctrlKey&&shiftKey]">
Control+Shift Click Me
</div>
<div hx-get="/news" hx-trigger="every 2s"></div>
<div hx-get="/story/42" hx-trigger="revealed"></div>
htmx
Memes
HTMX
- No reduction in the application’s user experience (UX)
-
They reduced the code base size by 67% (21,500 LOC to 7200 LOC)
-
They increased python code by 140% (500 LOC to 1200 LOC)
-
They reduced their total JS dependencies by 96% (255 to 9)
-
They reduced their web build time by 88% (40 seconds to 5)
-
First load time-to-interactive was reduced by 50-60% (from 2 to 6 seconds to 1 to 2 seconds)
-
Much larger data sets were possible when using htmx, because react simply couldn’t handle the data
-
Web application memory usage was reduced by 46% (75MB to 45MB)
Turbo
The speed of a single-page web application without having to write any JavaScript.
Turbo
Installation
- Can use CDN but should install locally
- 19.1 kb gzipped
<head>
<script type="module">
import hotwiredTurbo from 'https://cdn.skypack.dev/@hotwired/turbo';
</script>
</head>
// import * as Turbo from "@hotwired/turbo"
Turbo
Documentation
- Decent but lacks examples
- You will likely use third-party sources
Turbo
- 272,553 npm downloads weekly
- 5,012 GitHub stars
- 161 issues, 52 pull requests
- last commit: 03/01/2023
Heartbeat
💓
Turbo - Drive
<head>
<script src="/application-cbd3cd4.js" data-turbo-track="reload"></script>
</head>
<body data-turbo="true">
<a href="/about" data-turbo="false">About</a>
<a href="/contact">Contact Us</a>
<a href="https://foo.bar">Foo</a>
<a href="/edit" data-turbo-action="replace">Edit</a>
<a href="/articles/54"
data-turbo-method="delete"
data-turbo-confirm="Are you sure you want to delete the article?">
Delete the article
</a>
</body>
- Navigate within a persistent process
- Application visit: advance or replace
- Restoration visit: restore
Boost
Turbo - Drive
<body data-turbo="true">
<form action="/contact-us">
<fieldset>
<label for="name">Enter your name: </label>
<input type="text" name="name" id="name" required>
</fieldset>
<fieldset>
<label for="email">Enter your email: </label>
<input type="email" name="email" id="email" required>
</fieldset>
<button type="submit">Submit</button>
</form>
</body>
- Expects a 303 on success and 422 "Unprocessable Entity" on failure
- Turbo will stay on the current URL rather than change it to the form action to avoid a reload GET
Forms
Turbo
Instant Form Validations
- Return a 422 with the errors and form values
- Respond with a Turbo Stream to target empty error elements
Turbo - Frames
<div id="navigation">Links targeting the entire page</div>
<turbo-frame id="message_1">
<h1>My message title</h1>
<p>My message content</p>
<a href="/messages/1/edit">Edit this message</a>
<form action="/messages" data-turbo-frame="navigation">
Submitting form will replace the navigation frame.
</form>
</turbo-frame>
<turbo-frame id="navigation"></turbo-frame>
<turbo-frame id="comments" src="/messages/1/comments" loading="lazy">
<div id="comment_1">One comment</div>
<div id="comment_2">Two comments</div>
</turbo-frame>
- Compartmentalize for discrete navigation and updates
- "src" will lazy-load the frame's content
- [data-turbo-frame] replaces other frame contents
Fragments
Turbo
- Examples using JS
- Might add/remove classes while swapping ...but bad docs
Animations
Turbo - Streams
<turbo-stream action="append" target="messages">
<template>
<!-- render partial: "messages/message", collection: @messages -->
<div id="message_1">...</div>
</template>
</turbo-stream>
<turbo-stream action="prepend" target="messages">
<turbo-stream action="replace" target="message_1">
<turbo-stream action="update" targets=".sports_messages">
<turbo-stream action="remove" targets=".old_messages"></turbo-stream>
<turbo-stream action="before" target="current_step">
<turbo-stream action="after" target="current_step">
<!-- Content-Type: text/vnd.turbo-stream.html; charset=utf-8 -->
- Seven different actions, but can add custom actions
- Easy to re-use templates for stream updates
- Special content type for Turbo Streams
Streams
Turbo
- Turbo Native for iOS manages a single WKWebView instance across multiple view controllers
- Turbo Native for Android manages a single WebView instance across multiple Fragment destinations
- Strada is...supposedly coming in 2023
Native
Turbo
Unpoly
SPAs are a good fit for a certain class of UI. For us, that class is the exception, not the default.
Unpoly
Installation
<head>
<link rel="stylesheet" href="https://unpkg.com/unpoly@2.7.2/unpoly.css" />
<script src="https://unpkg.com/unpoly@2.7.2/unpoly.js"></script>
</head>
// npm install unpoly
- Can use CDN but should install locally
- Does have a CSS asset
- 47.4 kb gzipped for JS + 1 kb CSS
Unpoly
Documentation
- Tutorials, demos, and docs are alright
- Ran into a bug that made me...full page refresh
- A bit confusing, I was jumping around
Unpoly
- 6,557 npm downloads weekly
- 1,478 GitHub stars
- 24 issues, 0 pull requests
- last commit: yesterday
Heartbeat
💓
Unpoly
<body>
<a href="/about" up-follow>About Us</a>
<a href="/contact" up-follow up-instant>Contact Us</a>
<a href="/faq" up-follow up-preload>FAQ</a>
</body>
<script>
up.link.config.followSelectors.push('a[href]');
up.link.config.instantSelectors.push('a[href]');
up.link.config.preloadSelectors.push('a[href]');
</script>
- Use [up-follow] to boost and add to URL History
- You may omit the
[up-follow]
attribute if the link has other attributes like[up-target]
- [up-instant|preload] for eager loading
- You can programmatically follow all links
Boost
Unpoly
<form method="post" action="/users" up-submit>
<div class="form-group">
<label>First Name</label>
<input type="text" class="form-control" name="firstName">
</div>
<!-- Other stuff... -->
<button type="submit">Submit</button>
</form>
<script>
up.form.config.submitSelectors.push(['form']);
</script>
- Use [up-submit] to boost forms
- You may omit the
[up-submit]
attribute if the link has other attributes like[up-target]
- You can programmatically follow all forms
Forms
Unpoly
<form action="/users" up-submit>
<fieldset>
<label for="email" up-validate>E-mail</label>
<input type="text" id="email" name="email">
<!-- <div class="error">E-mail has already been taken!</div> -->
</fieldset>
<!-- Other stuff... -->
<button type="submit">Register</button>
</form>
- Uses [up-validate] and header values when a field value changes
- By default, Unpoly will only update the closest form group around the validating field.
Instant Form Validations
POST /users HTTP/1.1
X-Up-Validate: email
X-Up-Target: fieldset:has(#email)
Content-Type: application/x-www-form-urlencoded
email=foo%40bar.com&password=secret
Unpoly
<div>
<a href="back.html" class="one" up-target=".one">
♥
</a>
<a href="front.html" class="two" up-target=".two">
Flip
</a>
</div>
<a href="/card/5" up-target=".content:before, .unread-count:maybe">...</a>
<article class="content">...</article>
- [up-target] tells where to swap response content
- You can have multiple targets and ways to place content
- If the target might not be present, you can use a pseudo selector `:maybe`
Fragments
Unpoly
// Transition new elements.
<a href="/users"
up-target=".list"
up-transition="cross-fade">
Show users
</a>
// Animate existing elements.
<a href="/users"
up-target=".list"
up-layer="new"
up-animation="move-from-top">
Show users
</a>
- Use [up-transition] or [up-animation] for animations
- Differs between new and existing elements
- Many predefined transitions and predefined animations with the ability to add custom ones
Animations
Unpoly
<div id="cart-items" up-hungry>
4 items
</div>
<div id="unread-count" up-poll
up-interval="10000"
up-source="/unread-count">
2 new messages
</div>
- No support for WebSockets or Serve Sent Events
- [up-hungry] has to have an identifying attribute, like an
[id]
or a unique[class]
attribute. -
[up-poll]
elements are reloaded from the server periodically.
Streams
Unpoly
// preview.html
<main>
<p>Story summary</p>
<a href="full.html" up-layer="new">
Read full story
</a>
</main>
// full.html
<main>
<h2>Full story</h2>
<p>Lorem ipsum dolor sit amet.</p>
</main>
- Fragments can be opened as modal dialogs, popup overlays, or drawers in infinitely stacked "layers".
-
[up-layer]s are isolated so you can't accidentally match a fragment in another layer.
Layers
Unpoly
Layers
Unpoly
The Roundup
SPAs are a good fit for a certain class of UI. For us, that class is the exception, not the default.
htmx | Turbo | Unpoly | Categories |
---|---|---|---|
1 | 2 | 3 | Installation |
1 | 3 | 2 | Documentation |
2 | 1 | 3 | Heartbeat |
2 | 1 | 3 | Boost |
1 | 3 | 2 | Form Validations |
1 | 2 | 3 | Fragments |
2 | 3 | 1 | Animations |
2 | 1 | 3 | Streams |
1 | 2 | 3 | Bonus |
13 | 18 | 23 | Total |
When You Need More
- Slow interaction feedback
- Page loads destroy transient state (scroll positions, unsaved form values, focus)
- Layered interactions are hard (modals, drop-downs, drawers)
- Animation is complicated
- Complex forms
- Offline apps
👎 |
😔 |
When You Need More
UpUp
<html>
<head>
<meta charset="UTF-8">
<title>Lonely Globe Advisor</title>
</head>
<body>
<h1>Top Hotels in Rome</h1>
<ol>
<li>Villa Domus - Via Piacenza 9, Rome, Italy</li>
<li>Hotel Trivelli - Piazza Barberini 11, Rome, Italy</li>
</ol>
<script src="/upup.min.js"></script>
<script>
UpUp.start({
'content-url': 'hotels.html?user=bob',
'assets': ['css/bootstrap.min.css', 'css/offline.css']
});
</script>
</body>
</html>
When You Need More
Hyperscript
<div _="on htmx:afterSettle log 'Settled!'"></div>
<div _="on load wait 5s then transition opacity to 0 then remove me">
Here is a temporary message!
</div>
<div hx-target="#content"
_="on htmx:beforeOnLoad take .active from .tabs for event.target">
<a class="tabs active" hx-get="/tabl1" >Tab 1</a>
<a class="tabs" hx-get="/tabl2">Tab 2</a>
<a class="tabs" hx-get="/tabl3">Tab 3</a>
</div>
<div id="content">Tab 1 Content</div>
<form class="sortable" hx-post="/items" hx-trigger="end">
<div class="htmx-indicator">Updating...</div>
<div><input type='hidden' name='item' value='1'/>Item 1</div>
<div><input type='hidden' name='item' value='2'/>Item 2</div>
<div><input type='hidden' name='item' value='2'/>Item 3</div>
</form>
When You Need More
Stimulus
<script>// hello_controller.js
import { Controller } from "stimulus";
export default class extends Controller {
static targets = [ "name", "output" ];
greet() {
this.outputTarget.textContent =
`Hello, ${this.nameTarget.value}!`
}
}</script>
<div data-controller="hello">
<input data-hello-target="name" type="text">
<button data-action="click->hello#greet">
Greet
</button>
<span data-hello-target="output"></span>
</div>
When You Need More
- Angular
- Vue
- React
- Chex-mix Run
- MemeQL
- 1000's of friends on npm!!!
Abort
The Wrapup
- You probably don't need as much JS in your app as you have currently
- Hypermedia systems are your friend
- htmx is my fav!
- Turbo is great with Rails
- Unpoly is...interesting
- Stop calling any API that doesn't return Hypermedia RESTful
Questions?
Ditch JS for HTML
By afinnarn
Ditch JS for HTML
- 293