This article is The first step for a fledgling engineer! This is his 16th day article on Advent Calendar 2020.
This is the first article to participate in Advent Calendar. I am in my second year as an engineer.
The article of the like function itself has a feeling of brewing, but what I wanted to do is that it is difficult to write in JS because the code increases.
Create a like function.
First of all, the model is common to both patterns and looks like this.
app/models/like.rb
class Like < ApplicationRecord
  belongs_to :movie
  belongs_to :user
  LIKED_COLOR = '#ff3366'.freeze
  UNLIKED_COLOR = '#A0A0A0'.freeze
end
app/models/movie.rb
class Movie < ApplicationRecord
  has_many :likes, dependent: :destroy
  def like_by(user)
    likes.where(likes: { user_id: user }).last
  end
  def liked_by?(user)
    like_by(user).present?
  end
end
Define routing.
config/routes.rb
resources :movies, only: :show
resources :likes, only: %i[create destroy]
The point of the controller is that if ** asynchronous ** create, destroy actions occur,
Since create.js.slim`` destroy.js.slim is automatically render, @movie to be passed to each is defined by before_action.
app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :set_movie
  def create
    Like.create!(user_id: current_user, movie_id: params[:movie_id])
  end
  def destroy
    Like.find(params[:id]).destroy!
  end
  private
  def set_movie
    @movie = Movie.find(params[:movie_id])
  end
end
view will be written in slim this time. It doesn't matter, but the heart symbol is supposed to use fontawesome.
link_to is
movie_id: movie.id is defined as an argument so thatparams [: movie_id]can be passed to the controller.remote: true.app/views/movies/show.html.slim
#js-like-button
  = render 'like', movie: @movie
app/views/movies/_like.html.slim
- if movie.liked_by?(current_user)
  = link_to like_path(movie.like_by(current_user).id, movie_id: movie.id), method: :delete, remote: :true do
    i.fas.fa-heart style="color: #{Like::LIKED_COLOR}"
- else
  = link_to likes_path(movie_id: movie.id), method: :post, remote: :true do
    i.fas.fa-heart style="color: #{Like::UNLIKED_COLOR}"
The notation of js.slim may be a little unique. Both create and destroy render the same content.
app/views/likes/create.js.slim
| document.getElementById('js-like-button').innerHTML = "#{j(render 'movies/like', movie: @movie)}";
app/views/likes/destroy.js.slim
| document.getElementById('js-like-button').innerHTML = "#{j(render 'movie/like', movie: @user)}";
Pattern 1 is now complete. Thanks to Rails, you can do it quickly.
Define routing. The create and destroy actions for the like feature are defined as APIs that pass JSON to JavaScript.
config/routes.rb
resources :movies, only: :show
namespace :api, format: :json do
  namespace :v1 do
    resources :likes, only: %i[create destroy]
  end
end
Defines a controller for the API.
If you do not write skip_forgery_protection, you will be caught in CSRF and you will not be able to hit the API from the JavaScript side. * I don't know much in detail, so I would appreciate it if an expert could teach.
app/controllers/api/v1/likes_controller.rb
module Api
  module V1
    class LikesController < ApplicationController
      skip_forgery_protection
      def create
        like = Like.create!(user: current_user, movie_id: params[:movie_id])
        render json: { like_id: like.id }
      end
      def destroy
        Like.find(params[:id]).destroy!
        render json: { } #It's a bit ugly, but I couldn't process it on the js side unless I returned json.
      end
    end
  end
end
The view once shows whether or not it is currently liked.
input type ='hidden' defines the parameters like_id and movie_id to receive in JS.
app/views/movies/show.html.slim
#js-like-button
  - like_button_color = @movie.liked_by?(current_user) ? Like::LIKED_COLOR : Like::UNLIKED_COLOR
  input type='hidden' id='like_id' value="#{@movie.like_by(current_user).id}"
  input type='hidden' id='movie_id' value="#{@movie.id}"
  i.fas.fa-heart style="color: #{like_button_color}"
Write a JS that hits the create and destroy APIs when the like button is clicked.
rgbTo16 () converts RGB colors to hexadecimal for comparison.POST (create) or DELETE (destroy), but it may have been a subtle method.app/javascript/likes.js
document.addEventListener('turbolinks:load', () => {
  const LIKED_COLOR = '#ff3366';
  const UNLIKED_COLOR = '#a0a0a0';
  const LIKE_ENDPOINT = '/api/v1/likes';
  const rgbTo16 = rgb => {
    return '#' + rgb.match(/\d+/g).map((value) => {
      return ('0' + parseInt(value).toString(16)).slice(-2)
    }).join('');
  }
  const sendRequest = async (endpoint, method, json) => {
    const response = await fetch(endpoint, {
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      method: method,
      credentials: 'same-origin',
      body: JSON.stringify(json)
    });
    if (!response.ok) {
      throw Error(response.statusText);
    } else {
      return response.json();
    }
  }
  const createLike = movieId => {
    sendRequest(LIKE_ENDPOINT, 'POST', { movie_id: movieId })
      .then(data => { 
        document.getElementById('like_id').value = data.like_id
      });
  }
  const deleteLike = likeId => {
    const DELETE_LIKE_ENDPOINT = LIKE_ENDPOINT + '/' + `${likeId}`;
    sendRequest(DELETE_LIKE_ENDPOINT, 'DELETE', { id: likeId })
      .then(() => {
        document.getElementById('like_id').value = '';
      });
  }
  const likeButton = document.getElementById('js-like-button');
  if (!!likeButton) {
    likeButton.addEventListener('click', () => {
      const currentColor = rgbTo16(likeButton.style.color);
      const likeId = document.getElementById('like_id').value;
      const movieId = document.getElementById('movie_id').value;
      if (currentColor === UNLIKED_COLOR) {
        likeButton.style.color = LIKED_COLOR;
        createLike(movieId);
      }
      else {
        likeButton.style.color = UNLIKED_COLOR;
        deleteLike(likeId);
      }
    });  
  }
});
Don't forget to read the written file with application.js.
I always think, is it okay to write application.js with import ~ from ~?
app/javascript/packs/application.js
require('../likes')
Pattern 2 is over. It's a whole JS code, but it's long sweat
The easy thing is to use what Rails provides.
I will paste the reference link when I remember it.
We would appreciate it if you could give us your impressions and reviews.
Recommended Posts