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)

  1. Implement the Form Submit
  2. Add State to a common parent or Flux or Redux
  3. Another Component to render the list of colors
  4. First Load / Hydration & Dehydration
  5. Data Refreshes

2019 (Swap 2 & 3 for)

Pros

  1. Avoids Full Page Reload
  2. Components
  3. Complex State Management
  4. Ability to function offline with more code

Cons

  1. More complexity (Less of Django Batteries)
  2. React performance overheads vs Raw HTML
  3. Consideration for search engines, auth, etc.
  4. npm build
  5. 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>
  1. Post returns list
  2. List gets injected into DOM
  3. 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

  1. The ones from simpler times
  2. Interactivity

Cons

  1. Not good for very stateful apps with cascading changes e.g. google sheets
  2. 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
Examples: load, afterSettle, afterRequest, abort, etc

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

  1. 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()
  1. 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>
  1. The body will get swapped via an ajax request when a link is clicked
  2. 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