---
title: Astro課程 0807.0810 - Rails (Day6.Day7)
tags: astro, rails
---
[第一篇筆記:Astro課程 0723-0731 - Rails (Day1-Day4)](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view)
[第二篇筆記:Astro課程 0803 - Rails (Day5)](https://hackmd.io/uSW5LI6aS1GyCsEMtGBnCg?view)
# 幫文章加上使用者
### 想法: 先建migration,在posts加user欄位
為了把user id 指定給posts
```
rails g migration add_user_to_post
```
`null: false` 先空著(原本應該文章有作者才合理)
```
class AddUserToPost < ActiveRecord::Migration[5.2]
def change
add_reference :posts, :user, null: false
end
end
```
編按: 08010的migrate我是這樣寫(預設之前文章都是user id為 6的作者)
```
class AddUserToPost < ActiveRecord::Migration[6.0]
def change
add_reference :posts, :user, null: false, foreign_key: true, default: 6
end
end
```
### 修改model
user.rb
has_many :posts
post.rb
belongs_to :user
### 修改controller
- 權限控管: 登入才可發文
有兩種方式可以指定post給user
```
# 用看板的角度新增文章,然後再指定給current_user
@post = @board.posts.new(post_params)
@post.user = current_user
# 從current_user建posts,再指定看板
# @post = current_user.posts.new(post_params)
# @post.board = @board
```
另一種:直接帶入user
比對虛擬欄位: 在post_controller清洗參數時,把application controller定義好的method `current_user`加進去
因為model已經建了`post belongs_to :user`
```
private
def post_params
params.require(:post)
.permit(:title, :content)
.merge(user: current_user)
# .merge(user_id: current_user.id)
end
```
所以merge完之後指定`current_user`這行就可以刪掉了
```
@post = @board.posts.new(post_params)
#@post.user = current_user
```
## 使用者只能編輯自己的文章
編輯完後送出更新
成功更新的話,回到原來那篇文章
```
def edit
# 用find :找不到文章的話就會出現404
@post = current_user.posts.find(params[:id])
end
def update
@post = current_user.posts.find(params[:id])
if @post.update(post_params)
redirect_to @post, notice: '文章更新成功'
else
render :edit
end
end
```
## 在看板列表,列出所有文章的作者
`post.user.display_name`
如果沒有名字,顯示未知
```
def display_name
account || "未知"
end
```
方法寫在`helper`還是`model`比較好?
- 如果希望物件有這個方法(有直接關聯),寫在`model`上
- 如果泛用的情況,可以寫在helper
```
def display_name
user.nil? ? "未知" : user.account
end
```
post.rb
```
def display_name
if user.nil?
"未知"
else
user.account
end
end
```
## 解決N + 1問題
這個頁面有幾次SQL?

進看板的時候,
- 看板查詢一次
- 版主也查詢
- 每篇文章也都查詢一次
`http://localhost:3004/boards/1`

查詢看板的時候,把user加進去
board controller
```
def show
@posts = @board.posts.includes(:user)
end
```
## 其他使用者相關權限設定功能
0810假日實作:
- 只有版主群才可以修改和刪除看板
user.rb
```
def self.has_this_id?(id)
one?{ |user| user.id == id }
end
```
boards#index 看板列表
```
<% @boards.each do |board|%>
<tr>
<td><%= link_to board.title, board_path(board)%></td>
<%# byebug %>
<td><%= link_to '編輯', edit_board_path(board) if authorized_user_for_board?(board)%></td>
<td><%= link_to '刪除', board_path(board), method: 'delete', data: { confirm: '確認刪除?' } if authorized_user_for_board?(board)%></td>
</tr>
<% end %>
```
boards_helper
```
def authorized_user_for_board?(board)
user_signed_in? && board.users.has_this_id?(current_user.id)
end
```
- 只有文章作者才可以編輯和刪除自己的文章
boards#show 文章列表
```
<% @board.posts.each do |post| %>
<tr>
<td class=""><%= link_to post.title, post_path(post) %></td>
<%# byebug %>
<td><%= link_to "編輯", edit_post_path(post) if authorized_user_for_post?(post.user.id) %></td>
<td><%= link_to "刪除", post_path(post), method: 'delete', data: { confirm: '確認刪除?' } if authorized_user_for_post?(post.user.id) %></td>
</tr>
<% end %>
```
posts_helper.rb
```
def authorized_user_for_post?(user_id)
user_signed_in? && (user_id == current_user.id)
end
```
# javascript和css
[webpack](https://webpack.js.org/)
[webpacker](https://github.com/rails/webpacker)
打包的目的:減少空格的數量、壓縮檔案的大小
Rails 5: assets pipeline (ruby寫的)
Rails 6: 加上了webpacker (js寫的)
有些東西放在public資料夾
不用經過routes就可以取用檔案
### webpack,你去旁邊跑吧!
由於修改JS檔內容可能會造成reload時間過長
所以要另外開一個單獨給webpack的server
1. 另開terminal分頁
執行`bin/webpack-dev/server`以節省開發時間
2. 直接裝 foreman gem,在專案目錄下建立 Procfile
file-content自己寫:
`名稱: bin/rails server -p (port:3000)`
`名稱: bin/webpack-dev-server`
foreman start -f Procfile.dev`
## webpacker gem
幫忙設定webpack的gem
`/assets/javascripts/application.js`
# 新增`我的最愛`看板
```
rails g model FavoriteBoard user:belongs_to board:belongs_to position:integer
```
路徑設計
```
POST -> /boards/2/favorite
member/collection (collection必須帶id)
```
在這個情境下
動詞和路徑沒有絕對的關聯
```
resources :favorites, only: [:index]
resources :boards do
member do
post :favorite
end
resources :posts, shallow: true
end
```
```
<h1>看板 <%= @board.title %>
| <%= link_to "我的最愛", favorite_board_path(@board), method: 'POST' %>
</h1>
```
## 做一個toggle_favorite
board conctroller
```
def favorite
#current_user.favorited_boards << @board
current_user.toggle_favorite_board(@board)
redirect_to favorites_board_path, notice: 'OK!'
end
```
user.rb
```
def toggle_favorite_board(b)
if favorited_boards.exists?(b.id)
favorited_boards.destroy(b)
else
favorited_boards << b
end
end
```
# rails 加上javascript
GET /api/users.json
讓ruby語法產生json
創一個index.html
```
json.array! @boards
```
eg. 可以輕易地做到api改版
```
GET /api/v1/boards.json
GET /api/v2/boards.json
```
# 加上文章推文功能
做個表單,按下送出後不會離開這一頁
```
Comment
- post:belongs_to
- user:belongs_to
- content:string
- ip_address:string
```
```
rails g model Comment post:belongs_to user:belongs_to content ip_address
```
不要使用type關鍵字。[STI](https://api.rubyonrails.org/classes/ActiveRecord/Inheritance.html)
routes
```
/post/5
-> POST
/posts/5/comments
對資源做新增
```
```
resources :boards do
member do
post :favorite
end
resources :posts, shallow: true do
resources :comments, shallow: true, only: [:create]
end
end
```
post#show加一個表單
```
<%= form_with(model: @comment, url: post_comments_path(@post)) do |form| %>
<%= form.text_field :content %>
<%= form.submit "送出" %>
<% end %>
```
接下來的步驟
```
建立comments controller
檢查登入
建立create action
redirect_to 原本的文章
```
comments controller
```
class CommentsController < ApplicationController
before_action :authenticate_user!
def create
@post = Post.find(params[:post_id])
@comment = @post.comments.new(comment_params)
redirect_to @post
end
private
def comment_params
params.require(:comment).permit(:content).merge(user: current_user)
end
end
```
post controller
```
def show
@post = Post.find(params[:id])
@comment = @post.comments.new
# 排序最新留言的在上面
@comments = @post.comments.order(id: :desc)
#@comments = @post.comments
end
```
`@comments`讓board的show.html.erb用
```
<% @comments.each do |comment| %>
<i><%= comment.id %></i>
<i><%= comment.content %></i><br>
<% end %>
```
重點:剛剛的comment controller要記得把`@comment`存起來~
```
def create
@post = Post.find(params[:post_id])
@comment = @post.comments.new(comment_params)
if @comment.save
redirect_to @post
else
redirect_to @post
end
end
```
## 非同步處理
表單畫面是否有重新refresh的差異
```
form with的預設值
使用form_with時,建立時就已經預設了remote: true
有了這個屬性之後,表單會透過 Ajax 提交,而不是瀏覽器平常的提交機制。
如果不要-form-for-跟-form-with-有什麼不同設定Ajax提交,則要另訂 local: true。
form for
要寫記得寫:remote true (預設 local: false)
```
[Ref: form-for-跟-form-with-有什麼不同](https://medium.com/@anneju/rails%E5%9C%A8%E5%BB%BA%E7%AB%8B%E8%A1%A8%E5%96%AE%E7%9A%84%E6%99%82%E5%80%99-form-for-%E8%B7%9F-form-with-%E6%9C%89%E4%BB%80%E9%BA%BC%E4%B8%8D%E5%90%8C-ec45cebbbf92)
Q: 如何讓使用者知道自己的留言已經被存進去,又不重新送出表單?
祕技:偷懶的做法
```
views/comments/create.js.erb
```
```
alert('hi!<%= @comment.content %>');
```
XHR (XML HTTP Request)
## 把`加到我的最愛`換成Ajax寫法
### 第一個方式: 搜尋gem
[fontawesome gem](https://github.com/FortAwesome/font-awesome-sass)
但是不要等別人打包好再用
### npm
[NPM](https://www.npmjs.com/package/font-awesome)
eg:
```
npm i bootstrap
```
### yarn
`yarn add boostrap`
`yarn install --check-files`
```
yarn add boostrap
yarn add v1.22.4
[1/4] 🔍 Resolving packages...
warning boostrap@2.0.0: Package no longer supported. Contact support@npmjs.com for more info.
[2/4] 🚚 Fetching packages...
[3/4] 🔗 Linking dependencies...
warning " > webpack-dev-server@3.11.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
[4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ boostrap@2.0.0
info All dependencies
└─ boostrap@2.0.0
✨ Done in 10.90s.
```
## 如何把boostrap require進rails專案?
```
require("scripts")
```
對 webpacker來說 所有東西都是js
## 下載font-awoesome
[fontawesome](https://fontawesome.com/how-to-use/on-the-web/setup/using-package-managers)
```
yarn add @fortawesome/fontawesome-free
```
在`/stylesheets/index.js` 把
```
import "bootstrap/dist/css/bootstrap";
import "@fortawesome/fontawesome-free/css/all";
import "./frontend/home.scss";
import "./frontend/form.scss";
```
## 點`愛心`加到我的最愛
/scripts/index.js
```
function hello() {
alert('hhh');
}
```
html.erb
```
<a href="javascript:hello();">
```
這段js不會動的原因是因為webpacker把函數包起來了
IIFE (Immediately Invoked Function Expression) 立即呼叫函式表達式
## javascript 套件: stimulus js
[stimulus js](https://stimulusjs.org/)
架構
```
<div data-controller="hello">
<input data-target="hello.name" type="text">
<button data-action="click->hello#greet">
Greet
</button>
<span data-target="hello.output">
</span>
</div>
```
在rails專案安裝`stimulus`
```
webpacker:install:stimulus
```
安裝後,會在前端的資料夾長出`controller`資料夾
```javascript
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "output" ]
connect() {
console.log('hi!')
// this.outputTarget.textContent = 'Hello, Stimulus!'
}
}
```
```htmlmixed
<p class="m-1" data-controller="heart">
<%= link_to "加到我的最愛", favorite_board_path(@board), method: 'POST', class: "fa fa-heart" %>
<button><i class="fa fa-heart" data-target="heart.heart"></i></button>
</p>
```
把愛心換掉
```
import { Controller } from "stimulus"
import ax from "axios"
export default class extends Controller {
static targets = [ "heart" ]
favorite() {
this.heartTarget.classList.add("fa")
this.heartTarget.classList.remove("fas")
// console.log(this.heartTarget);
// far -> fas
}
}
```
[axios](https://github.com/axios/axios):Promise based HTTP client for the browser and node.js
```
yarn add axios
```
安裝好後,import
```
import { Controller } from "stimulus"
import ax from "axios";
export default class extends Controller {
static targets = [ "heart" ]
favorite() {
ax.post
}
}
```
axios的寫法`then`
```javascript
const axios = require('axios');
// Make a request for a user with a given ID
axios.get('/user?ID=12345')
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
```
## 透過erb把看板的數字塞進來
```
favorite() {
// 用某個動作、打去某個地方,然後做什麼事情
ax.post("/boards/2/favorite").then(function(result){
})
}
```
透過chrome dev印出值
```
const heart = document.querySelector('.far')
> undefined
heart
> <i class="far fa-heart fa fas" data-target="heart.heart">…</i>
heart.dataset
> DOMStringMap {target: "heart.heart"}
```
要怎麼把資料塞進去? 放在`data-heart-id`
[參考stimulus Reference](https://stimulusjs.org/reference/data-maps)
Eg.
```
<h1 data-controller="heart"
data-heart-id="<%= @board.id %>"></h1>
```
```
favorite() {
let board_id = this.data.get("board");
// 用某個動作、打某個地方,然後做什麼事情
ax.post(`/boards/${board_id}/favorite`)
.then(function(result){
})
}
```

## 如果失敗的話印出catch
chrome console出現422訊息
```
xhr.js:175 POST http://localhost:3000/boards/1/favorite 422 (Unprocessable Entity)
dispatchXhrRequest @ xhr.js:175
xhrAdapter @ xhr.js:18
dispatchRequest @ dispatchRequest.js:40
Promise.then (async)
request @ Axios.js:64
Axios.<computed> @ Axios.js:88
wrap @ bind.js:11
favorite @ heart_controller.js:19
Binding.invokeWithEvent @ binding.js:60
Binding.handleEvent @ binding.js:33
EventListener.handleEvent @ event_listener.js:40
heart_controller.js:24 Error: Request failed with status code 422
at createError (createError.js:17)
at settle (settle.js:19)
at XMLHttpRequest.handleLoad (xhr.js:63)
```
用`axios rails authenticity token`關鍵字找解答
[Specify XSRF config defaults for axios](https://github.com/rails/webpacker/issues/1015)
參考一下別人的解法
```
// app/javascript/setupCSRFToken.js
import axios from 'axios'
export default function() {
const csrfToken = document.querySelector("meta[name=csrf-token]").content
axios.defaults.headers.common[‘X-CSRF-Token’] = csrfToken
}
// app/javascript/packs/application.js
import setupCSRFToken from '../setupCSRFToken'
setupCSRFToken()
// or
window.addEventListener('DOMContentLoaded', setupCSRFToken)
```
改寫成自己的
```javascript
import { Controller } from "stimulus"
import ax from "axios";
export default class extends Controller {
static targets = [ "heart" ]
favorite() {
let board_id = this.data.get("board");
const token = document.querySelector("meta[name=csrf-token]").content;
ax.defaults.headers.common['X-CSRF-Token'] = token;
ax.post(`/boards/${board_id}/favorite`)
.then(function(result) {
console.log(result);
})
.catch(function(err) {
console.log(err);
})
}
}
```
以上方式可以成功通過POST的驗證
但是要如何將成功的結果顯示在頁面?
如果是
json - > render (愛心按鈕改變,但是該打的資料都有打)
html - > redirect (我的最愛連結)
修改`board controller`
`respond_to` 一個block
[rails手冊舉例](https://api.rubyonrails.org/v5.2.4/classes/ActionController/MimeResponds.html#method-i-respond_to)
```
def index
@people = Person.all
respond_to do |format|
format.html
format.js
format.xml { render xml: @people }
end
end
```
```
def favorite
current_user.toggle_favorite_board(@board)
respond_to do |format|
format.html { redirect_to favorites_path, notice: 'OK!' }
format.json { render json: {status: 1} }
end
end
```
Eg.
```
# 找到show後,根據格式分發到不同地方
# 預設是回傳html, 如果請求json, 回傳json格式的資料
def show
@posts = @board.posts.includes(:user)
respond_to do |f|
f.html { render :show }
f.json { render json: {aa: 1}}
end
end
```
json格式:如果這個版被喜歡過,回傳喜歡的使用者
```
respond_to do |format|
format.html { redirect_to favorites_path, notice: 'OK!' }
format.json { status: @board.favorited_by?(current_user) }
# @board.favorited
end
```
```
favorite() {
let board_id = this.data.get("board");
const token = document.querySelector("meta[name=csrf-token]").content;
ax.defaults.headers.common['X-CSRF-Token'] = token;
ax.post(`/boards/${board_id}/favorite.json`)
.then(function(result) {
if (result.data["status"] == true) {
this.heartTarget.classList.remove("far");
this.heartTarget.classList.add("fas");
} else {
this.heartTarget.classList.remove("fas");
this.heartTarget.classList.add("far");
}
})
.catch(function(err) {
console.log(err);
})
}
```
`this`會出錯 => 改成箭頭函式
```javascript
import { Controller } from "stimulus"
import ax from "axios";
export default class extends Controller {
static targets = [ "heart" ]
favorite() {
let board_id = this.data.get("board");
const token = document.querySelector("meta[name=csrf-token]").content;
ax.defaults.headers.common['X-CSRF-Token'] = token;
ax.post(`/boards/${board_id}/favorite.json`)
.then((result) => {
if (result.data["status"] == true) {
this.heartTarget.classList.remove("far");
this.heartTarget.classList.add("fas");
} else {
this.heartTarget.classList.remove("fas");
this.heartTarget.classList.add("far");
}
})
.catch(function(err) {
console.log(err);
})
}
}
```
- 如何讓js controller知道這個版被喜歡
目前後端只做了response_to
也可以用 SSR(Server Side Rendering) 伺服器端渲染
## Rails 的ajax
[Ref](https://www.rubyguides.com/2019/03/rails-ajax/)
```
Rails.ajax({
url: "/books",
type: "get",
data: "",
success: function(data) {},
error: function(data) {}
})
```
改寫
```
import { Controller } from "stimulus"
import Rails from "@rails/ujs";
export default class extends Controller {
static targets = [ "heart" ]
favorite() {
let board_id = this.data.get("board");
Rails.ajax({
url: `/boards/${board_id}/favorite.json`,
type: 'post',
success: (result) => {
if (result["status"] == true) {
this.heartTarget.classList.remove("far");
this.heartTarget.classList.add("fas");
} else {
this.heartTarget.classList.remove("fas");
this.heartTarget.classList.add("far");
}
},
error: (err) => {
console.log(err);
}
})
}
```
## 自訂事件:按下按鈕時,讓其他的js controller收到事件
```
<ul>
<% @posts.each do |post| %>
<li data-controller="post">
<%= post.display_username %>
<%= link_to post.title, post, data: { target: 'post.title' } %>
<%= link_to "編輯", edit_post_path(post) %>
</li>
<% end %>
</ul>
```
[自訂事件](https://developer.mozilla.org/zh-TW/docs/Web/Guide/Events/Creating_and_triggering_events)
[stimulus js custom event]()
heart_controller.js
```
import { Controller } from "stimulus"
// import Rails from "@rails/ujs";
export default class extends Controller {
static targets = [ "heart" ]
favorite() {
// 發出事件
const event = new CustomEvent("cat", {
detail: {
hazcheeseburger: true
}
});
window.dispatchEvent(event);
}
}
```
post_controller.js
```
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ 'title' ]
something(e) {
#把標題換成cheeseburger
this.titleTarget.textContent = "cheeseburger";
console.log(e.detail["hazcheeseburger"]);
//console.log(e);
}
}
```
##
hi.js
```
puts "aaa"
```
```
~/Documents/projects/astro_js master ruby hi.js
aaa
```
## rails 的vue
app.vue
css的作用範圍被固定 (不用再想名字)
```
<template>
<div id="app">
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data: function () {
return {
message: "Hello Vue!"
}
}
}
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
```