Upgrading to rails 5.2 - remote forms now refreshing the page

559 views Asked by At

I am trying to upgrade a Rails 5.1.7 application to 5.2.0. Some forms on the site were broken by the upgrade. The template for the form looks like this:

<%= form_with model: record, url: client_field_value_path(@dataset, record), remote: true do |form| %>
  <%= the form contents %>
<% end %>

I think the only relevant part of the form is that it uses remote: true. I have found out that in Rails 5.2 remote: true was changed to the default for the form_with helper. I believe this is implemented with unobtrusive javascript, but I don't understand ujs very well.

The form is meant to be submitted without reloading the page, and the state changes in-page are handled with ajax listeners. We use jquery - the client-side code includes the line:

  $(this.form).trigger('submit');

which is now performing a local submission, and not a remote one. So the page refreshes, and the JSON response gets rendered. I found an issue raised against rails-ujs which looks relevant, and they suggest replacing $(form).submit() by

form = document.querySelector('form');
form.dispatchEvent(new Event('submit', {bubbles: true}));

or, with rails-ujs,

form = document.querySelector('form');
Rails.fire(form, 'submit');

I have added rails-ujs to my application.js file:

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
// ....
//= require jquery
//= require jquery_ujs
//= require rails-ujs
// ....

(jquery and jquery_ujs were already there). I tried both of the suggestions above and neither of them seems to trigger a submission of the form. I'm really stuck! If there's any more detail I can provide, please ask. I don't understand the situation with ujs very well, so I'm a bit lost.

EDIT: Full body of the save call - this is triggered when a "save" button is clicked (but that button is not <input type=submit>, it's just a styled button).

  save(event) {
    if( event ) {
      event.preventDefault();
      event.stopPropagation();
    }

    this.container.addClass('saving');

    // this.form.submit();
    Rails.fire(this.form[0], 'submit');


    this.container.find(".field-controls button").prop('disabled', true);
  }

Update: I have managed to get it working. I would really appreciate some help understanding where the following changes came from, and whether something is broken in my application config that's making it harder than necessary.

  • Explicitly changed remote: true to local: false in the form.
    • Without this, the data-remote=true attribute is not set in the HTML.
    • Leaving it blank is not enough, despite the fact that it should be the default. If I don't provide local: false I get in the console:
$('form[data-remote=true]').length
0
    • According to this answer the default value is configured by Rails.application.config.action_view.form_with_generates_remote_forms. This value is nil for me in the console. I can't find anywhere it's overridden.
  • Added rails-ujs and removed jquery_ujs in my JS assets.

    • I needed to include rails-ujs in order for Rails to be defined (needed for the next step).
    • When including rails-ujs, I got a JS warning that jquery_ujs should be removed. I think this is included by the jquery-rails gem - am I safe to remove that, too?
  • Changed this.form.submit() to Rails.fire(this.form[0], 'submit').

    • Any variant of $(form).submit() performs a local submission and not a remote one.
    • In previous versions, the jQuery submission was remote and not local.
    • I don't know anywhere where this is documented. The answer I linked above explains it - but is this really the way RoR intends for you to submit ajax forms? I find it extremely unintuitive that there is a "right way" and a "wrong way" to submit forms in this sense.
  • Changed the signature of the ajax:success handler.

    • Likewise, I didn't find anywhere where this change was explained, but I guess it's just "how rails-ujs does it". I had to change by ajax handler from
  form.on('ajax:success', (event, response, status) => {
    $.each(response, (i, key) => {
      do_stuff();
    });
  });

to

  form.on('ajax:success', (event) => {
    const [response, _status, _xhr] = event.detail;
    $.each(response, (i, key) => {
      do_stuff();
    });
  });

Again, I guess this is because the event is handled by rails-ujs and not by jquery, but... why? How are you supposed to know?

2

There are 2 answers

3
Dakota Lee Martinez On

Okay, so a couple of things hop to mind.

local: false instead of remote: true

First of all, form_with doesn't have a remote option anymore, it's called local:

<%= form_with model: record, url: client_field_value_path(@dataset, record), local: false do |form| %>
  <%= the form contents %>
<% end %>

Also, you should be able to skip this altogether because it's the default. You can confirm this by removing it and then inspecting the html markup for the route pointing to this form view and you should see data-remote="true" within the form tag. This probably isn't the problem.

Where is your jQuery code?

Usually, the way ujs works is you'd have a file in your views directory called create.js.erb. The js in that file will run when you submit the form and you'll be able to use erb tags within that template to add dynamic content to the JS. For example, you can use render here to render a partial and pass in an instance variable defined in the controller.

Of course, if you've got all your javascript set up separately and it should only work properly with your javascript engaged, then it's no longer UJS. UJS stands for unobtrusive javascript. The idea is that the javascript enhances functionality rather than replacing it. This means that if your javascript didn't load for some reason, the form would still submit and work properly without javascript engaged. This may not be the case for your app, and that's fine, but it would be useful to know to provide a better answer.

The way the UJS pattern works relies on some controller code. So you'd have a respond_to block in your controller that would respond both to requests for html and requests for js. If the form is remote, then it will run the javascript in the file matching the controller action. For example,

def create 
  @record = Record.new(record_params)
  respond_to do |format|
    if @record.save
      format.html { redirect_to records_path }
      format.js {}
    else 
      format.html { render :new }
      format.js { render :error }
    end
  end
end
end
0
Daniel Littlewood On

I now understand the issue! I was previously very confused.

The answer

For some reason (which I still don't understand),

Rails.application.config.action_view.form_with_generates_remote_forms

was unset following the upgrade. Because it was unset, the data-remote="true" attribute was not present in the HTML, so jquery-ujs was not submitting the form with AJAX. Setting this config variable to true in my initialization fixed the issue.

Digression

I got very confused thinking that rails-ujs was a drop-in replacement for jquery-ujs. It is not. They work in very different ways! jquery-ujs intercepts events called through jquery to (for example) get remote forms to be submitted with AJAX. rails-ujs has the same functional effect by creating its own events and calling them with Rails.fire. They don't play well together!

All the confusion I got after adding rails-ujs stems from the fact that they are basically different libraries with different APIs.