# 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 檔案最底端