今天要講Stimulus & Datatable
的用法,不過不會Stimulus
的讀者們不用擔心,因為在Rails
可以寫 Datatable
的方式相當多種
DOMContentLoaded
hook 執行Stimulus
我們也在 Day26, Day27介紹如何使用Stimulus
,讀者都可以查閱。
安裝 dataTable,以及對應的 package.json
npm install datatables.net
npm install datatables.net-bs4
{
"name": "tungrp_backend",
"private": true,
"dependencies": {
// ...
"datatables.net-bs4": "^1.10.22",
"datatables.net-dt": "^1.10.22",
// ...
},
"version": "0.1.0",
"devDependencies": {
"webpack-dev-server": "^3.11.0"
}
}
在剛接觸dataTable的時候,有看過一些使用者會安裝相關的Gem
,不過後來發現只要掌握些精髓,就不用使用額外的第三方來使用!
另外,dataTable 是基於JQuery
的套件,因此會出現很多JQuery
的語法
首先thead
必須要我們手動加入,我們將頁籤1加上欲加入的內容。我們將table
加入了id=blog-lists
,會在 JS
的部分使用
= tag.div data: { controller: 'admin--blogs',
'admin--blogs-view-id-value': 40,
'admin--blogs-reload_at-value': Time.current.strftime('%F %T') }
/ 卡片內容
= card do
= card_header(title: '部落格列表') do
= link_to '新增', '#', class: 'ml-auto btn btn-primary',
data: { toggle: 'modal', target: '#new-blog-modal' }
= card_body do
= tab_list(blog_genre)
= tab_contents do
/ 當前顯示內容
= tab_active_content(blog_genre.first[:id]) do
/ 頁籤1內容
table.table.data-table#blog-lists
thead
tr
th.col-2 id
th.col-2 創建時間
th.col-1 文章分類
th.col-1 標題
th.col-1
表單最重要的當然是濾除功能,因此我們將欲濾除的樣式加上去
= form_tag '#', id: 'reset_usage' do
= tag.div data: { controller: 'admin--blogs',
'admin--blogs-view-id-value': 40,
'admin--blogs-reload_at-value': Time.current.strftime('%F %T') }
/ Stimulus 卡片內容
= card do
= card_header(title: 'Stimulus')
= card_body do
= tag.input type: 'text', data: { 'admin--blogs-target': 'name' }
= button_tag '輸出', type: 'button', data: { action: 'click->admin--blogs#greet' },
class: 'btn btn-primary mx-2'
= tag.span data: { 'admin--blogs-target': 'output' }
/ 卡片內容
= card do
= card_header(title: '部落格列表') do
= link_to '新增', '#', class: 'ml-auto btn btn-primary',
data: { toggle: 'modal', target: '#new-blog-modal' }
= card_body do
= tab_list(blog_genre)
= tab_contents do
/ 當前顯示內容
= tab_active_content(blog_genre.first[:id]) do
= tag.div class: 'd-flex ml-1 mb-1 flex-nowrap'
= datatable_search_field placeholder: '搜尋部落格標題/內容', form: 'reset_usage',
data: { 'admin--blogs-target': 'keyword' }
= button_tag '清除重填', type: 'reset', form: 'reset_usage',
class: 'btn btn-secondary btn-sm ml-2 mb-2',
data: { action: 'admin--blogs#reset', 'admin--blogs-target': 'resetBtn' }
= tag.div class: 'd-flex mb-2 flex-nowrap'
= datatable_select_tag [['請選擇', nil], *Blog.genres.to_a], 'genre-select',
controller: 'admin--blogs', target: 'genre', form: 'reset_usage'
/ 時間區間
= search_interval(controller: 'admin--blogs', form: 'reset_usage')
/ 頁籤1內容
table.table.data-table#blog-lists
thead
tr
th.col-2 id
th.col-2 創建時間
th.col-1 文章分類
th.col-1 標題
th.col-1
其中有幾個之前提過的重點
我們在 Day22 提過時間區間helper ➡️ search_interval
我們知道按鈕一共有三個屬性 ➡️ 這裡用的是type=reset
屬性
使用該屬性,可以很輕鬆的將表單內的元素重置,但前提是要在同個表單內,所以我們需要用到Day25 提到的form屬性指定特定form的方式。我們將負責清除重填的工作交給 = form_tag '#', id: 'reset_usage' do
,利用 Day25 提到的特性,將其搬往外面,而不影響其內部的樣式排列。
以下為加入濾除功能樣式 & 清除重填的功能後的結果。目前只有清除功能,尚未有顯示表單的功能,以及濾除功能。
dataTable 相關的後端邏輯我們寫在Controller,DataTable 會透過給參數、打json
的方式呼叫後端,再將結果回傳給 DataTable
module Admin
class BlogsController < ApplicationController
def index
respond_to do |format|
filtered_objects = Blog.ransack(search_params).result
results = filtered_objects.offset(params[:start])
.limit(params[:length])
# html 內容
format.html
# json 內容 (給 dataTable 取用)
format.json {
render json: {
draw: params[:draw].to_i,
recordsTotal: filtered_objects.count,
recordsFiltered: filtered_objects.count,
data: results.map do |result|
[result.id, result.created_at.strftime('%F %T'), result.genre, result.title, result.id]
end
}
}
end
end
private
def search_params
return if params[:ransack_search].nil?
params.require(:ransack_search)
.permit(:genre_eq, :title_or_content_cont,
:created_at_gteq, :created_at_lt)
end
end
end
其中我們看下列這行,其實是使用搜尋功能的一個名叫ransack
的gem
,它可以很簡單的將我們要搜尋的欄位轉換成SQL
語句,而使用的Strong Params
在 search_params
。
filtered_objects = Blog.ransack(search_params).result
由於這篇的重點在dataTable
,並且在這系列的文章內,都還沒有提過基本的Sql Statement
,因此這邊,讀者就先將搜尋當作是黑盒子,等到後面的篇章介紹 model
/ orm
/ ransack
的用法會再回頭講。
另外,render :json
大括號包覆住的 data
,即為Table
內容,裡頭的陣列分別對應Table的 <thead>
屬性。
Controller 的 render,除了渲染基本的html
,還可以渲染其他種類型
js.erb
講完基本的controller
的設定,接著開始講 JS 的部分
我們先將基本的 dataTable 功能上去
import { Controller } from 'stimulus';
const previousTarget = ["name", "output", "searchedContent", 'brand', 'store', 'ajaxId']
const dataTableTarget = ['genre', 'startedAt', 'endedAt', 'keyword', 'resetBtn']
export const ajaxReload = (table) => () => table.api().ajax.reload();
export default class extends Controller {
static targets = [...previousTarget, ...dataTableTarget]
static values = { testId: Number, viewId: Number, reloadAt: String, queryUrl: String }
connect() {
/* DataTable with server side render */
let table = this.dataTable();
}
// dataTable
dataTable() {
return $('#blog-lists').dataTable({
serverSide: true,
bStateSave: true,
bAutoWidth: false,
searchDelay: 1200,
ordering: false,
searching: false,
language: { url: '/datatable.chinese_traditional.lang.json' },
ajax: {
url: '/admin/blogs.json',
data: {
// 先省略濾除內容...
},
},
'rowCallback': (row, data) => {
$('td:eq(4)', row).html(this.blogShowButton(data[4]));
}
});
}
// 部落格詳細頁
blogShowButton(data) {
return `<div class="d-flex justify-content-end">
<a class="btn btn-info ml-2" href="/admin/blogs/${data[2]}">詳細頁</a>
</div>`
}
}
在Stimulusconnect
hook加入下列這行,該Table就會被DataTable
所調用
let table = this.dataTable();
題外話,我們將該 Table 加入樣式display: none;
/ 頁籤1內容
table.table.data-table#blog-lists(style="display:none;")
thead
tr
th.col-2 id
th.col-2 創建時間
th.col-1 文章分類
th.col-1 標題
th.col-1
會發現,有些已經被DataTable
調用的地方藏不住。我曾經藏試過藏Table,發生了這種狀況,所以才有這個小提醒 ?
我們先看加入濾除功能後的效果如何 ⬇️
接著,我們介紹加上濾除功能的JS
要如何寫。
const previousTarget = ["name", "output", "searchedContent", 'brand', 'store', 'ajaxId']
const dataTableTarget = ['genre', 'startedAt', 'endedAt', 'keyword', 'resetBtn']
/* 重新載入table */
export const ajaxReload = (table) => () => table.api().ajax.reload();
export default class extends Controller {
static targets = [...previousTarget, ...dataTableTarget]
static values = { testId: Number, viewId: Number, reloadAt: String, queryUrl: String }
connect() {
/* DataTable with server side render */
let table = this.dataTable();
/* addEventListener: change */
dataTableTarget.forEach((e) => this[`${e}Target`].addEventListener('change', ajaxReload(table)))
/* 搜尋框 */
this.keywordTarget.addEventListener('input', ajaxReload(table))
/* 重置按鈕 */
this.resetBtnTarget.addEventListener('click', ajaxReload(table))
}
// dataTable
dataTable() {
return $('#blog-lists').dataTable({
serverSide: true,
bStateSave: true,
bAutoWidth: false,
searchDelay: 1200,
ordering: false,
searching: false, // 搜尋關掉。
language: { url: '/datatable.chinese_traditional.lang.json' },
ajax: {
url: '/admin/blogs.json',
data: {
'ransack_search[created_at_gteq]': () => this.startedAtTarget.value,
'ransack_search[created_at_lt]': () => this.endedAtTarget.value,
'ransack_search[genre_eq]': () => this.genreTarget.value,
'ransack_search[title_or_content_cont]': () => this.keywordTarget.value,
},
},
'rowCallback': (row, data) => {
$('td:eq(4)', row).html(this.blogShowButton(data[4]));
}
});
}
reset() {
this.resetBtnTarget.click()
}
// 部落格詳細頁
blogShowButton(data) {
return `<div class="d-flex justify-content-end">
<a class="btn btn-info ml-2" href="/admin/blogs/${data[2]}">詳細頁</a>
</div>`
}
}
這裡有幾個重點想要講,首先先講解 dataTable 的參數
api().ajax.reload() # 重新載入Table
serverSide: true # 如果此值為 false,伺服器會將資料全撈,並且濾除的功能會在客戶端做。
rowCallback # 渲染之前的 callback
data request.parameters
關於rowCallback
,將詳細頁的按鈕先寫成一個常數,在第5個位置(從0開始算
)來使用它。
// 部落格詳細頁
blogShowButton(data) {
return `<div class="d-flex justify-content-end">
<a class="btn btn-info ml-2" href="/admin/blogs/${data[2]}">詳細頁</a>
</div>`
}
關於data
為傳到後端的參數,這裡指的是負責濾除的值,也就是前面提到的ransack
清除重填的地方。我們來分別看對應的Slim
& JS
= button_tag '清除重填', type: 'reset', form: 'reset_usage',
class: 'btn btn-secondary btn-sm ml-2 mb-2',
data: { action: 'admin--blogs#reset', 'admin--blogs-target': 'resetBtn' }
reset() {
this.resetBtnTarget.click()
}
這邊的reset
只做一件事情,就是點擊的時候會再點擊一次。會做這件事情的目的為,當我們按下了清除重填,因為生命週期的關係,送出新的DataTable的時間會比清除資料的時間還要早,所以再按下清除重填的時候,非同步的資料還停留在清除以前的狀態。
因此我們再按按鈕的時候會再觸發一次點擊,等於是兩次點擊。這樣一來就會變成 點擊 ➡️ Ajax表單送出(非預期的結果) ➡️ 輸入框清除 ➡️ 再點擊一次 ➡️ Ajax表單送出(預期結果)➡️ 輸入框再清除一次
對於寫糞code的工程師來說,我發現click()
還蠻好用的
event.target.click()
目前部落格頁面如下
如果想要變成點擊新增為active
,讀者要怎麼變 ⬇️
除了直接改變畫面以外,我們還可以寫一個點擊特殊新增
的腳本,只要我們這樣寫即可 ⬇️
首先我們先將頁籤新增data
屬性
def blog_genre
[
{ id: 'index', wording: '列表頁', data: { 'admin--blogs-target': 'datatable' } },
{ id: 'special', wording: '特殊新增', data: { 'admin--blogs-target': 'multiform' } },
]
end
對應的頁籤畫面為
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<a
href="#index-tab"
class="nav-link"
data-toggle="tab"
data-admin--blogs-target="datatable"
aria-controls="index-tab"
aria-selected="true"
>列表頁</a
>
</li>
<li class="nav-item" role="presentation">
<a
href="#special-tab"
class="nav-link active"
data-toggle="tab"
data-admin--blogs-target="multiform"
aria-controls="special-tab"
aria-selected="false"
>特殊新增</a
>
</li>
</ul>
接著我們在 connect()
執行腳本。
export default class extends Controller {
static targets = ["datatable", "multiform"]
static values = { testId: Number, viewId: Number, reloadAt: String, queryUrl: String }
connect() {
/* 腳本內容 */
this.multiformTarget.click();
}
}
除了上述的腳本內容,我們也可以在腳本寫比較複雜的內容,例如點擊按鈕開啟視窗,並在編輯表單上填寫預設值等等。
之前曾經接過一個任務,任務內容為在點開下拉式選單時,要依據不同筆的資料填寫不同的值,因此我就在點擊事件的動作,觸發寫好的腳本。這樣一來就不用動到已經大到不行的專案,只需寫腳本即可。
覺得寫這篇需要比較多的背景常識,很怕讀者看不懂我想要表達什麼? 如果有問題的歡迎在下方留言
或者私訊我的信箱 ➡️ k445566778899k@gmail.com
或許讀者的意見,可以讓我補充原本過於缺漏的內容