Search code examples
ruby-on-railsrubyfavorites

How to mark and unmark an object as a 'favorite' in Rails with a single button?


I implemented a feature where a user marks jokes as favorites. Because I'm already using acts_as_votable for upvoting/downvoting and want favoriting to be distinct from a users upvotes, I've built it on my own.

Right now, A user creates a favorite by clicking a 'favorite' link or 'unfavorite' link. This isn't ideal because a user can create duplicate favorites this way.

I'd prefer to change it to a toggle where I first check to see if a user has marked the joke as a favorite, and then display the appropriate action based on that state. I'm not sure how to go about it though. Can anyone point me in the right direction?

My jokes controller:

  # Add and remove favorite jokes
  # for current_user
  def favorite
    @joke = Joke.find(params[:id])
    type = params[:type]
    if type == "favorite"
      current_user.favorites << @joke
      redirect_to :back, notice: 'Added to favorites'

    elsif type == "unfavorite"
      current_user.favorites.delete(@joke)
      redirect_to :back, notice: 'Removed from favorites'

    else
      # Type missing, nothing happens
      redirect_to :back, notice: 'Nothing happened.'
    end
  end

My models: (Joke)

has_many :favorite_jokes
has_many :favorited_by, through: :favorite_jokes, source: :user

(Favorite_Joke)

belongs_to :joke
belongs_to :user

(User)

  has_many :favorite_jokes
  has_many :favorites, through: :favorite_jokes, source: :joke

My View:

<div class="col-sm-4 col-xs-12 text-center">
        <button type="button" class="btn btn-danger btn-block">
            <i class="fa fa-heart"></i>
            <span class="badge">favorite</span>
        </button>
</div>

<% if current_user %>
  <%= link_to "favorite", favorite_joke_path(@joke, type: "favorite"), method: :put %>
  <%= link_to "unfavorite", favorite_joke_path(@joke, type: "unfavorite"), method: :put %>
<% end %>

And finally, my routes:

resources :jokes do
    member do
      put "like" => "jokes#upvote"
      put "unlike" => "jokes#downvote"
    end
      put :favorite, on: :member
  end

Solution

  • One option would be to check in the action that renders that view (which appears to be jokes_controller#show) if the current_user has a row in the favorite_jokes table for that particular joke.

    In the jokes_controller#show action:

    @favorited = FavoriteJoke.find_by(user: current_user, joke: @joke).present?
    

    Then in the view, something like:

    <% if @favorited %>
      <%= link_to "unfavorite", unfavorite_joke_path(@joke), method: :put %>
    <% else %>
      <%= link_to "favorite", favorite_joke_path(@joke), method: :put %>
    <% end %>
    

    Then in your routes.rb file, create a route for both favoriting and unfavoriting, rather than doing that logic in one action.