我們在 Day21 提到 data-remote=true
、Day25 提到了一些與Ajax
相關的例子,今天為正式的介紹在Rails
如何完美的搭配Stimulus & Rails
。今天我們會用3個情境教導讀者如何使用Stimulus Ajax
相依性的下拉式表單為標準以非同步處理的標準模型之一。以下的操作為先選品牌,當選品牌的同時,右方的下拉選單的選項會隨著品牌的不同而有改變
⭐️ 下列為相依性表單的Slim & Stimulus Code
= card(controller: 'admin--blogs') do
= card_header(title: 'Stimulus Ajax')
= card_body do
= tag.div class: 'mx-1 d-flex' do
= datatable_select_tag Brand.all.pluck(:title, :id).prepend(['請選擇品牌', nil]),
'brand-select', controller: 'admin--blogs',
target: 'brand', action: 'fetchStores',
style: 'max-width: 300px'
= datatable_select_tag [['請選擇店舖', nil]], 'store-select',
controller: 'admin--blogs', target: 'store',
style: 'max-width: 300px'
import { isEmpty, isNil, map, prepend, equals } from 'ramda';
import { Controller } from 'stimulus';
import Rails from '@rails/ujs';
/* 下拉式選單的選項 */
const selectOption = ({ value, text, select = null }) => `<option value=${value} ${equals(select, true) ? 'selected' : ''}>${text}</option>`
const fetchStores = ({ brandId, storeComp }) => {
if(isEmpty(brandId)) {
storeComp.innerHTML = selectOption({ value: '', text: '請選擇店舖' })
return
}
Rails.ajax({
type: 'get',
url: `/admin/brands/${brandId}/get_stores`,
success: (response) => {
/* 選單內容 */
const resContent = prepend({ value: '', text: '請選擇店舖'}, response)
/* 找不到店櫃的下拉式選單 => 跳出 */
if (isNil(storeComp)) return
/* Ajax 內容 */
storeComp.innerHTML =
`${map((e) => selectOption({ value: e.value,
text: e.text }),
resContent).join('')}`
},
error: (error) => {
console.log('error response:', error);
}
})
}
export default class extends Controller {
static targets = ['brand', 'store']
// 依照廠牌取得店舖
fetchStores() {
fetchStores({ brandId: this.brandTarget.value,
storeComp: this.storeTarget, prependWording: '請選擇店舖'});
}
}
可以看到打非同步的地方網址為 ⬇️
`/admin/brands/${brandId}/get_stores`
⭐️ 上面的網址列,對應的routes
, controller
分別如下
resources :brands do
get :get_stores, on: :member
end
class Admin::BrandsController < Admin::ApplicationController
# 下拉式選單的 Ajax
def get_stores
@stores = Brand.find_by_id(params[:id])&.stores
render json: @stores&.map { |s| { value: s.id, text: s.title_zh } }
end
end
⭐️ 透過非同步的動作取得的成功回應為底下的response
,並且非同步回傳的結果前方加上{ value: '', text: '請選擇店舖'}
,並且使用selectOption
組成下拉式選單的DOM
,成為了搭配非同步處理的相依性選單。
import Rails from '@rails/ujs';
Rails.ajax({
type: 'get',
url: `/admin/brands/${brandId}/get_stores`,
success: (response) => {
/* 選單內容 */
const resContent = prepend({ value: '', text: '請選擇店舖'}, response)
/* 找不到店櫃的下拉式選單 => 跳出 */
if (isNil(storeComp)) return
/* Ajax 內容 */
storeComp.innerHTML =
`${map((e) => selectOption({ value: e.value,
text: e.text }),
resContent).join('')}`
},
error: (error) => {
console.log('error response:', error);
}
})
還記得Day26的greet()
嗎? 當時我們使用greet()
來觸發簡單的JS動作,而我們可以透過事件觸發非同步的動作,接著我們要來介紹,如何使用 Ajax 處理非同步的問題,以下為呈現的結果。
剛剛提到,只要打非同步就會需要打到後端,就需要事先設定並打通routes
, controller
,因此我們先將講路徑和邏輯寫出來
⭐️ 下列為routes
resources :blogs do
#====== ajax
post :search, on: :collection
end
⭐️ 下列為 controller
, view
。順帶一提,render partial
的寫法不只是view
的專利,我們也可以在controller
寫。
module Admin
class BlogsController < ApplicationController
def search
blog = Blog.find_by_id params[:id]
render partial: 'searched_blog', locals: { blog: blog }
end
end
end
⭐️ 在 app/views/admin/blogs/_searched_blog
中寫下欲渲染的畫面
br
= log_template do
= log_item title: 'id' do
= blog&.id || ''
= log_item title: '標題' do
= blog&.title || '找不到標題'
= log_item title: '內文' do
= blog&.content || '找不到內文'
⭐️ 由於想要用實際的例子讓讀者感受使用Value的用法,我們將路徑
放在Value當中
= card(controller: 'admin--blogs', data: { 'admin--blogs-query-url-value': search_admin_blogs_path }) do
= card_header(title: 'Stimulus Ajax')
= card_body do
// 內容省略......
= tag.div class: 'mx-2 my-3 p-2', style: 'width: 300px; border: 1px solid black' do
= tag.div(class: 'form-group string required admin_blogs_search_id')
= tag.label tag.strong('搜尋id')
= tag.input name: nil, class: "form-control string required",
data: { 'admin--blogs-target': 'ajaxId' }
= button_tag '搜尋', type: 'button', data: { action: 'admin--blogs#ajaxGreet' },
class: 'btn btn-primary'
= tag.div data: { 'admin--blogs-target': 'searchedContent' }
import { isEmpty, isNil, map, prepend, equals } from 'ramda';
import { Controller } from 'stimulus';
import Rails from '@rails/ujs';
/* 下拉式選單的選項 */
const selectOption = ({ value, text, select = null }) =>
`<option value=${value} ${equals(select, true) ? 'selected' : ''}>${text}</option>`
const fetchStores = ({ brandId, storeComp }) => {
if(isEmpty(brandId)) {
storeComp.innerHTML = selectOption({ value: '', text: '請選擇店舖' })
return
}
Rails.ajax({
type: 'get',
url: `/admin/brands/${brandId}/get_stores`,
success: (response) => {
/* 選單內容 */
const resContent = prepend({ value: '', text: '請選擇店舖'}, response)
/* 找不到店櫃的下拉式選單 => 跳出 */
if (isNil(storeComp)) return
/* Ajax 內容 */
storeComp.innerHTML =
`${map((e) => selectOption({ value: e.value,
text: e.text }),
resContent).join('')}`
},
error: (error) => {
console.log('error response:', error);
}
})
}
export default class extends Controller {
static targets = ["searchedContent", 'ajaxId']
static values = { queryUrl: String }
connect() {
console.log('this.queryUrlValue', this.queryUrlValue)
}
ajaxGreet() {
Rails.ajax({
type: 'post',
url: this.queryUrlValue,
data: new URLSearchParams({
id: this.ajaxIdTarget.value
}),
success: (data, status, xhr) => {
this.searchedContentTarget.innerHTML = xhr.response;
},
error: (error) => {
console.log('error response:', error);
}
})
}
}
我們將後端傳過來的search_admin_blogs_path
,傳到 this.queryUrlValue
給Rails.ajax
,並且將參數 this.ajaxIdTarget.value
傳進 Controller,並且將結果打回來顯示在this.searchedContentTarget
。
寫好之後,就可以使用了
過往我們常用js.erb
渲染非同步的表單,而當我們引入了Stimulus 以後,我們可以不用重新開一個js.erb
的檔案。
⭐️ 此例跟上例用的是一樣的routes
, controller
,與上例的情境相同,只不過這邊是使用Ajax
送出表單的方式進行非同步,因此使用到data-remote=true
的helper
。
import { Controller } from 'stimulus';
export default class extends Controller {
static targets = ["searchedContent"]
onBlogSuccess(event) {
let [data, status, xhr] = event.detail;
this.searchedContentTarget.innerHTML = xhr.response;
}
onBlogError(event) {
let [data, status, xhr] = event.detail;
console.log(xhr.response);
}
}
= tag.div data: { controller: 'admin--blogs' } do
/ 中間內容省略...
= modal(id: 'new-blog-modal', confirm_wording: '送出文章',
confirm_form: 'new_modal', title: '新增文章') do
/ 中間內容省略...
= simple_form_for(@blog, url: search_admin_blogs_path, method: :post,
html: { data: { remote: true,
action: "ajax:success->admin--blogs#onBlogSuccess
ajax:error->admin--blogs#onBlogError" },
id: 'form2' }) do
= tag.div style: "border: 1px solid black"
= tag.div(class: 'form-group string required admin_blogs_search_id"')
= tag.label tag.strong('搜尋id')
= tag.input name: 'id', class: "form-control string required", form: 'form2'
= submit_tag "搜尋", class: "btn btn-primary btn-sm", form: 'form2',
data: { disable_with: '載入中...' }
= tag.div data: { 'admin--blogs-target': 'searchedContent' }
我們在Day25 提過下列的例子,但Day25的重點是講述表單包覆表單
的問題,今天的重點在於Ajax
資料的打法。
對於上述的程式碼,我們來進行解析
在表單加入remote: true
,代表該表單需要做非同步的處理
ajax:success->admin--blogs#onBlogSuccess
➡️ 當非同步進入成功階段的反應
ajax:error->admin--blogs#onBlogError
➡️ 當非同步進入失敗階段的反應
其中onBlogSuccess
, onBlogError
分別為在Stimulus自定義的兩個動作。
今天介紹了三種Stimulus
搭配非同步表單的例子,還在使用React
, Vue
的朋友們,漢漢老師想要告訴你們,Stimulus
這種基於SSR
的框架很棒,一點也不遜於主流框架,並且目前在社群上已經有很多星星數不多,但實際上已經很好用的套件。
這幾天不斷地寫文章,因此運動跟作息都變得比較不規律,偶爾會寫到懷疑人生。寫著寫著,發現自己想要分享的內容比預期的還要多很多,但總覺得時間不夠、文筆不夠、實力不夠,因此我們會在最後一天寫下遺珠之憾,為下次的IT鐵人賽做引言。
Rails 真的超棒!希望大家能夠認識Rails
今天除了寫了Day27以外,還回頭順Day1-5
的文章。