檢視原始碼 Binding
Phoenix 支援 DOM 元素 binding,用於進行用戶端與伺服器互動。例如,若要回應按鈕的 click 事件,可以這樣渲染元素:
<button phx-click="inc_temperature">+</button>
然後在伺服器上,所有 LiveView binding 都以 handle_event
callback 來處理,範例如下:
def handle_event("inc_temperature", _value, socket) do
{:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
Binding | 屬性 |
---|---|
參數 | phx-value-* |
Click Events | phx-click 、phx-click-away |
Form Events | phx-change 、phx-submit 、phx-feedback-for 、phx-feedback-group 、phx-disable-with 、phx-trigger-action 、phx-auto-recover |
Focus Events | phx-blur 、phx-focus 、phx-window-blur 、phx-window-focus |
Key Events | phx-keydown 、phx-keyup 、phx-window-keydown 、phx-window-keyup 、phx-key |
Scroll Events | phx-viewport-top 、phx-viewport-bottom |
DOM 修補 | phx-mounted 、phx-update 、phx-remove |
JS Interop | phx-hook |
生命週期事件 | phx-connected 、phx-disconnected |
速率限制 | phx-debounce 、phx-throttle |
靜態追蹤 | phx-track-static |
Click Events
phx-click
binding 用於將 click 事件傳送到伺服器。當任何用戶端事件,例如 phx-click
click 觸發時,傳送到伺服器的值會依照下列優先權進行挑選:
Phoenix.LiveView.JS.push/3
中指定的:value
,例如:<div phx-click={JS.push("inc", value: %{myvar1: @val1})}>
任何數量的自訂
phx-value-
的前綴屬性,例如:<div phx-click="inc" phx-value-myvar1="val1" phx-value-myvar2="val2">
會將下列的參數映射傳送至伺服器:
def handle_event("inc", %{"myvar1" => "val1", "myvar2" => "val2"}, socket) do
如果使用
phx-value-
的前綴,如果元素的 value 屬性存在,伺服器 payload 也會包含"value"
。payload 也會包含用戶端事件的任何其他自訂 metadata。例如,下列
LiveSocket
用戶端選項會傳送所有 click 的座標和altKey
資訊:let liveSocket = new LiveSocket("/live", Socket, { params: {_csrf_token: csrfToken}, metadata: { click: (e, el) => { return { altKey: e.altKey, clientX: e.clientX, clientY: e.clientY } } } })
當 click 事件發生在元素外部時,會觸發 phx-click-away
事件。這對於隱藏下拉選單等已切換的容器很有用。
Focus and Blur Events
對應 DOM 元素可以使用 phx-blur
和 phx-focus
繫結來繫結對應會觸發此類事件的事件,例如
<input name="email" phx-focus="myfocus" phx-blur="myblur"/>
若要偵測頁面本身何時已經收到焦點或失去焦點,可以指定 phx-window-focus
和 phx-window-blur
。如果考慮中的元素(最常是沒有標籤索引的 div
)無法接收焦點,則可能需要使用這些視窗等級的事件。與其他繫結類似,可以在繫結的元素上提供 phx-value-*
,而這些值會作為有效負載的一部分傳送。例如
<div class="container"
phx-window-focus="page-active"
phx-window-blur="page-inactive"
phx-value-page="123">
...
</div>
按鍵事件
使用 phx-keydown
和 phx-keyup
繫結支援 onkeydown
和 onkeyup
事件。每個繫結都支援 phx-key
屬性,這個屬性會為特定按鍵觸發事件。如果沒有提供 phx-key
,則會針對任何按鍵觸發事件。按一下時,傳送到伺服器的值會包含按下的 "key"
,以及任何使用者定義的元資料。例如,按下 Escape 鍵會看起來像這樣
%{"key" => "Escape"}
可以提供 keydown 事件的 metadata
選項給 LiveSocket
建構函數,以擷取其他使用者定義的元資料。例如
let liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken},
metadata: {
keydown: (e, el) => {
return {
key: e.key,
metaKey: e.metaKey,
repeat: e.repeat
}
}
}
})
若要判斷按下了哪個按鍵,您應該使用 key
值。可用的選項可以在 MDN 上找到,或透過 按鍵事件檢視器 找到。
注意:phx-keyup
和 phx-keydown
不支援輸入。請使用表單繫結,例如 phx-change
、phx-submit
等。
注意:某些瀏覽器功能(如自動填寫)可能會觸發按鍵事件,且傳送到伺服器的值對應中沒有 "key"
欄位。因此,我們建議針對 LiveView 按鍵繫結始終有一個後備的萬用事件處理常式。預設情況下,繫結的元素會是事件聆聽器,但是可以透過 phx-window-keydown
或 phx-window-keyup
等方式提供視窗等級繫結,例如
def render(assigns) do
~H"""
<div id="thermostat" phx-window-keyup="update_temp">
Current temperature: <%= @temperature %>
</div>
"""
end
def handle_event("update_temp", %{"key" => "ArrowUp"}, socket) do
{:ok, new_temp} = Thermostat.inc_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
def handle_event("update_temp", %{"key" => "ArrowDown"}, socket) do
{:ok, new_temp} = Thermostat.dec_temperature(socket.assigns.id)
{:noreply, assign(socket, :temperature, new_temp)}
end
def handle_event("update_temp", _, socket) do
{:noreply, socket}
end
使用防彈和節流來限制事件速率
除了 phx-blur
繫結(會立即觸發)之外,所有事件都可以在用戶端使用 phx-debounce
和 phx-throttle
繫結來限制速率。
限制速率和防彈的事件有下列行為
phx-debounce
- 可接受整數逾時值(以毫秒為單位)或"blur"
。當提供整數時,會延遲發射事件指定的毫秒數。當提供"blur"
時,會延遲發射事件,直到使用者使得欄位模糊為止。當省略值時,將使用 300 毫秒的預設值。去除抖動通常用於輸入元素。phx-throttle
- 可接受以毫秒為單位調整事件的整數逾時值。與去除抖動不同,調整節流會立即發射事件,然後每提供一次逾時值限定一次速率。當省略值時,將使用 300 毫秒的預設值。調整節流通常用於限定按一下、滑鼠和鍵盤動作的速率。
例如,要避免驗證電子郵件,直到欄位模糊為止,同時在使用者變更欄位後最多每 2 秒驗證一次使用者名稱
<form phx-change="validate" phx-submit="save">
<input type="text" name="user[email]" phx-debounce="blur"/>
<input type="text" name="user[username]" phx-debounce="2000"/>
</form>
並將增加音量按一下的速率限定為每秒一次
<button phx-click="volume_up" phx-throttle="1000">+</button>
同樣地,你可以調整按住的鍵盤
<div phx-window-keydown="keydown" phx-throttle="500">
...
</div>
除非需要按住的按鍵,否則更好的作法通常是使用 phx-keyup
繫結,它只會觸發一次按鍵,因此會自限。然而,phx-keydown
對於遊戲和其他需要持續按下按鍵的用例是有用的。在這種情況下,應始終使用調整節流。
Debounce 和 Throttle 的特殊行為
對表單和鍵盤繫結執行以下特定行為
觸發
phx-submit
或不同輸入的phx-change
時,任何現有的去除抖動或調整節流計時器對現有輸入都會重設。phx-keydown
繫結只對重複按鍵進行調整節流。連續按單個按鍵會發送已按按鍵的事件。
JS 指令
LiveView 繫結透過 Phoenix.LiveView.JS
模組支援 JavaScript 指令介面,這允許你在觸發 phx-
繫結事件(例如 phx-click
、phx-change
等)時指定在用戶端執行的工具程式操作。指令可組成在一起,讓你發送事件、將類別新增至元素、將元素淡入淡出等等。請參閱 Phoenix.LiveView.JS
文件,以取得完整的用法。
對於可能發生的情況,一個小範例是想像你要在頁面上顯示和隱藏一個 modal,而不需要來回伺服器來呈現內容
<div id="modal" class="modal">
My Modal
</div>
<button phx-click={JS.show(to: "#modal", transition: "fade-in")}>
show modal
</button>
<button phx-click={JS.hide(to: "#modal", transition: "fade-out")}>
hide modal
</button>
<button phx-click={JS.toggle(to: "#modal", in: "fade-in", out: "fade-out")}>
toggle modal
</button>
或者如果你的 UI 程式庫依賴類別來執行顯示或隱藏
<div id="modal" class="modal">
My Modal
</div>
<button phx-click={JS.add_class("show", to: "#modal", transition: "fade-in")}>
show modal
</button>
<button phx-click={JS.remove_class("show", to: "#modal", transition: "fade-out")}>
hide modal
</button>
指令可組成在一起。例如,你可以將一個事件推播到伺服器,並立即在用戶端隱藏 modal
<div id="modal" class="modal">
My Modal
</div>
<button phx-click={JS.push("modal-closed") |> JS.remove_class("show", to: "#modal", transition: "fade-out")}>
hide modal
</button>
將指令萃取到它們自己的函式中也很有用
alias Phoenix.LiveView.JS
def hide_modal(js \\ %JS{}, selector) do
js
|> JS.push("modal-closed")
|> JS.remove_class("show", to: selector, transition: "fade-out")
end
<button phx-click={hide_modal("#modal")}>hide modal</button>
Phoenix.LiveView.JS.push/3
指令特別強大,能讓你自訂推送到伺服器的事件。例如,想像你從一個常見的 phx-click
開始,當按下後會推播訊息到伺服器
<button phx-click="clicked">click</button>
現在想像你想自訂在 "clicked"
事件被推播時會發生什麼,像是要針對哪個元件、哪個元素會接受 CSS 載入狀態類別等等。這可以使用 JS push 指令中的選項達成。例如
<button phx-click={JS.push("clicked", target: @myself, loading: ".container")}>click</button>
有關所有支援選項,請參閱 Phoenix.LiveView.JS.push/3
。
DOM 修補
容器可以用 phx-update
標記,用於設定 DOM 的更新方式。支援以下數值
replace
- 預設操作。將元素替換為內容stream
- 支援串流操作。串流用於在 UI 中管理大型集合,而不需要將集合儲存在伺服器上ignore
- 無視 DOM 更新,無論新內容如何變更。這對於自訂用戶端與使用自有 DOM 操作的現有函式庫整合時很有用
使用 phx-update
時,必須在容器中設定獨特的 DOM ID。如果使用「串流」,也必須為每個子項目設定 DOM ID。當插入包含已存在於容器中的 ID 的串流元素時,LiveView 會用新內容取代現有元素。有關更多資訊,請參閱 Phoenix.LiveView.stream/3
。
當你需要與其他 JS 函式庫整合時,經常會使用「ignore」行為。伺服器對元素的內容和屬性的更新會被忽略,資料屬性除外。從服務端對資料屬性進行變更、新增和移除等動作會與被忽略的元素合併,可用於將資料傳遞給 JS 處理常式。
若要讓元素對掛載到 DOM,可以使用 phx-mounted
繫結。例如,在掛載時為元素新增動畫效果
<div phx-mounted={JS.transition("animate-ping", time: 500)}>
如果在初始頁面渲染時使用了 phx-mounted
,則只有在建立初始 WebSocket 連接後才會呼叫它。
若要讓元素對從 DOM 中移除做出反應,可以指定 phx-remove
繫結,其中可以包含要執行的 Phoenix.LiveView.JS
指令。phx-remove
指令只會針對已移除的父元素執行。它不會遞迴到子項目。
生命週期事件
LiveView 支援 phx-connected
和 phx-disconnected
綁定,以使用 JS 命令對連線生命週期事件做出反應。舉例來說,當 LiveView 失去連線時顯示元素,當連線復原時隱藏元素
<div id="status" class="hidden" phx-disconnected={JS.show()} phx-connected={JS.hide()}>
Attempting to reconnect...
</div>
phx-connected
和 phx-disconnected
僅在 LiveView 容器內運作時執行。對於靜態範本,它們不會有任何效果。
LiveView 專屬事件
lv:
事件字首支援 LiveView 專屬功能,由 LiveView 處理而不呼叫使用者的 handle_event/3
回呼。目前,支援以下事件
lv:clear-flash
– 傳送給伺服器時清除閃光訊息。如果提供phx-value-key
,將從閃光訊息中移除特定金鑰。
舉例來說
<p class="alert" phx-click="lv:clear-flash" phx-value-key="info">
<%= Phoenix.Flash.get(@flash, :info) %>
</p>
載入狀態與錯誤
所有 phx-
事件綁定在推播時套用它們自己的 CSS 類別。例如以下標記
<button phx-click="clicked" phx-window-keydown="key">...</button>
點擊時,會收到 phx-click-loading
類別,按下鍵時會收到 phx-keydown-loading
類別。CSS 載入類別會保留,直到在用戶端收到推播事件的確認為止。
對於表單,傳送 phx-change
給伺服器時,傳出變更的輸入元素會收到 phx-change-loading
類別,以及父層表單標籤。以下事件會收到 CSS 載入類別
phx-click
-phx-click-loading
phx-change
-phx-change-loading
phx-submit
-phx-submit-loading
phx-focus
-phx-focus-loading
phx-blur
-phx-blur-loading
phx-window-keydown
-phx-keydown-loading
phx-window-keyup
-phx-keyup-loading
此外,會套用以下類別至 LiveView 的父層容器
"phx-connected"
- 當檢視連線到伺服器時套用"phx-loading"
- 當檢視未連線到伺服器時套用"phx-error"
- 當伺服器發生錯誤時套用。請注意,如果失去與伺服器的連線,此類別將與"phx-loading"
一併套用。
對於導覽相關的載入狀態(自動與手動),請參閱 JavaScript 互操作性:Live 導覽事件 中所述的 phx-page-loading
。
捲動事件與無限串流分頁
透過phx-viewport-top
和 phx-viewport-bottom
繫結,可以偵測某個容器的第一個子項目何時抵達視窗頂端,或是最後一個子項目何時抵達視窗底端。這在無窮捲動中非常實用,因為你能在使用者上下捲動、抵達視窗頂部或底部時傳送分頁事件給下一組結果或前一組結果。
執行無窮捲動時,應用程式通常會在容器上方和下方加入內邊距,以便在載入結果時維持順暢捲動。搭配 Phoenix.LiveView.stream/3
,phx-viewport-top
和 phx-viewport-bottom
能產生無窮虛擬清單,這個清單只會在 DOM 中保留一組很小的實際元素。例如
def mount(_, _, socket) do
{:ok,
socket
|> assign(page: 1, per_page: 20)
|> paginate_posts(1)}
end
defp paginate_posts(socket, new_page) when new_page >= 1 do
%{per_page: per_page, page: cur_page} = socket.assigns
posts = Blog.list_posts(offset: (new_page - 1) * per_page, limit: per_page)
{posts, at, limit} =
if new_page >= cur_page do
{posts, -1, per_page * 3 * -1}
else
{Enum.reverse(posts), 0, per_page * 3}
end
case posts do
[] ->
assign(socket, end_of_timeline?: at == -1)
[_ | _] = posts ->
socket
|> assign(end_of_timeline?: false)
|> assign(:page, new_page)
|> stream(:posts, posts, at: at, limit: limit)
end
end
我們的 paginate_posts
函式會擷取一頁文章,再判斷使用者是否要分頁到前一頁或下一頁。根據分頁方向,串流可以前置,或分別附加至 0
或 -1
的 at
。我們也會將串流的 limit
設為 per_page
的三倍,以確保使用者介面有足夠文章顯現成無窮清單,但又夠小以維持使用者介面效能。我們也會設定 @end_of_timeline?
指定變數,以追蹤使用者是否已抵達結果尾端。最後,我們更新 @page
指定變數和文章串流。然後,我們可以連結容器以支援視窗事件
<ul
id="posts"
phx-update="stream"
phx-viewport-top={@page > 1 && "prev-page"}
phx-viewport-bottom={!@end_of_timeline? && "next-page"}
phx-page-loading
class={[
if(@end_of_timeline?, do: "pb-10", else: "pb-[calc(200vh)]"),
if(@page == 1, do: "pt-10", else: "pt-[calc(200vh)]")
]}
>
<li :for={{id, post} <- @streams.posts} id={id}>
<.post_card post={post} />
</li>
</ul>
<div :if={@end_of_timeline?} class="mt-5 text-[50px] text-center">
🎉 You made it to the beginning of time 🎉
</div>
這段程式碼沒什麼,不過那正是重點!這個小小的使用者介面片段正在驅動一個全虛擬化的清單,配備雙向無窮捲動。我們使用 phx-viewport-top
繫結將 "prev-page"
事件傳送至 LiveView,但前提是使用者已超過第一頁。載入負頁面結果並沒有意義,因此我們在這些情況下會完全移除繫結。接下來,我們連結 phx-viewport-bottom
以傳送 "next-page"
事件,但前提是我們尚未抵達時間軸尾端。最後,我們視情況套用一些 CSS 類別,以根據目前的分页設定,將上、下內邊距設定為視窗高度兩倍的大小,以利順暢捲動。
若要完成我們的解決方案,我們只需要處理 LiveView 中的 "prev-page"
和 "next-page"
事件
def handle_event("next-page", _, socket) do
{:noreply, paginate_posts(socket, socket.assigns.page + 1)}
end
def handle_event("prev-page", %{"_overran" => true}, socket) do
{:noreply, paginate_posts(socket, 1)}
end
def handle_event("prev-page", _, socket) do
if socket.assigns.page > 1 do
{:noreply, paginate_posts(socket, socket.assigns.page - 1)}
else
{:noreply, socket}
end
end
這個程式碼僅呼叫 `paginate_posts` 函式,這是我們在第一步時定義的,使用目前或下一個頁面來引導結果。請注意我們在 `prev-page` 事件中匹配特殊參數 `"_overran" => true`。當使用者「越過」視窗頂部或底部時,視窗事件會傳送此參數。想像一種狀況,使用者向上捲動瀏覽許多頁面的結果,但抓住捲軸並立即返回頁面頂部。這表示我們的容器 `<ul id="posts">` 被視窗頂部越過,我們需要將 UI 重設為第一個頁面。