第一篇筆記:Astro課程 0723-0731 - Rails (Day1-Day4)
第二篇筆記:Astro課程 0803 - Rails (Day5)
為了把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
user.rb
has_many :posts
post.rb
belongs_to :user
有兩種方式可以指定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
上 def display_name
user.nil? ? "未知" : user.account
end
post.rb
def display_name
if user.nil?
"未知"
else
user.account
end
end
這個頁面有幾次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
打包的目的:減少空格的數量、壓縮檔案的大小
Rails 5: assets pipeline (ruby寫的)
Rails 6: 加上了webpacker (js寫的)
有些東西放在public資料夾
不用經過routes就可以取用檔案
由於修改JS檔內容可能會造成reload時間過長
所以要另外開一個單獨給webpack的server
另開terminal分頁
執行bin/webpack-dev/server
以節省開發時間
直接裝 foreman gem,在專案目錄下建立 Procfile
file-content自己寫:
名稱: bin/rails server -p (port:3000)
名稱: bin/webpack-dev-server
foreman start -f Procfile.dev`
幫忙設定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>
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
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
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-有什麼不同
Q: 如何讓使用者知道自己的留言已經被存進去,又不重新送出表單?
祕技:偷懶的做法
views/comments/create.js.erb
alert('hi!<%= @comment.content %>');
XHR (XML HTTP Request)
加到我的最愛
換成Ajax寫法但是不要等別人打包好再用
eg:
npm i bootstrap
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.
require("scripts")
對 webpacker來說 所有東西都是js
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) 立即呼叫函式表達式
架構
<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
資料夾
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "output" ]
connect() {
console.log('hi!')
// this.outputTarget.textContent = 'Hello, Stimulus!'
}
}
<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: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
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
});
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
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){
})
}
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
參考一下別人的解法
// 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)
改寫成自己的
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手冊舉例
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
會出錯 => 改成箭頭函式
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);
})
}
}
目前後端只做了response_to
也可以用 SSR(Server Side Rendering) 伺服器端渲染
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);
}
})
}
<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>
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
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>