Inline form validation with Django and Htmx

Published on , under Programming, tagged with python, django, javascript and htmx.

Hypermedia as the engine of application state puts strong emphasis on giving control to the server to render the state of the data and what actions can be done with it.

Although client side scripting is allowed, this should only be a sidecar for simple state manipulations. Most UI interactions that affect the state of the app would need a roundtrip to the server to get the new state of the data.

Client side vs server side validation

Call me purist, but in a traditional MPA, frontend scripting should only help to aid in non-stateful changes, like toggling a dropdown, clearing an input, hiding an alert after X seconds, etc.

Progressive form validation is a gray area. HTML already provides some basic attributes to control valid inputs like required, maxlength, type, etc:

<label for="id_email">What's your e-mail address?</label>
<input type="email" id="id_email" name="email" required maxlength="140">

While valuable, this is no replacement for server side validation, since clients should never be trusted. Other times we need to do some validation that really depends on the values of other fields or lookups on a database (is this email already used?).

What is really nice about client side validation is the short feedback cycle. The user can see the errors as the input is being manipulated or as soon as focus is set on the next form element. Otherwise the user would have to fill the entire form, wait for the server to validate and then scroll to see what errors where found.

Server side validation is required for security, but client side validation is desirable for it has better UX. Can't we have both?

Partial and progressive server side form validation

With the help of htmx, we will submit the form for validation after the user interacts with an input and we will partially update the field with the result from the server.

htmx inline form validation

The idea is to wrap each form field with a div + some hx- directives. I'm using hx-select & hx-trigger=blur directives to only replace current input element with the validation for that field.

  <div
      id="email_field"
      {% if form.email.errors %}class="error"{% endif %}
      hx-select="#email_field"
      hx-post="{% url 'form-demo' %}"
      hx-trigger="blur from:find input"
      hx-target="#email_field">
    {{ form.email.label_tag }}
    {{ form.email }}
    {{ form.email.errors }}
  </div>

htmx inline form validation

The django backend code is still pretty agnostic. If the form is invalid, django will re-render the page with the errors displayed. If the form is valid, and it was submitted via htmx request, we re-render the page (without any errors).

class SignUpView(FormView):
    form_class: forms.Form = SignupForm
    template_name: str = 'form-demo.html'
    success_url = reverse_lazy('form-demo')

    def form_valid(self, form):
        if self.request.htmx:
            # The submitted form is valid, just render it `as is` for htmx.
            return self.render_to_response(self.get_context_data(form=form))
        return super().form_valid(form)

If the form is manually submitted through a regular http post request, we just follow the normal FormView flow, and we can redirect or make some change in the database.

With this simple solution, quick to implement and easy to maintain, we get the best of both worlds.

Immediate feedback to the user, no client-side code logic repetition for validating fields, progressive enhancement approach when JS is not available and it is framework agnostic, can be adapted with any CSS toolkit and any backend language.