跳至主要内容

[Rails] 小技巧 Rails Tips

keywords: helper

[TOC]

常用

  • [判斷內容是空或有]([Rails] 判斷內容是空或有.md)
  • [判斷物件有哪些方法?上一層是誰?屬於哪個型態]([Rails] 判斷物件有哪些方法?上一層是誰?屬於哪個型態.md)

其他

清除 Rails 內的快取(Cache)

# 必要時需要手動 rm -rf 刪除
$ rails log:clear
$ rake tmp:clear
$ rm -rf tmp/cache tmp/data tmp/miniprofiler

檢視 secret / credentials 檔案

$ EDITOR="sublime -w" rails credentials:edit
$ EDITOR=vim rails credentials:edit

SCSS 載入圖片

在 SCSS 中使用圖片 url 時,需使用 image-url 這個 helper,否則開發環境看得到,但到 production mode 就會炸掉:

.banner {
background: image-url('banner.png') center center no-repeat;
}

How to reference images in CSS within Rails 4 @ StackOverflow

如果是在 Webpacker 中要用圖片,則是要使用 asset-image() 這個 helper:

// 圖片路徑 app/assets/images/event-header-bg.png
background-image: asset-image('event-header-bg');

自訂錯誤訊息樣式(Customize Field Error)

# ./config/environment.rb
ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
html_tag.html_safe
end

Customize Field Error in Rails 5 @ Ruby Plus

Methods

陣列(Array)

元素分群

# 把 @products 這個大陣列以 3 個為一組拆成小陣列,不足的用 false 填補
Array.in_groups_of(3, false){|group|}

取得陣列元素數目

# 取得陣列元素數目建議使用 size 效能較好(替代掉 count 或 length)
Order.all.size

產生 Secure Token

在建立某欄位的時候自動產生 Token:

# ./app/models/event.rb
class Event < ApplicationRecord
validates_uniqueness_of :authentication_token
before_create :generate_unique_secure_token

private
def generate_unique_secure_token
# return if self.authentication_token.present?
loop do
token = SecureRandom.base58(24)
self.authentication_token = token
break token unless Event.find_by(authentication_token: token)
end
end
end

驗證該 token 是否符合格式:

# ./app/controllers/api_controller.rb
def token_format_valid?(authentication_token)
base58_alphabet = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - ["0", "O", "I", "l"]
(authentication_token.split('') - base58_alphabet).blank? && authentication_token.size == 24
end

View Helper

<!-- 刪減多於字數 -->
<%= truncate("Once upon a time in a world far far away", length: 17) %>

<!-- 檢驗路由是否為 -->
<% current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2') %>
<% current_page?('/shop/checkout') %>

常用變數

<!-- in erb file -->
<%= root_url %> <!-- 取得根網址 -->
<%= controller_name %> <!-- 取得 controller 名稱 -->
<%= action_name %> <!-- 取得 action 的名稱 -->

動態 i18n

# zh-TW.yaml
form:
en: 英文
zh-TW: 中文
<!-- 直接使用 t("form.#{...}") 即可動態使用 i18n
<% I18n.available_locales.each do |locale| %>
<%= t("form.#{locale}") %>
<% end %>

避免 HTML Encode

<%= raw(data) %>
<%= string.html_safe %>

如果有傳入 local_variable 才使用該變數

keywords: local_assigns
<!-- Without default value -->
<% if local_assigns.has_key? :headline %>
Headline: <%= headline %>
<% end %>

<!-- With default value -->
<% some_local = default_value if local_assigns[:some_local].nil? %>

Optional local variables in rails partial templates @ StackOverflow

使用 content_tag 自定義元素

do...end block 中如果希望有 if 判斷,可以在 end 後面使用 if

<%= content_tag :p, 'Hello', class: 'mb-1' %>

<!-- 使用 do end block -->
<%= content_tag :p, class: 'mb-1' do %>
<i class="fas fa-fw fa-globe"></i>
<%= link_to 'Website', @event.website, class: 'pl-2', target: :_blank %>
<% end if @event.website.present? %>

使用 radio 和 label

keywords: f.radio_button, label_tag
<div class="form-group">
<label>Gender</label>
<div class="w-100"></div>
<div class="custom-control custom-radio custom-control-inline">
<%= f.radio_button :gender, 'male', checked: @personal_info.gender == 'male', class: 'custom-control-input' %>
<%= label_tag 'personal_info_gender_male', 'Male', class: 'custom-control-label' %>    </div>
<div class="custom-control custom-radio custom-control-inline">
<%= f.radio_button :gender, 'female', checked: @personal_info.gender == 'female', class: 'custom-control-input' %>
<%= label_tag 'personal_info_gender_female', 'Female', class: 'custom-control-label' %>    </div>
</div>

選擇國家的下拉選單(select)

# app/helpers/common_helper.rb
def country_options(selected = nil, options = nil, locale = I18n.locale)
options =
if options.blank?
ISO3166::Country.translations(locale).sort.to_h.invert.entries
else
ISO3166::Country.translations(locale).select{ |k, v| options.include?(k) }.sort.to_h.invert.entries
end
options_for_select(options, selected)
end
<!-- _form.html.erb -->
<div class="form-group">
<%= f.label :country, class: 'col-form-label fz-13 text-muted' %>
<%= f.select(:country, country_options(event.country), { prompt: '- 請選擇 -' }, { class: 'custom-select' }) %>
<div class="invalid-feedback feedback-icon">
<i class="fas fa-exclamation-triangle"></i>
</div>
</div>

在 partial / template 中使用 yield 和 content_for

這是 partial / template,在這裡面的 <%= yield %> 一定要寫在其他有名稱的 <%= yield :foo %> 之前,才能把要 render 它的樣板的內容帶進來:

<%# ./app/views/shared/_navbar.html.erb %>
<div>
<!-- 沒有名稱的 yield 一定要寫在最前面 -->
<%= yield %>

<ul class="left">
<%= yield :before_left_nav %>
<li>Some items ...</li>
<%= yield :after_left_nav %>
</ul>

<ul class="right">
<%= yield :before_right_nav %>
<li>Some items ...</li>
<%= yield :after_right_nav %>
</ul>
</div>

這是要 render 上面這個 partial 的樣板,在 renderdo ... end 中的所有內容都會帶入到 _navbar.html.erb 裡面的 <%= yield %> 裡面,在從中看要對應到哪個有名稱的 yield

<%# ./app/views/pages/tester.html.erb %>

<%= render 'shared/navbar', navbar_class: 'navbar-darker-background' do %>
<% content_for :before_left_nav do %>
<li class="nav-item">
<%= link_to 'Before', products_path, class: "nav-link #{'active' if action_name == 'products'}" %>
</li>
<% end %>

<% content_for :after_left_nav do %>
<%# ... %>
<% end %>

<% content_for :before_right_nav do %>
<%# ... %>
<% end %>

<% content_for :after_right_nav do %>
<%# ... %>
<% end %>
<% end %>

Controller

在 controller 轉譯某個 partial view

# app/controllers/*_controller.rb
# 把轉譯出來的 html 塞到 innerHTML 這個變數
innerHTML = ApplicationController.renderer.render(partial: 'projects/collects/issue_next', locals: { app: @app, collect: @collect, issue: @issue, editable: true })

建立一個 controller helper 讓所有 controller 都能用

將要共用的 helper 寫在 application_controller.rb 中:

# ./app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
# STEP 2:補上 helper_method
helper_method :current_cart

protected

# STEP 1: 將要共用的 method 寫在這
def current_cart
@current_cart ||= Cart.find_by!(user: current_user)
rescue ActiveRecord::RecordNotFound
Cart.create(user: current_user)
end

end

使用 transaction

透過 Model.transaction@instance.transaction 當 model 變更的過程中如果有任何錯誤發生,則這個變更會整個 rol back 回去。

# ShoppingMall/app/controllers/admin/orders_controller.rb

def shipped
if @order.may_mark_as_shipped?
begin
Order.transaction do
@order.mark_as_shipped!

redirect_to admin_orders_url, notice: '訂單狀態變更成功'
end
rescue
redirect_to admin_orders_url, alert: '訂單狀態變更失敗'
end
else
redirect_to admin_orders_url, alert: '訂單狀態錯誤'
end
end

動態 Hash key 、動態呼叫 methods、移除空值

keywords: dynamic hash key, dynamic invoke methods, reject empty item
  • 使用 Hash[<key>, <value>],可以動態建立 Hash 的 Key
  • 使用 project.send(<method>) 可以動態的呼叫 project 的方法
available_medias = ["website", "facebook", "twitter", "instagram"]

project_links = available_medias.map do |media|
Hash[media, project[:"#{media}"]] if project[:"#{media}"]
end.reject(&:blank?)

一次取出所有 id 的值(ids)

User.all.ids      # 回傳陣列,可以取得所有 User 的 id

Models

使用 params

keywords: fetch, dig
# 一般取得 params 的方式
params[:product][:remove_photos_index]

# 如果有定義 params 的話
product_params

def product_params
params.fetch(:product, {}).permit(:name, :brief, :description)
# params.require(:product).permit(:name, :brief, :description)
end
# 使用 dig 取得 order 裡面的 promo_code 時,如果 promo_code 不存在時程市不會炸掉
unless params.dig(:order, :promo_code).blank?
promo_code = PromoCode.where(code: params.dig(:order, :promo_code)).first
@order.promo_code = promo_code
end

可參考 Action Controller Overview

操作 params

keywords: update model, params, delete
刪除某個 params
# 刪除 product params 中的 photos
params[:product].delete(:photos)
把 params 存起來
# 使用 update
@product.update(product_params)

# 使用 assign_attribute
@product.assign_attributes(product_params)

使用 Safe Navigation Operator (&.

keywords: &.

不用擔心前面的值是 nil 時會拋錯誤(undefined method for nil:NilClass),類似於前面存在時才會跑後面

@person&.spouse&.name

@person.spouse.name if @person && @person.spouse # 等同於上面那句

但是不可作為判斷

nil.nil?    # true
nil&.nil? # nil
nil&.foobar # nil

What does ampers and dot mean in ruby @ Stack Overflow

<meta name="turbolinks-visit-control" content="reload" />

使用矛點 # 的連結卻會重新 refresh 頁面

在目前 turbolinks 使用 anchor links<a href="#foo") 還是會 refresh page,這時候可以加上 data-turbolinks="false"

<!-- 此 issue 正在修正中 -->
<a href="#foo" data-turbolinks="false"></a>

為了避免使用 data-turbolinks="false" 之後,使用者無法重新返回上一頁,可以使用 window.location 搭配 window.history.pushState() 來保留 turbolinks 的 state 物件:

此 Issue 正在修正中 @ Turbolinks Github

const turbolinksState = { ...window.history.state };
$('[href^="#"]').off('click');
$('[href^="#"]').on('click', (e) => {
Rails.stopEverything(e);
let anchor = e.target.getAttribute('href');
window.location = anchor;
window.history.pushState(turbolinksState, null);
});

留意 window.history.pushSate() 需放在 window.location 才不會又被 turbolinks 清空。

解決使用 Vue 時被 cache 的問題

VueJS and Turbolinks @ Turbolinks Wiki

檢視錯誤訊息

order.mark_as_canceled!      # ROLLBACK => false
order.errors # 檢視錯誤訊息
$! # 前一次拋出的錯誤訊息

設定 config

支援 ES6

要讓 rails 能夠正常編譯 ES6 的語法,需要在 OnePageShop/config/environments/production.rb 這支檔案中,把 config.assets.js_compressor = :uglifier 改成 config.assets.js_compressor = Uglifier.new(harmony: true)

# OnePageShop/config/environments/production.rb
config.assets.js_compressor = Uglifier.new(harmony: true)

開發者工具

Pry-Rails (gem)

作用:在 .erb 檔中,加上 <%= binding.pry %> 當程式執行到該處時,會停在那。按 Ctrl + D 可以跳出 Pry。

# in pry
$ exit-program # 跳出所有 pry
$! # 列出錯誤訊息

安裝:透過在 Rails 中的 Gemfile 安裝 gem

group :development do
# Use Pry as your rails console
gem 'pry-rails', '~> 0.3.7'
end

Rails Panel (Chrome extension) and meta_request (gem)

作用:可以在 chrome dev tool 中看到和 rails 相關的變數資訊

安裝

  • STEP1: 安裝 Chrome Extension Rails Panel
  • STEP2: 透過 Rails 中的 gemfile 安裝 gem:
# 寫在 gemfile 中,接著執行 bundle install
group :development do
# Supporting gem for Rails Panel (Google Chrome extension for Rails development)
gem 'meta_request', '~> 0.4.3'
end

web-console(gem)

作用:可以在 view 中使用 <%= console %>,接著就可以在 view 中使用 Rails 的指令測試。

安裝:透過 Rails 中的 Gemfile 安裝 gem

group :development do
# Access an IRB console on exception pages or by using <%= console %> anywhere in the code.
gem 'web-console', '>= 3.7.0'
end