HTMX All the things
*with some caveats
Retro
<form action="/contacts/{{ color.id }}/edit" method="post">
<fieldset>
<legend>Contact Values</legend>
<p>
<label for="email">Email</label>
<input name="email" type="text" value="{{ color.name }}">
<span class="error">{{ color.errors['name'] }}</span>
</p>
<p>
<button>Save</button>
</fieldset>
</form>
<form action="." method="POST">
{{ form.as_p }}
</form>
class ModelForm(forms.ModelForm):
class Meta:
model = Model
fields = [
"color"
]
+
=
Old School - Color Form
1
2
3
<li>
{% for color in colors %}
<ul>{{ color.name }}</ul>
{% endfor %}
</li>
Old School - Color List
The philosophy (Locality of Behaviour)
"The primary feature for easy maintenance is locality: Locality is that characteristic of source code that enables a programmer to understand that source by looking at only a small portion of it."
-- Richard Gabriel (https://www.dreamsongs.com/Files/PatternsOfSoftware.pdf)
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
2019 (Swap 2 & 3 for)
- Implement the Form Submit
- Add State to a common parent or Flux or Redux
- Another Component to render the list of colors
- First Load / Hydration & Dehydration
- Data Refreshes
2019 (Swap 2 & 3 for)
Pros
- Avoids Full Page Reload
- Components
- Complex State Management
- Ability to function offline with more code
Cons
- More complexity (Less of Django Batteries)
- React performance overheads vs Raw HTML
- Consideration for search engines, auth, etc.
- npm build
- Security
<form action="." method="POST">
{{ form.as_p }}
<button hx-post="{% url 'partials:list_colors' %}"
hx-target="#colors-list"hx-swap="innerHTML" >
Add Color
</button>
</form>
New Age
<li id="colors-list">
{% for color in colors %}
<ul>{{ color.name }}</ul>
{% endfor %}
</li>
- Post returns list
- List gets injected into DOM
- Happy days
<li id="colors-list" hx-get="{% url 'partials:list_colors' %}" hx-trigger="every 1s">
{% for color in colors %}
<ul>{{ color.name }}</ul>
{% endfor %}
</li>
Polling Pattern
Server Sent Events / SSE Push
<li id="colors-list" sse-connect="{% url 'partials:sse_updates' sse-swap="ColorUpdate" %}">
{% for color in colors %}
<ul>{{ color.name }}</ul>
{% endfor %}
</li>
Pros
- The ones from simpler times
- Interactivity
Cons
- Not good for very stateful apps with cascading changes e.g. google sheets
- Nascent ecosystem
Text
All examples with Django-HTMX
https://django-htmx.readthedocs.io/en/latest/installation.html
Attributes | Description |
---|---|
hx-get, hx-post, etc | AJAX requests |
hx-trigger |
Event that triggers a request |
hx-swap | How to swap HTML content into DOM |
hx-target |
Where in the DOM to swap the returned HTML content |
Events | Description |
---|---|
SSE | Server generated events |
Native |
Can be registered with document.body.addEventListener
|
https://htmx.org/reference/#attributes
https://htmx.org/reference/#events
Response Headers | Description |
---|---|
HX-Location | Causes a client-side redirection to a new location |
HX-Push-Url |
Pushes a new URL into the location bar / History |
https://htmx.org/reference/#response_headers
Django Batteries: Lists with Action
- Delete Button should delete the row and dynamically refresh the table
import django_tables2 as tables
from django_tables2.utils import A
class ColorTable(tables.Table):
def extract_url(**kwargs):
if "record" in kwargs:
return reverse(
"color_app:delete_url",
kwargs={"pk": kwargs.get("record").id},
)
return "header"
edit = tables.LinkColumn(
"color_app:edit_color", orderable=False, text="Edit", args=[A("id")]
)
delete = tables.URLColumn(
text="Delete",
orderable=False,
attrs={
"td": {
"style": "text-decoration: underline; color: #0a58ca",
"hx-confirm": "Are you sure?",
"hx-target": "closest tr",
"hx-get": extract_url,
}
},
)
class Meta:
model = Color
fields = ("name", "hex_code")
{% extends "./_base.html" %}
{% load render_table from django_tables2 %}
{% block main %}
<div class="container overflow-hidden">
<div class="row gy-5">
<div class="col">
<h3> Colors Table </h3>
</div>
</div>
<div class="row gy-5">
<div class="col">
{% render_table table %}
</div>
</div>
</div>
{% endblock %}
class RulesetsTableView(SingleTableView):
template_name = "color-table.html"
table_class = ColorTable
def get_queryset(self):
return Colors.objects.all()
- Django Tables has filters and lots of other goodies
https://taskbadger.net/blog/tables.html
Legacy Apps
<div id="app" class="container" hx-boost="true">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="{% url 'chat_app:puppies' intent=intent %}">Kittens</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'chat_app:kittens' intent=intent %}">Puppies</a>
</li>
</ul>
</div>
- The body will get swapped via an ajax request when a link is clicked
- This will fallback to default behavior if JS is disabled
Chat
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def chat_message(self, event):
message = event["message"]
ulid_str = event["ulid"]
timestamp = ULID.from_str(ulid_str).timestamp
# dt = datetime.datetime.fromtimestamp(timestamp, tzinfo=pytz.timezone("Australia/Sydney"))
dt = datetime.datetime.fromtimestamp(
timestamp
)
text_data = render_to_string(
"chat/message_partial.html",
{
"author": "Mr Roboto",
"message": message,
"dt": dt,
},
)
# Send message to WebSocket
self.send(text_data=text_data)
<div id="content" class="row h-75 overflow-auto" style="max-height: 75vh; overflow-y: scroll">
{% comment %}Install Forloop to hydrate messages{% endcomment %}
</div>
<div hx-ws="connect:/ws/chat/test_room/">
<form action="#" hx-ws="send:submit">
<input id="chat-message-input" name="chat_message" >
<button/>
</form>
</div>
<div hx-swap-oob="beforeend:#content" class="media w-75 mb-3">
<div class="media-body ml-3">
<div class="bg-light rounded py-2 px-3 mb-2">
<p class="text-info">{{author}}</p>
<p class="text-small mb-0 text-muted">{{message}}</p>
</div>
<p class="small text-muted">{{ dt|time:"h:i A" }} | {{ dt|date:"M j" }}</p>
</div>
</div>
Chat Room
Server Partial
Contexte
https://about.contexte.com/fr/notre-actualite/how-we-developed-a-custom-collaborative-editor-for-our-journalists
Some Numbers
https://docs.google.com/presentation/d/1jW7vTiHFzA71m2EoCywjNXch-RPQJuAkTiLpleYFQjI/edit
More Examples + Questions
From Production / https://www.youtube.com/watch?v=3GObi93tjZI&t=488s
<input type="text" name="q"
hx-get="/trigger_delay"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-results"
placeholder="Search..."
>
<div id="search-results"></div>
Autocomplete
Me
https://au.linkedin.com/pub/iqbal-bhatti/14/63/493
HTMX All the things
By Iqbal Talaat Bhatti
HTMX All the things
- 72