railsのAction Cableでチャットアプリを作る

##作成したサンプルアプリ

https://which-omoide-in-my-head.herokuapp.com/

##環境
Ruby 2.6.3
Rails 5.2.3

##プロジェクトの作成と下準備
Herokuへデプロイする前提なので、DBMSにPostgresqlを指定。
また、coffee Scriptを使いたく無いため、skipしてます。

```command
$ rails new chat_app -d postgresql ---coffee
```

jqueryとBootstrap(バージョン4)をyarnを使って導入

```
$ yarn add jquery
$ yarn add bootstrap
```

マニフェストファイルの修正

```command:/app/assets/javascripts/application.js
//= require rails-ujs
//= require jquery
//= require jquery/dist/jquery.js
//= require bootstrap/dist/js/bootstrap.min
//= require activestorage
//= require turbolinks
//= require_tree .
```
```command:/app/assets/stylesheets/application.css
*= require bootstrap/dist/css/bootstrap.min
*= require_tree .
*= require_self
```

```command:DBの作成
$ rails db:create
```

```command:チャンネルの作成
$ rails g channel Room speak
```

##最終的な各ファイルのコードと構成

```ruby:model/music.rb
class Music < ApplicationRecord
  has_many :posts, dependent: :destroy
end
```

```ruby:model/post.rb
class Post < ApplicationRecord
  belongs_to :music

  validates :content, presence: true

  def created_time
    created_at.strftime("%Y年%m月%d日　%-H時%-M分")
  end
end
```

```ruby:controllers/musics_controller.rb
class MusicsController < ApplicationController
  def index
    @musics = Music.all
  end

  def new
    @music = Music.new
  end

  def create
    @music = Music.new(set_params)
    if @music.save!
      redirect_to musics_path
    else
      render 'new'
    end
  end

  def show
    @title = params[:title]
    @url = params[:url]

    @music = Music.find(params[:id])
    @posts = @music.posts.order(created_at: :desc).page(params[:page]).per(5)
    @post = @music.posts.build
    respond_to do |format|
      format.js{render :show} 
      format.html
    end    
  end

  def destroy
    @music = Music.find(params[:id])

  end

  private

  def set_params
    params.require(:music).permit(:title, :url, :image)
  end

end
```

```html:views/musics/index.html.erb
<h1 class="text-center">どの<span>OMOIDE IN IN MY HEAD</span>について語る？</h1>

<div class="music-index">
  <div class="row">
    <% @musics.each do |music| %>
      <div class="col-sm-6 col-md-4 music-col">
        <%= link_to music_path(music, title: music.title, url: music.url) do %>
          <div>
            <%= image_tag(music.image) %>
          </div>
          <div>
            <%= music.title %>
          </div>
          <hr>
        <% end %>
      </div>
    <% end %>
  </div>
</div>
```
```html:views/musics/new.html.erb
<h1 class="text-center">動画を登録</h1>

<div class="row justify-content-center">
  <div class="col-md-8 col-xs-8">
    <%= form_with(model: @music, local:true) do |f| %>
      <div class="form-group">
        <%= f.label :title, "タイトル" %>
        <%= f.text_field :title, class: "form-control" %>
      </div>
      <div class="form-group">
        <%= f.label :url, "動画のURL(embedの記載がある埋め込み用のURLを記載)" %>
        <%= f.text_field :url, class: "form-control" %>
      </div>
      <div class="form-group">
        <%= f.label :image, "画像のURL" %>
        <%= f.text_field :image, class: "form-control" %>
      </div>
      <div class="form-group text-center">
        <%= f.submit "登録", class: "btn btn-primary" %>
      </div>
    <% end %>
  </div>
</div>
```

```html:views/musics/show.html.erb
<h1 class="text-center">この動画どう？</h1>

<div class="text-center">
  <iframe class="embed-responsive-item" id='iframe' width="560" height="315" 
    src="<%= @url %>"
    frameborder="0" 
    allowfullscreen>
  </iframe>
</div>

<form>
  <div class="form-group">
    <input type="text" class="form-control" id="chat-input" placeholder="何を思う？">
  </div>
  <div class="text-right">
    <button class="btn btn-primary btn-lg" id="chat-button">送信</button>
  </div>
</form>

<div hidden>
  <div id="music-id"><%= @music.id %></div>
</div>

<div id="posts" class="posts-block">
  <ul class="list-group">
    <%= render @posts %>
  </ul>
</div>

<div class="text-center loading-btn">
  <%= link_to_next_page @posts, 'もっと見る', remote: true, id: 'more_link' %>
</div>
```

```js:views/musics/show.js.erb
<% @posts.each do |post| %>
  $("#posts").append("<%= escape_javascript(render partial: 'posts/post', locals: { post: post }) %>");
<% end %>
$("#more_link").replaceWith("<%= escape_javascript(link_to_next_page(@posts, 'もっと見る', remote: true, id: 'more_link')) %>");
```

```html:views/posts/_post.html.erb
<% cache post do %>
  <li class="list-group-item">
    <h4><%= post.content %></h4>
    <div class="post-time">
      <%= post.created_time %>
    </div> 
  </li>
<% end %>
```
```ruby:routes.rb
Rails.application.routes.draw do
  root to: 'musics#index'
  
  resources :musics do
    resources :posts
  end

  mount ActionCable.server => '/cable'
end
```

```ruby:channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    post = Post.create!(content: data['content'], music_id: data['music_id'])
    template = ApplicationController.renderer.render(partial: 'posts/post', locals: {post: post})
    ActionCable.server.broadcast 'room_channel' ,template
  end
end
```

```js:assets/javascripts/channels/room.js
App.room = App.cable.subscriptions.create("RoomChannel", {
  connected: function() {
  },

  disconnected: function() {
  },

  received: function(post) {
    const posts = document.getElementById('posts')
    posts.insertAdjacentHTML('afterbegin', post);
    // posts.innerHTML += post

  },

  speak: function(content, music_id) {
    return this.perform('speak', {content: content, music_id: music_id});
  }
});

document.addEventListener('turbolinks:load', function(){
  const input = document.getElementById('chat-input');
  const button = document.getElementById('chat-button');
  button.addEventListener('click', function(e){
    e.preventDefault();
    const content = input.value;
    const music = document.getElementById('music-id')
    const music_id = music.textContent;
    App.room.speak(content, music_id);
    input.value = '';
  })
});
```

## Herokuデプロイ時の設定

```ruby:producton.rb(JSでconstを使ったため)
# Compress JavaScripts and CSS.
config.assets.js_compressor = Uglifier.new(harmony: true)
```

```ruby:config/environment/production.rb
 config.action_cable.allowed_request_origins = [ /http:\/\/.*/ ]
```

```ruby:config/cable.yml
production:
  # adapter: redis
  # url: redis://localhost:6379/1
  # channel_prefix: ac_test_production
  adapter: async
```

```command
$ heroku buildpacks:add --index 1 heroku/nodejs
$ heroku buildpacks:add --index 2 heroku/ruby
```
あとは、

```command
$ heroku run rails db:migrate
```
また、validationの設定は、個々にModelで行う。


## 参考サイト
https://qiita.com/Hijiri-K/items/c3774c72a2cb68e1a720

https://freelance-in-six-month.com/tech-blog/rails-turbolinks-js-notworking/