Form submission and validation

I realize it’s pre-hotwire thinking to validate a form on the client side. But is it possible?
I can catch the turbo events with:

 let validate = function(event){
      event.preventDefault()
      return false }

$('body').on('turbo:submit-start', '#reschedule_form', validate)

but the form submission proceeds anyway. How can I intercept and validate the form submission?

What types of rules are you wanting to validate (required, email formatting, etc)?

I’m validating dates and times for appointments. But my main point here is that, whilst events (e.g. turbo:submit-start) are available, the result (e.g. false) from the handler (in my case it’s validate) seems to be ignored.

Perhaps include stopImmediatePropagation and see if it helps :

 let validate = function(event){
      event.preventDefault()
      event.stopImmediatePropagation();
      return false }

$('body').on('turbo:submit-start', '#reschedule_form', validate)

Thanks, yeah tried that without success.

Even with Hotwire, I think it is still valid and useful to validate a form on the client side, mainly to validate required fields. This is how I do:

I created a Stimulus controller to handle the validation.

// app/javascript/controllers/form_validator_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {

  // Reference: https://getbootstrap.com/docs/5.1/forms/validation
  validate(event) {
    const form = event.target

    if (form.checkValidity() == false) {
      const invalidFields = form.querySelectorAll(":invalid")

      this.displayInvalidFeedback(invalidFields)
      this.focusAndAnimateField(invalidFields[0])

      event.preventDefault()
      event.stopPropagation()
    }

    form.classList.add("was-validated")
  }

  displayInvalidFeedback(invalidFields) {
    invalidFields.forEach(invalidField => {
      const invalidFeedback = this.findOrCreateInvalidFeedback(invalidField)
      invalidFeedback.innerHTML = invalidField.validationMessage
    })
  }

  findOrCreateInvalidFeedback(invalidField) {
    let invalidFeedback = this.findInvalidFeedback(invalidField)

    if (invalidFeedback == null) {
      invalidFeedback = this.createInvalidFeedback(invalidField)
    }

    return invalidFeedback
  }

  findInvalidFeedback(invalidField) {
    return invalidField.parentElement.querySelector(".invalid-feedback")
  }

  createInvalidFeedback(invalidField) {
    const invalidFeedback = document.createElement("div")
    invalidFeedback.className = "invalid-feedback"

    this.insertInvalidFeedback(invalidFeedback, invalidField)

    return invalidFeedback
  }

  insertInvalidFeedback(invalidFeedback, invalidField) {
    const invalidFieldParent = invalidField.parentElement

    if (invalidFieldParent.classList.contains("input-group") ||
      invalidFieldParent.classList.contains("form-check")) {
      invalidFieldParent.insertBefore(invalidFeedback, null)
    }
    else {
      const invalidFieldSibling = invalidField.nextSibling
      invalidFieldParent.insertBefore(invalidFeedback, invalidFieldSibling)
    }
  }

  focusAndAnimateField(field) {
    const animationClass = "vibrate-1"

    field.addEventListener("animationend", () => {
      field.classList.remove(animationClass)
    })

    field.classList.add(animationClass)
    field.focus()
  }
}

I’m using Bootstrap, so the code above may need some adjustments to apply different form styles.

Finally I use this Stimulus controller to validate my form:

<%= form_with model: user, data: { controller: 'form_validator', action: 'form_validator#validate' } do |f| %>
  ...
<% end %>

This is how it looks like:

1 Like

Thanks, @repoles. Yes indeed I ended up doing something like this. Even though it seems to me like a workaround for the fact that Turbo doesn’t respond to the “false” return value for the event handler of the ‘turbo:submit-start’ event.

1 Like

For the benefit of future Googlers… there is a way to terminate form submission e.g. due to invalid attributes. If this solves your validation scenario, make sure you have test coverage as it doesn’t seem to be documented as part of the public API, so maybe could change in future versions?

let validate = function(event){
  let valid_attributes = ... // whatever your validation criteria requires

  if(!valid_attributes){ event.originalEvent.detail.formSubmission.stop() }
}

$('body').on('turbo:submit-start', '#my_form', validate)
1 Like