檢視原始碼 表單繫結

表單事件

若要處理表單變更及提交,請使用 phx-changephx-submit 事件。一般而言,較建議在表單層級處理輸入變更,其中所有表單欄位會在任何單一輸入變更時傳遞至 LiveView 的回呼。舉例來說,若要處理即時表單驗證及儲存,表單會同時使用 phx-changephx-submit 繫結。我們來看看一個範例

<.form for={@form} phx-change="validate" phx-submit="save">
  <.input type="text" field={@form[:username]} />
  <.input type="email" field={@form[:email]} />
  <button>Save</button>
</.form>

.formPhoenix.Component.form/1 中定義的功能元件,我們建議參閱其說明文件,以取得更詳盡的運作方式和所有支援選項。 .form 預期 @form 指定,可透過 Phoenix.Component.to_form/1 從變更組或使用者參數建立指定。

input/1 是用於呈現輸入的功能元件,最常在您自己的應用程式中定義,通常會封裝標籤、錯誤處理等等。以下是入門的簡易版本

attr :field, Phoenix.HTML.FormField
attr :rest, :global, include: ~w(type)
def input(assigns) do
  ~H"""
  <input id={@field.id} name={@field.name} value={@field.value} {@rest} />
  """
end

CoreComponents 模組

如果您使用 Phoenix v1.7 產生應用程式,那麼 mix phx.new 會自動匯入許多準備好使用的功能元件,例如內建功能及樣式的 .input 元件。

在表單呈現後,您的 LiveView 會在 handle_event 回呼中挑選事件,以驗證並嘗試儲存相應的參數

def render(assigns) ...

def mount(_params, _session, socket) do
  {:ok, assign(socket, form: to_form(Accounts.change_user(%User{})))}
end

def handle_event("validate", %{"user" => params}, socket) do
  form =
    %User{}
    |> Accounts.change_user(params)
    |> to_form(action: :validate)

  {:noreply, assign(socket, form: form)}
end

def handle_event("save", %{"user" => user_params}, socket) do
  case Accounts.create_user(user_params) do
    {:ok, user} ->
      {:noreply,
       socket
       |> put_flash(:info, "user created")
       |> redirect(to: ~p"/users/#{user}")}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}
  end
end

驗證回呼只會根據所有表單輸入值更新變更組,再將變更組轉換成表單並指定給 socket。如果表單變更(例如產生新的錯誤),就會呼叫 render/1 並重新呈現表單。

phx-submit 繫結而言也是如此,相同的回呼會被呼叫,並嘗試執行儲存操作。若成功執行,則會傳回 :noreply 元組,並用 Phoenix.LiveView.redirect/2 將 socket 標註為重新導向至新的使用者頁面,否則會使用出錯的變更組更新 socket 指定,以重新呈現給用戶端。

您可能希望單獨的輸入使用自己的變更事件或鎖定不同的元件。這可以使用 phx-change 注解輸入元素本身來達成,例如

<.form for={@form} phx-change="validate" phx-submit="save">
  ...
  <.input field={@form[:email]}  phx-change="email_changed" phx-target={@myself} />
</.form>

然後您的 LiveView 或 LiveComponent 將會處理事件

def handle_event("email_changed", %{"user" => %{"email" => email}}, socket) do
  ...
end

注意:只有個別輸入做為已標示為 phx-change 輸入的參數發送。

錯誤回饋

為了在表單更新上提供適當的錯誤回饋,錯誤標籤必須指定它們所屬的輸入。這可透過 phx-feedback-for 達成。

phx-feedback-for 注解指定它所屬輸入的名稱(或為了向後相容而有的 ID)。未新增 phx-feedback-for 屬性會導致顯示表單欄位的錯誤訊息,而這些欄位使用者的尚未變更(例如頁面下方有必填欄位)。

例如,您的 MyAppWeb.CoreComponents 可能會使用這個函式

def input(assigns) do
  ~H"""
  <div phx-feedback-for={@name}>
    <input
      type={@type}
      name={@name}
      id={@id || @name}
      value={Phoenix.HTML.Form.normalize_value(@type, @value)}
      class={[
        "phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400",
        "border-zinc-300 focus:border-zinc-400 focus:ring-zinc-800/5",
      ]}
      {@rest}
    />
    <.error :for={msg <- @errors}><%= msg %></.error>
  </div>
  """
end

def error(assigns) do
  ~H"""
  <p class="phx-no-feedback:hidden">
    <Heroicons.exclamation_circle mini class="mt-0.5 h-5 w-5 flex-none fill-rose-500" />
    <%= render_slot(@inner_block) %>
  </p>
  """
end

現在,任何具有 phx-feedback-for 屬性的 DOM 容器將在表單欄位尚未收到使用者輸入/焦點的情況下收到 phx-no-feedback 類別。使用新的 CSS 規則或 tailwindcss 變體,您可以在變更回饋時顯示、隱藏和調整錯誤的樣式。

數字輸入

數字輸入在 LiveView 表單中是特殊情況。在程式性更新中,某些瀏覽器會清除無效的輸入。因此,當輸入無效時,LiveView 過程中不會從客戶端發送變更事件,轉而允許瀏覽器的原生驗證 UI 驅動使用者互動。一旦輸入變得有效,變更和提交事件將會正常發送。

<input type="number">

這是已知的包含許多問題,例如無障礙性、大量的數字會轉換成科學記號,而且捲動會意外地增加或減少數字。

其中一個替代方案是 inputmode 屬性,這可能更能滿足您的應用程式需求,且對使用者更友善。根據 Can I Use?,以下為 86% 的全球市場支援(截至 2021 年 9 月)

<input type="text" inputmode="numeric" pattern="[0-9]*">

密碼輸入

密碼輸入在 Phoenix.HTML 中也被特殊處理。出於安全原因,於重新產生密碼輸入標籤時,會不再使用密碼欄位值。這需要在標記中明確設定 :value,例如

<.input field={f[:password]} value={input_value(f[:password].value)} />
<.input field={f[:password_confirmation]} value={input_value(f[:password_confirmation].value)} />

巢狀輸入

巢狀輸入使用 .inputs_for 函式元件來處理。它預設會新增必要的隱藏輸入欄位,來追蹤 Ecto 關聯的 ID。

<.inputs_for :let={fp} field={f[:friends]}>
  <.input field={fp[:name]} type="text" />
</.inputs_for>

檔案輸入

LiveView 表單支援 反應式檔案輸入,包含拖放支援透過 phx-drop-target 屬性

<div class="container" phx-drop-target={@uploads.avatar.ref}>
  ...
  <.live_file_input upload={@uploads.avatar} />
</div>

進一步資訊請參閱 Phoenix.Component.live_file_input/1

透過 HTTP 傳送表單動作

可以使用 phx-trigger-action 屬性新增至表單中,在進行 DOM 修補後觸發標準表單送出至它在表單標準 action 屬性中指定的網址。這對在針對需要 Plug 會話突變的作業送出至控制器路由之前進行 LiveView 表單送出的最終驗證前非常實用。例如,您可以在 LiveView 範本中註解 phx-trigger-action 以布林值指定

<.form :let={f} for={@changeset}
  action={~p"/users/reset_password"}
  phx-submit="save"
  phx-trigger-action={@trigger_submit}>

接著您可以在 LiveView 中,切換指定以在新一次的渲染中觸發帶有目前欄位的表單

def handle_event("save", params, socket) do
  case validate_change_password(socket.assigns.user, params) do
    {:ok, changeset} ->
      {:noreply, assign(socket, changeset: changeset, trigger_submit: true)}

    {:error, changeset} ->
      {:noreply, assign(socket, changeset: changeset)}
  end
end

一旦 phx-trigger-action 為 true,LiveView 便會中斷然後送出表單。

當機或斷線後復原

預設上,所有以 phx-change 標記並且具有 id 屬性的表單會在使用者重新連線或 LiveView 在當機後重新掛載後自動復原輸入值。這時會由客戶端在完成掛載後立即對伺服器觸發相同的 phx-change 來達成。

注意: 如果你想要讓表單復原功能在開發中運作,請確定在 endpoint.ex 檔案中註解掉 LiveReload plug 或是設定 config/dev.exs 中的 code_reloader: false,以關閉開發中的動態重新載入。否則,動態重新載入會在你重新啟動伺服器後讓目前頁面重新載入,而這會廢棄所有的表單狀態。

在大部分的使用案例中,這些就已經足夠了,而表單復原會不經考量地發生。在某些案例中,表單會以狀態式的方式逐漸建立,這時可能需要在您現有的 phx-change 呼叫回函碼之外,針對伺服器進行額外的復原處理。如需啟用特殊化的復原,請在表單上提供 phx-auto-recover 繫結,用以指定觸發復原的不同事件,它將像往常一樣接收表單參數。例如,想像一個 LiveView 嚮導表單,這個表單具有狀態,並且會根據使用者所處的步驟與之前所做的選擇來建立

<form id="wizard" phx-change="validate_wizard_step" phx-auto-recover="recover_wizard">

在伺服器上,"validate_wizard_step" 事件只會關注現在的客戶端表單資料,但伺服器會維護整個嚮導狀態。要在這種情況下復原,您可以在 LiveView 中指定一個復原事件,例如上述的 "recover_wizard",它會與下列伺服器呼叫回函碼連結

def handle_event("validate_wizard_step", params, socket) do
  # regular validations for current step
  {:noreply, socket}
end

def handle_event("recover_wizard", params, socket) do
  # rebuild state based on client input data up to the current step
  {:noreply, socket}
end

如需擱置自動表單復原,請設定 phx-auto-recover="ignore"

重設表單

若要重設 LiveView 表單,可使用表單按鈕或輸入上的標準 type="reset"。按一下之後,表單輸入會重設為其原始值。重設表單後,會傳送一個 phx-change 事件,其中 _target 參數包含重置 name。例如,下列元素

<form phx-change="changed">
  ...
  <button type="reset" name="reset">Reset</button>
</form>

可與常規變更函數在伺服器上以不同的方式處理

def handle_event("changed", %{"_target" => ["reset"]} = params, socket) do
  # handle form reset
end

def handle_event("changed", params, socket) do
  # handle regular form change
end

JavaScript 客戶端具體資訊

JavaScript 客戶端始終是目前輸入值的實質依據。對於有焦點的任何一組輸入,即使它與伺服器的已呈示更新不同,LiveView 也絕不會覆寫輸入的目前值。這對於預期不會產生重大副作用的更新十分有效,例如表單驗證錯誤,或隨著使用者填寫表單時,在使用者的輸入值周圍新增的 UX。

對於這些用例,phx-change 輸入不會在傳送事件到伺服器的過程中擔心停用輸入編輯。事件傳送至伺服器時,輸入標籤及父系表單標籤會接收到 phx-change-loading CSS 類別,然後有效負載會連同根有效負載中的 "_target" 參數(內含觸發變更事件的輸入名稱的鍵域),推送到伺服器。

例如,如果下列輸入觸發一個變更事件

<input name="user[username]"/>

伺服器的 handle_event/3 會接收到一個有效負載

%{"_target" => ["user", "username"], "user" => %{"username" => "Name"}}

phx-submit 事件用於表單提交,其中通常會發生重大副作用,例如呈現新容器、呼叫外部服務或重新導向到新頁面。

提交與 phx-submit 事件綁定的表單

  1. 表單的輸入設定為 readonly
  2. 表單上的任何提交按鈕都會停用
  3. 表單接收到 "phx-submit-loading" 類別

完成 phx-submit 事件的伺服器處理之後

  1. 提交的表單會重新啟動並移除 "phx-submit-loading" 類別
  2. 還原最後一個有焦點的輸入(除非另一個輸入接收了焦點)
  3. 像平常一樣將更新修補到 DOM

若要處理延遲事件,可對表單的 <button> 標籤加上 phx-disable-with 注解,這會在事件提交期間將元素的 innerText 與提供的值互換。例如,下列程式碼會將「儲存」按鈕變更為「儲存中...」,並且在確認時還原為「儲存」

<button type="submit" phx-disable-with="Saving...">Save</button>

您也可以善加利用 LiveView 的 CSS 載入狀態類別,在表單提交過程中替換表單內容。例如,在 app.css 中有下列規則時

.while-submitting { display: none; }
.inputs { display: block; }

.phx-submit-loading .while-submitting { display: block; }
.phx-submit-loading .inputs { display: none; }

您可以利用下列標記顯示和隱藏內容

<form phx-change="update">
  <div class="while-submitting">Please wait while we save our content...</div>
  <div class="inputs">
    <input type="text" name="text" value={@text}>
  </div>
</form>

此外,我們強烈建議在表單上包含一個唯一的 HTML「id」屬性。當 DOM 同層元素變更時,沒有 ID 的元素會遭到取代而不是移動,這可能會造成表單欄位失去焦點等問題。

使用 JavaScript 引發 phx- 表單事件

通常會希望在沒有使用者在元素上執行明確互動的情況下,觸發 DOM 元素上的事件。例如,自訂表單元素,例如日期選擇器或自訂選取輸入項,會使用隱藏的輸入元素來儲存已選取的狀態。

在這些情況下,可以用 DOM API 上的事件函式,例如觸發 phx-change 事件

document.getElementById("my-select").dispatchEvent(
  new Event("input", {bubbles: true})
)

在使用客戶端掛鉤時,this.el 可以用來判定元素,如「客戶端掛鉤」文件所述。

也可以使用「submit」事件來觸發 phx-submit

document.getElementById("my-form").dispatchEvent(
  new Event("submit", {bubbles: true, cancelable: true})
)