Alternative Way 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:

  1. Keep all the data in memory and send it to the server when you finish the Wizard
  2. 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:
Empty Wizard

What happens when you hit Continue?

  1. 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(), ...} },
        ...
      }
    }
    

  2. 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  
    

  3. The JS view first_step.js will look roughly like this:
    $('#first_step_container').html("<%= escape_javascript(render 'first_step_partial') %>");
    

  4. 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 -->
          ...
    
  5. The rendered result will look like this: First Page With Errors


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.

Ariel Presa

Night owl. Free spirit. Computer geek. Film lover. Bedroom DJ. Spider hater. Sports enthusiast.

Buenos Aires

Subscribe to Beatcoding Blog

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!