---
# System prepended metadata

title: Astro課程 0807.0810 - Rails (Day6.Day7)
tags: [astro, ' rails']

---

---
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?
![](https://i.imgur.com/23gxeY3.png)

進看板的時候，

- 看板查詢一次
- 版主也查詢
- 每篇文章也都查詢一次

`http://localhost:3004/boards/1`

![](https://i.imgur.com/v3UwdVr.png)



查詢看板的時候，把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){

    })

  }
```

![](https://i.imgur.com/rwSM3qt.png)

## 如果失敗的話印出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>

```