I wanted to give a quick overview on a simple method to illustrate some interesting StimulusJS and JavaScript concepts.

When submitting forms we use Rails’ built in disable_with functionality to prevent double clicks when forms are submitted. When a form is submitted, the submit button becomes disabled, preventing the user from submitting the form multiple times. Also, the text of the button is replaced with text value that is passed to the disable_with attribute.

To enable disable_with, all you need to do is attach it as a parameter to the element being clicked or submitted along with a string. The string represents the content that gets displayed after the button is clicked. Like so: data: { disable_with: "Saving" }, resulting with the following data attribute: data-disable-with="Saving...". When the form is submitted, Rails adds the disabled attribute to the button and replaces the text with the value of data-disable-with.

Before submitting form:

Save button

After submitting form:

Save button disabled

disable_with is helpful and effective but there is a (relatively minor) limitation. We felt, in addition to disabling, we wanted to enhance the user experience by placing a spinner on the submit button to give the user more visual feedback. Presumably the simplest way to do this would be for disable_with to have the ability to pass the class name as an additional value. Currently you can’t do this.

We needed a simple way of adding a CSS class name that shows a spinner on the submit button via JavaScript using StimulusJS. In Stimulus we attach methods to DOM elements by using using data-action attributes. The action that gets added to the submit button in our scenario looks like this: data-action=“click->forms#showLoading”. When the user clicks the submit button the showLoading method within the forms controller is called.

All that happens in the method is that a class name of button--loading gets added to the button and the spinner appears. Whatever styles are applied to a button with the disabled attribute are overritten by the button--loading styles. Also, the button--loading styles were done in a way so that they will work with an enabled or disabled button.

Save button with spinner

Pretty straightforward - except we had one problem. The solution didn’t account for when the submit button was clicked but the form didn’t actually submit because one of the HTML5 inputs (like a number type) was invalid. This scenario resulted in rendering a submit button that was not disabled (because the form had not been submitted and disable_with had not been triggered) but the button was still active because the click event had been triggered and the spinner never went away. This made for a very confusing user experience. In order to prevent this from happening I needed to add an if condition that checked for this scenario to prevent the loading class from being added to the button prematurely.

This method is the end result:

showLoading(event) {
  if (!this.element.querySelector(‘:invalid’)) {
    event.target.classList.add(‘button--loading’)
  }
}

I got a question from one of my colleagues about the if condition in the showLoading method. The question was:

What is this, this.element and how does that query the entire form for :invalid?

So, here’s my attempt to answer this question:

In this context, this is an instance of the forms Stimulus controller. In itself this on its own isn’t especially useful but this.element is very useful. In Stimulus, a controller is always attached to a DOM element, like so <div data-controller="name-of-controller">[some more markup]</div>. In this instance of our scenario this.element is always the attached form element (which is why it is called forms). The name of the file that the controller is contained in is forms_controller.js.

The :invalid pseudo-class is automatically added by the browser to an element that doesn’t pass whatever validations are relevant to the HTML5 element. For example, if you have a form that uses a number input and you enter a non-numeric character in to that input the browser will automatically add an :invalid pseudo-class to that input. Also, if there’s an element within the form that is invalid, the browser won’t let the form be submitted. Therefore if a form is not being submitted we don’t need to add the spinner/loading class to the submit button. We just do a quick check to see if any of the form fields have the invalid pseudo-class and if none have been added to any of the form elements we can be confident that the form is going to be submitted and we can add the spinner/loading class to the submit button.

I’m hope that in the future Rails’ disable_with functionality can be augmented to allow for a class name to be passed to the submit button without having to add any extra JavaScript.

Reference: