Stimulus and Rails UJS

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?

1 Like

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

2 Likes

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);
})

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?

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
}
2 Likes

I’ve followd this thread and am using

Rails.fire(form, 'submit')

To successfully submit my form. Is there a way to have an onsuccess method with that? I’d like to change some stuff on my page once it finishes, based on which controller action triggered it.

You can have an action that triggers on ajax success like so:

<form data-action="ajax:success->controller-name#actionName">

A complete list of ajax lifecycle events can be found here: https://edgeguides.rubyonrails.org/working_with_javascript_in_rails.html#rails-ujs-event-handlers

1 Like

I want to know when it has finished within the action where I triggered the success, as I want to do more with the part of the form that triggered the submit.

Essentially, I just want it to be synchronus.

This might explain what I am struggling with better.

Is there any particular reason why you need it all to happen inside one action? It will be synchronous if you use the ajax:success event.

I’m not sure what you mean when you say “I want to do more with the part of the form that triggered the submit”. You’re submitting the form programmatically with Rails.fire, and you should be able to query the form object again in another action, or even better: make it a target.

I looked at the SO post you referred to, and it looks like you want to perform a request after the form is submitted. Is that correct? You should be able to to that inside the action:

<form data-action="ajax:success->survey#getMoreData"
      data-target="survey.form">
// survey_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  static targets = ['form']
  
  submit() {
    Rails.fire(this.formTarget, 'submit')
  }

  getMoreData(event) {
    // The `event` object might have the information you need.
    // The `formTarget` object is available here as well.
    Rails.ajax({type: "GET", url: /* ... */ })
  }
}

There are multiple parts of the form that can be changed and that triggers the form submit. After the form has submitted, i want to fetch some info on that little part of the form. I won’t know which part of the form was clicked if I just use the ajax:success.

Is it not possible to wait? It seems like this would be simple and I’m just being awful at JS. But judging from responses I’m actually trying to do the impossible?

I’d love to be able to get the promise to work and to wait. Partially because having looked into how to do this all friday, it would be great to get it working : )

I’m trying to trigger an action on creation of a model object using this example. From my testing, it appears that ajax:success gets triggered whether or not the object gets created (I see it happening even when validations fail - is that still considered a successful ajax call? If so, what’s the preferred way to have Stimulus perform an action only on successful object creation?)

Thanks in advance!

The ajax:success event is triggered when your Rails controller action (or any other endpoint) responds with a HTTP status code in the 2xx range. It’s not concerned with object creation.

Can you tell a bit more about how your solution is implemented? Do you use a server-side rendered response to create the modal or is it all client side?

I see what you’re saying. I was confusing the success of object creation with ajax:success. Whether the model saves or not is immaterial - it returns 200 status either way, and so it’s always ajax:success.

What I’m trying to do is trigger a Stimulus action once an object is created. The controller action:

 def create
    @comment = @commentable.comments.new(comment_params)
    @comment.user = current_user
    respond_to do |format|
      if @comment.save
        format.html { redirect_to @commentable }
        format.js
      else
        format.html { redirect_to @commentable, flash: { error: 'Problem saving comment.' }  }
        format.js { flash.now[:error] = @comment.errors.full_messages }
      end
    end
  end

I had (mistakenly) believed that ajax:success might be the event I needed to listen for, but now see that’s entirely independent of object creation. So I’m assuming I’ve got to use the rendering of create.js.erb to do that, but not quite sure how? Currently I’m checking for errors and calling alert in that file in case of validation errors:

<% if @comment.errors.any? %>
alert("<%= j flash[:error].join('; ') %>")
<% else %>
.... (main code)

Just not sure how to trigger an action when it’s not directly attached to a click event or something similar. Or is there some other success event I’m overlooking?

Are you creating your modal element inside the Stimulus controller action that listens to the ajax:success event?

If so, you can render a status outside the 2xx range from your Rails controller to ensure the ajax:success event never fires when a comment didn’t save.

format.js do
  flash.now[:error] = @comment.errors.full_messages
  render status: :unprocessable_entity
end
2 Likes