A Simpler Newsletter Form : Building The Form with HTMX
— OR —
“How to show API results nicely and dynamically”
Introduction
Context
In the last tutorial we made a view to create contacts in a Brevo list. I will now show you how to integrate it with a webpage using HTMX. HTMX can be seen as a replacement to JQuery that is written in your HTML code. Despite being simple, it’s very powerful and can do a lot of things. I used it for a project and I fell in love like never before (sorry to my ex-partners). We will use it to send our post request to the view we created last time, and affect the DOM according to the results. I’m new to it so there might be things that could be done in a more idiomatic way.
The other nice framework I will use is TailwindCSS. In the same manner, it allows us to style elements directly in HTML. I’ve found it to be a much faster way to prototype, and a nice way to remember the attributes of an element. I will not explain everything except some patterns, but encourage you to check it out.
Plan
First, a quick Tailwind tour, showing off some small useful patterns.. Then, we’ll get to HTMX to display a success icon if the contact was created. Finally, we will display the errors in the form, because the user need to know they messed up.
Making the form
I won’t go into many details as there is nothing really special here, just your average form doing an honest job. The HTML is pretty basic : make a form that contains labels linked to inputs, and a submit button. The inputs are mostly of type text
. Don’t forget to set the mail one to email
, you know, it’s nicer. I put my label+input into a div to style them together. Nothing more to say.
Tailwind allow us to easily make a responsive form that looks nice. To do that, you add special class that are then parsed and added to your stylesheet by Tailwind. For example, bg-gray-100
will be interpreted as :
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(247 246 243 / var(--tw-bg-opacity));
}
With gray-100
corresponding to a color in a gray
palette I defined. w-full
means that the width
take a value of 100%
. Of course, I won’t explain everything here (even if reading the documentation together could be fun) but you can see how it’s way shorter than writing plain css.
Demo
You can resize the width of the box to simulate responsiveness, and see my beautiful js in action.
Nice to know
Responsiveness
The demo size on my website is very small (it’s a blog post, not designed to fill a full HD screen) so I had to tweak a few things to make sure it plays nice. Those wouldn’t be a problem normally (but might be of interest to you, especially if you want to copy the code):
First, I defined two new breakpoints, xxsm
and xsm
:
.exports = {
modulecontent: ['*.html'],
theme: {
extend: {
screens: {
'xxsm': '256px', // for a pre 2000 screen
'xsm': '416px', // for a more reasonable size
,
}
}, }
Then I had to make sure that everything wouldn’t get too small by settings a min width to the body
with min-w-40
. Then I had to make sure that everything wouldn’t get too big with w-full max-w-screen-sm mx-auto
on the main div. Then I had to… nope that’s all.
To collapse the names field when there isn’t enough room, I use this little grid pattern :
<div class="grid grid-cols-1 xsm:grid-cols-2 gap-4">
<!-- The grid has one column on small size, and 2 otherwise. Notice I can use my custom breakpoint without issue-->
<div class="inline-block w-full">
<!-- w-full to take the full size of the column-->
...</div>
<div class="inline-block w-full">
...</div>
</div>
Also remember that input field can have Regex pattern. Our email field, for example, uses one pattern="^.+@.+\..+$"
that tells it to only accept email of form “something”@“something”.”something”. That should prevent a few typo/random input.
Full static code
<body class="bg-slate-100 w-full min-w-40 bg-gray-100 font-body text-gray-900">
<div class="w-full max-w-screen-sm mx-auto p-4">
<h1
class="text-center text-xl text-pacific-blue-600">
Newsletter</h1>
<p class="text-center">Subscribe to follow our news</p>
<form class="pt-8"
id="newsletter-form"
>
<div class="mb-4">
<label for="form-email" class="text-gray-700"
>E-mail address</label
>
<input
id="form-email"
name="email"
type="email"
pattern="^.+@.+\..+$"
placeholder="jeandupont@mail.com"
value="a@a.fr"
required
class="w-full focus:outline-none" />
</div>
<div class="grid grid-cols-1 xsm:grid-cols-2 gap-4">
<div class="inline-block min-w-40 w-full">
<label for="form-prenom" class="text-gray-700"
>First name</label
>
<input
id="form-prenom"
name="first_name"
type="text"
pattern="[^\d]+"
placeholder="Jean"
value="Thomas"
required
class="w-full focus:outline-none" />
</div>
<div class="inline-block min-w-40 w-full">
<label for="form-nom" class="text-gray-700">Last Name</label>
<input
id="form-nom"
name="last_name"
type="text"
pattern="[^\d]+"
placeholder="Dupont"
value="Dargent"
required
class="w-full focus:outline-none" />
</div>
</div>
<label for="form-consent" class="mt-8 inline-flex items-center">
<input
id="form-consent"
name="form-consent"
type="checkbox"
class="m-4" required/>
I agree to your legal documents bla bla bla.</label>
<button
type="submit"
class="block mx-auto text-pacific-blue-100 bg-pacific-blue-900 p-4 hover:scale-110 hover:shadow-md">
Subscribe !</button>
</form>
</div>
</div>
</body>
The power of HTMX
Getting the API results
Careful, it will be quick ! Don’t blink !
<form class="pt-8"
id="newsletter-form"
method="post"
hx-post="{% url 'brevo_handler' %}"
hx-target="#newsletter-form"
>
There’s two new tags there : hx-post
and hx-target
. Let’s see what they do :
hx-post
: You know when you use a submit button, the page reload. For small forms like this that might not be the sole content of the page, it’s a bad idea. This HTMX attribute catch the post request and send it to “brevo_handler” without reloading.hx-target
: Swap the target with the return of the api. In this case, it will replace the form with a success page. Wait. That means we have to define a success page ? …And we have to return it 😱 ?!
Returning a success div
Ok, so first we need to return html code in our view. This is simple, instead of using a HttpResponse
we use render
.
# ...
= requests.post(BREVO_URL, json=obj, headers=headers)
response
if response.status_code == 201: # Success
return render(request, "_success_newsletter.html")
else:
# Handle Brevo API errors
Next we will design a small success page :
<!doctype html>
<div class="text-gray-500 relative h-72 overflow-hidden pt-8">
<h2 class="text-center text-lg text-pacific-blue-600">Successfully subscribed !</h2>
<div
id="checkmark"
class="popping absolute mx-auto rounded-full bg-pacific-blue-500 text-pacific-blue-100">
<svg
class="stroke-pacific-blue-100"
="http://www.w3.org/2000/svg"
xmlns="none"
fill-width="2"
stroke-linecap="round"
stroke-linejoin="round"
stroke="0 0 20 20"
viewBox="50"
width="50">
height<polyline points="15 4 10 16 5 11"></polyline>
</svg>
</div>
</div>
And to make it reaaaally nice, define an animation for the popping
class, which you may do in your tailwind.css
file (not the built one). It will make sure your users feel the satisfaction of subscribing. :
.popping {
opacity: 1;
animation: fadeInScale 0.5s forwards;
position: absolute; /* Ensure it's positioned relative to the parent */
left: 50%;
top: 50%;
}@keyframes fadeInScale {
0% {
opacity: 0;
transform: translate(-500%, -50%) scale(1); /* Start transparent on the left */
}100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1); /* End centered */
} }
A successful success page that leads to success :
Showing the user errors
So HTML can swap an element on return, but what if we just want to add information to the same element ? Well, HTMX allows us to do that, using some plain old vanilla Js too !
First, let’s add a div to be filled with errors and sorrow, just like its author :
<div id="error-msg" class="mb-4 border-pink-800 text-pink-800 shadow-pink-800 font-bold border-2 shadow-punk p-1 hidden"></div>
<button
type="submit"
class="block mx-auto text-pacific-blue-100 bg-pacific-blue-900 p-4 hover:scale-110 hover:shadow-md">
Subscribe !</button>
Ignore shadow-punk (just a cool custom shadow class I made). What matters here is the div being hidden initially.
Then we can add our HTMX handling in our form :
hx-on:htmx:response-error="
document.querySelector('#error-msg').classList.remove('hidden'); document.querySelector('#error-msg').innerHTML = event.detail.xhr.responseText;"
Three things to note :
hx-on
allows us to do custom js on a custom event reception.htmx:response-error
is triggered when the call returns an error (4** or 5**). The doc recommend usingresponseError
but I always get unreliable results with it. If you know more, email me !- We simply remove the hidden class to show the error div, and then inject the error message from the server’s response.
And just like that, we got a working error message.
Demonstrating our errors :
Conclusion
We’ve built a nice looking, modern form using HTMX, TailwindCSS, and Django. No more page reloads, and users get instant feedback thanks to HTMX. Whether it’s a smooth success message or a user-friendly error, everything happens seamlessly within the page.
Next time, we’ll take things up a notch by adding Captcha for an extra layer of security. XOXO