[第二十只羊] 迷雾森林舞会XIV 进房间聊天 hotwire + stimulus 起步走

天亮了 昨晚是平安夜

关於迷雾森林故事

习惯

洛神: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

天黑请闭眼


<<:  【D21】修改食谱#2:根据市价,模拟小台改价

>>:  RISC-V: ECALL/EBREAK 指令

[自学笔记]LINQ资料查询技术

LINQ(发音为link 但很多人都说LIN Q) 最大的特质是具备资料查询的能力以及和 VB、C#...

33岁转职者的前端笔记-DAY 11 一些网页切版技巧的小笔记-Part 2

承上篇 CSS 小笔记 渐变(淡入淡出):CSS属性 秒数 速度曲线 范例code如下: .g-10...

Day 9 合格了吗?

启动引擎,把车开回夜晚的车阵中,虽然可能只是处在车流中,默默无名的行驶着,或者快速的疾驶着,又或者处...

Day 26 | SQLite资料库(一)

Android系统内建SQLite供开发者使用,通常用於存放使用者或系统相关的资料,如果资料除了本地...

[Day 19] 针对网页的单元测试(五)

再写登入的验证及功能 今天我们要来做登入的判断跟动作, 我们在HomeController.php引...