How to Update Rails Views via AJAX
If you still feel a little frustrated like me, circling around AJAX and Rails views, I think you might like this.
Some Use Cases
I faced this problem when I had to tackle these scenarios:
- Wizards partial saving
- Pure HTML Table editing/refresh
- Messaging/Notification refresh
- Infinite Scrolling
Let me dive into the first case; saving each step of a wizard gets a little bit complicated if you have to refresh the whole page every time you hit Continue. That is a known bad practice and to do it the right way, you might consider these strategies:
- Keep all the data in memory and send it to the server when you finish the Wizard
- Do partial step-by-step validation and saving
The second option handles the errors better and is more intuitive for the user. Personally, I think that's the way all the Wizards should be designed.
Let's Spice Up The Example:
Consider that you have to present the Wizard for the first time as a rendered (empty) HTML, directly from your controller action:
def new_wizard
@step_one = StepOne.new
render 'new_wizard_view'
end
You're loading the empty Wizard for the first time and it looks somewhat like this:
What happens when you hit Continue?
- The button calls a JavaScript function that executes an AJAX call:
function saveFirstStep(form){
$.ajax{
url: '/save/first/step',
method: 'POST', // This also could be PUT or PATCH
dataType: 'JSON',
data: { step_one: { val_1: form.input_1.val(), ...} },
...
}
}
- The save Action is triggered in the Rails server:
def save_first_step
# Use a before_action to filter params and skip this line
@step_one = StepOne.new(params[:step_one])
respond_to do |format|
if @step_one.save
format.js { render 'second_step' }
else
format.js { render 'first_step' }
end
end
end
- The JS view
first_step.js
will look roughly like this:
$('#first_step_container').html("<%= escape_javascript(render 'first_step_partial') %>");
- Finally, to make it work you have to have a second partial ERB view
first_step_partial.html.erb
with all the HTML and model's errors from validations:
<div class="form-group">
<div class="row">
<div class="col-md-offset-1 col-md-3">
<%= f.input :id, :as => :hidden %>
<%= f.input :icon, :collection => survey_icons %>
<%= f.input :title %>
<!-- the rest of the form goes on -->
...
The rendered result will look like this:
Time for Plan B:
I personally don't like having two different views (JS + ERB partial) per step because when you have a +3 steps Wizard, there will be too many views and it becomes a big problem for future maintenance.
Also, if you work with multiple models in the same Wizard, it becomes messy to store all the views in the directory hierarchy.
Instead of using the JS first_step.js
view, you can attempt to return a JSON object from your action:
respond_to do |format|
if @step_one.save
format.json { render :json => {
# Returns an empty object without errors
:html => {} },
# Returns the status to AJAX
:status => :ok }
else
format.json { render :json => {
# Renders the ERB partial to a string
:html => render_to_string(
:template => 'first_step_partial.html.erb', # The ERB partial
:formats => :html, # The string format
:layout => false, # Skip the application layout
:locals => {:step_one => @step_one}) # Pass the model object with errors
},
# Returns the status to AJAX
:status => :unprocessable_entity) }
end
end
And re-write your AJAX call like this:
function saveFirstStep(form){
$.ajax{
url: '/save/first/step',
method: 'POST', // This also could be PUT or PATCH
dataType: 'JSON',
data: { step_one: { val_1: form.input_1.val(), ...} },
success: function(response){
// Moves to the next step
$('#wizard').move_next;
},
error: function(jqXHR, textStatus, errorThrown){
// Rewrites the DOM object with the updated first step
$('#first_step_container').html(jqXHR.responseJSON.html);
}
...
}
}
With this workaround you certainly get rid of all the JS intermediate views and also keep all your HTML error handling in the ERBs. If you are fond of SimpleForm (or a similar gem), this solution helps handling DOM classes to show all input errors in the form.
Final Note and Coding Disclaimer:
This solution is a hack, and it is not considered the best way to handle AJAX calls in Rails, but is neat and it WORKS like a charm. If you know what you're doing, it's completely SAFE to use it.