# 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中,就可以顯示圖示![](https://i.imgur.com/FiyGpmG.png) --- * 不過我們要做的是可點選的按讚圖案,所以改成 `<%= 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" ![](https://i.imgur.com/pcdkK3r.png) * `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> ``` * 會呈現出以下原始碼 ![](https://i.imgur.com/j0RLuKb.png) --- * 在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") ``` ---