Corex.PasswordInput (Corex v0.1.0-rc.0)

View Source

Phoenix implementation of Zag.js Password Input.

Anatomy

Minimal

<.password_input class="password-input">
  <:label>Password</:label>
  <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
  <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
</.password_input>

Custom Error

<.password_input field={@form[:password]} class="password-input">
  <:label>Password</:label>
  <:error :let={msg}>
    <.heroicon name="hero-exclamation-circle" class="icon" />
    {msg}
  </:error>
  <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
  <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
</.password_input>

Form

When using with Phoenix forms, set the form id in to_form/2 (for example to_form(changeset, as: :name, id: "my-form")) and use <.form for={@form}>.

For cross-cutting invalid styling and error presentation, see the Forms guide. Pass invalid={Corex.FormField.invalid?(@form[:password])} when you want alert borders after validation.

Controller

Build the form from an Ecto changeset:

def form_page(conn, _params) do
  form =
    %MyApp.Form.PasswordForm{}
    |> MyApp.Form.PasswordForm.changeset(%{})
    |> Phoenix.Component.to_form(as: :password_form, id: "password-form")
  render(conn, :form_page, form: form)
end
<.form :let={f} for={@form} action={@action} method="post">
  <.password_input field={f[:password]} class="password-input">
    <:label>Password</:label>
    <:error :let={msg}>
      <.heroicon name="hero-exclamation-circle" class="icon" />
      {msg}
    </:error>
    <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
    <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
  </.password_input>
  <button type="submit">Submit</button>
</.form>

Live View

Prefer building the form from an Ecto changeset (see "With Ecto changeset" below).

With Ecto changeset

First create your schema and changeset:

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :password, :string
    timestamps(type: :utc_datetime)
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :password])
    |> validate_required([:email, :password])
    |> validate_length(:password, min: 8)
  end
end
defmodule MyAppWeb.UserLive do
  use MyAppWeb, :live_view
  alias MyApp.Accounts.User

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

  def handle_event("validate", %{"user" => user_params}, socket) do
    changeset = User.changeset(%User{}, user_params)
    {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
  end

  def render(assigns) do
    ~H"""
    <.form for={@form} phx-change="validate">
      <.password_input field={@form[:password]} class="password-input">
        <:label>Password</:label>
        <:error :let={msg}>
          <.heroicon name="hero-exclamation-circle" class="icon" />
          {msg}
        </:error>
        <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
        <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
      </.password_input>
    </.form>
    """
  end
end

API

Requires a stable id on <.password_input>.

FunctionActionReturns
set_visible/2Set visibility (client)%Phoenix.LiveView.JS{}
set_visible/3Set visibility (server)socket
toggle_visible/1Toggle visibility (client)%Phoenix.LiveView.JS{}
toggle_visible/2Toggle visibility (server)socket
focus/1Focus input (client)%Phoenix.LiveView.JS{}
focus/2Focus input (server)socket

Events

Pick an event name and pass it to on_* on <.password_input>.

Server events

EventWhenPayload
on_visibility_change="password_visibility_changed"Visibility toggles%{"id" => id, "visible" => boolean}

on_visibility_change

<.password_input
  class="password-input"
  on_visibility_change="password_visibility_changed"
>
  <:label>Password</:label>
  <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
  <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
</.password_input>
def handle_event("password_visibility_changed", %{"id" => _id, "visible" => visible}, socket) do
  {:noreply, assign(socket, :password_visible, visible)}
end

Client events

EventWhenevent.detail
on_visibility_change_client="password-visibility-changed"Visibility togglesid, visible

Style

Use data attributes to target elements:

[data-scope="password-input"][data-part="root"] {}
[data-scope="password-input"][data-part="label"] {}
[data-scope="password-input"][data-part="control"] {}
[data-scope="password-input"][data-part="input"] {}
[data-scope="password-input"][data-part="visibility-trigger"] {}
[data-scope="password-input"][data-part="indicator"] {}
@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/password-input.css";

Stack modifiers on the host (class on <.password_input>).

Color

ModifierClasses
Defaultpassword-input
Accentpassword-input password-input--accent
Brandpassword-input password-input--brand
Alertpassword-input password-input--alert
Infopassword-input password-input--info
Successpassword-input password-input--success

Size

ModifierClasses
SMpassword-input password-input--sm
MDpassword-input password-input--md
LGpassword-input password-input--lg
XLpassword-input password-input--xl

Summary

API

Focus the input from a control (phx-click).

Focus the input from handle_event.

Set whether the password is visible from a control (phx-click).

Set visibility from handle_event.

Toggle visibility from a control (phx-click).

Toggle visibility from handle_event.

Components

password_input(assigns)

Attributes

  • id (:string)
  • value (:string) - Defaults to nil.
  • visible (:boolean) - Defaults to false.
  • disabled (:boolean) - Defaults to false.
  • invalid (:boolean) - Defaults to false.
  • read_only (:boolean) - Defaults to false.
  • required (:boolean) - Defaults to false.
  • ignore_password_managers (:boolean) - Defaults to true.
  • name (:string)
  • form (:string)
  • dir (:string) - Defaults to nil.Must be one of nil, "ltr", or "rtl".
  • orientation (:string) - Defaults to "vertical". Must be one of "horizontal", or "vertical".
  • auto_complete (:string) - Defaults to "current-password". Must be one of "current-password", or "new-password".
  • on_visibility_change (:string) - Defaults to nil.
  • on_visibility_change_client (:string) - Defaults to nil.
  • errors (:list) - List of error messages to display. Defaults to [].
  • translation (Corex.PasswordInput.Translation) - Override translatable strings. Defaults to nil.
  • field (Phoenix.HTML.FormField) - A form field struct retrieved from the form, for example: @form[:password]. Automatically sets id, name, form, and errors from the form field. Pass invalid explicitly for alert styling.
  • Global attributes are accepted.

Slots

  • label - Accepts attributes:
    • class (:string)
  • error - Accepts attributes:
    • class (:string)
  • visible_indicator - Icon shown when password is visible. Accepts attributes:
    • class (:string)
  • hidden_indicator - Icon shown when password is hidden. Accepts attributes:
    • class (:string)

API

focus(password_input_id)

Focus the input from a control (phx-click).

<.action phx-click={Corex.PasswordInput.focus("my-password-input")}>Focus</.action>
<.password_input id="my-password-input" class="password-input" name="pw">
  <:label>Password</:label>
  <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
  <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
</.password_input>
document.getElementById("my-password-input")?.dispatchEvent(
  new CustomEvent("corex:password-input:focus", { bubbles: false })
);

focus(socket, password_input_id)

Focus the input from handle_event.

<.action phx-click="focus_pw">Focus</.action>
<.password_input id="my-password-input" class="password-input" name="pw">
  <:label>Password</:label>
  <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
  <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
</.password_input>
def handle_event("focus_pw", _, socket) do
  {:noreply, Corex.PasswordInput.focus(socket, "my-password-input")}
end

set_visible(password_input_id, visible)

Set whether the password is visible from a control (phx-click).

<.action phx-click={Corex.PasswordInput.set_visible("my-password-input", true)}>Reveal</.action>
<.password_input id="my-password-input" class="password-input" name="pw">
  <:label>Password</:label>
  <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
  <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
</.password_input>
document.getElementById("my-password-input")?.dispatchEvent(
  new CustomEvent("corex:password-input:set-visible", {
    bubbles: false,
    detail: { visible: true },
  })
);

set_visible(socket, password_input_id, visible)

Set visibility from handle_event.

<.action phx-click="reveal_pw">Reveal</.action>
<.password_input id="my-password-input" class="password-input" name="pw">
  <:label>Password</:label>
  <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
  <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
</.password_input>
def handle_event("reveal_pw", _, socket) do
  {:noreply, Corex.PasswordInput.set_visible(socket, "my-password-input", true)}
end

toggle_visible(password_input_id)

Toggle visibility from a control (phx-click).

<.action phx-click={Corex.PasswordInput.toggle_visible("my-password-input")}>Toggle</.action>
<.password_input id="my-password-input" class="password-input" name="pw">
  <:label>Password</:label>
  <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
  <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
</.password_input>
document.getElementById("my-password-input")?.dispatchEvent(
  new CustomEvent("corex:password-input:toggle-visible", { bubbles: false })
);

toggle_visible(socket, password_input_id)

Toggle visibility from handle_event.

<.action phx-click="toggle_pw">Toggle</.action>
<.password_input id="my-password-input" class="password-input" name="pw">
  <:label>Password</:label>
  <:visible_indicator><.heroicon name="hero-eye" /></:visible_indicator>
  <:hidden_indicator><.heroicon name="hero-eye-slash" /></:hidden_indicator>
</.password_input>
def handle_event("toggle_pw", _, socket) do
  {:noreply, Corex.PasswordInput.toggle_visible(socket, "my-password-input")}
end