[Rails / JavaScript / Ajax] I tried to create a like function in two ways.

start

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

[Pattern 1] Rails Way version

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

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.

[Pattern 2] JavaScript version

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.

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

Summary

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

[Rails / JavaScript / Ajax] I tried to create a like function in two ways.
I tried to implement Ajax processing of like function in Rails
How to implement a like feature in Ajax in Rails
I tried to create a Clova skill in Java
I tried to make a login function in Java
I want to define a function in Rails Console
[Rails] I tried to create a mini app with FullCalendar
[Rails] I tried to implement "Like function" using rails and js
I tried to make a message function of Rails Tutorial extension (Part 1): Create a model
I tried to create a simple map app in Android Studio
I tried to write code like a type declaration in Ruby
Rails Tutorial Extension: I tried to create an RSS feed function
I tried to make a message function of Rails Tutorial extension (Part 2): Create a screen to display
I tried to make a group function (bulletin board) with Rails
Two ways to start a thread in Java + @
I tried to organize the session in Rails
I tried to create a LINE clone app
How to implement a like feature in Rails
How to easily create a pull-down in Rails
I tried to create Alexa skill in Java
How to make a follow function in Rails
[First environment construction] I tried to create a Rails 6 + MySQL 8.0 + Docker environment on Windows 10.
I want to use a little icon in Rails
Implement user follow function in Rails (I use Ajax) ②
Implement user follow function in Rails (I use Ajax) ①
[Rails] How to load JavaScript in a specific view
I tried to create a shopping site administrator function / screen with Java and Spring
I tried to implement the like function by asynchronous communication
I tried to create a java8 development environment with Chocolatey
I tried using Hotwire to make Rails 6.1 scaffold a SPA
I want to create a form to select the [Rails] category
A story I was addicted to in Rails validation settings
I came across a guy with two dots in Rails
I tried to convert a string to a LocalDate type in Java
I want to create a Parquet file even in Ruby
I tried to implement a buggy web application in Kotlin
I tried to make a client of RESAS-API in Java
I tried to create a padrino development environment with Docker
I tried to make a reply function of Rails Tutorial extension (Part 3): Corrected a misunderstanding of specifications
[Rails 6.0, Docker] I tried to summarize the Docker environment construction and commands necessary to create a portfolio
[Rails] Two ways to write form_with
Add a search function in Rails.
Preparing to create a Rails application
Create a new app in Rails
I tried to illuminate the Christmas tree in a life game
I tried to sort the data in descending order, ascending order / Rails
[Implementation procedure] Create a user authentication function using sorcery in Rails
I tried to implement the image preview function with Rails / jQuery
I tried to create a Spring MVC development environment on Mac
I tried to build a simple application using Dockder + Rails Scaffold
(Ruby on Rails6) Create a function to edit the posted content
[Rails] Implementation of multi-layer category function using ancestry "I tried to make a window with Bootstrap 3"
I tried to create an API to get data from a spreadsheet in Ruby (with service account)
I tried to make a parent class of a value object in Ruby
I tried to introduce CircleCI 2.0 to Rails app
How to create a query using variables in GraphQL [Using Ruby on Rails]
[Rails] I tried to implement a transaction that combines multiple DB processes
How to insert a video in Rails
[iOS] I tried to make a processing application like Instagram with Swift
[Rails withdrawal] Create a simple withdrawal function with rails
I tried to build a Firebase application development environment with Docker in 2020