Software engineering notes

Rails View

(最後更新: 2016-04-27)

fields_for 一對一, 一對多

籍由 user 去更新 profile(1對1), user_languages(1對多) 欄位

user.rb

has_one  :profile
has_many :user_languages
accepts_nested_attributes_for :profile
accepts_nested_attributes_for :user_languages

view

<%= form_for @user, url: user_path(@user), html: {method: :put} do |f| %>
    <%= f.fields_for :profile do |s| %>
        <%= s.text_field :about_me %>
    <% end %>

    <%= f.fields_for :user_languages do |l| %>
        <%= l.check_box :has_badge %>
        <%= l.object.from %>
        <%= l.object.to %>
    <% end %>
<% end %>

產生的 HTML Name :

user[profile_attributes][:about_me]
user[user_languages_attributes][0][has_badge]

controller

user = User.find(params[:id])
user.update(admin_update_params)

def admin_update_params
    params[:user].permit(profile_attributes: [:id, :about_me], user_languages_attributes: [:id, :has_badge])
end

它就會更新一對一或一對多了

接下來看一下它到底是怎麼更新的

params[:user] 參數 :

profile_attributes: {
    about_me: "Hello world!",
    id: "66"
},
user_languages_attributes: {
    0: {
        has_badge: "1",
        id: "143"
    }
}

原來 params 會帶該 record 的 id, 所以上面 controller 取參數時要記得 permit :id

Rails 會依照 User model 的關聯下對應的 sql, 注意的是它會先用 user_id + IN (..上面的id參數..), 所以可以確定是這名 User 的資料才可以被 Update

Profile Load (0.3ms)  SELECT  "profiles".* FROM "profiles" WHERE "profiles"."user_id" = ? LIMIT 1  [["user_id", 69]]
UserLanguage Load (0.3ms)  SELECT "user_languages".* FROM "user_languages" WHERE "user_languages"."user_id" = ? AND "user_languages"."id" IN (143, 141, 142)  [["user_id", 69]]
SQL (2.8ms)  UPDATE "profiles" SET "about_me" = ?, "updated_at" = ? WHERE "profiles"."id" = ?  [["about_me", "Hello world!"], ["updated_at", "2015-08-18 09:01:20.299243"], ["id", 66]]
SQL (0.6ms)  UPDATE "user_languages" SET "has_badge" = ?, "updated_at" = ? WHERE "user_languages"."id" = ?  [["has_badge", "f"], ["updated_at", "2015-08-18 09:01:20.305686"], ["id", 143]]

測試它是否真的安全, 將 fields_for 產生的 HTML 隱藏欄位的值改為一個不屬於此 User 的 id

<input type="hidden" value="143" name="user[user_languages_attributes][0][id]" id="user_user_languages_attributes_0_id">
                            改成
<input type="hidden" value="123" name="user[user_languages_attributes][0][id]" id="user_user_languages_attributes_0_id">

送出後, 可以看到 Rails 噴錯了:D , 符合預期結果

Couldn't find UserLanguage with ID=123 for User with ID=69

Partial

Pass a variable into a partial

如果是 instance variable (ex: @user), 不需要特別傳入到 partial, 如果是一般變數才需要 (ex: user)

<%= form_for @user, url: users_path(@user) do |f| %>
    <%= render 'partial file', f: f %>
<% end %>

partial file :

<%= f.text_field :name %>

注意!! 判斷傳入 partial 的變數, 要用 local_assigns 去判斷, 如果直接用

if name   <= 會噴錯 undefined local variable or method `name`

OK :
    if local_assigns.has_key? :name
    if local_assigns[:name]

代入 template 取得 html

<% @progress_bar = render :partial => 'template/progress_bar', :locals => { :num => num, :part_num => part_num, :progress_bar_status => "progress-bar-danger" } %>
<%= render :partial => 'template/url_item', :locals => { :num => num, :progress_bar => @progress_bar } %>

注意檔名要底線 _progress_bar.html.erb, _url_item.html.erb

layout Template

views/layouts/application.html.erb :

<!DOCTYPE html>
<html>
<head>
  <title>Template</title>
  <meta charset="utf-8">
  <%= favicon_link_tag '/favicon.ico' %>        # 要注意一定要 / 因為是從根目錄開始讀, 否則到其他 show 的頁面會把它當 id 在 load
  <%= stylesheet_link_tag @controller_name, media: 'all', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body id="page-top" class="index">
  <%= render 'layouts/header' %>
  <div class="content">
    <%= yield %>
  </div>
  <%= render 'layouts/footer' %>
  <%= javascript_include_tag @controller_name, 'data-turbolinks-track' => true %>
</body>
</html>

不同頁面引入相同 partial, path 如何根據不同 route 顯示

很簡單, ex:

link_to '評價', request.path

引入個別 view 定義的 CSS / JS

像有些共用的 js 你可以寫在 appication.js,但有些只有這一頁才會用到的 js 可以直接寫在該 view,這樣開發及維護都很方便

layout/application.html.erb

<%= yield :js %>

在個別 view 寫 javascript

<% content_for :js do %>
    <script>
        $(document).ready(function () {
            alert(1);
        })
    </script>
<% end %>

讓表單送出錯誤後返回資料不清除

controller

def update

  ...(省略)...

  if 驗證錯誤
    render :edit
  end
end

edit.html.erb

<input type="text" class="form-control" name="xxx" value="<%= params[:xxx] %>">

不要用 redirect_to action: :edit, 因為 post 的資料不會被帶到 edit 頁面, 它相當於是一個"重新"的 request 請求。 用 render 直接在 update 生出 edit 頁面, post 資料也會被保存下來

或讓 rails 自動幫你綁定回填

<%= f.text_field :nickname, class: 'form-control' %>

但它有個問題是, 一定要執行到 model 裡, 例如 @post.update(params[:post]) 最後再 redner :edit, 如果還沒執行到 model 就 render 那麼資料就不會回填

刪除 validate 錯誤產生多的 div wrapper

正常 :

<%= f.text_field :present_price  %>

錯誤時 :

<div class="field_with_errors"><input type="text" value="" name="product[present_price]" id="product_present_price"></div>

解決方法 : 拿掉它, 在 config/environment.rb 最後面加上

ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
  html_tag.html_safe
end

JSONP (jbuilder - 預設包在 Rails 的 Gemfile 裡)

ajax 送出請求給 show, show 會去返回 show.js.erb 的 js code 回去給 browser 執行

ajax 以 bootstrap modal 顯示 show action

index.html.erb

<div class="modal fade" id="myModal"
    ..略..
</div>

link_to '訂單明細', order_path(o), remote: true, data: {toggle: 'modal', target: '#myModal'}

show.js.erb

$(".modal-title").html("訂單明細");

// 填入 html, escape_javascript 可簡寫成 j
$(".modal-body").html("<%= escape_javascript render(partial: 'orders/show')%>");

_show.html.erb

html + ruby code

ajax submit form

view

form_for 加上 remote: true

controller

respond_to do |format|
  format.js
end

create.js.erb

alert('Success');

JSON 檔

index.json.jbuilder

json.array!(@posts) do |post|
    json.extract! post, :id, :title, :content
    json.url post_url(post, format: :json)
end

Form helper

radio_button

<% UserDomain.domains.each do |k, v| %>
  <div>
    <%= radio_button_tag 'user[domain]', v %>
    <%= label_tag "user_domain_#{v}", t("domain.#{k}") %>
  </div>
<% end %>

radio_button("post", "category", "rails")
radio_button("post", "category", "java")


f.radio_button :gender, 'male', checked: true

collection_radio_buttons(:item, :owner_id, Owner.all, :id, :name)

select

未指定值 : 頁面上及 option value 皆顯示一樣

f.select :gender, ['male', 'female', 'others']

有指定值 : 頁面上顯示 male/female/others; option value 為 1 / 2 / 3

f.select :city, {male: 1, female: 2, others: 3}

取出 name 及 id 欄位,直接放入

f.select :category, Category.pluck(:name, :id)        要注意順序是 :name, :id, 否則 select 下拉顯示的會是 id
f.select :category, Category.map { |s| [s.name, s.id] }

空白值, 給 default 值及 class name

f.select :product_spec_id, @product.product_specs.map { |s| [s.name, s.id] }, { include_blank: ture, selected: params[:spec] }, class: 'form-control'

設定 Default

f.select :parent_id, @parent_categories.unshift(["不選擇", 0])

select_tag

select_tag :category_id, options_from_collection_for_select(Category.all, "id", "name")

<select name="category_id" id="category_id">
    <option value="1">Music, Games & Kids</option>
    <option value="2">Games, Movies & Baby</option>
</select>

collection_select

User has_many UserLocation, 將 User 住的 Location 每項以 Select 列出來

current_user.user_locations.each do |l|
    collection_select('user_locations', '', UserLocation.locations_i18n, :last, :first, { include_blank: false, selected: l.location }, { class: 'location', id: nil } )
end

備註

1) f.collection_select(:category_id, Category.all, :id, :name)
等於
f.select :category_id, Category.all.map{ |c| [c.name, c.id] }

2)
s.select :recommended, options_for_select(Translator.recommendeds)

checkbox

f.check_box :rotting

<%= check_box_tag 'user_domains[]', k, @user_domains.has_key?(k), id: "domain-#{k}" %>

Object :
<div class="field">
    <%= f.label "Categories" %><br />
    <% for category in Category.all %>
        <%= check_box_tag 'user[category_ids][]', category.id, @user.category_ids.include?(category.id), :id => dom_id(category) %>
        <%= label_tag dom_id(category), category.name, :class => "check_box_label" %>
    <% end %>
</div>

Hash : dom_id 無法用, 它只吃 object
<% UserDomain.domain.each do |k, v| %>
  <div>
    <%= check_box_tag 'user_domains[]', k, @user_domains.has_key?(k), id: "domain-#{k}" %>
    <%= label_tag "domain-#{k}", t("domain.#{k}") %>
  </div>
<% end %>

參數:

  1. 第一個參數 name
  2. 第二個參數 value
  3. 第三個參數 是否 checked

collection_check_boxes

models/owner.rb

has_many :items

models/item.rb

belongs_to :owner

view :

collection_check_boxes(:item, :owner_id, Owner.all, :id, :name)

file

一般用法

<%= form_for @user, url: users_path, method: :post, html: { class: 'form-inline' } do |f| %>
  <%= f.file_field :name %>
<% end %>

Rename resource

<%= form_for @user, as: 'man', url: users_path, method: :post, html: { class: 'form-inline' } do |f| %>
  <%= f.file_field :name %>
<% end %>

<input type="text" name="man[name]" id="man_name">

label + text_field

<%= f.label :nickname %>
<%= f.text_field :nickname %>

# 讓 text input disable
disabled: true

# 重新給值或 format datetime
value: @case.deadline.strftime("%Y-%m-%d")

數字

<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>

text_area

f.text_area :bio

button

<button>

<%= button_tag(type: 'submit', class: "btn btn-primary") do %>
 <i class="icon-ok icon-white"></i> Save
<% end %>

<input type="submit">

f.submit "Submit", :disable_with => 'Submiting...'
submit_tag "Submit", id: "foo-submit", data: { disable_with: "Please wait..." }

form

<%= form_for @user, url: user_path(@user), html: { method: :put, id: 'edit-user' } do |f| %>

取值

@user.email 或
f.object.email

刪除 hidden 欄位 - utf8

<%= form_for (略), enforce_utf8: false %>

兩者是一樣意思的

<%= link_to user.contacts.name, contact_path(user.contact) %>
<%= link_to user.contacts.name, user.contact %>

block 寫法

<%= link_to edit_user_registration_path do %>
    <span class="glyphicon glyphicon-user" aria-hidden="true"></span>
    Edit profile
<% end %>

表單 ajax (使用 Unobtrusive JavaScript - UJS)

<%= link_to 'ajax show', event_path(event), :remote => true %>
form_for @user, :remote => true

ajax 送出加上 remote: true

Ajax Link

link_to 'Del', post_path(post), class: 'btn btn-danger', method: :delete, data: {confirm: 'Are you sure?', disable_with: 'Removing'}

label

f.label :name

tag

<%= label_tag "domain-#{k}", t("domain.#{k}") %>

參數:

  1. id
  2. 顯示名稱

div

<% @users.each do |u| %>
    ...
    <%= div_for u do %>                 <div id="user_<%= u.id %>" class="user">
        <%= u.ages %>      same as =>     <%= u.ages %>
    <% end %>                           </div>
    ...
<% end %>

dom id

dom_id(@user) => user_2

輸出

nil 即顯示預設值

<%= @xxx || 'none' %>

輸出換行字元,Replace \n<br>

<%= h(c.text).gsub("\n", "<br>").html_safe %>

截斷指定長度的字串

truncate(user.about_me, length: 100)
 => Stella appositus odio cilicium. Adopto quia magni textus stips libero vergo enim. Iste delibero c...

脫逸

脫逸

<%= @user.ages %>

不脫逸

<%= raw @user.ages %>

脫逸危險標籤

<%=raw sanitize "<script>alert(1);</script>" %>             # alert(1);

(?)脫逸 javascript

escape_javascript()

# 可以縮寫為
j()

數字及日期

數字口語化

number_to_human 1234567890                                      # 1.2 十億

數字 3 位一撇

number_with_delimiter(8400)                                     # 8,400

貨幣符號

number_to_currency(8400)                                        # NT$ 8,400.00  # 如果 locale 為 en => $8,400.00


# 強制指定 locale 及小數位數
number_to_currency(8400, precision: 0, locale: 'zh-TW')         # NT$ 8,400

在 model 裡面用 :

  1. ActiveSupport::NumberHelper::number_to_currency(self.price, unit: '$', precision: 0)
  2. extend ActionView::Helpers::NumberHelper

Percent

number_to_percentage("98")                                       # => 98.000%
number_to_percentage(100, precision: 0)                          # => 100%

日期

time_ago_in_words current_user.created_at                       # 大約 6 小時

語意化

名詞單複數

pluralize(3, "user")                                            # 3 users

標題化 - 單字的開頭大寫

"man from the boondocks".titleize                               # Man From The Boondocks

連接詞 (and)

['one', 'two', 'three'].to_sentence                             # one, two, 和 three

Debug

It will return a <pre> tag that renders the object using the YAML format

<%= debug @article %>

Displaying an instance variable, or any other object or method, in YAML format

<%= simple_format @article.to_yaml %>

Displaying object values

<%= [1, 2, 3, 4, 5].inspect %>

簡化判斷 + 迴圈的寫法

View 迴圈

如果要使用 each 前需要先判斷他是否為 nil 或 empty, 否則物件空的可能會噴 Error

此寫法只適合傳回空值是 empty 的, 因為可以將 if 及 each 寫在同一行, 就不需要兩層了(if 一層, each 一層),

但在寫法上也比較麻煩一些, 得先知道他是 empty, 還是 nil

取多筆時, 如果不存在, 返回 empty 的話, 如下 :

>   @posts = Post.where(user_id: 3333)
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 3333]]
 => #<ActiveRecord::Relation []>

因為返回的是 empty, 不是 nil 所以它會是 ture, 以下寫法可以將 ifeach 寫成同一行

<% if @posts.each do |post| %>
  <%= post.title %>
<% end.empty? %>
  You have no posts.
<% end %>

不顯示錯誤訊息的話 :

<% if @posts.each do |post| %>
  <%= post.title %>
<% end.empty?; end %>

對於不顯示錯誤訊息的寫法我覺得 end 那邊有點稍亂

希望能繼續找到更好的寫法

其他

判斷目前所在頁面

current_page?(root_path)