Stimulus Discourse

Stimulus and Rails UJS


#1

I have an XHR form that triggers an action when the submit button is clicked:

<input type="submit" name="commit" value="Enabled " data-disable-with="Disabled" data-action="click->myController#myAction">

The action prevents the initial submitting before calling submit directly on the form. Everything works great except the data-disable-with. I could do it manually, but would rather rely on the Rails UJS.

How do I allow Rails UJS to disable and reenable the button within the Stimulus controller?


#2

Unless you have a good reason to submit the form yourself you might be better off letting rails-ujs handle it.

You can use all the rails-ujs request lifecycle events on your actions, so if you need to perform some logic before the request is sent, you can listen to ajax:beforeSend on your action:

<form data-action="ajax:beforeSend->controller#prepareParams" ... >

If you have to enable/disable the button yourself, you can:

<form data-action="ajax:send->controller#disableButton ajax:complete->controller#enableButton" ... >

You can see the full list of events here: https://guides.rubyonrails.org/working_with_javascript_in_rails.html#rails-ujs-event-handlers


#3

Ah! I like it, but can’t get it to work! I’ve tried putting the data-action="ajax:beforeSend->controller#prepareParams" on both the submit button and the form, but the form just submits over XHR without ever triggering my Stimulus action. I tried ajax:before as well, but no joy? What could I be missing? Since we are using Turbolinks, are the same events still triggered?

I added this to the connect method in my controller and don’t see anything in my console:

document.body.addEventListener('ajax:beforeSend', function(event) {
  console.log(event);
})

#4

Thanks for all the help on this! We figured this out, rails-ujs was still being loaded through the Asset Pipeline. In order to hook into those events, we moved rails-ujs into our web packer. Now, I’m able to bind to the ajax:beforeSend method.

One last question related to this form submission with UJS. The prepareParams action actually has to use a callback to update the form. Unfotunately, the action completes and the form submits before the callback executes. I can use event.preventDefault() in the action, but then I have to submit the form old school with form.submit() in the callback. Is that the best approach?


#5

Ah, you’re right, the actions have to be attached to the form, not the submit button — sorry! I’m glad you figured it out. I updated my example above.

I don’t think form.submit() will work in your case because it won’t submit via XHR. In what way does the callback have to update the form? You can try to listen for the ajax:before or submit event instead of ajax:beforeSend and see if the callback executes in time. Those events should fire even earlier.

If that doesn’t work, maybe the external service handling the callback provides a way to determine if the callback has finished? Maybe it supports a second callback which is executed when the first callback completes or perhaps it emits an event you can listen for?

If all hope is lost, you can try to wait for an empty promise or use setTimeout, which might execute after the callback completes, and then trigger Rails UJS to submit the form:

<form data-target="controller.form" data-action="ajax:before->controller#prepareForm" ... >
static targets = ["form"]

prepareForm(event) {
  event.preventDefault()
  ExternalService.call(this.callback.bind(this))

  // Solution 1
  Promise.resolve().then(() => {
    Rails.fire(this.formTarget, 'submit')
  })

  // Solution 2
  setTimeout(() => {
    Rails.fire(this.formTarget, 'submit')
  })  
}

callback(event) {
  // Do the callback dance
}