檢視原始碼 Phoenix.Component (Phoenix LiveView v0.20.17)
使用 HEEx 範本來定義可重複使用的函式元件。
函式元件是將 assigns 映射值接收為引數並傳回使用 符號 ~H
建立的已呈現結構體的函式。
defmodule MyComponent do
# In Phoenix apps, the line is typically: use MyAppWeb, :html
use Phoenix.Component
def greet(assigns) do
~H"""
<p>Hello, <%= @name %>!</p>
"""
end
end
此函式使用 ~H
符號來傳回一個已呈現的範本。 ~H
代表 HEEx (HTML + EEx)。HEEx 是一種範本語言,用於撰寫混合 Elixir 插值的 HTML。我們可以使用 <%= ... %>
標籤在 HEEx 內撰寫 Elixir 程式碼,我們使用 @name
來存取在 assigns
內定義的 name
鍵。
如在 ~H
符號或 HEEx 範本檔案中呼叫時
<MyComponent.greet name="Jane" />
會呈現以下 HTML
<p>Hello, Jane!</p>
如果函式元件定義為區域性的,或者匯入其模組,則呼叫者可以在不指定模組的情況下直接呼叫函式
<.greet name="Jane" />
對於動態值,您可以將 Elixir 表達式內插到函式元件中
<.greet name={@user.name} />
函式元件也可以接受 HEEx 內容區塊(稍後再說明)
<.card>
<p>This is the body of my card!</p>
</.card>
在此模組中,我們將學習如何建立豐富且可組成的元件,以便在我們的應用程式中使用。
屬性
Phoenix.Component
提供 attr/3
巨集,用於宣告在呼叫時後續的函式元件預期接收哪些屬性
attr :name, :string, required: true
def greet(assigns) do
~H"""
<p>Hello, <%= @name %>!</p>
"""
end
透過呼叫 attr/3
,現在很明顯的是 greet/1
需要其 assigns 映射中存在的稱為 name
的字串屬性才能適當地呈現。如果無法做到這一點,則會產生編譯警告
<MyComponent.greet />
<!-- warning: missing required attribute "name" for component MyAppWeb.MyComponent.greet/1
lib/app_web/my_component.ex:15 -->
屬性可以提供自動合併到 assigns 映射中的預設值
attr :name, :string, default: "Bob"
現在,您可以呼叫函式元件而不為 name
指定值
<.greet />
呈現下列 HTML
<p>Hello, Bob!</p>
存取所需的屬性且不具有預設值將會失敗。您必須使用 assign_new/3
函式明確宣告 default: nil
或以編程方式指定一個值。
可以為相同的函式元件宣告多個屬性
attr :name, :string, required: true
attr :age, :integer, required: true
def celebrate(assigns) do
~H"""
<p>
Happy birthday <%= @name %>!
You are <%= @age %> years old.
</p>
"""
end
允許呼叫者傳遞多個值
<.celebrate name={"Genevieve"} age={34} />
呈現下列 HTML
<p>
Happy birthday Genevieve!
You are 34 years old.
</p>
相同的模組中可以定義多個具備不同屬性的 function components,以下範例中,<Components.greet/>
需要 name
,不需要 title
,<Components.heading>
需要 title
,不需要 name
。
defmodule Components do
# In Phoenix apps, the line is typically: use MyAppWeb, :html
use Phoenix.Component
attr :title, :string, required: true
def heading(assigns) do
~H"""
<h1><%= @title %></h1>
"""
end
attr :name, :string, required: true
def greet(assigns) do
~H"""
<p>Hello <%= @name %></p>
"""
end
end
使用 attr/3
巨集會取得建立重複使用的 function components 的核心成分。但如果需要 function components 支援動態屬性會如何,例如加入 component 容器的常見 HTML 屬性?
全域屬性
全域屬性是 function component 在宣告 :global
類型的屬性時可以使用的一組屬性。預設情況下,可以接受的屬性集是指所有標準 HTML 標記共通的那些屬性。有關屬性的完整清單,請參閱 全域屬性。
一旦宣告全域屬性,呼叫者可以傳遞集合中的任意數量的屬性而不用修改 function component 本身。
下方範例為接受動態數量全域屬性的 function component。
attr :message, :string, required: true
attr :rest, :global
def notification(assigns) do
~H"""
<span {@rest}><%= @message %></span>
"""
end
呼叫者可以傳遞多個全域屬性(例如 phx-*
繫結或 class
屬性。
<.notification message="You've got mail!" class="bg-green-200" phx-click="close" />
呈現下列 HTML
<span class="bg-green-200" phx-click="close">You've got mail!</span>
請注意,為讓 function component 呈現,不必明確宣告 class
或 phx-click
屬性。
全域屬性可以定義預設值,這些預設值會合併到呼叫者提供的屬性。例如,如果呼叫者未提供 class
,您可以宣告一個預設值。
attr :rest, :global, default: %{class: "bg-blue-200"}
現在您可以在沒有 class
屬性的情況下呼叫 function component。
<.notification message="You've got mail!" phx-click="close" />
呈現下列 HTML
<span class="bg-blue-200" phx-click="close">You've got mail!</span>
請注意,不能直接提供全域屬性,如果這樣做會產生警告。換句話說,這是無效的
<.notification message="You've got mail!" rest={%{"phx-click" => "close"}} />
包含的全域
您也可以使用 :include
選項指定除了已知全域屬性之外,還要包含哪些屬性。例如,要在按鈕元件支援 form
屬性。
# <.button form="my-form"/>
attr :rest, :global, include: ~w(form)
slot :inner_block
def button(assigns) do
~H"""
<button {@rest}><%= render_slot(@inner_block) %></button>
"""
end
:include
選項用於個案方式套用全域附加,但是有時候您會想要使用新的全域屬性擴充現有的 components,例如 Alpine.js 的 x-
前置詞,我們將於下文概述。
自訂全域屬性前置詞
你可以透過提供屬性字首清單給 use Phoenix.Component
來延伸全域屬性的集合。就像所有 HTML 元素常見的預設屬性一樣,任何以全域字首開頭的屬性數量都會在當前模組所呼叫的功能元件中被接受。預設支援以下字首:phx-
、aria-
和 data-
。例如,若要支援 Alpine.js 使用的 x-
字首,你可以傳遞 :global_prefixes
選項給 use Phoenix.Component
use Phoenix.Component, global_prefixes: ~w(x-)
在你的 Phoenix 應用程式中,這通常會在 lib/my_app_web.ex
檔案中的 def html
定義內執行
def html do
quote do
use Phoenix.Component, global_prefixes: ~w(x-)
# ...
end
end
現在,除了預設全域字首之外,所有由這個模組呼叫的功能元件都會接受任何個數字首為 x-
的屬性。
你可以透過閱讀 attr/3
的說明文件來深入了解屬性。
插槽
除了屬性之外,功能元件可以接受稱為插槽的 HEEx 內容區塊。插槽會啟用渲染 HTML 的進一步自訂,因為呼叫者可以將功能元件傳遞其希望元件渲染的 HEEx 內容。Phoenix.Component
提供了 slot/3
巨集,用於宣告功能元件的插槽
slot :inner_block, required: true
def button(assigns) do
~H"""
<button>
<%= render_slot(@inner_block) %>
</button>
"""
end
表達式 render_slot(@inner_block)
渲染 HEEx 內容。你可以像這樣呼叫這個功能元件
<.button>
This renders <strong>inside</strong> the button!
</.button>
會渲染以下 HTML
<button>
This renders <strong>inside</strong> the button!
</button>
就像 attr/3
巨集一樣,使用 slot/3
巨集會提供編譯時期驗證。例如,呼叫 button/1
而沒有任何 HEEx 內容插槽,會導致發出編譯警告
<.button />
<!-- warning: missing required slot "inner_block" for component MyAppWeb.MyComponent.button/1
lib/app_web/my_component.ex:15 -->
預設插槽
上面的範例使用預設插槽,可以作為一個名為 @inner_block
的指定來使用,透過 render_slot/1
函數渲染 HEEx 內容。
如果需要插槽中渲染的值為動態,你可以透過呼叫 render_slot/2
將第二個值傳遞回 HEEx 內容
slot :inner_block, required: true
attr :entries, :list, default: []
def unordered_list(assigns) do
~H"""
<ul>
<%= for entry <- @entries do %>
<li><%= render_slot(@inner_block, entry) %></li>
<% end %>
</ul>
"""
end
在呼叫功能元件時,你可以使用特殊屬性 :let
來取得功能元件傳回的值,並將其繫結到變數
<.unordered_list :let={fruit} entries={~w(apples bananas cherries)}>
I like <b><%= fruit %></b>!
</.unordered_list>
呈現下列 HTML
<ul>
<li>I like <b>apples</b>!</li>
<li>I like <b>bananas</b>!</li>
<li>I like <b>cherries</b>!</li>
</ul>
現在,關注點的分離得以維持:呼叫者可以在清單屬性中指定多個值,而不必指定將其包圍和分隔開來的 HEEx 內容。
有命名的插槽
除了預設的插槽外,功能組件還可以接受 HEEx 內容的多重、有命名的插槽。例如,假設您想要建立一個包含標題、內文和尾端的對話框
slot :header
slot :inner_block, required: true
slot :footer, required: true
def modal(assigns) do
~H"""
<div class="modal">
<div class="modal-header">
<%= render_slot(@header) || "Modal" %>
</div>
<div class="modal-body">
<%= render_slot(@inner_block) %>
</div>
<div class="modal-footer">
<%= render_slot(@footer) %>
</div>
</div>
"""
end
您可以使用有命名的插槽 HEEx 語法呼叫此功能組件
<.modal>
This is the body, everything not in a named slot is rendered in the default slot.
<:footer>
This is the bottom of the modal.
</:footer>
</.modal>
呈現下列 HTML
<div class="modal">
<div class="modal-header">
Modal.
</div>
<div class="modal-body">
This is the body, everything not in a named slot is rendered in the default slot.
</div>
<div class="modal-footer">
This is the bottom of the modal.
</div>
</div>
如同上方範例所示,render_slot/1
在宣告一個選填插槽時,且未給予任何插槽時會回傳 nil
。這可以用於加入預設行為。
插槽屬性
與預設插槽不同的是,有可能傳遞有命名插槽多個 HEEx 內容。有命名的插槽也可以接受屬性,方法是將區塊傳遞給 slot/3
巨集。如果傳遞多個內容,render_slot/2
會合併所有值並呈現。
以下是表格組件,說明具有屬性的多個有命名的插槽
slot :column, doc: "Columns with column labels" do
attr :label, :string, required: true, doc: "Column label"
end
attr :rows, :list, default: []
def table(assigns) do
~H"""
<table>
<tr>
<%= for col <- @column do %>
<th><%= col.label %></th>
<% end %>
</tr>
<%= for row <- @rows do %>
<tr>
<%= for col <- @column do %>
<td><%= render_slot(col, row) %></td>
<% end %>
</tr>
<% end %>
</table>
"""
end
您可以像這樣呼叫此功能組件
<.table rows={[%{name: "Jane", age: "34"}, %{name: "Bob", age: "51"}]}>
<:column :let={user} label="Name">
<%= user.name %>
</:column>
<:column :let={user} label="Age">
<%= user.age %>
</:column>
</.table>
呈現下列 HTML
<table>
<tr>
<th>Name</th>
<th>Age</th>
</tr>
<tr>
<td>Jane</td>
<td>34</td>
</tr>
<tr>
<td>Bob</td>
<td>51</td>
</tr>
</table>
內嵌外部範本檔案
巨集 embed_templates/1
可以用於將 .html.heex
檔內嵌為功能組件。目錄路徑是以目前的模組(__DIR__
)為基準,而且萬用字元模式可用於選取目錄樹中的所有檔案。例如,假設有一個目錄清單
├── components.ex
├── cards
│ ├── pricing_card.html.heex
│ └── features_card.html.heex
然後您可以將頁面範本內嵌在您的 components.ex
模組中,並且像呼叫任何其他功能組件一樣呼叫它們
defmodule MyAppWeb.Components do
use Phoenix.Component
embed_templates "cards/*"
def landing_hero(assigns) do
~H"""
<.pricing_card />
<.features_card />
"""
end
end
請參閱 embed_templates/1
以取得更多資訊,包含宣告式指派對內嵌範本的支援。
偵錯註解
HEEx 範本支援偵錯註解,這是一種特殊的 HTML 註解,會將呈式的組件包覆起來,以協助您辨識在您的 HTML 文件中的標記是在功能組件樹中的何處呈式的。
例如,假設有以下 HEEx 範本
<.header>
<.button>Click</.button>
</.header>
在偵錯註解啟用時,HTML 文件會收到以下註解
<!-- @caller lib/app_web/home_live.ex:20 -->
<!-- <AppWeb.CoreComponents.header> lib/app_web/core_components.ex:123 -->
<header class="p-5">
<!-- @caller lib/app_web/home_live.ex:48 -->
<!-- <AppWeb.CoreComponents.button> lib/app_web/core_components.ex:456 -->
<button class="px-2 bg-indigo-500 text-white">Click</button>
<!-- </AppWeb.CoreComponents.button> -->
</header>
<!-- </AppWeb.CoreComponents.header> -->
偵錯註解適用於任何 ~H
或 .html.heex
範本。可以在 config/dev.exs
檔中使用以下組態來全面啟用
config :phoenix_live_view, debug_heex_annotations: true
變更此組態將需要 mix clean
和完整的重新編譯。
摘要
組件
為不同的載入狀態呈現一個非同步作業,並帶有插槽。結果狀態優先於後續載入和失敗狀態。
產生一個動態命名的 HTML 標籤。
將 tab 焦點包覆在一個無障礙的容器中。
呈現一個表單。
呈現關聯或嵌入的巢狀表單輸入值。
在可列舉資料間穿插分隔符號插槽。
產生一個連結指向指定的路由。
用於在父級 LiveView 中呈現 Phoenix.LiveComponent
的函式組件。
建立一個檔案輸入標籤用於 LiveView 上傳。
為已選檔案在用戶端產生一個圖片預覽。
呈現一個標題,並在 @page_title
更新時自動加上前後綴。
巨集
宣告 HEEx 函式組件的屬性。
將外部範本檔案嵌入模組中作為函式組件。
用於在原始檔中撰寫 HEEx 範本的 ~H
巨集。
宣告一個插槽。更多資訊請參閱 slot/3
。
宣告一個函式組件插槽。
函式
新增關鍵字值對至 assigns。
新增一組 key
-value
至 socket_or_assigns
。
如果 socket_or_assigns
中還不存在 key
,則指定 key
的值為 fun
的回傳值。
過濾 assigns,產生一個關鍵字清單,用於動態標籤屬性。
檢查 socket_or_assigns
中的特定關鍵字值是否改變。
傳回 LiveView flash assign 中的快閃訊息。
在範本中呈現一個 LiveView。
呈現一個插槽條目,並傳入一個可選的 argument
。
將指定的資料結構轉換為 Phoenix.HTML.Form
。
使用 fun
更新 socket_or_assigns
中現有的 key
值。
傳回整體上傳的錯誤。
傳回上傳條目的錯誤。
組件
為不同的載入狀態呈現一個非同步作業,並帶有插槽。結果狀態優先於後續載入和失敗狀態。
備註:內部區塊接收非同步指定為 :let 的結果。let 僅可存取內部區塊,其他插槽無法存取。
範例
<.async_result :let={org} assign={@org}>
<:loading>Loading organization...</:loading>
<:failed :let={_failure}>there was an error loading the organization</:failed>
<%= if org do %>
<%= org.name %>
<% else %>
You don't have an organization yet.
<% end %>
</.async_result>
若要在後續 assign_async
呼叫時再次顯示載入和失敗狀態,請將指定重新設定為不含結果的 %AsyncResult{}
{:noreply,
socket
|> assign_async(:page, :data, &reload_data/0)
|> assign(:page, AsyncResult.loading())}
屬性
assign
(Phoenix.LiveView.AsyncResult
) (必要)
插槽
loading
- 在指定首次載入時呈現。failed
- 當出現錯誤或退出,或是 assign_async 在第一次回傳{:error, reason}
時呈現。將錯誤接收為:let
。inner_block
- 當指定透過AsyncResult.ok/2
成功載入時呈現。將結果接收為:let
。
產生一個動態命名的 HTML 標籤。
如果發現標籤名稱是不安全的 HTML,則引發 ArgumentError
。
屬性
name
(:string
) (必要) - 標籤的名稱,例如div
。- 接受全域屬性。要新增至標籤的其他 HTML 屬性,確保妥善逸出。
插槽
inner_block
範例
<.dynamic_tag name="input" type="text"/>
<input type="text"/>
<.dynamic_tag name="p">content</.dynamic_tag>
<p>content</p>
將 tab 焦點包覆在一個無障礙的容器中。
這對於模式對話框和選單等介面而言,是一項基本的無障礙功能。
屬性
id
(:string
) (必要) - 容器標籤的 DOM 識別碼。- 接受全域屬性。要新增至容器標籤的其他 HTML 屬性。
插槽
inner_block
(必要) - 呈現於容器標籤中的內容。
範例
只須將您的內部內容呈現在此元件中,焦點就會隨著使用者於容器內容中標籤而包覆在容器周圍
<.focus_wrap id="my-modal" class="bg-white">
<div id="modal-content">
Are you sure?
<button phx-click="cancel">Cancel</button>
<button phx-click="confirm">OK</button>
</div>
</.focus_wrap>
呈現一個表單。
此函式接收 Phoenix.HTML.Form
結構,通常以 to_form/2
建立,並產生相關的表單標籤。可以在 LiveView 內部或外部使用。
若要實際了解表單運作的方式,您可以在 Phoenix 應用程式中執行
mix phx.gen.live Blog Post posts title body:text
,它將會設定必要的資料庫表格和 LiveView,以管理您的資料。
範例:在 LiveView 中
在 LiveView 中,此函式元件通常會以 for={@form}
呼叫,其中 @form
是 to_form/1
函式的結果。 to_form/1
預期地圖或 Ecto.Changeset
為資料來源,並將其正規化為 Phoenix.HTML.Form
結構。
例如,您可以在 Phoenix.LiveView.handle_event/3
回呼中使用接收到的參數建立 Ecto 變更組,然後使用 to_form/1
將其轉換為表單。然後,在您的範本中,您將 @form
傳遞為 :for
的引數
<.form
for={@form}
phx-change="change_name"
>
<.input field={@form[:email]} />
</.form>
.input
元件通常定義為您自己的應用程式的一部分,並加入所有必要的樣式
def input(assigns) do
~H"""
<input type="text" name={@field.name} id={@field.id} value={@field.value} class="..." />
"""
end
表單接受多個選項。例如,如果您要執行檔案上傳,並要撷取提交,那麼您可以撰寫為
<.form
for={@form}
multipart
phx-change="change_user"
phx-submit="save_user"
>
...
<input type="submit" value="Save" />
</.form>
請注意這兩個範例如何使用 phx-change
。LiveView 必須實作 phx-change
事件,並在變更時儲存輸入值。這是很重要的,因為如果頁面上發生了不相關的變更,則 LiveView 應使用其更新的值重新呈現輸入。沒有 phx-change
的話,輸入將會被清除。或者,您可以在表單上使用 phx-update="ignore"
,以略過所有更新。
使用 for
屬性
for
屬性也可以是地圖或 Ecto.Changeset。在這種情況下,表單會即時建立,您可以使用 :let
擷取
<.form
:let={form}
for={@changeset}
phx-change="change_user"
>
然而,由於兩個原因,不建議在 LiveView 中採用此方式
如果您透過
@form[:field]
來存取表單欄位,而不是透過 let-變數form
,LiveView 能夠更好地最佳化您的程式碼Ecto 變更集旨在讓您一次性使用。透過不再將變更集儲存在 assign 中,您較不容易在各種操作中使用它
關於 :errors
的說明
即使 changeset.errors
不是空的,如果 變更集 :action
是 nil
或 :ignore
,表單中仍不會顯示錯誤
這是件有用的事,例如表單欄位的驗證提示,例如某個新表單使用的空變更集。該變更集並非有效,但是我們不希望在實際使用者動作執行之前顯示錯誤
例如,如果使用者提交某樣東西,且呼叫了 Repo.insert/1
,變更集驗證卻失敗了,則動作將設定為 :insert
以顯示已經嘗試插入,而該動作的存在會導致錯誤顯示。對於 Repo.update/delete 也一樣
如果您想要手動顯示錯誤,也可以自行設定動作,直接在 Ecto.Changeset
結構欄位上設定,或是使用 Ecto.Changeset.apply_action/2
。由於動作可以任意設定,因此可以將它設定為 :validate
或其他任何值,以避免造成資料庫操作實際上已經嘗試的假象
範例:LiveView 之外(一般 HTTP 要求)
form
元件仍然可以使用來提交 LiveView 之外的表單。在這種情況下,action
屬性必須給定。如果沒有該屬性,form
方法和 CSRF 令牌將會被捨棄
<.form :let={f} for={@changeset} action={~p"/comments/#{@comment}"}>
<.input field={f[:body]} />
</.form>
在上述範例中,我們將變更集傳遞給 for
並使用 :let={f}
擷取值。這種做法在 LiveView 外部是可以的,因為沒有變更追蹤最佳化需要考慮
跨站請求偽造 (CSRF) 防護
跨站請求偽造 (CSRF) 防護是一種機制,用來確保呈現該表單的使用者是實際提交該表單的人。這個模組會預設產生一個 CSRF 令牌。您的應用程式應該在伺服器端檢查這個令牌,以避免攻擊者代表其他使用者在您的伺服器上發出要求。Phoenix 預設會檢查這個令牌
當使用包含在其地址中的主機來發布表單時,如「//host.com/path」而非只使用「/path」,Phoenix 會在憑證中包含主機簽章,然後僅在存取的主機與憑證中的主機相符時驗證憑證。這是為了避免憑證外洩至第三方應用程式。如果這個行為有問題,您可以用 Plug.CSRFProtection.get_csrf_token/0
產生非特定於主機的憑證,並透過 :csrf_token
選項將其傳遞至表單產生器。
屬性
for
(:any
)(必填) - 現有的表單或表單原始資料。action
(:string
) - 送出表單的動作。如果您打算在沒有 LiveView 的情況下將表單送出至網址,則必須提供此屬性。as
(:atom
) - 由表單產生之名稱和識別碼中要使用的字首。例如,設定as: :user_params
意指參數會在handle_event
內嵌套「user_params」,或在一般 HTTP 要求中為conn.params["user_params"]
。如果您設定此選項,您必須使用:let
擷取表單。csrf_token
(:any
) - 用於驗證要求有效性的憑證。當提供動作且方法不是get
時,會自動產生一組憑證。設定為false
時,不會產生任何憑證。errors
(:list
) - 使用此選項可手動將錯誤的關鍵字清單傳遞至表單。當以一般地圖作為表單來源時,這個選項非常有用,而且會讓錯誤在f.errors
下方顯示。如果您設定此選項,您必須使用:let
擷取表單。method
(:string
) - HTTP 方法。僅在提供:action
時使用。如果方法既不是get
也不是post
,就會在表單標籤旁邊產生一個輸入標籤,名稱為_method
。如果提供了:action
但沒有提供方法,方法預設為post
。multipart
(:boolean
) - 將enctype
設定為multipart/form-data
。上傳檔案時需要。預設值為
false
。接受全域屬性。此外,還可將 HTML 屬性新增至表單標籤。支援所有全域屬性加上下列項目:
["autocomplete", "name", "rel", "enctype", "novalidate", "target"]
。
插槽
inner_block
(必填) - 在表單標籤內指定的內容。
呈現關聯或嵌入的巢狀表單輸入值。
屬性
field
(Phoenix.HTML.FormField
)(必要)- %Phoenix.HTML.Form{}/field 名稱的元組,例如:{@form[:email]}。id
(:string
)- 表單中使用的 id,預設值為給定的欄位與父表單 id 的串接。as
(:atom
)- 表單中使用的名稱,預設值為給定的欄位與父表單名稱的串接。default
(:any
)- 如果沒有可用值,要使用的值。prepend
(:list
)- 渲染時要預先加上去的數值。這只適用於欄位值是清單且沒有參數透過表單發送的情況。append
(:list
)- 渲染時要附加的數值。這只適用於欄位值是清單且沒有參數透過表單發送的情況。skip_hidden
(:boolean
)- 略過自動渲染隱藏欄位,以允許更嚴格控制產生的標記。預設值為
false
。options
(:list
)-Phoenix.HTML.FormData
協定實作的任何其他選項。預設值為
[]
。
插槽
inner_block
(必要)- 為每個巢狀表單渲染的內容。
範例
<.form
:let={f}
phx-change="change_name"
>
<.inputs_for :let={f_nested} field={f[:nested]}>
<.input type="text" field={f_nested[:name]} />
</.inputs_for>
</.form>
動態新增和移除輸入
動態新增和移除輸入是透過為插入和移除渲染具名稱按鈕來支援的。與輸入一樣,帶有名稱/數值配對的按鈕會在變更和提交事件時,序列化為表單資料。然後 Ecto 等函式庫或自訂參數篩選功能可以檢查參數並處理已新增或已移除的欄位。這可以與 Ecto.Changeset.cast/3
的 :sort_param
和 :drop_param
選項結合使用。例如,假設一個父項具有 :emails
has_many
或 embeds_many
關聯。若要從巢狀表單套用使用者輸入,只需設定選項
schema "mailing_lists" do
field :title, :string
embeds_many :emails, EmailNotification, on_replace: :delete do
field :email, :string
field :name, :string
end
end
def changeset(list, attrs) do
list
|> cast(attrs, [:title])
|> cast_embed(:emails,
with: &email_changeset/2,
sort_param: :emails_sort,
drop_param: :emails_drop
)
end
以下是 :sort_param
和 :drop_param
選項的作用。
注意:使用這些選項時,
has_many
和embeds_many
上的on_replace: :delete
是必要的。
當Ecto從表單中看到指定排序或刪除參數時,它會根據參數在表單中出現的順序為子項排序、新增未見到的子項或刪除參數指示刪除的子項。
此類架構和關聯的標記會如下所示
<.inputs_for :let={ef} field={@form[:emails]}>
<input type="hidden" name="mailing_list[emails_sort][]" value={ef.index} />
<.input type="text" field={ef[:email]} placeholder="email" />
<.input type="text" field={ef[:name]} placeholder="name" />
<button
type="button"
name="mailing_list[emails_drop][]"
value={ef.index}
phx-click={JS.dispatch("change")}
>
<.icon name="hero-x-mark" class="w-6 h-6 relative top-2" />
</button>
</.inputs_for>
<input type="hidden" name="mailing_list[emails_drop][]" />
<button type="button" name="mailing_list[emails_sort][]" value="new" phx-click={JS.dispatch("change")}>
add more
</button>
我們使用inputs_for
來為:emails
關聯渲染輸入,其中包含每個子項的電子郵件地址和名稱輸入。在巢狀輸入中,我們渲染一個隱藏的mailing_list[emails_sort][]
輸入,它設定為指定子項的索引。它會告知Ecto的轉換作業如何排序現有子項或要插入新子項的位置。接著,我們照常渲染電子郵件和名稱輸入。然後,我們渲染一個包含「刪除」文字、名稱為mailing_list[emails_drop][]
、並含有子項索引做為其值的按鈕。
如同前面所述,這會告知Ecto在按鈕被點選時刪除此索引的子項。我們在按鈕上使用phx-click={JS.dispatch("change")}
告知LiveView將此按鈕點選當成變更事件,而非表單上的提交事件,這會呼叫我們表單上的phx-change
綁定。
在inputs_for
之外,我們渲染一個空mailing_list[emails_drop][]
輸入,以確保在儲存使用者已刪除所有輸入的表單時,會刪除所有子項。每當刪除關聯時,均需要此隱藏輸入。
最後,我們還渲染另一個按鈕,其排序參數名稱為mailing_list[emails_sort][]
和value="new"
名稱,並附帶「新增更多」文字。請注意,此按鈕必須具有type="button"
,以防止它提交表單。Ecto會將未知的排序參數視為新子項,並建立一個新的子項。此按鈕是選用的,而且僅在您要動態新增輸入時才需要。在情況允許的情況下,您還可以在<.inputs_for>
前新增類似的按鈕,以預先新增輸入。
在可列舉資料間穿插分隔符號插槽。
在您需要於項目之間新增分隔符號時很有用,例如導覽麵包屑時。提供每個項目至內部區塊。
範例
<.intersperse :let={item} enum={["home", "profile", "settings"]}>
<:separator>
<span class="sep">|</span>
</:separator>
<%= item %>
</.intersperse>
渲染以下標記
home <span class="sep">|</span> profile <span class="sep">|</span> settings
屬性
enum
(:any
)(必需) - 與分隔符號穿插的列舉。
插槽
inner_block
(必需) - 為每個項目渲染的內部區塊。separator
(必需) - 分隔符號的插槽。
產生一個連結指向指定的路由。
使用傳統瀏覽器的導航模式在頁面間導航,請使用 href
屬性。若要修補目前的 LiveView 或在 LiveViews 之間導航,請分別使用 patch
和 navigate
。
屬性
navigate
(:string
) - 從一個 LiveView 導航到新的 LiveView。瀏覽器頁面會保留,但會掛載新的 LiveView 處理序並重新載入頁面上的內容。只能在同一個路由器下宣告的 LiveViews 之間導航Phoenix.LiveView.Router.live_session/3
。否則,會使用完整的瀏覽器重新導向。patch
(:string
) - 修補目前的 LiveView。會呼叫目前的 LiveView 的handle_params
callback,並且會透過連接線傳送最少內容,與其他 LiveView diff 一樣。href
(:any
) - 使用傳統的瀏覽器導航到新位置。表示瀏覽器會重新載入整個頁面。replace
(:boolean
) - 使用:patch
或:navigate
時,是否應以pushState
取代瀏覽器的歷程?預設值為
false
。method
(:string
) - 使用連結的 HTTP 方法。這用於 Phoenix LiveView 外,因此僅適用於href={...}
屬性。不會對patch
和navigate
指令產生任何影響。如果方法非
get
,連結會在設定適當資訊的表單內產生。瀏覽器中必須啟用 JavaScript 才能提交表單。預設為
"get"
。csrf_token
(:any
) - 布林值或自訂代碼,適用於使用 HTTP 方法且方法非get
的連結。預設為true
。接受全域屬性。新增到
a
標籤的其他 HTML 屬性。支援所有全域以及以下屬性:["download", "hreflang", "referrerpolicy", "rel", "target", "type"]
。
插槽
inner_block
(必要) - 在a
標籤內呈現的內容。
範例
<.link href="/">Regular anchor link</.link>
<.link navigate={~p"/"} class="underline">home</.link>
<.link navigate={~p"/?sort=asc"} replace={false}>
Sort By Price
</.link>
<.link patch={~p"/details"}>view details</.link>
<.link href={URI.parse("https://elixir.dev.org.tw")}>hello</.link>
<.link href="/the_world" method="delete" data-confirm="Really?">delete</.link>
JavaScript 依賴關係
為了支援連結方法 :method
不為 "get"
或使用上述資料屬性,Phoenix.HTML
仰賴 JavaScript。你可以將 priv/static/phoenix_html.js
載入至你的建置工具中。
資料屬性
資料屬性以關鍵字清單的形式新增至 data
鍵。支援以下資料屬性
data-confirm
- 當:method
不為"get"
時,在生成並提交表單之前顯示確認提示。
覆寫預設確認行為
phoenix_html.js
確實會在點擊發生時,於被點擊的 DOM 元素上觸發自訂事件 phoenix.link.click
。這能讓你攔截傳遞到 window
的途中不斷冒泡的事件,並透過自訂邏輯改善或替換 data-confirm
屬性的處理方式。例如,你可以使用自訂的 JavaScript 實作替換瀏覽器的 confirm()
行為
// Compared to a javascript window.confirm, the custom dialog does not block
// javascript execution. Therefore to make this work as expected we store
// the successful confirmation as an attribute and re-trigger the click event.
// On the second click, the `data-confirm-resolved` attribute is set and we proceed.
const RESOLVED_ATTRIBUTE = "data-confirm-resolved";
// listen on document.body, so it's executed before the default of
// phoenix_html, which is listening on the window object
document.body.addEventListener('phoenix.link.click', function (e) {
// Prevent default implementation
e.stopPropagation();
// Introduce alternative implementation
var message = e.target.getAttribute("data-confirm");
if(!message){ return; }
// Confirm is resolved execute the click event
if (e.target?.hasAttribute(RESOLVED_ATTRIBUTE)) {
e.target.removeAttribute(RESOLVED_ATTRIBUTE);
return;
}
// Confirm is needed, preventDefault and show your modal
e.preventDefault();
e.target?.setAttribute(RESOLVED_ATTRIBUTE, "");
vex.dialog.confirm({
message: message,
callback: function (value) {
if (value == true) {
// Customer confirmed, re-trigger the click event.
e.target?.click();
} else {
// Customer canceled
e.target?.removeAttribute(RESOLVED_ATTRIBUTE);
}
}
})
}, false);
或者,你可以附加你自己的自訂行為。
window.addEventListener('phoenix.link.click', function (e) {
// Introduce custom behaviour
var message = e.target.getAttribute("data-prompt");
var answer = e.target.getAttribute("data-prompt-answer");
if(message && answer && (answer != window.prompt(message))) {
e.preventDefault();
}
}, false);
後者也可以繫結到任何 click
事件,但這樣可以確保只有在執行 phoenix_html.js
的程式碼時才會執行你的自訂程式碼。
CSRF 保護
預設情況下,CSRF 令牌是透過 Plug.CSRFProtection
產生的。
用於在父級 LiveView 中呈現 Phoenix.LiveComponent
的函式組件。
儘管 LiveView 可以巢狀,但每個 LiveView 都會啟動自己的程序。LiveComponent 提供類似 LiveView 的功能,不過它們會在與 LiveView 相同的程序中執行,並有自己的封裝狀態。這就是為什麼它們被稱為有狀態元件。
屬性
id
(:string
)(必填) - LiveComponent 的唯一識別碼。請注意,id
不一定會用作 DOM 的id
。這要由元件自己決定。module
(:atom
)(必填) - 要渲染的 LiveComponent 模組。
提供的任何其他屬性都會以屬性檔案形式傳遞給 LiveComponent。有關更多資訊,請參閱 Phoenix.LiveComponent
。
範例
<.live_component module={MyApp.WeatherComponent} id="thermostat" city="Kraków" />
建立一個檔案輸入標籤用於 LiveView 上傳。
屬性
upload
(Phoenix.LiveView.UploadConfig
)(必填) -Phoenix.LiveView.UploadConfig
結構。accept
(:string
) - 覆寫 accept 屬性的選項。預設為 allow_upload 所指定的 :accept。- 接受全域屬性。支援所有全域以及:
["webkitdirectory", "required", "disabled", "capture", "form"]
。
拖曳和放置
透過為可放置的容器加上 phx-drop-target
屬性,並指向 UploadConfig ref
,即可支援拖曳和放置,因此,下列程式碼標記是拖曳和放置支援所需的所有內容
<div class="container" phx-drop-target={@uploads.avatar.ref}>
<!-- ... -->
<.live_file_input upload={@uploads.avatar} />
</div>
範例
呈現檔案輸入
<.live_file_input upload={@uploads.avatar} />
呈現帶標籤的檔案輸入
<label for={@uploads.avatar.ref}>Avatar</label>
<.live_file_input upload={@uploads.avatar} />
為已選檔案在用戶端產生一個圖片預覽。
屬性
entry
(Phoenix.LiveView.UploadEntry
) (必需) -Phoenix.LiveView.UploadEntry
結構。id
(:string
) - img 標籤的 id。預設來自 entry ref,但必要時可以覆寫,如果您需要在同一頁面多次呈現同一條目的預覽。預設為nil
。- 接受全域屬性。
範例
<%= for entry <- @uploads.avatar.entries do %>
<.live_img_preview entry={entry} width="75" />
<% end %>
當您需要多次使用時,請確定它們有不同的 id
<%= for entry <- @uploads.avatar.entries do %>
<.live_img_preview entry={entry} width="75" />
<% end %>
<%= for entry <- @uploads.avatar.entries do %>
<.live_img_preview id={"modal-#{entry.ref}"} entry={entry} width="500" />
<% end %>
呈現一個標題,並在 @page_title
更新時自動加上前後綴。
屬性
prefix
(:string
) -inner_block
內容之前加入的前置詞。預設為nil
suffix
(:string
) -inner_block
內容之後加入的後置詞。預設為nil
插槽
inner_block
(必需) - 在title
標籤內呈現的內容。
範例
<.live_title prefix="MyApp – ">
<%= assigns[:page_title] || "Welcome" %>
</.live_title>
<.live_title suffix="- MyApp">
<%= assigns[:page_title] || "Welcome" %>
</.live_title>
巨集
宣告 HEEx 函式組件的屬性。
引數
名稱
- 定義屬性的名稱的原子。請注意,屬性不能定義與宣告給相同元件的任何其他屬性或時隙相同的名稱。類型
- 定義屬性的類型的原子。選項
- 選項的關鍵字清單。預設為[]
。
類型
屬性由其名稱、類型和選項宣告。支援下列類型
名稱 | 描述 |
---|---|
:any | 任何詞彙 |
:string | 任何二進制字串 |
:atom | 任何原子(包含 true 、false 和 nil ) |
:boolean | 任何布林值 |
:integer | 任何整數 |
:float | 任何浮點數 |
:list | 任何包含任何任意類型的清單 |
:map | 任何包含任何任意類型的對應 |
:global | 任何常見的 HTML 屬性,加上 :global_prefixes 定義的那些 |
結構模組 | 透過 defstruct/1 定義結構的任何模組 |
選項
:required
- 將屬性標示為必要。如果呼叫方沒有傳遞給定的屬性,會發出編譯警告。:default
- 如果沒有提供屬性的預設值。如果未設定此選項,且未提供屬性,除非透過assign_new/3
明確設定值,否則存取屬性會失敗。:examples
- 屬性接受的非詳盡值清單,用於文件目的。:values
- 屬性接受的詳盡值清單。如果呼叫方傳遞未包含在此清單中的文字常數,會發出編譯警告。:doc
- 屬性的文件。
編譯時間驗證
LiveView 透過 :phoenix_live_view
編譯器對屬性執行一些驗證。當定義屬性時,如果
元件的必要屬性遺失。
給出未知的屬性。
您指定文字屬性(例如
value="string"
或value
,但沒有value={expr}
),資料類型不相符。當前支援文字驗證的資料類型有::string
、:atom
、:boolean
、:integer
、:float
、:map
和:list
。您指定文字屬性,但它不是
:values
清單的成員。
LiveView 執行階段不會進行任何驗證。這表示資料類型資訊主要用於文件編製和反映目的。
在 LiveView 元件本身這一端,定義屬性可提供下列改善生活品質的地方
系統會預先將所有屬性的預設值新增至
assigns
對應中。系統會為元件產生屬性文件。
系統會註解必要的結構資料類型,並發出編譯警告。例如,如果您指定
attr :user, User, required: true
,然後在範本中寫@user.non_valid_field
,會發出警告。呼叫元件以進行反映和驗證追蹤。
文件產生
定義屬性的公開函式元件,會將其屬性資料類型和文件插入至函式的文件中,視 @doc
模組屬性的值而定
如果
@doc
是字串,會將屬性文件插入至該字串中。可以選擇放置符[INSERT LVATTRDOCS]
來指定文件在字串中的插入位置。否則,文件會附加在@doc
字串的結尾。如果未指定
@doc
,會將屬性文件用於預設@doc
字串。如果
@doc
為false
,會完全略過屬性文件。
插入的屬性文件格式會使用 markdown 清單
name
(:type
) (必要) - 屬性文件,預設為:default
。
預設情況下,所有屬性的資料類型和文件會插入至函式的 @doc
字串中。若要隱藏特定屬性,可以將 :doc
的值設定為 false
。
範例
attr :name, :string, required: true
attr :age, :integer, required: true
def celebrate(assigns) do
~H"""
<p>
Happy birthday <%= @name %>!
You are <%= @age %> years old.
</p>
"""
end
將外部範本檔案嵌入模組中作為函式組件。
選項
:root
- 嵌入檔案的根目錄。預設為目前模組的目錄 (__DIR__
):suffix
- 要附加到內嵌函數名稱的字串值。預設情況下,函數名稱將是範本檔名,不含格式和引擎。
可以使用萬用字元模式來選擇目錄樹中的所有檔案。例如,想像一個目錄清單
├── components.ex
├── pages
│ ├── about_page.html.heex
│ └── welcome_page.html.heex
然後將頁面範本內嵌到您的 components.ex
模組
defmodule MyAppWeb.Components do
use Phoenix.Component
embed_templates "pages/*"
end
現在,您的模組將具備已定義的 about_page/1
和 welcome_page/1
函數元件。內嵌範本還支援透過無主體函數定義進行宣告指派,例如
defmodule MyAppWeb.Components do
use Phoenix.Component
embed_templates "pages/*"
attr :name, :string, required: true
def welcome_page(assigns)
slot :header
def about_page(assigns)
end
還支援多次呼叫 embed_templates
,如果您有多種範本格式,會很有用。例如
defmodule MyAppWeb.Emails do
use Phoenix.Component
embed_templates "emails/*.html", suffix: "_html"
embed_templates "emails/*.text", suffix: "_text"
end
注意:此函數與 Phoenix.Template.embed_templates/2
相同。基於方便性以及文件用途,也會在這裡提供。因此,如果您想要內嵌與 Phoenix.Component
無關的其他格式範本,請偏好用這個模組來 import Phoenix.Template, only: [embed_templates: 1]
,而非直接使用。
用於在原始檔中撰寫 HEEx 範本的 ~H
巨集。
HEEx
是 Elixir 內嵌語言 (EEx
) 的 HTML 感知元件友好延伸,它提供
內建的 HTML 屬性處理
注入函數元件的 HTML 類似表示法
範本結構的編譯時驗證
可將透過網際網路傳送的資料量降至最低的能力
透過
mix format
進行現成的程式碼格式化
範例
~H"""
<div title="My div" class={@class}>
<p>Hello <%= @name %></p>
<MyApp.Weather.city name="Kraków"/>
</div>
"""
語法
HEEx
建構在 Embedded Elixir (EEx
) 的基礎上。在這個區塊中,我們將介紹 HEEx
範本的基本構造以及其語法延伸。
內插
HEEx
和 EEx
範本都使用 <%= ... %>
在 HTML 標籤的主體內內插程式碼
<p>Hello, <%= @name %></p>
類似地,支援條件式和其他區塊 Elixir 構造
<%= if @show_greeting? do %>
<p>Hello, <%= @name %></p>
<% end %>
請注意,我們沒有在結束標籤 <% end %>
中包含等號 =
(因為結束標籤不會輸出任何內容)。
在 HEEx
與 Elixir 內建的 EEx
之間,有一個重要的差異。 HEEx
使用一種特定註解來內插 HTML 標籤和屬性。讓我們來檢視一下。
HEEx 擴充:定義屬性
由於 HEEx
必須解析和驗證 HTML 結構,因此使用 <%= ... %>
和 <% ... %>
進行程式內插的範圍僅限於 HTML/元件節點的主體(內部內容),而且不能應用於標籤內。
舉例來說,以下語法是無效的
<div class="<%= @class %>">
...
</div>
使用下列方式代替
<div class={@class}>
...
</div>
你可以將任何 Elixir 運算式放入 { ... }
之間。例如,如果你想設定類別,其中有些是靜態的、有些是動態的,你可以使用字串內插
<div class={"btn btn-#{@type}"}>
...
</div>
下列屬性值具有特殊意義
true
- 如果值為true
,屬性會在完全沒有值的情況下呈現。例如,<input required={true}>
與<input required>
相同;false
或nil
- 如果值為false
或nil
,屬性會被省略。為了最佳化目的,有些屬性可能會在具有與省略相同效果的情況下,使用空值呈現。例如,<checkbox checked={false}>
呈現為<checkbox>
,而<div class={false}>
呈現為<div class="">
;list
(僅適用於class
屬性)- 清單中的每個元素都會被視為一個不同的類別來處理。nil
和false
元素會被捨棄。
對於多個動態屬性,你可以使用相同的標記法,但不用將運算式指定給任何特定屬性。
<div {@dynamic_attrs}>
...
</div>
{...}
內部的運算式必須是關鍵字清單或包含表示動態屬性的鍵值對的映射。
HEEx 擴充:定義函式元件
函式元件是不帶狀態的元件,由純粹函式在 Phoenix.Component
模組的幫助下實作。它們可以是區域性的(相同模組)或遠端的(外部模組)。
HEEx
允許在範本中使用類似 HTML 的標記法,直接呼叫這些函式元件。例如,遠端函式
<MyApp.Weather.city name="Kraków"/>
小數點作為開頭可以呼叫區域函式
<.city name="Kraków"/>
元件可以定義如下
defmodule MyApp.Weather do
use Phoenix.Component
def city(assigns) do
~H"""
The chosen city is: <%= @name %>.
"""
end
def country(assigns) do
~H"""
The chosen country is: <%= @name %>.
"""
end
end
通常最好將相關功能分組至單一模組,與擁有許多具備單一 render/1
函式的模組不同。函式元件支援其他重要功能,例如插槽。您可以在 Phoenix.Component
中進一步瞭解元件。
HEEx 延伸:特殊屬性
除了正常的 HTML 屬性之外,HEEx 也支援一些特殊屬性,例如 :let
和 :for
。
:let
這由希望傳回值的呼叫者元件和插槽使用。舉例來說,請參閱 form/1
的運作方式
<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
<.input field={f[:username]} type="text" />
...
</.form>
請注意由 .form
定義的可變數 f
是如何由您的 input
元件使用。 Phoenix.Component
模組中有進一步的說明文件,說明如何使用及實作此類似功能。
:if 和 :for
這是 <%= if .. do %>
和 <%= for .. do %>
的語法糖,可用於一般 HTML、函式元件和插槽中。
例如在 HTML 標籤中
<table id="admin-table" :if={@admin?}>
<tr :for={user <- @users}>
<td><%= user.name %></td>
</tr>
<table>
上述程式碼片段僅會在 @admin?
為真時顯示表格,並針對每個使用者產生一個 tr
,正如您預期在集合中所顯示的。
:for
在函式元件中也可相似使用
<.error :for={msg <- @errors} message={msg}/>
這等同於編寫
<%= for msg <- @errors do %>
<.error message={msg} />
<% end %>
而 :for
在插槽中的行為方式相同
<.table id="my-table" rows={@users}>
<:col :for={header <- @headers} :let={user}>
<td><%= user[header] %></td>
</:col>
<table>
您也可以結合 :for
和 :if
用於標籤、元件和插槽,作為一個過濾器
<.error :for={msg <- @errors} :if={msg != nil} message={msg} />
請注意,與 Elixir 一般 for
不同的是,HEEx' :for
不支援在單一運算式中使用多個產生器。
程式碼格式化
您可以使用 Phoenix.LiveView.HTMLFormatter
自動格式化 HEEx 範本 (.heex) 和 ~H
sigil。請查看該模組以取得更多資訊。
宣告一個插槽。更多資訊請參閱 slot/3
。
宣告一個函式組件插槽。
引數
name
- 定義插槽名稱的原子。請注意,插槽無法定義與相同元件宣告的任何其他插槽或屬性相同的名稱。選項
- 選項的關鍵字清單。預設為[]
。block
- 放置呼叫attr/3
的程式碼區塊。預設為nil
。
選項
:required
- 標記一個所需插槽。如果呼叫者沒有傳遞給所需插槽一個值,會發出編譯警告。否則,被忽略的插槽會預設為[]
。:validate_attrs
- 當設為false
時,呼叫者傳遞屬性給沒有放置 do 區塊所定義的插槽時,不會發出警告。如果沒有設定,預設為true
。:doc
- 插槽的說明文件。任何已宣告的插槽屬性都會將其說明文件列在它的插槽旁邊。
插槽屬性
一個已命名插槽可以透過傳遞呼叫 attr/3
的區塊來宣告屬性。
不同於屬性,插槽屬性無法接受 :default
選項。傳遞它會導致發出一個編譯警告。
預設插槽
預設插槽可以透過傳遞 :inner_block
作為插槽的 name
來宣告。
請注意,:inner_block
插槽宣告無法接受一個區塊。傳遞一個區塊會導致發生編譯錯誤。
編譯時期驗證
LiveView 透過 :phoenix_live_view
編譯器對插槽執行一些驗證。當插槽被定義時,LiveView 會在呼叫者於編譯時期警告如果
元件所需插槽不見了。
給了一個未知插槽。
給了一個未知插槽屬性。
在函式元件自己那邊,定義屬性提供以下生活品質改善
插槽說明文件會為元件產生。
呼叫元件以進行反映和驗證追蹤。
說明文件產生
定義插槽的公開函式元件會將其文件注入到函式的說明文件中,這取決於 @doc
模組屬性的值
如果
@doc
是字串,插槽文件會注入到該字串中。可以選擇性宣告的佔位符[INSERT LVATTRDOCS]
能用來指定文件被注入到字串中位置。否則,文件會附加到@doc
字串的最後面。如果
@doc
沒有指定,插槽文件會被作為預設@doc
字串。如果
@doc
等於false
,則省略 slot 文件。
注入的 slot 文件格式化為 markdown 清單
name
(必要) - slot 文件。接受屬性name
(:type
) (必要) - 屬性文件,預設為:default
。
預設情況下,所有 slot 都會將其文件注入到 @doc
字串中。若要隱藏特定 slot,可以將 :doc
的值設為 false
。
範例
slot :header
slot :inner_block, required: true
slot :footer
def modal(assigns) do
~H"""
<div class="modal">
<div class="modal-header">
<%= render_slot(@header) || "Modal" %>
</div>
<div class="modal-body">
<%= render_slot(@inner_block) %>
</div>
<div class="modal-footer">
<%= render_slot(@footer) || submit_button() %>
</div>
</div>
"""
end
如同上方範例所示,render_slot/1
在宣告一個選填插槽時,且未給予任何插槽時會回傳 nil
。這可以用於加入預設行為。
函式
新增關鍵字值對至 assigns。
第一個引數可以是 LiveView socket
或函式元件的 assigns
對應。
必須提供關鍵字清單或 assigns 對應,將其合併到現有的 assigns 中。
範例
iex> assign(socket, name: "Elixir", logo: "💧")
iex> assign(socket, %{name: "Elixir"})
新增一組 key
-value
至 socket_or_assigns
。
第一個引數可以是 LiveView socket
或函式元件的 assigns
對應。
範例
iex> assign(socket, :name, "Elixir")
如果 socket_or_assigns
中還不存在 key
,則指定 key
的值為 fun
的回傳值。
第一個引數可以是 LiveView socket
或函式元件的 assigns
對應。
這個函式對於延遲指定值和分享 assigns 有幫助。我們接下來將介紹這兩種使用案例。
延遲指定
想像一下一個接受顏色的函式元件
<.my_component bg_color="red" />
這個顏色也是可選的,因此可以跳過
<.my_component />
在這種情況下,實作可以使用 assign_new
在沒有給定顏色時延遲指定一個顏色。讓我們這麼做,當沒有給定顏色時,從中選擇一個隨機的
def my_component(assigns) do
assigns = assign_new(assigns, :bg_color, fn -> Enum.random(~w(bg-red-200 bg-green-200 bg-blue-200)) end)
~H"""
<div class={@bg_color}>
Example
</div>
"""
end
分享 assigns
可以在在斷開的渲染中,於 Plug 管線和 LiveView 之間分享 assigns,或是在連線時,於父層級和子層級的 LiveView 之間分享 assigns。
斷開時
當使用者第一次使用 LiveView 存取應用程式時,LiveView 會先以斷開的狀態進行渲染,作為一般 HTML 回應的一部分。透過在 LiveView 的 mount callback 中使用 assign_new
,可以指示 LiveView 在斷開的狀態期間,重新使用已在 conn
中設定的任何 assigns。
想像一下一個 Plug 執行
# A plug
def authenticate(conn, _opts) do
if user_id = get_session(conn, :user_id) do
assign(conn, :current_user, Accounts.get_user!(user_id))
else
send_resp(conn, :forbidden)
end
end
可以在初始渲染期間,在 LiveView 中重新使用 :current_user
assign
def mount(_params, %{"user_id" => user_id}, socket) do
{:ok, assign_new(socket, :current_user, fn -> Accounts.get_user!(user_id) end)}
end
如果 conn.assigns.current_user
存在,就會在這種情況下使用它。如果沒有這樣的 :current_user
assign,或 LiveView 是作為實際導覽的一部分進行 mount,其中沒有呼叫任何 Plug 管線,則會呼叫匿名函式來執行查詢。
連線時
LiveView 也能透過 assign_new
與子 LiveView 共享指派,只要子 LiveView 在父 LiveView 掛載時也掛載的話。以下是一個範例。
假如父 LiveView 定義了一個 :current_user
指派,而子 LiveView 也在 mount/3
回呼中使用 assign_new/3
來擷取 :current_user
,就像前一個小節一樣,會從父 LiveView 擷取指派,再一次避免其他資料庫查詢。
注意,fun
也有提供取得之前指派的值的能力
assigns =
assigns
|> assign_new(:foo, fn -> "foo" end)
|> assign_new(:bar, fn %{foo: foo} -> foo <> "bar" end)
指派共享會在可能的情況下進行,但並非保證。因此,您必須確保提供給 assign_new/3
的函式的結果,必須跟從父元件擷取的值一樣。否則,考慮將值作為其階段的一部分,傳遞給子 LiveView。
過濾 assigns,產生一個關鍵字清單,用於動態標籤屬性。
應該優先使用宣告指派和 :global
屬性,而不是這個函式。
範例
想像以下 my_link
元件,允許呼叫者傳遞 new_window
指派,以及他們想要新增到元素的任何其他屬性,例如 class、資料屬性等。
<.my_link to="/" id={@id} new_window={true} class="my-class">Home</.my_link>
我們可以使用以下元件支援動態屬性
def my_link(assigns) do
target = if assigns[:new_window], do: "_blank", else: false
extra = assigns_to_attributes(assigns, [:new_window, :to])
assigns =
assigns
|> assign(:target, target)
|> assign(:extra, extra)
~H"""
<a href={@to} target={@target} {@extra}>
<%= render_slot(@inner_block) %>
</a>
"""
end
上述程式碼會產生以下呈現的 HTML
<a href="/" target="_blank" id="1" class="my-class">Home</a>
傳遞給 assigns_to_attributes
的第二個引數(自選)是排除的 key 清單。通常包含元件本身保留的 key,這些 key 既不屬於標記中,或是元件已經明確處理。
檢查 socket_or_assigns
中的特定關鍵字值是否改變。
第一個引數可以是 LiveView socket
或函式元件的 assigns
對應。
範例
iex> changed?(socket, :count)
傳回 LiveView flash assign 中的快閃訊息。
範例
<p class="alert alert-info"><%= live_flash(@flash, :info) %></p>
<p class="alert alert-danger"><%= live_flash(@flash, :error) %></p>
在範本中呈現一個 LiveView。
有兩個情況會用到這個函式
在 LiveView 中呈現子 LiveView 時。
在一般(非即時)控制器/檢視中呈現 LiveView 時。
選項
:session
- 使用二進位 key 的對應,其包含要序列化並傳送到用戶端的其他階段資料。連線中目前的所有階段資料,在 LiveView 中都能自動取得。您可以使用這個選項來提供額外的資料。請記住,所有階段資料都會序列化並傳送到用戶端,所以您應該始終將階段中的資料維持在最低限度。例如,不要儲存 User 結構,您應該儲存「user_id」,並在載入 LiveView 時載入 User。:container
- 用於視圖容器的 HTML 標籤和 DOM 屬性的選用元組。範例:{:li, style: "color: blue;"}
。預設使用模組定義容器。如需更多資訊,請參閱以下「容器」區段。:id
- 用於唯一識別 LiveView 的 DOM ID 和 ID。在渲染根 LiveView 時,將自動產生:id
,但在渲染子 LiveView 時,:id
為必填選項。:sticky
- 即使嵌入在其他 LiveView 中,也可在動態重新導向中維持 LiveView 的選用標誌。如果在動態佈局內渲染 sticky 視圖,請確定 sticky 視圖本身未使用相同的佈局。透過返回{:ok, socket, layout: false}
可達成此目的,其方法為從掛載中提取。
範例
從控制器/視圖中,您可以呼叫
<%= live_render(@conn, MyApp.ThermostatLive) %>
或
<%= live_render(@conn, MyApp.ThermostatLive, session: %{"home_id" => @home.id}) %>
在另一個 LiveView 中,您必須傳遞 :id
選項
<%= live_render(@socket, MyApp.ThermostatLive, id: "thermostat") %>
容器
在 LiveView 被渲染時,其內容會被包在一個容器中。預設,容器是一個包含許多 LiveView 特定屬性的 div
標籤。
可以使用不同的方式自訂容器
可以在
use Phoenix.LiveView
中變更預設container
use Phoenix.LiveView, container: {:tr, id: "foo-bar"}
您可以在呼叫
live_render
時覆寫容器標籤並傳遞其他屬性(以及在路由器中的live
呼叫中)live_render socket, MyLiveView, container: {:tr, class: "highlight"}
如果您不希望容器影響佈局,可以使用 CSS 屬性 display: contents
或可以套用的類別,例如 Tailwind 的 .contents
。
如果您設定此屬性為 :body
,請小心,因為在 LiveView 連線後,內文注入的任何內容(例如 Phoenix.LiveReload
功能)將會丟棄
呈現一個插槽條目,並傳入一個可選的 argument
。
<%= render_slot(@inner_block, @form) %>
如果槽沒有任何條目,將會傳回 nil。
如果為同一個槽定義多個槽條目,render_slot/2
將自動渲染所有條目,合併其內容。如果您想要使用條目的屬性,則需要逐一瀏覽清單來個別存取每個槽。
例如,想像一個表格元件
<.table rows={@users}>
<:col :let={user} label="Name">
<%= user.name %>
</:col>
<:col :let={user} label="Address">
<%= user.address %>
</:col>
</.table>
在頂層,我們將列傳遞為 assign,並為表格中我們想要的每個欄定義一個 :col
槽。每個欄還有一個 label
,我們將它用於表格標題。
在組件內,你可以用標題、列和欄來渲染表格
def table(assigns) do
~H"""
<table>
<tr>
<%= for col <- @col do %>
<th><%= col.label %></th>
<% end %>
</tr>
<%= for row <- @rows do %>
<tr>
<%= for col <- @col do %>
<td><%= render_slot(col, row) %></td>
<% end %>
</tr>
<% end %>
</table>
"""
end
將指定的資料結構轉換為 Phoenix.HTML.Form
。
這通常用於將 map 或 Ecto changeset 轉換成表單,供 form/1
組件使用。
從參數建立表單
若要根據 handle_event
參數建立表單,可以這麼做
def handle_event("submitted", params, socket) do
{:noreply, assign(socket, form: to_form(params))}
end
將 map 傳遞給 to_form/1
時,它會假設該 map 包含表單參數,預計表單參數具有字串鍵。
你也可以指定一個名稱來巢狀參數
def handle_event("submitted", %{"user" => user_params}, socket) do
{:noreply, assign(socket, form: to_form(user_params, as: :user))}
end
從 changeset 建立表單
在使用 changeset 時,基礎資料、表單參數和錯誤從中擷取。 :as
選項也會自動計算。例如,如果你有使用者架構
defmodule MyApp.Users.User do
use Ecto.Schema
schema "..." do
...
end
end
然後你建立一個你傳遞給 to_form
的 changeset
%MyApp.Users.User{}
|> Ecto.Changeset.change()
|> to_form()
在這種情況下,表單提交後,參數將會在 %{"user" => user_params}
中提供。
選項
:as
- 將用於表單輸入的name
前綴:id
- 將用於表單輸入的id
前綴:errors
- 錯誤關鍵字清單(專門用於 maps)
轉換成表單時,基礎資料可能會接受額外的選項。例如,map 接受 :errors
來列出錯誤,但 changeset 不接受此選項。 :errors
是形狀為 {error_message, options_list}
的元組關鍵字。以下是一個範例
to_form(%{"search" => nil}, errors: [search: {"Can't be blank", []}])
如果給定現有的 Phoenix.HTML.Form
結構體,以上選項將覆寫其現有的值(如果給定的話)。然後,其餘的選項會合併到現有的表單選項中。
表單中的錯誤只有在 changeset 的 action
欄位已設定(且未設定為 :ignore
)時才會顯示。更多資訊請參閱 有關 :errors 的注意事項。
使用 fun
更新 socket_or_assigns
中現有的 key
值。
第一個引數可以是 LiveView socket
或函式元件的 assigns
對應。
更新函式接收目前的 key 值並回傳更新後的 value。如果 key 不存在,會擲回例外。
更新函數也可以是 2 元,這種情況下,它會將目前的 key 值接收為第一個引數,並且將目前的 assigns 接收為第二個引數。如果 key 不存在,會擲回例外。
範例
iex> update(socket, :count, fn count -> count + 1 end)
iex> update(socket, :count, &(&1 + 1))
iex> update(socket, :max_users_this_session, fn current_max, %{users: users} ->
...> max(current_max, length(users))
...> end)
傳回整體上傳的錯誤。
針對適用於特定上傳條目的錯誤,請使用 upload_errors/2
。
輸出為清單。可能會傳回下列錯誤
:too_many_files
- 選取的檔案數量超過:max_entries
約束
範例
def upload_error_to_string(:too_many_files), do: "You have selected too many files"
<div :for={err <- upload_errors(@uploads.avatar)} class="alert alert-danger">
<%= upload_error_to_string(err) %>
</div>
傳回上傳條目的錯誤。
針對適用於上傳整體的錯誤,請使用 upload_errors/1
。
輸出為清單。可能會傳回下列錯誤
:too_large
- 該條目超過:max_file_size
約束:not_accepted
- 該條目不符合:accept
MIME 類型:external_client_failure
- 外部上傳失敗時{:writer_failure, reason}
- 使用者定義的撰寫器失敗,且有reason
範例
defp upload_error_to_string(:too_large), do: "The file is too large"
defp upload_error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
defp upload_error_to_string(:external_client_failure), do: "Something went terribly wrong"
<%= for entry <- @uploads.avatar.entries do %>
<div :for={err <- upload_errors(@uploads.avatar, entry)} class="alert alert-danger">
<%= upload_error_to_string(err) %>
</div>
<% end %>