iT邦幫忙

2021 iThome 鐵人賽

DAY 20
0
Software Development

透過迷霧,看破一切~~ZOOPARTY 動物園派對桌遊設計系列 第 20

[第二十隻羊] 迷霧森林舞會XIV 進房間聊天 hotwire + stimulus 起步走

  • 分享至 

  • twitterImage
  •  

天亮了 昨晚是平安夜

關於迷霧森林故事

習慣

洛神:2號玩家請繼續發言

5號:我其實第一輪第二輪都覺得2號蠻好的,因為有點分不清楚預言家給7號容忍度是好人會想的思考量,所以我覺得2號是好的,2號敢保8號我覺得應該蠻好的,9號當然是比7好飽滿很多,但是作為好人我還是有點分不出來,所以我比較懷疑10號,不確定他有沒有可能做成一張倒鉤是因為前面我是第一個點出6號這張牌,10號,再來就是馬上可以刀中神刀刀中神,感覺得3跟8號比較有可能,因為在我視角已經把2號放在偏好了,然後3號沒有打2打的那麼嚴重所以又有一點像好人,可能會想從8 10出,著重聽8 10

待續..

動物園派對

今天我們透過hotwire來整合聊天
首先建立 messages 的 controller model 跟 views
並把他跟房間做has many的關聯

/2021100416xxxx_create_messages.rb
class CreateMessages < ActiveRecord::Migration[6.1]
  def change
    create_table :messages do |t|
      t.references :room, null: false, foreign_key: true
      t.string :user_id
      t.string :nickname
      t.text :content

      t.timestamps
    end
  end
end

把db整合進我們的資料庫

$ bundle exec rake db:migrate
class MessagesController < ApplicationController
  before_action :set_room, only: %i[ new create ]
  before_action :set_message, only: %i[ update edit destroy show ]

  def new
    @message = @room.messages.new
  end

  def create
    @message = @room.messages.new(message_params)
    @message.user_id = current_user.id
    @message.nickname = current_user.name

    if @message.save
      render turbo_stream: turbo_stream.append(:messages, @message)
    else
      render 'new', layout: false, status: :unprocessable_entity
    end
  end

  private
  def set_room
    @room = Room.find(params[:room_id])
  end

  def set_message
    @message = Message.find(params[:id])
  end

  def message_params
    params.require(:message).permit(:content, :room_id, :nickname, :user_id)
  end
end
/rooms_controller

  def show
    assign_seat_to_user(@room)
    @messages = @room.messages
                     .order(:created_at)

    @new_message = Message.new(room: @room)
  end

加上broadcasts讓hotwire內建的ActionCable
也就是要把訊息廣播到房間裡面

/message.rb
class Message < ApplicationRecord
  belongs_to :room
  broadcasts_to :room

  validates  :room, :content, presence: true

  def edited?
    created_at != updated_at
  end
end

room這邊也需要加上broadcasts
並與messages是一對多的關聯

/room.rb

class Room < ApplicationRecord
  resourcify
  has_many :seats, dependent: :destroy
  has_many :messages, dependent: :destroy
  broadcasts
end

在room show page中加入訊息的顯示

/show.erb
<div id="messages">
  <%= render @room.messages %>
</div>

<%= turbo_frame_tag "new_message", src: new_room_message_path(@room), target: "_top" %>

補上messages的view端,所需要的分別是訊息顯示partial與form表單

/views/messages/_message.html.erb

<%= turbo_frame_tag dom_id(message), class: 'pb-1 px-3',
                    data: { controller: 'message', action: 'mouseout->message#toggleActions mouseover->message#toggleActions', 'message-author-id-value': message.user_id } do %>
  <div class='row row-cols-auto gx-2'>
    <div class='col fw-bold'>
      <%= message.nickname %>
    </div>
    <div class='col'>
      <%= local_time message.created_at, format: :short, class: 'fw-light fs-7' %>
    </div>
    <% if message.edited? %>
      <div class='col'>
        <span class='fw-light fs-7'>edited <%= local_time message.updated_at, format: :short %></span>
      </div>
    <% end %>
  </div>

  <div class='formatted-content'>
    <%= message.content  %>
  </div>
<% end %>
/views/messages/form.html.erb

<%= form_with model: message.persisted? ? message : [message.room, message], data: { controller: 'form', action: 'turbo:submit-end->form#resetForm' } do |f| %>
  <%= f.text_field :content, class: 'form-control', required: true, autocomplete: 'off', autofocus: true, placeholder: "Message ##{message.room.name}" %>

  <% if message.persisted? %>
    <div class='pt-2'>
      <%= link_to 'Cancel', @message, class: 'btn btn-sm btn-outline-secondary' %>
      <%= f.submit 'Save changes', class: 'btn btn-sm btn-primary'%>
    <div>
  <% end %>
<% end %>
/views/messages/new.html.erb

<%= turbo_frame_tag @message do %>
  <%= render 'messages/form', message: @message %>
<% end %>

在javascript下controller資料夾新增清空輸入框的js

/javascipts/controllers/reset_form_controller.js
import { Controller } from "stimulus"

export default class extends Controller {
  reset() {
    this.element.reset()
  }
}

/views/messages/edit.html.erb

<%= turbo_frame_tag dom_id(@message) do %>
  <div data-controller='scroll-into-view' class='py-2'>
    <%= render 'form', message: @message %>
  </div>
<% end %>
/views/messages/show.html.erb

<%= render @message %>

這樣我們就算是把hotwire的turbo_frame設定好了
再來我們研究一點前幾天沒碰到的turbo_stimulus
先在vite的 /javascripts/entrypoints/application.js 把檔案import進來

/javascripts/entrypoints/application.js

import Rails from '@rails/ujs'
import '@hotwired/turbo-rails'
import '../controllers'
import '../channels'

Rails.start()

在controllers的資料夾下
先處理message_controller.js
這樣我們就算是把hotwire的turbo_frame設定好了
再來我們研究一點前幾天沒碰到的turbo_stimulus
先在vite的 /javascripts/entrypoints 把檔案import進來

/javascripts/controllers/message_controller.js

import { Controller } from "@hotwired/stimulus"
import { currentUserId } from '../helpers/auth'

export default class extends Controller {
  static targets = ['actions']
  static values = {
    userId: String,
  }

  connect() {
    if (document.querySelectorAll(`#${this.element.id}`).length > 1) {
      this.element.remove()
      return
    }

    this.element.scrollIntoView({ block: 'nearest' })
  }

  toggleActions() {
    if (this.hasActionsTarget && this.authorIdValue === currentUserId()) {
      this.actionsTarget.classList.toggle('invisible')
    }
  }
}

/javascripts/controllers/message_list_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.scrollToBottom()
  }

  scrollToBottom() {
    this.element.scrollTop = this.element.scrollHeight
  }
}

/javascripts/controllers/scroll_into_view_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.scrollIntoView({ block: 'nearest' })
  }
}

在這邊設定訊息不能空白

/javascripts/controllers/form_controller.js

import { Controller } from "@hotwired/stimulus"

const ERROR_BLANK = "Can't be blank"

const validatePresence = (input) => {
  if (input.hasAttribute('required')) {
    if (input.value.trim() === '') {
      input.setCustomValidity(ERROR_BLANK)
      return false
    } else {
      if (input.validity.customError) input.setCustomValidity('')
      return true
    }
  } else {
    return true
  }
}

export default class extends Controller {
  connect() {
    this.element.querySelectorAll('input[required]').forEach((input) => {
      input.addEventListener('input', this.validateInput)
    })
  }

  disconnect() {
    this.element.querySelectorAll('input[required]').forEach((input) => {
      input.removeEventListener('input', this.validateInput)
    })
  }

  validateInput() {
    validatePresence(this) // && validateSomethingElse()
  }

  resetForm() {
    this.element.reset()
  }
}
/javascripts/helpers/auth.js

const DATA_ATTR_NAME = 'data-current-user-id'

export const currentUserId = () => {
  return (
    document
      .querySelector(`[${DATA_ATTR_NAME}]`)
      ?.getAttribute(DATA_ATTR_NAME) || null
  )
}

最後再route設定

/routes.rb
  resources :rooms do
    resources :messages
  end

這麼一來訊息就可以出現在房間內囉
https://ithelp.ithome.com.tw/upload/images/20211006/20131155xsqtxLZDiW.png

下面這幾個是特別找的 hotwire 完整專案 我覺得現在資料蠻散的
推薦剛接觸hotwire的朋友可以先從這幾個完整專案開始接觸唷

reference:

  1. hotwire-rails-demo-chat
  2. hotwire-chat
  3. Tickerizer
  4. hotwire-twitter-clone
  5. Hotwire:Reactive Rails with no JavaScript?

阿虎每日選幣

$loom 可以等過 $0.118

天黑請閉眼


上一篇
[第十九隻羊] 迷霧森林舞會XIII 設定form 綁定dom 同步房間(單押)
下一篇
[第二十一隻羊] 迷霧森林舞會XV 建立村莊 遊戲角色設定
系列文
透過迷霧,看破一切~~ZOOPARTY 動物園派對桌遊設計30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言