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
- Rails 4.2
- Ruby 2.2.0
- Live Demo
- Lesson Repo
Lesson Checkpoints
1. One-Page App Design
There are 4 main actions that we want the User to perform from their dashboard page:
- Create new tasks
- Edit existing tasks
- Delete old tasks
- 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
3. Setting Up Links for AJAX
We need to tell our links to render Javascript instead of HTML. In rails, we only need one piece of code in our links and forms to make AJAX happen: remote: true
. This tells the controller to look for the corresponding js.erb file, rather than the default html.erb file.
Let's refactor the index view to loop through tasks, then display the remote link for a New Task.
views/tasks/index.html.erb
<div class="my-tasks">
<% @tasks.each do |task| %>
<%= render task %>
<% end %>
</div>
<%= link_to “New Task”, new_task_path, remote: true, class: “new-task-button” %>
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.