Setting button/other element display state based on form input values using Hotwire Stimulus

24 views Asked by At

I have a form which has a text input, a file input, and a submit button.

I want to modify the display of the button in response to input changes, where it should be enabled if either of the input types are present, and disabled otherwise.

Here is my current implementation. I define two flags in the Stimulus controller:

static values = {
        hasAttachments: Boolean,
        hasTextInput: Boolean
    }

When the user enters text or selects a file, I set the flags manually based on the input change.

Next, I use two [...]valueChanged() Stimulus methods that respond to these flags being updated and are basically doing the exact same thing:

hasAttachmentsValueChanged() {
        this.setSendBtnState(this.hasAttachmentsValue || this.hasTextInputValue);
    }

    hasTextInputValueChanged() {
        this.setSendBtnState(this.hasTextInputValue || this.hasAttachmentsValue);
    }

Lastly I have the setSendBtnState() method that makes changes to the display of the button:

setSendBtnState(enabled) {
        if (enabled) {
            this.sendBtnTarget.classList.remove("disabled")
            this.sendBtnTarget.removeAttribute("disabled")
        } else {
            this.sendBtnTarget.classList.add("disabled")
            this.sendBtnTarget.setAttribute("disabled", "disabled")
        }
}

Question: How can I improve this implementation to:

  1. Avoid duplicating the same code in the valueChanged() methods
  2. Is there a way to have Simulus track the presence of form inputs without me setting those flags manually whenever inputs are changed?
  3. How can I avoid code duplication if I need to write another setState() method that controls display of some other part of the form, but essentially does the same thing and responds to the same flags, albeit setting a different attribute?
1

There are 1 answers

2
LB Ben Johnston On

Before writing any more Stimulus code, I strongly recommend you read about how HTML and built in browser can help you get to your goal.

HTML forms and browsers have a substantial amount of built in functionality for making sure that forms can be submitted only when valid.

You can use <input ... required /> to indicate that a field must have a value to be submitted. You can also use styling on the form and input elements with the :invalid pseudo-class to style things so that the user knows what do to visually, even if that means toggling the visibility of a label.

Finally, you should try to avoid disabling a submit button dynamically, it creates problems for accessibility and focus management and usually confuses users. It's better to let them press the button and then take them or show them the action to take that they have missed.

Please have a read of these MDN pages to understand more

After that, you should have a HTML form that gets you 90% of what you want without a single line of JavaScript.

From there you can look at enhancing this to get a more fine-tuned behaviour for your users if you need. Or, you may find at this point the job is done and you can move on to your next task.

If you want to control parts of a form (including input elements) with a Stimulus controller I would recommend you attach your controller to the form.

Instead of adding a new value tracking for each input, think about the inputs you want to track as targets instead.

Finally, you want to listen to changes for the form as a whole, so one event listener on the outer form should be enough.

Here's an example HTML to start with.

<form class="section" data-controller="form" data-action="change->form#check" data-form-valid-class="is-valid">
  <fieldset>
    <legend>Name and file must be selected before submission.</legend>
    <label for="name">Name</label>
    <input id="name" type="text" placeholder="Type something..." required data-form-target="field" />
    <label for="attachment">File</label>
    <input id="attachment" type="file" required data-form-target="field" />
  </fieldset>
  <button type="submit" data-action="form#submit">Submit</button>
</form>

Here's a basic Stimulus controller that should achieve similar goals to your current approach but with a little bit of abstraction.

Whenever the controller connects AND there is a change in the form we run the check method that checks all fields for their own validity. Using the JavaScript DOM interface instead of manually checking values, see https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation

We track a single over-arching value in the controller for isValid, when this changes we use this ...valueChanged callback to update classes.

Finally, we have a submit method that allows us to block the form submit and focus on the field that has the error. You may not even need this but you can maybe dynamically show some other content. Remember the :invalid CSS selector could do some of this job also.

import { Controller } from '@hotwired/stimulus';

class FormController extends Controller {
  static classes = ['valid'];
  static targets = ['field'];
  static values = { isValid: Boolean };

  connect() {
    this.check();
  }

  check() {
    this.isValidValue = this.fieldTargets.every(
      (field) => field.validity.valid
    );
  }

  isValidValueChanged(isValid = false) {
    this.element.classList.toggle(this.validClass, isValid);
  }

  submit(event) {
    if (!this.isValidValue) return;

    // if not valid, stop submission & focus on the first invalid input
    event.preventDefault();
    this.fieldTargets.find((field) => field.validity.valid).focus();
  }
}

export default FormController;

Remember, as per the Stimulus docs, always start with the HTML first. Understand what it can do (in the browsers you have to support) and write semantic, accessible HTML from the start.

Then, let JavaScript help you fill in the gaps where needed, low touch and with a bit of your mind always thinking 'the less I write the better'. The browser cannot do everything you want usually but it can do a lot more than we can by trying to solve the whole problem in JavaScript alone.

Have fun.