[Rails, JS] How to implement asynchronous display of comments

In this article, I will explain how to implement the comment display asynchronously.

--I'm using Rails_6.0.0_. -Add a comment to the application that the product can be listed or purchased. --There is a place to comment on the detail page of each item. --Code Ruby required to save and send comments, and JavaScript to display saved comments immediately. -** The comment posting function has already been completed, and we will implement only the asynchronous function. ** **

画面収録 2020-10-06 -1.mov.gif

Implementation details

--Implement using channel --Display comments asynchronously --The name (name), comment date (created_at), and comment content (text) are displayed in the comment.

What is a channel?

What is a channel?

A channel is a server-side mechanism that provides immediate update functionality. Set the data route and display the sent data on the client screen.

What can you do with channel?

If you set the data route or write JavaScript to display the sent data, the sent data can be displayed asynchronously.

channel file creation

Execute the following command in the terminal % rails g channel comment (Enter the name of the file you create in comment) There are several files, but this time we will use the following two files. app/channel/comment_channel.rb It is a file for connecting the client and the server.

app/javascript/channels/comment_channel.js This file is used to display the data sent from the server on the client screen.

Description of comment_channel.rb

class MessageChannel < ApplicationCable::Channel
  def subscribed
    stream_from "comment_channel"
  end

  def unsubscribed
  end
end

You can connect the server and client by writing stream_from" comment_channel ".

Description of comments_controller.rb

A description of the controller. Since the comment implementation function that is not asynchronous has already been completed, explanations other than the description related to asynchronous are omitted.

Describe the data you want to pass from the database to JS

The data I want to reflect in JS this time is as follows

--User nickname --Commented time --Comment text --Item id (necessary to determine which item to comment on)

It is necessary to pass these three pieces of information to JS by the controller.

class CommentsController < ApplicationController
  before_action :authenticate_user!
  def create
    @comment = Comment.new(comment_params)
    @item = Item.find(params[:item_id])
    @comments = @item.comments.includes(:user).order('created_at DESC')
    if @comment.valid?
      @comment.save
            ActionCable.server.broadcast  'comment_channel', content: @comment, nickname: @comment.user.nickname, time: @comment.created_at.strftime("%Y/%m/%d %H:%M:%S"), id: @item.id
    else
      render "items/show"
    end
  end

  private
    def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, item_id: params[:item_id])
    end
end

Only the following sentence was added in this implementation. ActionCable.server.broadcast 'comment_channel', content: @comment, nickname: @comment.user.nickname, time: @comment.created_at.strftime("%Y/%m/%d %H:%M:%S"), id: @item.id

Since content, ʻuser, time and ʻid are used in JS, I have defined them. content Defined by @comment: text, the text column of the Comment table, that is, the comment you entered. user I'm getting the nickname of the user associated with @comment. (We have an association with comment and user.) time The created_at column of the Comment table. Any date and time setting can be displayed by writing strftime ("% Y /% m /% d% H:% M:% S "). I referred to the following article. I can't remember strftime (ruby) item JS needs to fire only on the page of the item you are currently viewing, so use it to determine that.

Description of comment_channel.rb (JavaScript)

This time, I will describe it in the received () part of ʻapp / javascript / channels / comment_channel.js`.

app/javascript/channels/comment_channel.js


received(data) {
}

By writing data in (), the value defined by the controller will be fetched. received means to receive, so when you receive the data, execute the JS described in it! It means that. Now, I will describe JS in this!

Write a conditional branch so that comments can be displayed only on the item page that is currently open.

app/javascript/channels/comment_channel.js


//Get the URL of the currently open page
let url = window.location.href
//Slash(/)Extract elements for each
let param = url.split('/');
//In the case of this app, the item id comes at the very end of the URL and it is defined as a paramItem.
let paramItem = param[param.length-1]
//Parameter id(The id contained in the URL)Sent from the controller`data.id`Determine if
if (paramItem == data.id) {}

Conditional branching is done with the if statement. Next, I will write the contents of the process in the conditional branch. The content to be described is

--Create a div element --Display the generated element on the browser --Generate the text to be displayed

It is a flow like.

Make a div to display

app/javascript/channels/comment_channel.js


    //Get the ID of the div of the place to display
    const comments = document.getElementById('comments');
    //Create a div to be the same as an existing view file
    const textElement = document.createElement('div');
    textElement.setAttribute('class', "comment-display");
    
    const topElement = document.createElement('div');
    topElement.setAttribute('class', "comment-top");

    const nameElement = document.createElement('div');
    const timeElement = document.createElement('div');

    const bottomElement = document.createElement('div');
    bottomElement.setAttribute('class', "comment-bottom");

Use the createElement method to create a div element, and give each required class name with the setAttribute method.

By the way, the view to display comments is as follows

  <div id='comments'>
  </div>
  <% @comments.each do |comment| %>
    <div class='comment-display'>
      <div class='comment-top'>
        <div><%= comment.user.nickname %></div>
        <div><%= l comment.created_at %></div>
      </div>
      <div class='comment-bottom'>
        <p><%= comment.text %></p>
      </div>
    </div>
  <% end %>

I have generated a div element, but it is not yet visible in the browser. Let's display it on the browser and create a parent-child relationship.

app/javascript/channels/comment_channel.js


      //Display the generated HTML element in the browser
      comments.insertBefore(textElement, comments.firstElementChild);
      textElement.appendChild(topElement);
      textElement.appendChild(bottomElement);
      topElement.appendChild(nameElement);
      topElement.appendChild(timeElement);

Use the insertBefire and appendChild methods. Parent element.insertBefore (element to add, where to add) Parent element .appendChild (element to add) So insertBefire can put an element anywhere, and appendChild can put an element at the end of the parent class.

If you want to see a little more detail, please see below [JavaScript] Difference between appendChild and insertBefore

After creating the div element, let's get the information to be displayed next.

app/javascript/channels/comment_channel.js


      const name = `${data.nickname}`;
      nameElement.innerHTML = name;
      const time = `${data.time}`;
      timeElement.innerHTML = time;
      const text = `<p>${data.content.text}</p>`;
      bottomElement.innerHTML = text;

The information to be displayed is put in each variable. data is the data for received (data) {}. It is the value defined in the controller. You have defined content, nickname, and time, respectively. Overwrite existing elements with innerHTML.

The display is complete up to this point! However, there are two problems at this rate.

  1. The data is displayed, but the comment remains in the comment input field.
  2. HTML is designed so that the button can be pressed only once by default. Let's solve this!
Delete the comment in the comment input field after sending the data

app/javascript/channels/comment_channel.js


    const newComment = document.getElementById('comment_text');
    newComment.value='';

It is a description that the ID of the comment input field is fetched and the value there is emptied.

Allows you to press the comment button many times

app/javascript/channels/comment_channel.js


    const inputElement = document.querySelector('input[name="commit"]');
    inputElement.disabled = false;

You can click it many times by fetching the name attribute of the "Comment" button and setting it as disabled = false.

The description is summarized below.

app/javascript/channels/comment_channel.js


import consumer from "./consumer"

consumer.subscriptions.create("CommentChannel", {
  connected() {
  },

  disconnected() {
  },
  //↓ When you receive the data, execute it.
  received(data) {
    let url = window.location.href
    let param = url.split('/');
    let paramItem = param[param.length-1]
    if (paramItem == data.id) {
      const comments = document.getElementById('comments');
      const comment = document.getElementsByClassName('comment-display');
      //Creating elements to use
      const textElement = document.createElement('div');
      textElement.setAttribute('class', "comment-display");
      const topElement = document.createElement('div');
      topElement.setAttribute('class', "comment-top");
      const nameElement = document.createElement('div');
      const timeElement = document.createElement('div');
      const bottomElement = document.createElement('div');
      bottomElement.setAttribute('class', "comment-bottom");
      //Display the generated HTML element in the browser
      comments.insertBefore(textElement, comments.firstElementChild);
      textElement.appendChild(topElement);
      textElement.appendChild(bottomElement);
      topElement.appendChild(nameElement);
      topElement.appendChild(timeElement);
      //Generate text to display
      const name = `${data.nickname}`;
      nameElement.innerHTML = name;
      const time = `${data.time}`;
      timeElement.innerHTML = time;
      const text = `<p>${data.content.text}</p>`;
      bottomElement.innerHTML = text;
      //After sending a comment, leave the comment field empty
      const newComment = document.getElementById('comment_text');
      newComment.value='';
      //Allows you to press the button many times
      const inputElement = document.querySelector('input[name="commit"]');
      inputElement.disabled = false;
    }
  }
});

in conclusion

I thought it was completed, but at the time of writing, I found a number of mistakes and some unclear descriptions. I understand how important refactoring is. There may be incorrect statements, but I hope it helps someone.

Recommended Posts

[Rails, JS] How to implement asynchronous display of comments
[Rails] How to implement scraping
[rails] How to display db information
[Rails] How to implement star rating
[Rails] How to display the list of posts by category
How to implement one-line display of TextView in Android development
How to implement search functionality in Rails
[Rails] How to display error messages individually
[Rails] How to omit the display of the character string of the link_to method
How to implement ranking functionality in Rails
How to implement image posting using rails
How to implement asynchronous processing in Outsystems
[Rails] How to use video_tag to display videos
[Ruby on Rails] How to display error messages
[Rails] How to implement unit tests for models
How to implement a like feature in Rails
How to use JQuery in js.erb of Rails6
[Rails] How to easily implement numbers with pull-down
How to display the result of form input
[Rails] How to convert UC time display to Japanese time display
How to uninstall Rails
[Ruby on Rails] Rails tutorial Chapter 14 Summary of how to implement the status feed
How to implement login request processing (Rails / for beginners)
How to set the display time to Japan time in Rails
How to implement guest login in 5 minutes in rails portfolio
How to implement a like feature in Ajax in Rails
[Java] Types of comments and how to write them
[Rails] How to change the column name of the table
[Rails] How to get the contents of strong parameters
Summary of how to implement default arguments in Java
Rails learning How to implement search function using ActiveModel
[Rails] How to display an image in the view
Try to implement tagging function using rails and js
[Rails] How to display the weather forecast of the registered address in Japanese using OpenWeatherMap
[rails] How to post images
[Rails] How to use enum
[Rails] How to install devise
[Rails] How to use enum
How to write java comments
How to read rails routes
[Java] How to display Wingdings
How to use rails join
How to terminate rails server
How to write Rails validation
How to write Rails seed
[Rails] How to use validation
[Rails] How to disable turbolinks
[Rails] How to use authenticate_user!
[Rails] How to use "kaminari"
[Rails] How to make seed
How to write Rails routing
[Rails] How to install simple_calendar
[Java] How to implement multithreading
[Rails] How to install reCAPTCHA
[Rails] How to use Scope
How to display the select field of time_select every 30 minutes
How to display a graph in Ruby on Rails (LazyHighChart)
Let's summarize how to extend the expiration date of Rails
[Rails 5] How to use gem gon ~ How to pass variables from Rails to JS ~
[Rails] I tried to implement "Like function" using rails and js
[Rails] How to display information stored in the database in view