狀態管理
目前多數主流的前端框架主張我們把狀態全部交給 JavaScript 來處理。將 DOM 單純用來渲染輸出,由客戶端的樣板搭配伺服器端來的 JSON 去產生最後的結果。
Stimulus 採取不同的方式。一個 Stimulus 應用程式的狀態基本上會存在 DOM 的屬性上;controller 基本上不管理狀態。這樣的作法讓我們可以和各式各樣的情況下和 HTML 搭配,例如:一般初始化的頁面,Ajax 請求,Turbolinks 的 visit 機制,甚至其他的 JavaScript 函式庫。 套用的 controller 會自動建立而不需要其他初始化的步驟。
投影片效果(slide)範例
在上一章,我們已經看過了 Stimulus controller 如何在 HTML 中通過加入一個 class 樣式名稱來達成一個簡單的狀態管理。但如果我們需要的是儲存一個較為複雜的值,而不是簡單的註記?
我們將透過建置一個 slideshow controller 讓它能夠在 DOM 屬性上儲存當前選擇的 slide 的索引來探討這個問題。
一如往常我們從 HTML 開始著手:
<div data-controller="slideshow">
<button data-action="slideshow#previous">←</button>
<button data-action="slideshow#next">→</button>
<div data-target="slideshow.slide" class="slide">🐵</div>
<div data-target="slideshow.slide" class="slide">🙈</div>
<div data-target="slideshow.slide" class="slide">🙉</div>
<div data-target="slideshow.slide" class="slide">🙊</div>
</div>
每一個 slide
target 代表一系列投影片中的一頁。我們的 controller 將會負責一次只顯示一頁內容
的處理。
一開始我們可以使用 CSS 來把所有投影片隱藏起來,只有當 slide--current
套用在元素上的時候才顯示:
.slide {
display: none;
}
.slide.slide--current {
display: block;
}
現在,我們可以開始來建立我們的 controller。開新檔案 src/controllers/slideshow_controller.js
// src/controllers/slideshow_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "slide" ]
initialize() {
this.showSlide(0)
}
next() {
this.showSlide(this.index + 1)
}
previous() {
this.showSlide(this.index - 1)
}
showSlide(index) {
this.index = index
this.slideTargets.forEach((el, i) => {
el.classList.toggle("slide--current", index == i)
})
}
}
我們在 controller 定義了一個方法 showSlide()
,它會執行一次迴圈遍歷每一個投影片,如果切換的索引 index
和遍歷的索引 i
一致就把 slide--current
class 樣式加上。
還有,我們在 controller 初始化的時候設定顯示第一個投影片,然後 next()
和 previous()
action 則處理下一頁或上一頁的行為。
生命週期回調(callback)
在這個範例中我們使用
initialize()
是什麼?這個和我們之前提到的connect()
有什麼不同呢?在 Stimulus 中 controller 從初始化到解構有其生命週期,每個階段有對應的生命週期回調,這些方法通常可以協助我們設定狀態或解除繫結。
回調 由 Stimulus 自動調用 initialize 僅執行一次,當 controller 建立物件實例的時候 connect 當 controller 和 DOM 完成繫結的時候 disconnect 當 controller 和 DOM 解除繫結的時候
重新載入頁面測試我們的上下頁按鈕可以正常運作。
從 DOM 讀取初始化狀態
注意到 controller 是如何使用 this.index
追蹤被選取顯示的投影片。
現在假設我們想要從第二則內容開始顯示,我們該如何在 HTML 提供起始投影片的索引設定呢?
其中一種載入起始索引的方式是在 HTML 使用 data
屬性。例如:我們可以加入 data-slideshow-index
屬性到 controller 的根元素上:
<div data-controller="slideshow" data-slideshow-index="1">
然後在 initialize()
方法中,我們就可以讀取 DOM 的屬性,然後將值傳入 showSlide()
:
initialize() {
const index = parseInt(this.element.getAttribute("data-slideshow-index"))
this.showSlide(index)
}
在 controller 的根元素上使用 data
屬性來帶入參數是很常見的狀況,因此 Stimulus 提供了一個 API 方便我們讀取屬性值,我們可以使用 this.data.get()
:
initialize() {
const index = parseInt(this.data.get("index"))
this.showSlide(index)
}
Data API 說明
每一個 Stimulus controller 都有一個
this.data
物件,其中有has()
,get()
,set()
方法。這些方法提供一種較方便的方式讓我們存取 controller 根元素上的屬性並且需要搭配 controller 的識別名稱作為限制範圍。例如我們上面的 controller:
this.data.has('index')
假如 controller 根元素有data-slideshow-index
屬性的話,會回傳true
this.data.get('index')
取得根元素上data-slideshow-index
屬性值this.data.set('index', index)
將index
的值設定到根元素的data-slideshow-index
屬性上如果屬性的名稱超過一個單字,那麼在 JavaScript 中會使用
camelCase
格式,HTML 中則是attribute-name
格式。例如:data-slideshow-current-class-name
屬性則是this.data.get('currentClassName')
讓我們在 controller 根元素加入 data-slideshow-index
屬性,確認 controller 的程式碼也如上面更新了。然後重新載入頁面測試功能。
於 DOM 管理狀態的一致性
我們已經理解如何使用 data
屬性來帶入參數,調整 controller 的初始化行為。
不過當我們使用 slideshow 切換頁面的時候,屬性並沒有和 controller 中的 index
同步。假設我們要複製這個 controller 元素,這個副本將會恢復到初始化的狀態。
針對這個狀況我們可以在 controller 中透過為 index
定義 getter 和 setter 並搭配 Data API 解決這個問題:
// src/controllers/slideshow_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = [ "slide" ]
initialize() {
this.showCurrentSlide()
}
next() {
this.index++
}
previous() {
this.index--
}
showCurrentSlide() {
this.slideTargets.forEach((el, i) => {
el.classList.toggle("slide--current", this.index == i)
})
}
get index() {
return parseInt(this.data.get("index"))
}
set index(value) {
this.data.set("index", value)
this.showCurrentSlide()
}
}
上面我們把 showSlide()
改成 showCurrentSlide()
然後不再帶參數,直接使用 this.index
。因為 getter get index()
的關係,現在我們存取 this.index
會回傳 data-slideshow-index
屬性並轉換成 integer。
而 setter set index()
則會在我們設定 this.index
的時候同步更新頁面屬性。
現在我們的 controller 狀態也完整的保存在 DOM 裡了。
譯者註:如果您想試試 clone 的效果可以使用下面這段範例:
// src/controllers/slideshow_controller.js
clone (e) {
var el = document.querySelector('[data-controller=slideshow]').cloneNode(true)
document.body.appendChild(el)
}
總結與下一步
在這一章我們理解了如何使用 Stimulus 的 Data API 來處理 index 狀態。
從實務應用的角度,我們的 controller 還不是很完整。 思考一下我們該怎麼修正 controller 來處理下面列出的問題:
- 當索引處在第一筆內容的時候,上一頁在內部會把索引值從
0
扣成-1
。我們能不能讓索引變成顯示最後一頁的值?(下一頁有類似的問題) - 如果我們忘記設定
data-slideshow-index
那麼get index()
裡面的parseInt()
會傳回NaN
。我們是不是可以讓忘記設定的時候預設帶0
?
下一章我們將要看看如何在 controller 處理外部資源,如 HTTP request 和 timer 該如何處理。