AJAXified One-Page Application with Rails

Blueprint Ruby on Rails

Last updated Feb 16, 2016

A solid understanding of AJAX is crucial for building user-friendly streamlined apps. In this lesson, we are building a one-page app where our users can View, Create, Update, and Delete the tasks that they've created. We'll also add a custom action that will allow Users to mark tasks complete. Setting this up is pretty simple, but we want to take things a step further and implement the entire app from one page using AJAX. The User will have the ability to update anything related to their Tasks without ever needing to refresh the page.

Let's Build TaskSnail

This is part 2 of Let's Build TaskSnail. In part 1, we created a gradual engagement system. So far, we have a User model (using Devise) that has_many tasks, and a Task Model that belongs_to the User. This lesson is modular, so don't feel obligated to go through part 1 for things to make sense.

Lesson Resources

Lesson Checkpoints

1. One-Page App Design

There are 4 main actions that we want the User to perform from their dashboard page:

  1. Create new tasks
  2. Edit existing tasks
  3. Delete old tasks
  4. Mark Tasks Complete

The backend structure for these actions already exists from the Rails scaffold generator. Our challenge is to use JavaScript to execute each of these actions without refreshing the page. We are going to modify the existing tasks#index action to serve as the root page. Let's start by making our our root URL point to the this action

config/routes.rb

root 'tasks#index'

2. Update the Tasks Controller

In Rails, AJAX works by rendering a JavaScript file, instead of the default html.erb file when an action is triggered. In rails 4.2, we can do this with one simple line of code. In older verisions of rails, you may need to tell each action to respond to js individually, or integrate the responders gem

controllers/tasks_controller.rb

respond_to :html
respond_to :js

Second, we need to update the tasks_controller to only display the Tasks created by a users. Instead of calling all tasks, we will only call the tasks created by the current_user.

def index
  @user = current_user
  @tasks = @user.tasks.all
  respond_with(@tasks)
end

4. Create a Partial for the Task Object

When the user performs a new action, such as creating a new task, we will need to update the task feed accordingly. We will do this by rending a partial into the page. Create a new file called _task.html.erb. The partial is also going to user the div_for rails helper, which will assign a unique id to the div. This will come in handy when we need update specific tasks with jQuery.

views/tasks/_task.html.erb

<%= div_for task do %>

  <h3><%= task.name %></h3>

  <ul>
   <li><%= link_to "Edit Task", new_task_path, remote: true %></li>
   <li><%= link_to "Mark Complete", complete_path, remote: true %></li>
   <li><%= link_to "Delete Task", task, remote: true, method: :delete %></li>
  </ul>
<% end %>

5. AJAXify the New Action

When the user clicks the “New Task” link, we want to render the task form. To do this, we need to create a new file:

views/tasks/new.js.erb

$('.new-task-button').after('<%= j render('form') %>');
$('.new-task-button').hide();

The first line of jQuery is going to find the link with the new-task-button CSS class, then replace it with the tasks/_form.html.erb partial.

The j is shorthand syntax for escape_javascript, which is required to embed in ruby code directly inside a line of Javascript.

The second line hides the new task button from the DOM to prevent the user from rendering multiple forms.

6. AJAXify the Create Action

Now that New Task form is being rendered into the page, we also need to set the form to remote: true.

views/tasks/_form.html.erb

<%= form_for(@task, remote: true) do |f| %>

Our next step is to allow the User to submit the form and have the newly created task automagically appear in the task list. Let's create a new javascript file to handle the create action.

views/tasks/create.js.erb

$('form').hide();
$('.my-tasks').append('<%= j render partial: “task”, locals: {task: @task} %>');
$('.new-task-button').show();

When the user clicks submit Rails will save the task to the database like normal, but we need to use JavaScript to append the new task to exisiting list. Because we're rendering this partial within an each loop in the view, we need to define the local task variable, otherwise we'll get an undefined variable error.

7. AJAXify the Edit Action

If a user wants to edit as task, we will render the form partial directly in that div. The div_for helper we used in the view will allow jQuery to easily identify the correct DOM element, which will be replaced with the edit form. Create the following file to handle the edit action:

views/tasks/edit.js.erb

$('form').hide();
$('div#<%= dom_id(@task) %>').replaceWith('<%= j render("form") %>');

First, we find any other form elements that are open on the page and hide them. Next, we find the unique div id for the task and replace it with the edit form.

8. AJAXify the Update Action

The update action needs to (1) remove the form from the DOM and (2) render the updated task. We can achieve both of these goals by calling the replaceWith method on the corresponding div. This will replace the old Task and the Edit form with the newly updated task.

views/tasks/update.js.erb

$('form').hide();
$('div#<%= dom_id(@task) %>').replaceWith('<%= j render partial: "task", locals: {task: @task} %>');

9. AJAXify the Destroy Action

Lastly, we need to remove a task from the screen after it has been destroyed.

views/tasks/destroy.js.erb

$('div#<%= dom_id(@task) %>').fadeOut();

10. Create a Custom Action to Mark Tasks Complete

What if we want our dashboard to have two sections - complete tasks and incomplete tasks? First, let's define a utility method to mark the Task's complete attribute to true. This is also a good time to define a couple scopes to sort between complete and incomplete tasks in the views.

models/task.rb

scope :complete, -> { where(complete: true) }
scope :incomplete, -> { where(complete: nil) }

def mark_complete!
  self.update_attribute(:complete, true)
end

Next, we need to use this utility method in the in our custom controller action.

controllers/tasks_controller.rb

def complete
  @task = Task.find(params[:id])
  @task.mark_complete!
end

We also need a new route for this custom action.

get '/complete/:id', to: 'tasks#complete', as: 'complete'

Refactor the View:

We need a new div hold the complete tasks. We will loop through the tasks, but this time, we will use our scope to only loop through tasks were complete is true.

views/tasks/index.html.erb

<div class="complete-tasks">
    <% @tasks.complete.each do |task| %>
    <h3><%= task.name %></h3>
    <% end %>
</div>

Lastly, we need to create the Javascript file.

views/tasks/complete.js.erb

$('div#<%= dom_id(@task) %>').fadeOut();
$('.complete-tasks').append('<%= j render partial: “task”, locals: {task: @task} %>');
$('div#<%= dom_id(@task) %>').find('h3').css('text-decoration', 'line-through');

The first line removes the task from the DOM. The second line appends the complete task to the complete-tasks CSS class. The third line adds a strikethrough to heading within the task to make the complete tasks stand out. That's it, we now have an AJAX powered one-page rails app.

Comments