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: truetolocal: falsein the form. -
- Without this, the
data-remote=trueattribute is not set in the HTML.
- Without this, the
-
- Leaving it blank is not enough, despite the fact that it should be the default. If I don't provide
local: falseI get in the console:
- Leaving it blank is not enough, despite the fact that it should be the default. If I don't provide
$('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 isnilfor me in the console. I can't find anywhere it's overridden.
- According to this answer the default value is configured by
Added
rails-ujsand removedjquery_ujsin my JS assets.-
- I needed to include
rails-ujsin order forRailsto be defined (needed for the next step).
- I needed to include
-
- When including
rails-ujs, I got a JS warning thatjquery_ujsshould be removed. I think this is included by thejquery-railsgem - am I safe to remove that, too?
- When including
Changed
this.form.submit()toRails.fire(this.form[0], 'submit').-
- Any variant of
$(form).submit()performs a local submission and not a remote one.
- Any variant of
-
- 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:successhandler.-
- 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?
Okay, so a couple of things hop to mind.
local: false instead of remote: true
First of all,
form_withdoesn't have aremoteoption anymore, it's called local: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 userenderhere 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_toblock 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,