今天要開始介紹,如何做自定義helper
,並使用。
helpers
依照慣例會寫在app/helpers
裡面,並且不管寫在哪個檔案,view
都讀得懂。我們不用知道在Rails Application
是怎麼樣載入helper
,只需要知道寫在helper
的東西都可以取用就好了。helper
在rails
為module
,我們在Day14介紹如何使用module#include
繼承裡面的方法,而我們只要有初步的認知是這些helper
在Rails
是被繼承的就好,至於是怎麼繼承的,有興趣再深入瞭解 ?
下面的樣式有部分出自sb-admin-2,讀者們可以參考下載連結內部的樣式並取用。
以下為卡片以及相關的 helper
module ApplicationHelper
# 版型規格
def grid_div(left = 1, right = 2, style: '')
tag.div(style: "display:grid; grid-template-columns: #{left}fr #{right}fr; grid-auto-rows: minmax(50px, auto); #{style}") {yield if block_given?}
end
# card, card_header, card_body, card_footer 為卡片樣式公用規格
def card(controller: nil, data: nil)
data_attribute = data.present? ?
{ controller: controller }.merge(data) :
{ controller: controller }
content_tag(:div, class: 'card shadow mb-4', data: data_attribute) do
yield if block_given?
end
end
def card_header(title:, &block)
content_tag(:div, class: 'card-header py-3') do
content_tag(:h6, class: 'h6 m-0 font-weight-bold text-primary d-flex align-items-center') do
content_tag(:span, title) + (capture(&block) if block_given?)
end
end
end
def card_body
content_tag(:div, class: 'card-body') { yield if block_given? }
end
def card_footer
content_tag(:div, class: 'card-footer') { yield if block_given? }
end
end
= card do
= card_header(title: '母訂單列表') do
= card_body do
= tag.div '卡片內容'
= card_footer do
= tag.div '卡片footer'
= grid_div(style: 'row-gap: 10px; column-gap: 10px') do
- (1..8).each do |num|
= card do
= card_header(title: "標題#{num}") do
= card_body do
= tag.div '卡片內容'
= card_footer do
= tag.div '卡片footer'
若改為左右比1:1
的話,效果如下
= grid_div(1, 1, style: 'row-gap: 10px; column-gap: 10px') do
/ ...
彈跳視窗相關的 helper
module ApplicationHelper
def modal(id: nil, confirm_wording: '確認', confirm_form:,
confirm_target: nil, title: nil, controller: nil,
close_btn: '取消')
content_tag :div, id: id, class: 'modal fade',
tabindex: -1, role: 'dialog', aria: { hidden: true },
data: { "#{controller}-target": 'modal' } do
content_tag :div, role: 'document', class: 'modal-dialog' do
content_tag :div, class: 'modal-content' do
# Header
content_tag(:div, class: 'modal-header') do
content_tag(:strong, title) +
button_tag(type: 'button', class: 'close',
data: { dismiss: 'modal' }, aria: { label: 'Close' }) do
content_tag :span, '×', aria: { hidden: true }
end
end +
# Body: 自定義內容交給 yield
content_tag(:div, class: 'modal-body') { yield if block_given? } +
# Footer
content_tag(:div, class: 'modal-footer') do
button_tag(close_btn, type: 'button', class: 'btn btn-warning',
data: { dismiss: 'modal' }, aria: { label: 'Close' }) +
(confirm_wording && button_tag(confirm_wording,
type: 'submit', class: 'btn btn-primary',
"data-#{controller}-target": confirm_target,
form: confirm_form, data: { confirm: "是否確定要送出編輯?\n請注意!送出後無法復原!", disable_with: "載入中..." }))
end
end
end
end
end
end
= link_to '編輯', '#',
class:'btn btn-primary mx-1 mb-3',
data: { toggle: 'modal', target: '#edit-modal' }
= modal(id: 'edit-modal', confirm_wording: '送出',
confirm_form: 'edit_modal', title: '編輯視窗') do
= tag.div "我是彈跳視窗"
其中 tag.div "我是彈跳視窗"
為自定義內容,其他都為 modal
方法(元件)的一部分。
module ApplicationHelper
def admin_sidebar_block(topic:, genre:, icon: 'fa-solar-panel')
content_tag(:li, class: 'nav-item') do
content_tag(:a, class: 'nav-link', href: '#', data: { toggle: 'collapse',
target: "##{genre}" }, aria: { expanded: true, controls: genre }) do
content_tag(:i, nil, class: "fas fa-fw #{icon}") + content_tag(:span, topic)
end + \
content_tag(:div, class: 'collapse', data: { parent: '#accordionSidebar' },
aria: { labelledby: genre }, id: genre) do
content_tag(:div, class: 'bg-white py-2 collapse-inner rounded') { yield if block_given? }
end
end
end
def sidebar_link_to(path, wording = nil)
if wording
link_to(content_tag(:span, wording), path, class: 'collapse-item')
else
link_to(path, class: 'collapse-item') { yield if block_given? }
end
end
end
使用方式為
= admin_sidebar_block topic: '策展', genre: 'curation', icon: 'fa-solar-panel' do
= sidebar_link_to admin_brand_home_pages_path(current_brand), '首頁管理'
= sidebar_link_to admin_brand_scrolling_texts_path(current_brand), '跑馬燈管理'
= sidebar_link_to admin_brand_event_pages_path(current_brand), '促銷頁管理'
= sidebar_link_to admin_brand_top_banners_path(current_brand), '置頂Banner'
= sidebar_link_to admin_brand_shop_the_looks_path(current_brand), '焦點商品'
= admin_sidebar_block topic: '商品管理', genre: 'products', icon: 'fa-capsules' do
= sidebar_link_to import_export_admin_brand_products_path(current_brand), '匯入/匯出'
= sidebar_link_to admin_brand_products_path(current_brand), '商品總覽'
= sidebar_link_to admin_brand_series_index_path(current_brand), '系列'
= sidebar_link_to admin_brand_categories_path(current_brand), '大類管理'
= sidebar_link_to admin_brand_collections_path(current_brand), '中類管理'
= sidebar_link_to admin_brand_sub_collections_path(current_brand), '小類管理'
= admin_sidebar_block topic: '訂單管理', genre: 'orders', icon: 'fa-envelope' do
- if can_edit?(:user)
= sidebar_link_to admin_orders_path, '母訂單列表'
= sidebar_link_to admin_brand_sub_orders_path(current_brand) do
= tag.span '訂單總覽'
= sidebar_link_to admin_unshipped_orders_path do
= tag.span '待出貨訂單'
= tag.span unshipped_order_count, class: 'badge badge-secondary ml-1'
= sidebar_link_to admin_return_orders_path do
= tag.span '退貨訂單'
= tag.span return_orders_count, class: 'badge badge-secondary ml-1'
- if can_edit? :user
= sidebar_link_to admin_pay_failed_orders_path do
= tag.span '刷退失敗訂單'
= tag.span pay_failed_orders, class: 'badge badge-secondary ml-1'
= admin_sidebar_block topic: '通知中心', genre: 'notifications', icon: 'fa-carrot' do
= sidebar_link_to admin_brand_push_notifications_path(current_brand), '推播列表'
= admin_sidebar_block topic: '行銷管理', genre: 'promotions', icon: 'fa-cart-plus' do
= sidebar_link_to admin_brand_promotions_path(current_brand), '品牌折扣'
= sidebar_link_to admin_target_price_discounts_path, '全館滿額'
以下為時間區間的helper
表單內常見的搜尋時間區間,我也有做相對應的helper
以方便好作取用。
關於裡面的參數內容我們先忽略。一個與表單有關、另外一個為stimulus
所取用的框架。
= search_interval(controller: 'orders', form: 'export-orders')
以下為 search_interval
的內容,而我們用了datatable_date_tag
來對原本的date_field_tag
y 做加工。
module ApplicationHelper
def search_interval(controller: nil, form: nil)
tag.div class: 'input-group mb-3' do
# 開始時間
datatable_date_tag(controller: controller, target: 'startedAt',
value: Date.today - 1.year, form: form,
name: 'ransack_search[created_at_gteq]') +
# 到
tag.div(class: 'input-group-append',
style: 'height: 100%;') { tag.label '至', class: 'input-group-text' } +
# 結束時間
datatable_date_tag(controller: controller, target: 'endedAt',
value: Date.today + 1.day, form: form,
name: 'ransack_search[created_at_lt]')
end
def datatable_date_tag(options = {})
stimulus_data = (options[:controller] &&
{ "#{options[:controller]}-target": options[:target] }).presence || {}
date_field_tag((options[:name] || options[:target]&.to_sym),
options[:value], class: 'form-control', style: 'height: 100%; max-width: 220px;',
data: stimulus_data.merge(options[:data].presence || {}),
form: options[:form],
disabled: options[:disabled] || false)
end
end
end
雖然bootstrap
本身已經做足了優化,但將其作為helper
使用,可以讓我們更便利使用。
= title '自我介紹'
= card do
= card_header(title: '關於我')
= card_body do
= tab_list(me_genre)
= tab_contents do
/ 當前顯示內容
= tab_active_content(me_genre.first[:id]) do
/ 頁籤1內容
= tag.span "我的興趣"
/ 隱藏內容
= tab_content(me_genre.second[:id]) do
/ 頁籤2內容
= tag.span "我的專長"
/ 隱藏內容
= tab_content(me_genre.third[:id]) do
/ 頁籤3內容
= tag.span "如何聯絡我"
而上述的helper
於下方
module ApplicationHelper
# 頁籤列表與頁籤內容
# @param [Hash] list
# @example: tab_list([{id: 'han001', wording: '漢漢1號'}, {id: 'han002', wording: '漢漢2號'}])
def tab_list(list)
data_attr = -> (content) { content.try(:[], :data).presence || {} }
content_tag :ul, class: 'nav nav-tabs', role: 'tablist' do
list.each_with_index.map do |content, index|
if index.zero?
content_tag(:li,
content_tag(:a, content[:wording], href: "##{content[:id]}-tab",
class: 'nav-link active', data: { toggle: 'tab',
**data_attr.(content) },
aria: { controls: "#{content[:id]}-tab", selected: 'true' }),
class: 'nav-item', role: 'presentation')
else
content_tag(:li,
content_tag(:a, content[:wording], href: "##{content[:id]}-tab",
class: 'nav-link', data: { toggle: 'tab',
**data_attr.(content) },
aria: { controls: "#{content[:id]}-tab", selected: 'false' }),
class: 'nav-item', role: 'presentation')
end
end .join.html_safe
end
end
def tab_contents
content_tag(:div, class: 'tab-content') { yield }
end
# 隱藏內容
def tab_content(id, options = {})
active_class, basic_class = 'show active', 'tab-pane fade'
content_tag(:div,
class: ((options[:active] || false) ?
(basic_class + active_class) : basic_class),
id: "#{id}-tab", role: 'tabpanel', data: options[:data]) { yield }
end
# 當前顯示內容
def tab_active_content(id, options = {})
options = options.merge(active: true)
tab_content(id, options) { yield }
end
end
頁籤內容在下方
module MeHelper
def me_genre
[
{ id: 'interest', wording: '我的興趣' },
{ id: 'skillSet', wording: '我的專長' },
{ id: 'contactMe', wording: '與我聯繫' },
]
end
end
編譯完的html
如下
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="h6 m-0 font-weight-bold text-primary d-flex align-items-center">
<span>關於我</span>
</h6>
</div>
<div class="card-body">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<a
href="#interest-tab"
class="nav-link"
data-toggle="tab"
aria-controls="interest-tab"
aria-selected="true">我的興趣</a>
</li>
<li class="nav-item" role="presentation">
<a
href="#skillSet-tab"
class="nav-link"
data-toggle="tab"
aria-controls="skillSet-tab"
aria-selected="false">我的專長</a>
</li>
<li class="nav-item" role="presentation">
<a
href="#contactMe-tab"
class="nav-link active"
data-toggle="tab"
aria-controls="contactMe-tab"
aria-selected="false">與我聯繫</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fadeshow" id="interest-tab" role="tabpanel">
<span>我的興趣</span>
</div>
<div class="tab-pane fade" id="skillSet-tab" role="tabpanel">
<span>我的專長</span>
</div>
<div class="tab-pane fade active show" id="contactMe-tab" role="tabpanel">
<span>如何聯絡我</span>
</div>
</div>
</div>
</div>
除了寫在helper
以外,另外常見的用法為將畫面抽換成 partial
。我們習慣會在被抽離的模板前面加底線,並且使用render
方法,使用前面為底線的檔案。例如,我們最常使用的方式為將新增、編輯的畫面輸入框的部分抽共用成_form.html.slim
,並且在編輯/新增
使用。
= render partial: 'form', locals: { path: <路徑>, method: :<方法> }
再來介紹使用區塊的例子。下列為 shared/_export_xlsx.html.slim
,一共有兩個yield
,各會被插去客製化的區塊
.row
.col-lg-12
.ibox.float-e-margins
.ibox-title
h5
| 福利中心
.ibox-content
/ 預設區塊
= yield
.ibox-content
= yield :a_section
若我們渲染畫面時要使用 block
時,使用render
字眼會產生錯誤,因此如果有block
出現要改用layout
。
'nil' is not an ActiveModel-compatible object. It must implement :to_partial_path.
以下為使用上述partial
檔案的方法
= render layout: 'shared/export_xlsx',
locals: { path: "/import_example.xlsx" } do
// 插入 a_section
= content_for :a_section do
a.btn.btn-info.m-l-sm href="export.xlsx" 匯出商品
// 插入預設區塊
= tag.span "預設區塊"
今天介紹了自定義的helper
,以及partail
的用法,這些Rails
小技巧能夠讓我們的畫面更簡潔,而畫面的部分大致上介紹到這裡。明天開始,會開始介紹表單。