---
# System prepended metadata

title: 0423 Rails 與 JS、現有專案新增工作表與關聯
tags: [Ruby / Rails]

---

# 0423 Rails 與 JS、現有專案新增工作表與關聯
###### tags: `Ruby / Rails`
## 在專案中寫 JS
- 回顧：[webpack](https://hackmd.io/zPpaNDW6SyW2Y8tFGQVSow?view#webpack-gt-JavaScript-%E5%B7%A5%E5%85%B7)
- [foreman 套件](https://rubygems.org/gems/foreman/versions/0.82.0?locale=zh-TW)
  - 把所有需要使用的套件都同時開起來
  - 問題：使用 debugger 時會有不知道關哪個套件的問題
  - 安裝好之後放進 Gemfile 的 group :develop :test
  - 執行 `$ foreman s`
  - 預設在 Procfile 裡面寫上要跑哪些服務 
  - 或是建立 procfile.dev 或其他檔案
    - 執行指令換成 `$ foreman s -f 建立檔案的名字`
```ruby=
a: bin/rails server -p 3000
b: bin/webpack-dev-server
  # a 跟 b 可以換成套件的名稱
  # 例：
web: bin/rails server -p 3000
pack: bin/webpack-dev-server
```
## uchef 新增評論系統
### 建立 model
#### 設計欄位
- Comment Model
  - comment: text
  - restaurant: belongs_to
  - user: belongs_to
  - deleted_at: datetime:index （假刪除）

* 階層回覆？
  - self references 自我參照 (關鍵字：rails model self references)
  - Comment Model
    - comment: text
    - restaurant: belongs_to
    - user: belongs_to
    - comment: belongs_to (針對留言再留言)
  - 加好友？
    - User has_many :users
  - Comment Model
    - comment: text
    - restaurant: belongs_to
    - user: belongs_to
    - reply_to: interger default: 0 => 如果是 0 就是最上層的回應
#### 用 rails 內建產生器產出
- `$ rails g model Comment content:text restaurant:belongs_to user:belongs_to deleted_at:datetime:index`
#### 放進資料庫
- `$ rails db:migrate`
#### 修改 controller
- 在 controller 裡面建立新的 comment 物件

controllers/restaurants_controller.rb
```ruby=
  def show
    @restaurant = Restaurant.find(params[:id])
    @comment = Comment.new
  end
```
- 在 model 裡新增關聯

user model
```ruby=
class User < ApplicationRecord
  has_many :comments
```
restaurant model
```ruby=
class Restaurant < ApplicationRecord
  has_many :comments
```
- 資料表關聯時，呼叫指令輸入的順序代表從哪個角度出發
- 所以最好把 controller 改寫成
controllers/restaurants_controller.rb
```ruby=
  def show
    @restaurant = Restaurant.find(params[:id])
    @comment = @restaurant.comments.new
  end
```

---
### 修改 routes
- 思考 routes 的寫法
  - 適當的控制網址的長度
  - POST /restaurants/restaurant_id/comments 
    - create
    - 如果沒有 restaurant 的話可能會有問題，找不到關聯
  - GET /restaurants/restaurant_id/comments
    - index
  - GET /rsetaurants/restaurant_id/comments/餐廳的第幾個留言
    - show => 很像不需要
    - 可以改成 GET /comments/comment_id
  - DELETE /restaurants/restaurant_id/comments/餐廳的第幾個留言
    - destroy
    - 刪除很像只要指定要刪除哪一個 comment 就好
    - DELETE /comments/comment_id

config/routes.rb
```ruby=
Rails.application.routes.draw do

  resources :restaurants do
    resources :comments, only: [:index, :new, :create]
  end
  resources :comments, except: [:index, :new, :create]
```
- 甚至可以
```ruby=
Rails.application.routes.draw do

  resources :restaurants do
    resources :comments, only: [:index, :create] 
    # 讓 POST restaurants#show 變成 comments new
    # 讓 GET restaurants#show 也顯示 comments#index
  end
  resources :comments, except: [:index, :new, :create]
```
- 或是讓它變膚淺 (shallow)
[官方文件說明](https://guides.rubyonrails.org/routing.html#shallow-nesting)
```ruby=
Rails.application.routes.draw do

  resources :restaurants do
    resources :comments, shallow: true
  end
  
  # 等於下面寫法
  # resources :restaurants do
  #   resources :comments, only: [:index, :new, :create]
  # end
  # resources :comments, except: [:index, :new, :create]
```
- 還可以把多餘的拔掉
```ruby=
Rails.application.routes.draw do
  
  resources :restaurants do
    resources :comments, shallow: true,
                         except: [:index, :new]
  end
```
- 多思考一下：
```ruby=
Rails.application.routes.draw do
  
  resources :restaurants do
    resources :comments, shallow: true,
                         only: [:create, :destroy]
  end
```
- 其實有套件可以直接處理歷史紀錄
  - [paper trail](https://github.com/paper-trail-gem/paper_trail)

---
### 建立 view
views/restaurants/show.html.erb
- 直覺的作法
```ruby=
<h1><%= @restaurant.title %></h1>

<h3>留言</h3>
# POST /comments <= 想要做到
<%= form_for(@comment) do |form| %>
<% end %>

# 實際會去找 /restaurant
```
- 所以要改成
```ruby=
<h1><%= @restaurant.title %></h1>

<h3>留言</h3>
<%= form_for(@comment, url: restaurant_comments_path(@restaurant)) do |form| %>
<% end %>
# 加 url 讓送出的地方變成新的位置
```
- 架出完整表單
```ruby=
<h1><%= @restaurant.title %></h1>

<h3>留言</h3>
<%= form_for(@comment, url: restaurant_comments_path(@restaurant)) do |form| %>
  <div class="field">
    <%= form.label :content, '留言內容' %>
    <%= form.text_area :content %>
  </div>
  <%= form.submit %>
<% end %>
```

---
### 建立 controller 跟 action
/app/controllers/comments_controller.rb
```ruby=
class CommentsController < ApplicationController
  before_action :check_user!

  def create
    @restaurant = Restaurant.find(params[:restaurant_id])  # 找到那間餐廳
    @comment = @restaurant.comments.new(comment_params)  # 建立出新留言
    @comment.user = current_user  # 找到那個留言對應的使用者
    
    if @comment.save
      redirect_to @restaurant  # 餐廳的show
      # redirect_to restaurant_path(@restaurant)  # 跟上面那行一樣
    else
      # ..
    end 
  end

  def destroy
  end

  private 
  def comment_params
    params.require(:comment).permit(:content)  # 清洗想要的資料
  end
end
```
- 因為目前使用者 id 是從 session 那邊過來的，所以可以相信
  - 可把程式碼修改成：
```ruby=
class CommentsController < ApplicationController
  before_action :check_user!

  def create
    @restaurant = Restaurant.find(params[:restaurant_id])
    @comment = @restaurant.comments.new(comment_params)
    # @comment.user = current_user

    if @comment.save
      redirect_to restaurant_path(@restaurant)  # 餐廳的 show
    else
      # ..
    end
  end

  def destroy
  end

  private
  def comment_params
    permited_params = params.require(:comment).permit(:content)
    permited_params[:user] = current_user

    return permited_params
  end
end

```
- 在物件中加一組 key value 的概念
```ruby=
a = {a: 1, b: 2}
a.merge({c: 3})
p a # {a: 1, b: 2, c: 3}

{a: a, b: b}.merge({c: c})
#回傳值{a: a, b: b, c: c}
```
- 把程式碼改寫（直接從源頭的 params 下手塞東西進去）
```ruby=
class CommentsController < ApplicationController
  before_action :check_user!

  def create
    @restaurant = Restaurant.find(params[:restaurant_id])
    @comment = @restaurant.comments.new(comment_params)
    # @comment.user = current_user

    if @comment.save
      redirect_to restaurant_path(@restaurant)  # 餐廳的 show
    else
      render 'restaurants/show'  # 檢查 view 裡面的變數 => 剛好都一樣
        # 變數名稱一樣其實是刻意營造的巧合（@restaurant, @comment）
    end
  end

  def destroy
  end

  private
  def comment_params
    params.require(:comment).permit(:content)
                            .merge({user: current_user})  # 不是使用者給的
    # 還可以把 ip_address 加進去
    # permited_params = params.require(:comment).permit(:content)
    # permited_params[:user] = current_user

    # return permited_params
  end
end

```
### 再次修改 view
- 把全部的 comment 加上去

/views/restaurants/show.html.erb
```ruby=
<h1><%= @restaurant.title %></h1>

<h3>留言</h3>
<%= form_for(@comment, url: restaurant_comments_path(@restaurant)) do |form| %>
  <div class="field">
    <%= form.label :content, '留言內容' %>
    <%= form.text_area :content %>
  </div>
  <%= form.submit %>
<% end %>

<% @comments.each do |comment| %>
  <li><%= comment.content %></li>
<% end %>
```
- 因為需要 @comments 所以修改 controller

/app/controllers/restaurant_controller.rb
```ruby=
class RestaurantsController < ApplicationController

  def show
    @restaurant = Restaurant.find(params[:id])
    @comment = @restaurant.comments.new
    @comments = @restaurant.comments.order(id: :desc)  # order 讓最新的在前面
  end
```
- ==思考!== 如果要加檢舉功能怎麼辦?
  - ugc(user generated content) 功能通常要加

* order + SQL 語法
  * 用字串只是使用 SQL 去執行
  * 在 SQL 中大小寫一樣效果
  * 在實務上還是都用 ruby/rails 語言比較好
```ruby=
    Comment.where(user_id: 2)
    Comment.where('user_id = 2')    # 效果相同
    
    @comments = @restaurant.comments.order(id: :desc)    # 但寫這個比較好啦
    @comments = @restaurant.comments.order('id desc')
    @comments = @restaurant.comments.order('id DESC')    # 效果相同
```
## 修改資料傳送方式
### remote 模式


[Ajax](https://ihower.tw/rails/ajax.html)
```ruby=
<%= form_for(@comment, url: restaurant_comments_path(@restaurant), remote: true)
# <%= form_for(@comment, url: restaurant_comments_path(@restaurant)) do |form| %>
```
- 在瀏覽器渲染時：form 的屬性會增加 data-remote: true
  - 如果沒增加這個屬性 => 使用基本的 post 方法傳出 request
  - 有了之後 => 網頁使用 xhr 方法傳出 request => 再用 js 去接它

controllers/comments.rb
```ruby=
def create
    @restaurant = Restaurant.find(params[:restaurant_id])
    @comment = @restaurant.comments.new(comment_params)
    # @comment.user = current_user

    if @comment.save
      # redirect_to restaurant_path(@restaurant)  => 把這行刪掉
    else
      render 'restaurants/show'
    end
  end
```
- 因為 @comment.save 後面沒東西所以會去找 create 的同名 view
  - ==給他一個view!!!!==

---
### 在 js 裡面塞 ruby 原始碼
- 把本來的 html.erb 換成 js.erb

/views/comments/create.js.erb
```ruby=
alert('你輸入了：<%= @comment.content %>')  => 會跳出一個小視窗
```
- 所以把它認真換掉：
```ruby=
var area = document.querySelector('#comment_area')
var li = document.createElement('li')
li.textContent = '<%= @comment.content %>'
area.prepend(li)
document.querySelector('#comment_content').value = ''
```
- 把 restaurant 的 show 改一下

```ruby=
<h1><%= @restaurant.title %></h1>

<h3>留言</h3>
<%= form_for(@comment, url: restaurant_comments_path(@restaurant), 
    remote: true) do |form| %>
  <div class="field">
    <%= form.label :content, '留言內容' %>
    <%= form.text_area :content %>
  </div>
  <%= form.submit %>
<% end %>

<ul id="comment_area">   ===> 加上 id 跟 ul, li 
<% @comments.each do |comment| %>
  <li><%= comment.content %></li>
<% end %>
</ul>
```
- 資料庫已經寫進去了但是沒有重新整理網頁
  - 後端寫入資料之後讀取 js
  - 前端收到 js 檔之後就直接在網頁上渲染（演）給使用者看
  - 但是其他使用者的頁面不會同時更新
  - rails 的 ujs 才會把後端來的 js 渲染在前端


## 在 rails 專案裡使用 npm/yarn
### 以 fontawesome 為例
- [官網介紹](https://fontawesome.com/how-to-use/on-the-web/setup/using-package-managers)
- 到 rails 專案資料夾底下執行 yarn...
- 下完指令會安裝相關套件到 
rails/ujs/lib/assets/package.json

* node_modules/@fortawesome/fontawsome-free/pachage.json 注意裡面的 main

SVG JavaScript core

app/javascript/packs/application.js

只用一個部分
webpacker.yml 裡面有寫位置
[webpacker 筆記](https://hackmd.io/zPpaNDW6SyW2Y8tFGQVSow?both#webpack-gt-JavaScript-%E5%B7%A5%E5%85%B7)
* app / javascript裡新增一個icon資料夾，再新增fontawesome的js檔
```ruby=
import { library, dom } from '@fortawesome/fontawesome-svg-core'
import { faUserAstronaut } from '@fortawesome/free-solid-svg-icons'
# 把套件 import 進來

library.add(faUserAstronaut)  # 把套件加進 library

dom.watch()  # 掃描整個畫面，有用到 library 的都渲染

# 注意 icon 的大小寫
```
yarn 安裝套件 => 跑進專案底下的 node_module 資料夾底下 => 放物件 => 去 application.js 裡 import 套件
或是在 js 資料夾底下創新的資料夾跟檔案，再去application.js import

下載別人的專案下來後：
`$ yarn install --check-files`

---
### 以 tailwind 為例
- 用法：在 html tag 的 class 裡面加東西
- [官網介紹](https://tailwindcss.com/)
- [玩玩看 tailwind](https://play.tailwindcss.com/)

---
## 題外話
### 使用上層方法 
- super 尋找上層的同名方法，前提是上層有同名方法
- alias不是方法，僅是特殊語法
  - [文件說明](https://ruby-doc.org/core-3.0.0/doc/syntax/miscellaneous_rdoc.html)
* 範例一
```ruby=
class Animal
  def destroy
    puts '真delete'
  end
end

class Cat < Animal
  alias :super_destroy :destroy
    # 做一個別名，指向後方 (destroy)
    # 因為 Cat 的 destroy 還沒被執行，所以會找上層 (Animal) 的 destroy
    # 要放在定義新的同名方法前才會有效
    
  def destroy
      # update deleted_at -> now
    puts '假刪除'
  end

  def really_destroy  
    super_destroy
      # self.class.superclass.instance_method(:destroy).bind(self).call
        # 或是直接使用這行，不寫 alias
        # 自己的物件(self.class)，的上層class(superclass)，
        # 再用(instance_method)呼喚他（上層）的實體方法，
        # 再去綁到自己身上(bind)，再呼叫(call)
  end
end

kitty.Cat
kitty.destroy
kitty.really_destroy
```
- 範例二
```ruby=
class Integer
  alias :old_plus :+
# alias不是方法，僅是特殊語法
  
  def +(n)
    puts "hi"
  end
  
  def add(n)
    self.old_plus(n)
  end
end

puts 1.add(2)
```
- 範例三
```ruby=
alias :log :puts

log(123)   # 123
```
### 製作會員系統的套件 devise
- [GitHub 連結](https://github.com/heartcombo/devise)
- 短短幾行字讓你做完會員登入系統


### meta programming 元編程
- 寫一段 code 去修改原本既定的行為
```ruby=
class Cat
  def method_missing(method_name, args)
    puts "yaya"
  end
  
  # def method_missing(method_name, args)
    # super
  # end
end

kitty = Cat.new
kitty.find_by_id(1) # 舊式的 find_by 寫法
```
- ruby 找方法的順序
  - 找不到方法 => 往上找 => 到底也找不到 => 回頭重新再找特別的方法 => 再依序往上找看有沒有特別的方法
- ruby 中每一個錯誤訊息也是這樣來的
  - 所以可以依照這個機制更改上層的反應

```ruby=
class Cat
  def method_missing(method_name, *args)
    if method_name.to_s.start_with?('find_by')
      puts "you just called #{method_name} with #{args}"
    else
      super
    end
  end
end

kitty = Cat.new
kitty.find_by_id(1)
```
- [過陣子可以找來看的書（最好看原文的）](https://pragprog.com/titles/ppmetr2/metaprogramming-ruby-2/)
- JS 的 metaprogramming：
  - reflect
  - proxy (vue.js相關)

---
### 套件安裝指令的不同
- `$ gem install xxx`
  - 在系統裡面安裝某個套件
- `$ bundle install`
  - 會產生 Gemfile 跟 Gemfile.lock
  - 在專案裡使用特別的套件版本
  - 根據 Gemfile 裡的東西去執行 gem install 然後建立 Gemfile.lock
    - lock 檔：描述目前專案使用的套件（包含版本名 ?.?.?）

---
### JS 沒寫好讓連結壞掉
rails 裡面有一支 JS 去掃描攔截超連結做特殊處理，
frontend/packs/application.js 如果沒寫好會讓檔案壞掉不能正常編譯

---
### SSR vs. SPA
- SSR = Server Side Render
  - 從伺服器撈資料之後渲染成頁面
- SPA = Single Page Application
  - 通過動態重寫當前頁面來與使用者互動，而非傳統的從伺服器重新載入整個新頁面。
  - [WIKI 說明](https://zh.wikipedia.org/wiki/%E5%8D%95%E9%A1%B5%E5%BA%94%E7%94%A8)

---
### form_for vs. form_tag vs. form_with
- form_for(@model)
  - 預設 remote: false
- form_tag(....)
  - 單純產出 <form> </form>
- form_with(model: @model)
  - 結合前兩種用法
  - 如果 @model 是 nil 的話不會出錯，會跳出單純的 form tag
  - 預設 remote: true
  - 後面其他參數的加法跟 form_for 一樣
    - url: ...
    - method: ...
  - 可能會看不懂 view 裡面的 js.erb

==提醒龍哥補充 form_with==

---
### npm & yarn
* rails 內建使用 yarn

- 裝好 yarn 
- 執行 `$ yarn init` 初始化
  - 會在指定的資料夾產生
    - `yarn.lock`
    - `node-modules`
      - 裡面就會放很多指令
    - `package.json`
- 可以用 npm 資料庫
  - npm: `$ npm i 套件名稱 --save`
  - yarn: `$ yarn add 套件名稱`
- 只要加在開發者模式
  - `$ yarn add 套件名稱 -D`
  - package.json 底下會被丟進 devDependencies => 上線之後不會使用
- 下完指令之後會產生很多相依性相關的檔案
  - 位置：node-modules

---
### bundle add
- bundle add 套件名稱
  - 自動安裝最新版本
  - 自動放在 Gemfile 檔案最底端