# 1204 按讚功能
* 到[Fontawesome ](https://fontawesome.com/)找到使用方法
* 在專案底下安裝 `$ yarn add @fortawesome/fontawesome-free`
* 在app/frontend(可能有javascript而沒有frontend資料夾)建立scripts資料夾(名稱可自取),底下建立index.js
* 到app/frontend/packs/application.js,引入`require("scripts")`
* 在app/frontend/styles/index.js,引入`import "@fortawesome/fontawesome-free/css/all.css"`,在node-modules找
* 以上步驟完成後,在Fontawasome找所要用的圖示,可以看到標籤,加在post的show.html.erb中,就可以顯示圖示
---
* 不過我們要做的是可點選的按讚圖案,所以改成
`<%= link_to "<i class='far fa-thumbs-up'></i>",'#'%>`
由於link_to本身會審查連結內容,會變成字串顯示,所以要在加上`.html_safe`
變成
`<%= link_to "<i class='far fa-thumbs-up'></i>".html_safe,'#'%>`
但是這寫法不好看,可以用do...end抱著原本要連結的東西來改寫
```
<% link_to '#' do %>
<i class="far fa-thumbs-up"></i>
<% end %>
```
---
* 接下來想'#'的路徑,因為是要對文章按讚作收藏,所以在routes.rb裡的posts下用member做出/posts/post_id/favorite
```
resources :posts, only:[] do
resources :comments, shallow: true, only: [:create, :destroy]
member do
post :favorite # POST /posts/:id/favorite
end
end
```
* 在查表可以看到建立favorite_post_path的路徑,把它代回'#',因為還要辨認是哪篇文章再加上@post以前post方法,就變成
`<% link_to favorite_post_path(@post), method: 'post' do %>`
* 現在回到瀏覽器按下圖示會出現缺少favorite action的錯誤,同時注意網址也有變化
---
* 我們希望用AJAX的形式不離開頁面就完成按讚的動作,先給予一個id
```
<% link_to '#',id: 'favorite_btn' do %>
<i class="far fa-thumbs-up"></i>
<% end %>
```
* 雖然可以在show頁面直接添加<script></script>但是不推薦這種寫法
* 按鑽屬於一種功能,所以也不適合寫在一開始建立的index.js檔裡,所以在app/frontend/scripts再建立一個post.js
* 將功能寫在post.js並且在index.js裡`require('./post')`
* 在post.js裡寫下JS功能
```
document.addEventListener("DOMContentLoaded", function() {
//監聽document,否則會出現抓不到圖示的錯誤,因為讀到這行時html還未建立
const favorite_btn = document.querySelector('#favorite_btn') //抓出圖示按鈕
favorite_btn.addEventListener("click", function(e) {
e.preventDefault() //暫停原來事件行為
//這裡可以打API/送資料/AJAX
//POST /post/:id/favorite
console.log(e.currentTarget)//印出發生事件的對象
})
})
```
* 但是只有在文章頁面才有按鈕,所以切換到其他頁面時,在開發者工具會出現錯誤監聽到null,可以再用一個if包起來,在按鈕存在時再監聽
```
document.addEventListener("DOMContentLoaded", function() {
const favorite_btn = document.querySelector('#favorite_btn')
if(favorite_btn){
favorite_btn.addEventListener("click", function(e) {
e.preventDefault()
//POST /post/:id/favorite
console.log(e.currentTarget)//印出發生事件的對象
})}
})
```
---
* 接下來要想辦法讓JS可以取到文章的id,用`data: { id: @post.id }`(當中的id:可以自取名字name:,會形成data-name="#"),將post_id帶給link_to,寫成
`<%= link_to '#',id: 'favorite_btn', data: { id: @post.id } do %>`
* 可以做出data-id="7"

* `console.log(e.currentTarget)`
可以印出發生的事件的對象,這裡是favorite_btn
* `console.log(e.currentTarget.dataset)`
加上dataset可以印出全部的data-name
* `console.log(e.currentTarget.dataset.id)`
加上.name(這裡是要id)可以印出指定的data-name
---
* axios 是一個可以專門為瀏覽器和node.js發送get、post請求的套件
安裝`$ yarn add axios`
* 依照官網說明格式在post.js裡
```
const ax = require('axios')
//require套件並指定給自定義的ax
ax.post(`/posts/7/favorite`, {})
//ax.方法('路徑',要傳送的值)
//以post方法打向/posts/7/favorite,因為不需要傳值所以給一個{}
.then(function(resp){
console.log(resp)
})
.catch(function(err) {
console.log(err);
})
非同步處理
```
* 路徑的7應該是文章id是個變數不能固定,所以
`const postId = e.currentTarget.dataset.id`
先將前面找到的data-id指定給變數postId
`/posts/${postId}/favorite`,在JS中帶變數要用 'esc下方的特殊符號' 包住
---
* 現在按圖示在開發者工具會出現404錯誤,因為缺少favorite action,先暫時建立
```
def favorite
render html: 'hi'
end
```
* 又會出現422錯誤因為缺少token(之前的token都由form_for自動建立,但目前的js是我們手刻所以沒有token,在頁面原始碼可以看到實際上頁面還是有token存在,所以可以用js抓來使用
```
const ax = require('axios')
const token = document.querySelector('[name=csrf-token]').content
//抓取頁面會有個name為csrf-token並且content為token的東西
ax.defaults.headers.common['X-CSRF-TOKEN'] = token
//將抓到的token塞入
```
這樣就完成token的放入
---
* 建立一個新的model來處理按讚
`$ rails g model FavoritePost user:belongs_to post:belongs_to`
* 確認migration沒問題再建立表格
`$ rails db:migrate`
* 在post.rb與user.rb加上
`has_many :favorite_posts`
* 到目前為止建立了post與favorite_post以及user與favorite_post之間的關聯,但實際上我們需要的是user與post的關聯,透過favorite_post就可以建立兩者之間的關聯
* 在post.rb加上
`has_many :users, through: :favorite_posts`
但是在post.rb已經有belongs_to :user,為了避免混淆可以改寫users
(這裡的users只會做出一個方法並不是真的model所以可以自由改名稱)
`has_many :favorite_users, through: :favorite_posts`
* 在user.rb加上
`has_many :posts, through: :favorite_posts`
* posts也與原有的has_many :posts衝突,所以也改名稱
`has_many :my_favorites, through: :favorite_posts`
* 在rails c中測試
```
p7 = Post.find(7) //抓出指定文章,這裡抓7
p7.favorite_user //找出喜歡7號文章的使用者
```
這裡會出現錯誤,因為我們把原來的users改掉,rails無法判斷要找尋的對象,要再加上一個source給它來源變成
`has_many :favorite_users, through: :favorite_posts, source: 'user'`
source後面可以用字串'user'或符號:user,不能用'User'
* 現在p7.favorite_user就會用INNER JOIN的方式去找user與favorite_post,就可以找到喜歡這篇文章的使用者
* 同樣的user.rb也需要改成
`has_many :my_favorites, through: :favorite_posts, source: 'post'`
---
* 接下來在favorite action寫儲存我的最愛的功能
```
def favorite
post = Post.find(params[:id]) //找出文章
current_user.my_favorites << post //加到我的最愛
render html: 'hi'
end
```
* 但是這樣沒有判斷這篇文章是否已經加入過我的最愛,再加上判斷式
```
if current_user.favorite?(post) //如果這篇文章已喜歡
# 移除我的最愛
else
# 加到我最愛
end
```
就變成
```
def favorite
post = Post.find(params[:id])
if current_user.favorite?(post)
# 移除我的最愛
else
# 加到我最愛
current_user.my_favorites << post
//my_favorites是一個陣列
end
end
```
* 因為current_user.favorite中的current_user是一個user model,所以favorite就需要寫在user.rb
```
def favorite?(post)
my_favorites.include?(post)
end
```
* 再回到posts_controller
```
def favorite
post = Post.find(params[:id])
if current_user.favorite?(post)
# 移除我的最愛
current_user.my_favorites.destroy(post)
render json: { status: 'removed' } //給前端的狀態值
else
# 加到我最愛
current_user.my_favorites << post
render json: { status: 'added' }
end
end
```
* 回到post.js,在status為added時執行...,其他狀態執行...
```
ax.post(`/posts/${postId}/favorite`, {})
.then(function(resp){
if (resp.data.status == "added") {
icon.classList.remove("far")
icon.classList.add("fas")
} else {
icon.classList.remove("fas")
icon.classList.add("far")
}
})
```
---
* 只有寫進資料庫沒有在show.html.erb做控制, 所以重新整理之後,還是會呈現fas的圖示
```
<%= link_to '#',id: 'favorite_btn', data: { id: @post.id } do %>
< i class="fas fa-heart">< /i>
<% end %>
```
*修改show.html.erb
```
<% if user_signed_in? %>
//使用者登入時才會出現按讚圖案,較好做法是未登入時按下會提醒請先登入
<%= link_to '#',id: 'favorite_btn', data: { id: @post.id } do %>
<% if current_user.favorite?(@post) %>
// 如果已經按過讚,class為fas
< i class="fas fa-heart">< /i>
<% else %>
// 如果沒按過讚,class為far
< i class="far fa-heart">< /i>
<% end %>
<% end %>
<% end %>
```
---
* 目前的post.js會在application.js中用require("scripts")引入,所以全部地方都會執行,雖然我們有檢查按鈕存在才執行,但萬一有出現一樣名稱favorite_btn的按鈕就會衝突
* 可以在app/views/layouts/application.html.erb的<body>加上類別,讓頁面的body變得可區分
`< body class="<%= controller_name %>-<%= action_name %>"> `
* 原來post.js的按鈕監聽加上.posts-show的class
```
const favorite_btn = document.querySelector('.posts-show #favorite_btn')
```
就可以避免監聽到其他的同名稱按鈕(如果有的話)
---
## 用stimulus改寫
* 前置作業
將app/frontend/scripts/index.js的require('./post')註解掉關閉本來post.js的功能
* 先安裝stimulus,會出現在package.json的清單中,同時在javascripts資料夾底下增加一個controllers的資料夾,還會在app/frontend/packs/application.js裡import "controllers"
`$ rails webpacker:install:stimulus`
* 將app/views/posts/show.html.erb的h2標籤加上data-controller="名稱",會去呼叫app/frontend/controllers/名稱_controller.js
`<h2 data-controller="favorite">`就會去呼叫favorite_controller.js
* 因為post.js已經關閉,所以show.html.erb本來提供給它用的`id: 'favorite_btn'`就可以清掉,並為圖示加上`data-target="favorite.icon`,link_to加上`action: 'favorite#go'`
* target用.連接目標名稱,action用#連接方法
```
< h2 data-controller="favorite">
<% if user_signed_in? %>
<%= link_to '#', data: { action: 'favorite#go',id: @post.id } do %>
<% if current_user.favorite?(@post) %>
< i class="fas fa-heart" data-target="favorite.icon"></i>
<% else %>
< i class="far fa-heart" data-target="favorite.icon"></i>
<% end %>
<% end %>
<% end %>
< /h2>
```
* 會呈現出以下原始碼

---
* 在app/frontend/controllers/favorite_controller.js,用console.log驗證是否有成功掛載
```
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "icon" ]
go() {
console.log('go!')
}
}
```
* 仔細看可以發現按讚後網址其實是有變化的,如果希望不要離開網頁,可以用preventDefault阻止預設行為改寫成
```
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "icon" ]
go(e) {
e.preventDefault()
console.log('go!')
}
}
```
---
* 將文章id從action移動到controller,方便我們在controller抓取id
* data-favorite-id中間規定要放controller的名稱,不能寫data-id,否則會抓不到東西
```
< h2 data-controller="favorite" data-favorite-id="<%= @post.id %>">
<% if user_signed_in? %>
<%= link_to '#', data: { action: 'favorite#go' } do %>
<% if current_user.favorite?(@post) %>
< i class="fas fa-heart" data-target="favorite.icon">< /i>
<% else %>
< i class="far fa-heart" data-target="favorite.icon">< /i>
<% end %>
<% end %>
<% end %>
< /h2>
```
* 在favorite_controller.js可以用`this.data.get`去抓取controller的資料,這裡我們要的是id,同樣用console.log印出id來檢查
```
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "icon" ]
go() {
const id = this.data.get('id')
console.log('id')
}
}
```
* 從post.js複製以下程式碼
```
// 打 API / 送資料 / AJAX
const ax = require('axios')
const token = document.querySelector('[name=csrf-token]').content
ax.defaults.headers.common['X-CSRF-TOKEN'] = token
const postId = e.currentTarget.dataset.id
const icon = e.target
ax.post(`/posts/${postId}/favorite`, {})
.then(function(resp){
if (resp.data.status == "added") {
icon.classList.remove("far")
icon.classList.add("fas")
} else {
icon.classList.remove("fas")
icon.classList.add("far")
}
})
.catch(function(err) {
console.log(err);
})
```
其中這兩行不需要
```
const postId = e.currentTarget.dataset.id
const icon = e.target
```
另外id已經抓出來了,所以`${postId}`可以改為`${id}`
* favorite_controller.js,註解原本要做的動作,先印出resp來檢查
```
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "icon" ]
go() {
const id = this.data.get('id')
console.log('id')
// 打 API / 送資料 / AJAX
const ax = require('axios')
const token = document.querySelector('[name=csrf-token]').content
ax.defaults.headers.common['X-CSRF-TOKEN'] = token
ax.post(`/posts/${id}/favorite`, {})
.then(function(resp){
console.log(resp)
//if (resp.data.status == "added") {
//icon.classList.remove("far")
//icon.classList.add("fas")
//} else {
//icon.classList.remove("fas")
//icon.classList.add("far")
}
})
.catch(function(err) {
console.log(err);
})
}
}
```
* 可以在最上面用`import ax from 'axios'`來取代`const ax = require('axios')`
```
import { Controller } from "stimulus"
import ax from 'axios'
export default class extends Controller {
static targets = [ "icon" ]
go() {
const id = this.data.get('id')
console.log('id')
// 打 API / 送資料 / AJAX
//刪除這行const ax = require('axios')
const token = document.querySelector('[name=csrf-token]').content
ax.defaults.headers.common['X-CSRF-TOKEN'] = token
ax.post(`/posts/${id}/favorite`, {})
.then(function(resp){
console.log(resp)
//if (resp.data.status == "added") {
//icon.classList.remove("far")
//icon.classList.add("fas")
//} else {
//icon.classList.remove("fas")
//icon.classList.add("far")
}
})
.catch(function(err) {
console.log(err);
})
}
}
```
### 注意 以下兩行程式碼寫法相似,但是意義不同
* `import { Controller } from "stimulus"`
有{}意思為stimulus中import叫Controller的東西,stimulus裡有叫Controller的東西
* `import ax from 'axios'`
意思為將axios預設的模組取名叫ax後取出,axios沒有叫ax的東西
---
* 原本在post.js中標的物用icon,在 favorite_controller.js中要用stimulus的寫法this.iconTarget
```
import { Controller } from "stimulus"
import ax from 'axios'
export default class extends Controller {
static targets = [ "icon" ]
go() {
const id = this.data.get('id')
console.log('id')
// 打 API / 送資料 / AJAX
//刪除這行const ax = require('axios')
const token = document.querySelector('[name=csrf-token]').content
ax.defaults.headers.common['X-CSRF-TOKEN'] = token
ax.post(`/posts/${id}/favorite`, {})
.then(function(resp){
if (resp.data.status == "added") {
this.iconTarget.classList.remove("far")
this.iconTarget.classList.add("fas")
} else {
this.iconTarget.classList.remove("fas")
this.iconTarget.classList.add("far")
}
})
.catch(function(err) {
console.log(err);
})
}
}
```
## 現在執行會出現iconTarget為undefind
因為現在的this指的是function(resp)本身,也就是
```
(function(resp){
console.log(resp)
if (resp.data.status == "added") {
this.iconTarget.classList.remove("far")
this.iconTarget.classList.add("fas")
} else {
this.iconTarget.classList.remove("fas")
this.iconTarget.classList.add("far")
}
```
iconTarget其實不存在這之中,所以會undefind
## 解法
function(resp)改成fat arrow(胖箭頭)寫法,因為function本身會有scope會擋住,fat arrow沒有自己的this所以會向外找iconTarget
```
ax.post(`/posts/${id}/favorite`, {})
.then((resp) => {
//fat arrow
if (resp.data.status == "added")
```
---