# [08/09&10_b] Rails實作 新專案Note
https://www.xmind.net/m/jMUqLi/
依照順序 **Route => Conroller => Model => DB =>** M/C **=> View**
views呈現畫面就好,要東西用controllers抓

## START
1. 在目標目錄 rails new 專案名稱 => 開新專案。
2. 在目錄中新目錄 rails s => 開伺服器。
3. 在目錄中新目錄 rails c => 沙盒。
*在 Rails c 裡面打 .all,只會噴 11 筆資料*
5. 可得到一個local 3000網址。
## (1) Route:創造路徑
在Rails,每條連結都是資源(resources)
在routes.rb用resources 創造8條路徑+7個action
```ruby=
resources :notes
```
例:路徑可改名articles
```ruby=
resources :notes, path: 'articles'
```
要背,會由上往下找,對了就執行那個路徑。
Resources資源,習慣用複數命名 (RESTful API概念)。路徑對照表可看:
* 運作rails s之後,任一不存在的網址
* 終端機指令 $rails route
* Controller#Action**其中的new(經由post)會送到create
* 凡有 :id 帶入後皆會傳入Controller,形成參數雜湊 **params[:id]**

Verb的功用:
**get 取得頁面;post 寫入資料庫;put 更新;delete 移除**
* 列表 GET /notes
* 寫入 POST /notes
* 看3號 GET /notes/3
* 新增 GET /notes/3/new
* 編輯3號 GET /notes/3/edit
* 更新3號 PUT /notes/3
* 刪除3號 DELETE /notes/3
### 路徑開關(很少用到)
只開部份路徑
```ruby=
resources:notes, only: [:index, :show]
```
除了此路徑全開
```ruby=
resources:notes, except: [:index]
```
### 追加功能&路徑 member/collection
例:取消訂單
除了8個路徑之外,想自製路徑取消訂單。
member會針對路徑中的id。
等於這個寫法=delete :cancel, {on: :member}
```ruby=
resources:notes, only: [:index, :show]
member do
delete :cancel
#比方:針對ID2號訂單 DELETE /order/2/cancel
end
```
collection會及於全部。
```ruby=
resources:notes, only: [:index, :show]
collection do
delete :cancel
#刪除所有訂單 DELETE /order/cancel
end
```
## (2) Con:建立方法
在app/views下建立相應檔案。詳筆記RB-13。建議終端機建立法:
```ruby=
$rails g controller notes
```
Rails自動規範:**class名稱蛇式<==>檔案名稱駝峰式**。
Controller內定義方法,可以**放@實體變數 給View用**。
本例手動順序:
* 在routes設定get "/notes", to: "notes#index"之後
* 在app/controllers下新增「NotesController.rb」並繼承ApplicationController
* 承上,**def index**
* 手動新增app/views/notes目錄及index.html
## (3) Model:建立預期傳入的資料
產生Model的過程,要建立一個資料表。
因為Model是抽象概念,資料表/資料庫是實體存在的。所以只建立Model不夠,還要**建立migrate(描述資料表內容)**。
* 預期結構「欄位」「資料型態」
> * **title**:string //到資料庫會變成 varchar(1~?)
> * **content**:text //到資料庫會變成 text(…~4G) 可放較多資料
> * **delete**:boolean (default: false) //判斷是否移除
> * **deleted_at**:datetime //判斷移除時間
> * **add_index** //建立索引,讓讀取速度更快(但寫入速度變慢)
> 因為隨著資料量遞增,撈資料時間呈線性成長。
> 其他資料型態還有:數字(integer)、文字(text)等等可用
### 開始建立
Model名稱用**首字大寫單數,對應migrate表格/Views目錄下的同名複數**。
(只有string屬性可以不寫)(欄位生成migration)
```ruby=
$rails g model Note title content:text
```
### 自動產生檔案
* 產生"app/model"下的新rb檔(不用編輯)
* 產生 **"db/migrate"下的新rb檔**
* 可建立真正的資料表
* 會得到**複數model名的migration!**
* 會自動加時間戳記timestamps
### 建立新表格(資料庫具現化)
會在建立在"db/development.sqlite3"
```ruby=
$rails db:migrate
```
詳細設定在"config/databade.yml"
**清理內容 rails db:migrate:reset**
## (04) View/Con:建立各功能網頁
利用resources 創造8條路徑中的:
> new(.:format)
1. 在views下的相應網頁檔寫link("/對應複數名/new")
1. 到**views新增**同名網頁檔(new.html.erb)
2. 尚不存在create或new,所以到controller定義一個,再到views新增同名網頁檔(同上2步)
若寫這個,可拿到表單p的內容:
```htmlmixed=
render html: params["p"]
或
render html: params[:p]
```
## (05) View/Con:新增一筆資料
新增候選人資料,牽涉到View(設表單)、Controller(設create方法)、Model(被動存入新資料)
### View:建立表單送資料 = 新增一筆資料庫
本動作要建立一筆資料庫
設置表單 其中之:
>
>
> **超連結寫法**
> 從"views/notes/index.html.erb"
> 連到內建的「new」action建議使用new_note_path方法Helper:
>
> ```ruby=
> <%= link_to "新增筆記", new_note_path %>
> ```
>
> 各種連結寫法:
> 
* **Note.new** //新增一筆文章,只會在記憶體裡,要用 **.save** 才能存進去資料庫。
* **Note.create** //新增一筆文章,不用寫 save 可以直接存進去資料庫。
### 觀念:params
就像雜湊
是一個**打包form送出的所有參數**的Hash(Key與Value)
*在 controller 塞 debugger 可以用 params 印出*

* 用Key叫值(Value)
> h= {name: "kk"}
> 印出kk=**h[:name]**
### View:解決invalidAuthenticityToken
rails網站的post行為必須帶token,否則無效(防止灌水)。
**方法1:加一行hidden(不建議)**
* action 是form送出的地方。
* method 是form送出方式、如get或push。
* value 帶 <%= form_authenticity_token %>。
* **各欄input對應Model的各欄名稱,資料庫抓name。**
* submit後form會把資料**包成param**,想單獨render其中的content的值的話:
> render html: **params["content"]**
> 或
> render html: **params[:content]**
```ruby=
<form action="/candidates" method="authenticity_token" value="<%= form_authenticity_token %>">
<input type="text" name="title">
<input type="textarea" name="content">
<input type="submit" value="go!">
</form>
```
**方法2:form_for(內建小幫手form helper)**
form_for後面要接一個Model,而此時「Notes」已經是Model的class,所以:
* 在MVC架構下,建立行為應移到controller內較佳:將**Note.new先定義在Controller**的new方法裡,再給出「@note」實體變數,外面(view)才拿得到。 (\<%= form_for(@note) do %>)
* def new方法中寫(本例title與content是要抓param的變數名稱)
> title = params[:title]
> content = params[:content]
> **@note=Note.new(title: title, content: content)**
* form_for會猜@note的路徑,若猜錯,就自己加一個url的雜湊(**@note, url: '/正確路徑'**)
* 由於Object-Relational Mapping(ORM) 物件關係對應,應該會自己對應SQL與正確的物件。
**在View=初期表單**
* 如此,action會指向/notes,方法是post,且自帶token。
* 各欄input對應Model的各欄名稱,資料庫抓name。
* 本來的name多加一層外包 **note[ ]** ,送出的params會多包一層key(後面透過form_for達成)
* 
* 這樣的話,原本 title = params[:title] 與 content = params[:content] 可以縮寫成:
* @note = Note.new(**params[:note]**)
* 這樣會遇到ForbiddenAttributesError,必須做資料清洗。因此params[:note]要獨立成一個「資料清洗 def」,見後面。
```ruby=
<%= form_for(@note) do %>
標題:<input type="text" name="note[title]">
內文:<textarea type="text" name="note[content]"></textarea>
<input type="submit" value="送出">
<% end %>
```
**在View=進化表單**
* do後面可**帶變數(如 |f|)** 以承接yield出的數值,批次製造表單。如:
* 本例f是一種FormBuilder物件,可透過 **text_field、text_area 或 submit 方法**做出對應的 <input> 標籤*
* form_for會將每一欄包起來(如candidate[name]、candidate[age]等),被params包成一個雜湊: **{candidate: {name: 'aaa', age: '20'}}**
```ruby=
<%= form_for(Note.new) do |f|%>
<div>
<%= f.label :title, "標題" %>
<%= f.text_field :title %>
</div>
<div>
<%= f.label :content, "內文" %>
<%= f.text_area :content %>
</div>
<%= f.submit %>
<% end %>
```
#### form_for的做事
承上,form_for會自動加給一個key,等於舊寫法的:
> **<input type="text" name="note[content]" value="<%= @note.title%>">**
#### form_for的行動
form_for 偷偷用了.persisted 這個方法來判斷內容物有沒有料,會回傳布林值,
如果沒料就判斷你是想新增東西,所以會走到 new,
如果有料就判斷你是想修改東西,所以會走到 edit。
## (6) Con/Model:儲存一筆資料庫 if else
在def new完成之後,
繼續在Controller的create方法中,設定new帶入create之後的動作。
* 第8行的經由form_for打包起來的Note欄位雜湊(title: title, content: content),**塞到new裡面給Model**。
* 因此,最初可以寫成 render html: **params[:note]**
* 寫實體變數@note 方便View拿到。而前一步網頁中form_for的()變數,也可改為同一實體變數。
* *在controller的方法(如create)中可加一行debugger,以便在終端機除錯(印 params[:note]。按contiune結束。)*
```ruby=
def new
@note = Note.new #寫進資料庫
end
def create
# title = params[:title] #寫進該當參數
# content = params[:content] #寫進該當參數
@note = Note.new(title: title, content: content) #寫進資料庫 接著再改寫進clean_params並獨立為private方法
end
if @note.save
redirect_to "/notes" #若寫入成功 轉進notes清單
else #若不成功
flash[:notice] = "新增失敗"
render :new #借views下的已經form_for的new重新渲染
end
end
```
若不成功
* 在**Model**驗證必填某欄位(本例name) **validates :name, presence: true**
* 在controller的else給條件 **render :new**
可在if條件下一行,加入快閃訊息 flash[:notice] = "Candidate created!" ,提示成功。
寫入成功後,可用SQLite打開資料庫,在Browse Data的對應table(本例note)看寫入紀錄。
### Con:解決ForbiddenAttributesError資料清洗
原因:資料未清洗
解決法:
在create開頭用 **強參數strong parameter**(建立白名單,名單內才給過)
抓(require)出前面hash(只要params裡面的:note)的key之後,只允許(permit)部分欄位過來
```ruby=
clean_params = params.require(:note).permit(:title, :content)
@note = Note.new(clean_params)
```
最後一行可能已在def new替換為實體變數的模樣 @note
參數習慣寫成clean_params
#### 包成一個新方法
為方便重複使用,獨立def成新的方法。
```ruby=
private
def clean_params
params.require(:note).permit(:title, :content)
end
```
原本create的實體變數new所帶參數,跟著改變
```ruby=
note = Note.new(clean_params)
```
### 終端:檢證儲存成功
* rails c 或 rails console //irb
* Note.all //取得所有的 Note 資料像是陣列(**all為類別方法**)
* Note.first //抓出第一筆資料
* rails routes //路徑對照表
#### IRB
- .any?:有沒有東西
- .errors.any?:有沒有錯誤
- .errors.full_message:錯誤訊息
## (07) Con/View:展示清單
在Controller透過類別方法all,建議實體變數「複數」命名
常用實體變數:all、where(篩選條件)、order(排序)、limit(限制筆數)
```ruby=
def index
@notes = Note.all
end
```
### 第一階段=印出每一條
在View的index檔案中,
@notes本質上會印出陣列,所以可利用**each迴圈**印出每一條
```ruby=
<table>
<tr>
<td>Title</td>
<% @notes.each do |note| %>
<tr>
<td>
<%= link_to note.title, note_path(note) %>
</td>
</tr>
<% end %>
</table>
```
### 第二階段=每一條內頁
承上,增加個別筆數(note)資料連結,也就是show
* 依據內建Helper用note_path
* **帶id**才知道要看哪一條,相當於/note/#{note.id}
* (note.id)的**id可以省略**,會自己抽id
* note_path也可以省略,變成 <%= link_to note.title, note %>
> <%= link_to 顯示字樣, 連結位址 %>
>
```ruby=
<% @notes.each do |note| %>
<tr>
<td><%= link_to note.title, note_path(note.id) %></td>
<td><%= note.content %></td>
</tr>
<% end %>
```
### 第三階段=做出「show」
在Controller新增方法「show」,利用參數params抓取id,結合SQL的find_by方法。加上實體變數給View使用。
* .find_by //找不到資料 會給nil
* .find //找不到資料 會給ActiveRecord::RecordNotFound
```ruby=
def show
@note = Note.find_by(id: params[:id])
end
```
在views目錄下新增show網頁檔:
```ruby=
<ul>
<h1><%= @note.title %></h1>
<li>Title: <%= @note.title %></li>
<li>Content: <%= @note.content %></li>
</ul>
```
### 解決NoMethodError找不到資料
預防undefined method,在網頁開頭加入:
(專屬括號有等號= 印出,此處不加)
```ruby=
<% if @note %>
主內容
<% else %>
<h1>找不到資料</h1>
<% end %>
```
### 標出異常
#### 迴圈文字
可在new網頁列表出列出錯誤訊息。
1. 利用專屬if判斷有無錯誤,note.errors,
1. 在錯誤訊息本身,前一行放each迴圈印出full_message到變數(|message|)。
1. 將message丟到列表 \<li> 中印出來。
```ruby=
<% if @note.errors.any? %>
<ul>
<% @note.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
```
#### css上色效果+排版
1. 出現錯誤時,原始碼中的div內會出現新的div **class="field_with_error"**。
2. 在app/assets/stylesheets/ 新增css檔。裡面的application.css會將所有css打包生效(以下兩行的功用,勿刪)
> *= require_tree //載入所有scss,若關掉會按英文字母順序載
*= require_self
3. 目錄中個別檔案檔名無所謂
4. 錯誤時,文字欄(type="text")邊框與標籤名變紅。
```css=
.field_with_error{
display: inline-block;
input[type="text"]{
border-color: red;
border-width:1px;
}
label{
color: red;
}
}
```
## (08) Con/View:編輯/更新清單
建立新網頁edit,直接複製new內容。
1. 根據route路徑表,會去Controller找update方法。
* patch:只換掉其中一個資料
* put:整行換掉
1. 因此在View的網頁index,連結選擇路徑表Helper中的edit_xxx_path(會生成隱藏欄位name="_method" value="patch")
```ruby=
<%= link_to "編輯", edit_note_path(note.id) %>
```
在Controller,定義edit繼承show的大標題寫法(抓資料名)
定義update也繼承create的結構,導入清洗資料,區分成(回列表index)敗(重新編輯edit)。
```ruby=
def edit
@note = Note.find_by(id: params[:id])
end
def update
@note = Note.find_by(id: params[:id])
if @note.update(clean_params)
flash[:notice] = "更新了"
redirect_to "/notes"
else
render :edit
end
```
## (09) Con:刪除清單
在View的網頁,連結根據路徑表的Helper,方法要寫,不然就是get。
1. 路徑後面接method。
2. 再接data-confirm防呆。
```ruby=
<%= link_to "刪除", note_path(note),
method: 'delete',
"data-confirm": "確定嗎?" %>
```
或用雜湊
```ruby=
<%= link_to "刪除", note_path(note),
data: {method: 'delete', confirm: '確定嗎?' } %>
```
根據route路徑表,會去Controller找destory方法。
```ruby=
def destroy
@note = Note.find_by(id: params[:id]) #找到他
@note.destroy #刪除他
flash[:notice] = "刪除了"
redirect_to "/notes" #離開他
end
```
## (10) Con/View:清理及簡化*8/12影片
### Controller中
重複使用的,獨立成方法def.....
### View中
**Partial Render局部渲染**
* 不要在 partial reder裡面用實體變數
* form_for獨立出去成開頭「_」之網頁檔,取消實體變數標記。
* 原網頁呼叫實體變數就好
<%= render "form", note: @note %>
https://guides.rubyonrails.org/layouts_and_rendering.html#passing-local-variables
**Randered collection**
底限單數網頁,在同名views目錄下。
(複雜 再確認)
原網頁
## (11) Con:找不到網頁&其他
捕捉錯誤訊息
移到controller的上一層,**在Model**裡的application_record.rb
```ruby=
begin
原內容
rescue ActiveRecord::RecordNotFound
render file:"public/404.html", status: 404
end
```
**在Model**的application_controller.rb
* 讓所有controller 都可以自動用rescue_from這個類別方法
* 如果出現ActiveRecord::RecordNotFound就用record_not_found方法
```ruby=
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
private
# 渲染rails提供的404頁面,狀態也同步改成404
def record_not_found
render file: "public/404.html", status: 404
end
end
```
**在Controller**
在notes_controller.rb
* before_action = before_filter,做指定 action 前先撈資料
* before_action 是一個類別方法,是 singleton method
```ruby=
before_action :find_note, only: [:show, :edit, :update, :destroy]
```
**在View**
在index.html.erb
* method 預設為 get 要改成delete。
* 也可以用 data 包起來
* <%# rel="nofollow" %>叫爬蟲不要跟過來
* 按超連結 link_to 會形成一個表單 ,JS 會把 method 跟 token 塞進來
* id: note.id 可搭配JS使用,取得特定資料。
* destroy 刪除內容,要多加delete方法,不然會走錯路,走到show頁面。
```ruby=
<%# data-method="delete"%>
<%= link_to "刪除", note_path(note), method: "delete",
data: {confirm: "確定嗎?",id: note.id } %>
```
_form.html.erb
* 從@note 改成 note 讓_form.html不要自己亂抓東西,怕會抓到重複的
* 只要根據餵進來的東西在這邊呈現就好
```ruby=
<% if note.errors.any? %>
<ul>
<% note.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
```
edit.html.erb
* view 提供的 render(局部渲染 partial render)不能把字串 form 改成符號,跟controller 的 render 不一樣
* 把 edit 的 @note 丟過來再丟到_form.html.erb,最後渲染回來
```ruby=
<%= render "form", note: @note %>
```
new.html.erb
* 把 new 的 @note 丟過來再丟到_form.html.erb,最後渲染回來
```ruby=
<%= render "form", note: @note %>
```
###### tags: `Rails`