Stimulus Controller (Form Submission)
Objective
- Improve form submission with Turbo
- Use Stimulus Controller to improve form submission
data-turbo-submits-with
Update hotwire_django_app/templates/stimulus_basic/create_page.html
{% extends "stimulus_basic/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button
type="submit"
class="btn-blue"
data-turbo-submits-with="Submitting..."
>
Submit
</button>
</form>
</div>
{% endblock %}
- We add
data-turbo-submits-with="Submitting..."
to the submit button. - With
data-turbo-submits-with
, we can give user feedback by showingSubmitting...
, while the operation is in progress
If we submit the form, we can see something like this
Notes:
- Button text has changed
- Button text has restored after form submission.
- Turbo do that for us automatically.
What if we want to display a spinner instead of text?
Workflow
Since the form submission is handled by Turbo , let's check below Turbo events
turbo:submit-start
fires during a form submission.turbo:submit-end
fires after the form submission-initiated network request completes
So we can do in this way:
- We let the controller listen to the
turbo:submit-start
event andturbo:submit-end
events. - When form submission start, we set
data-submitting=true
on the form element. - We add CSS to display a spinner when form
data-submitting
istrue
- When form submission finish, we set back
data-submitting=false
.
Spinner
Create hotwire_django_app/templates/stimulus_basic/spinner.html
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
The svg come from https://tailwindcss.com/docs/animation
Form Controller
Create frontend/src/controllers/form_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ["submit"];
disconnect() {
this.enableSubmits();
this.element.toggleAttribute("data-submitting", false);
}
disableSubmits() {
this.submitTargets.forEach(
function (submitTarget) {
submitTarget.disabled = true;
}
);
}
enableSubmits() {
this.submitTargets.forEach(
function (submitTarget) {
submitTarget.disabled = false;
}
);
}
submitStart() {
const form = this.element;
if (form) {
form.toggleAttribute("data-submitting", true);
this.disableSubmits();
}
}
submitEnd() {
const form = this.element;
if (form) {
form.toggleAttribute("data-submitting", false);
this.enableSubmits();
}
}
}
Notes:
- The form controller has
submit
target, which is thesubmit
button. - In
submitStart
, we add attributedata-submitting
to the form element and setdisabled=true
attribute to thesubmit
target - In
submitEnd
, we removedata-submitting
to the form element and setdisabled=false
attribute to thesubmit
target.
CSS
tailwind.config.js
Update tailwind.config.js
module.exports = {
content: contentPaths,
theme: {
extend: {},
},
variants: {
extend: {
opacity: ['disabled'], // new
}
},
plugins: [
require('@tailwindcss/forms'),
],
};
We do this to make disabled:opacity-XX
work in Tailwind.
CSS
Update frontend/src/styles/stimulus_basic.scss
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
.btn-blue {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-blue-500;
@apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75;
@apply disabled:opacity-50; // new
}
.btn-red {
@apply inline-flex items-center;
@apply px-4 py-2;
@apply font-semibold rounded-lg shadow-md;
@apply text-white bg-red-500;
@apply hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-opacity-75;
@apply disabled:opacity-50; // new
}
form[data-controller="form"] {
button[data-form-target="submit"] {
svg {
@apply hidden;
}
}
&[data-submitting] {
button[data-form-target="submit"] {
@apply cursor-not-allowed;
svg {
@apply inline-block;
}
}
}
}
Notes:
- We add
disabled:opacity-50
tobtn-blue
andbtn-red
, so if the button is disabled, the button color will change. - For the form which has
data-controller="form"
, by default, the svg in button is hidden, if thedata-submitting=true
, the svg icon will display.
Template
Create hotwire_django_app/templates/stimulus_basic/create_page.html
{% extends "stimulus_basic/base.html" %}
{% load crispy_forms_tags %}
{% block content %}
<div class="w-full max-w-7xl mx-auto px-4">
<form
method="post"
data-controller="form"
data-action="turbo:submit-start->form#submitStart turbo:submit-end->form#submitEnd"
>
{% csrf_token %}
{{ form|crispy }}
<button data-form-target="submit" type="submit" class="btn-blue">
{% include 'stimulus_basic/spinner.html' %}
Submit
</button>
</form>
</div>
{% endblock %}
Notes:
- with
data-controller="form"
,form controller
will connect to the DOM element. - With
data-action="turbo:submit-start->form#submitStart turbo:submit-end->form#submitEnd"
, the controller will listen to theturbo:submit-start
andturbo:submit-end
events. - We add
data-form-target="submit"
to the submit button, so the controller can access it without DOM searching.
Remember to restart webpack
to load the new controller, and then test on http://127.0.0.1:8000/stimulus-basic/create/
When we submit the form, we should see the spinner in the button.
Notes
Actually, we can also use the form_controller
in the project which has no Turbo
setup.
<form method="post" action="" data-controller="form" data-action="submit->form#submitStart">
</form>
For example, we can use Stimulus to listen to the form's submit event
and set data-submitting=true
to the form element.