If you’re looking to add a system of favoriting / unfavoriting to a single Rails 5 model, you’re in the right place.

This post uses the example of a single-user task list app that allows you to click a ☆ next to the task name to toggle that task’s “favorite” setting. The task is updated with javascript, so you stay right there on the tasks index page and there is no reloading of the page.

ex:
★ Wrestle with kittens  <== favorite & best ever!!
☆ Take out the trash    <== meh, not my favorite

These are the steps to accomplish the favoriting:

  • Add the “favorite” boolean field to the tasks table
  • Add favoriting methods to the task model
  • Set up the routes and controller actions
  • Add a view helper toggle ★|☆ and link destinations
  • Wire up the javascript for updating without refreshing

Add the Favorite Boolean to the Tasks Table

Generate a new migration to add the new boolean column to the existing tasks table. On the command line, type:

rails g migration AddFavoriteToTasks favorite:boolean

Open the migration file (the last file in your db/migrate directory) and add default: false to the add_column method:

# db/migrate/20180419165453_add_favorite_to_tasks.rb

class AddFavoriteToTasks < ActiveRecord::Migration[5.0]
  def change
    add_column :tasks, :favorite, :boolean, default: false
  end
end

Save the file and run the migration:

rake db:migrate

Take a look at the schema.rb file to ensure the new favorite column is there.

# db/schema.rb

ActiveRecord::Schema.define(version: 20180419165453) do

  create_table "tasks", force: :cascade do |t|
    t.text     "name"
    t.text     "description",   default: ""
    t.boolean  "favorite",      default: false
    t.datetime "created_at",    null: false
    t.datetime "updated_at",    null: false
  end

end

If your Rails server is running, now is a good time to restart it. I have forgotten to do this many times. It usually comes back to bite me when I try to save a value to my new field and it won’t save.

Another step that’s easy to miss is adding your new field to the params whitelist in the matching controller. Missing this step will also cause your form submissions to skip saving data in that field.

# app/controllers/tasks_controller.rb

class TasksController < ApplicationController
  ...
  private

  def task_params
    params.require(:task).permit(:name, :description, :favorite)
  end
 ...
end

Add Favoriting Methods to the Task Model

Open the task model and add these public methods.

# app/models/task.rb

class Task < ApplicationRecord
  ...
  # Set the task's favorite setting to true and save the task
  def favorite!
    self.favorite = true
    self.save!
  end

  # Set the task's favorite setting to false and save the task
  def unfavorite!
    self.favorite = false
    self.save!
  end
  ...
end

At this point, it’s a good idea to pop into your rails console to make sure these methods work.

# Start rails server
rails console

# Set variable t to the first task
t = Task.first

# Check the value of t's favorite boolean
t.favorite # ==> false

# Call the `favorite!` method
t.favorite!

# Check the value of t's favorite boolean
t.favorite # ==> true. Great! It works.

# Call the `unfavorite!` method
t.unfavorite!

# Check the value of t's favorite boolean
t.favorite # ==> false. Great! It works.

Set up the Routes and Controller Actions

The only actions we’ll need to do with our favorites are create and destroy.

# config/routes.rb

Rails.application.routes.draw do
  ...
  root 'tasks#index'
  resources :tasks
  resources :favorites, only: [:create, :destroy]

end

Create a new controller file for the favorites controller. Just like in the routes file, we only need the create and destroy actions.

# app/controllers/favorites_controller.rb

class FavoritesController < ApplicationController

  before_action :set_task, only: [:create, :destroy]

  # Write the create action that corresponds to the POST route
  def create
    # Use the `favorite!` method to set the task's favorite boolean to true
    @task.favorite!
    redirect_to tasks_url
  end

  # Write the destroy action that corresponds to the DELETE route
  def destroy
    # Use the `unfavorite!` method to set the task's favorite boolean to false
    @task.unfavorite!
    redirect_to tasks_url
  end

  private

  def set_task
    @task = Task.find(params[:id])
  end

end

With the routing and controller actions in place, it’s time to write the links in the view. Add a view helper called toggle_favorite to the task in the view. Pass it the task as an argument.

<!-- app/tasks/_task.html.erb  -->
...
<h1><%= toggle_favorite(task) %> <%= task.name %></h1>

This example uses Font Awesome icons. Check out their getting started to get a CDN link or…

 <!-- app/views/layouts/application.html.erb -->

 <!DOCTYPE html>
  <html>
    <head>
      ...
      <!-- Add CDN as the last line before your `</head>` -->
      <link href="https://use.fontawesome.com/releases/v5.0.6/css/all.css" rel="stylesheet">
    </head>
  ...
  </html>

Define the toggle_favorite method in the tasks helper using the icon classes and the links to the favorites_controller’s destroy and create methods.

# app/helpers/tasks_helper.rb

module TasksHelper

  def toggle_favorite(task)
    # If the task has been favorited...
    if task.favorite
      # Show the ★ and link to "unfavorite" it
      link_to raw("<i class='fa fa-star favorite'></i>"), favorite_path(task), method: :delete
    else
      # Show the ☆ and link to "favorite" it
      link_to raw("<i class='far fa-star'></i>"), favorites_path(id: task.id), method: :post
    end
  end
end

Lastly, give a little style to the stars.

/* app/assets/stylesheets/tasks.scss */

.fa-star {
  float: left;
  font-size: 70%;
  margin-top: 5px;
  margin-right: 5px;
}

.favorite { color: yellow; }

At this point, you should be able to click on a star by a task name to toggle its favorite state. The index page WILL be reloading at this point. But not for long…

Wire up the Javascript for Updating Without Refreshing the Index Page

Head back over to the task view and add a unique identifier to the parent object. Here we can take advantage of Rails’ dom_id method which will generate a unique id based on the object’s model and its id number in the table.

<!-- app/tasks/_task.html.erb -->

<article id="<%= dom_id(task) %>">
  <h1><%= toggle_favorite(task) %> <%= task.name %></h1>
  <p><%= task.description %></p>
</article>

The <article id="<%= dom_id(task) %>"> will output something like <article id="task_25">, which is perfect for our javascript needs.

In the task helper, update the links in the toggle_favorite method to include remote: true. This will indicate to the controller that we want to use javascript to carry out the response to this request.

# app/helpers/tasks_helper.rb

module TasksHelper
...
  def toggle_favorite(task)
    if task.favorite
      # Add `remote: true` to the link
      link_to raw("<i class='fa fa-star favorite'></i>"), favorite_path(task), remote: true, method: :delete
    else
      # Add `remote: true` to the link
      link_to raw("<i class='far fa-star'></i>"), favorites_path(id: task.id), remote: true, method: :post
    end
  end

end

Go back to the favorites controller and remove the instruction to redirect to the index from both the create and the destroy methods:

# app/controllers/favorites_controller.rb

class FavoritesController < ApplicationController
...
  def create
    @task.favorite!
    # redirect_to tasks_url <== remove this
  end

  def destroy
    @task.unfavorite!
    # redirect_to tasks_url <== remove this
  end
  ...
end

Create a new folder called favorites in app/views and make js.erb files for both the create and the destroy methods inside of it.

- app/views/favorites/
  - destroy.js.erb
  - create.js.erb

Put this code in both of those files. Yep, it’s redundant.

// Use that handy `dom_id` from before to identify the correct
// <article> on the index page and then grab its star <i>
let starIcon = document.querySelector("#task_<%= @task.id %>").querySelector('.fa-star')

// Reuse the logic from the `toggle_favorite` method to
// update the star icon styles and the link destination
starIcon.parentElement.outerHTML = "<%= escape_javascript(toggle_favorite(@task)) %>"

And there you have it! Now you can toggle the stars to your heart’s delight without reloading the index page. If you’d like to see a similar example of this feature done with jQuery, check out this post by Dan Cunning.