PhiaUi.Components.MentionInput (phia_ui v0.1.7)

Copy Markdown View Source

Mention input component for PhiaUI.

A textarea with @mention autocomplete support. When the user types @, the PhiaMentionInput JS hook detects the trigger character and emits a push_event("mention_search", %{query: query}) to the server. The LiveView filters the user list, reassigns suggestions, and re-renders the dropdown. When a suggestion is selected, the hook inserts a styled chip into the textarea and the mentioned user IDs are tracked in a hidden CSV input for form submission.

When to use

Use MentionInput anywhere users should be able to tag other users with @username:

  • Team collaboration comment threads (GitHub/Linear style)
  • Task assignment descriptions
  • Chat messages in a team messenger
  • Document editor annotations
  • Support ticket replies

Anatomy

ComponentElementPurpose
mention_input/1divRoot wrapper (textarea + hook + hidden input + dropdown)
mention_dropdown/1ulSuggestion listbox — hidden when open: false
mention_chip/1spanInline @name highlight for server-rendered previews

How it works

  1. User types @ in the textarea
  2. PhiaMentionInput hook fires push_event("mention_search", %{query: ""})
  3. LiveView runs handle_event("mention_search", ...) → assigns suggestions
  4. User types more → hook fires push_event("mention_search", %{query: "ali"})
  5. LiveView filters and re-assigns suggestions; dropdown opens (mention_open: true)
  6. User clicks / arrows to a suggestion → phx-click="mention_select"
  7. LiveView appends the ID to mentioned_ids; hook inserts a chip into the textarea
  8. On form submit, the hidden input name_ids carries the CSV of mentioned IDs

Complete example

defmodule MyAppWeb.CommentLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    {:ok, assign(socket,
      mention_suggestions: [],
      mention_open: false,
      mention_search: "",
      mentioned_ids: []
    )}
  end

  # Hook emits push_event; LiveView handles it as a regular event
  def handle_event("mention_search", %{"query" => q}, socket) do
    suggestions =
      Users.search(q, limit: 8)
      |> Enum.map(&%{id: &1.id, name: &1.name, avatar: &1.avatar_url})

    {:noreply, assign(socket,
      mention_suggestions: suggestions,
      mention_open: true,
      mention_search: q
    )}
  end

  def handle_event("mention_select", %{"id" => id, "name" => _name}, socket) do
    ids = [id | socket.assigns.mentioned_ids] |> Enum.uniq()
    {:noreply, assign(socket,
      mentioned_ids: ids,
      mention_open: false,
      mention_search: ""
    )}
  end
end

<%!-- Template --%>
<.mention_input
  id="comment-body"
  name="comment[body]"
  suggestions={@mention_suggestions}
  open={@mention_open}
  search={@mention_search}
  mentioned_ids={@mentioned_ids}
  on_mention="mention_search"
  on_select="mention_select"
  placeholder="Leave a comment… type @ to mention someone"
/>

Displaying resolved mentions in read-only content

When rendering submitted comment content (e.g. in a feed), use mention_chip/1 to highlight resolved user mentions with the same visual style:

<p>
  Hey
  <.mention_chip name="Alice" user_id={alice.id} />
  the PR is ready for review.
</p>

Hook setup

# app.js
import PhiaMentionInput from "./hooks/mention_input"
let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { PhiaMentionInput }
})

ARIA

The <textarea> carries role="combobox", aria-autocomplete="list", aria-haspopup="listbox", and aria-expanded toggled by the open assign. The dropdown has role="listbox". Each suggestion option has role="option".

Summary

Functions

Renders an inline @mention highlight chip for server-rendered content.

Renders the @mention suggestion dropdown.

Renders the mention input widget.

Functions

mention_chip(assigns)

Renders an inline @mention highlight chip for server-rendered content.

Use this component when displaying already-saved content that contains resolved mentions — for example, rendering a comment body from the database where mentioned user IDs have been resolved to names.

In client-side mode, the PhiaMentionInput hook inserts chips dynamically into the textarea. mention_chip/1 is for the read-only rendering path.

Example

<p>
  <.mention_chip name="Sarah Lin" user_id={sarah.id} />
  can you review the attached document?
</p>

Attributes

  • name (:string) (required) - Display name of the mentioned user (rendered as @name).
  • user_id (:string) (required) - User ID stored in the data-mention-id attribute for client-side processing.
  • class (:string) - Additional CSS classes for the chip span. Defaults to nil.

mention_dropdown(assigns)

Renders the @mention suggestion dropdown.

Uses two function heads:

  • open: false → renders ~H"" (empty fragment — no DOM element at all)
  • open: true → renders the <ul role="listbox"> with suggestion items

This pattern is preferred over CSS visibility toggling because it keeps the DOM clean and prevents screen readers from announcing the hidden list.

Each suggestion fires on_select with phx-value-id and phx-value-name.

Attributes

  • id (:string) - DOM id for the listbox element — referenced by aria-controls on the textarea. Defaults to nil.
  • suggestions (:list) - List of %{id, name, avatar} suggestion maps. Defaults to [].
  • open (:boolean) - Controls dropdown visibility. Uses two function heads: false → empty fragment. Defaults to false.
  • on_select (:string) - phx-click event name fired when a suggestion is chosen. Defaults to "mention_select".
  • class (:string) - Additional CSS classes for the listbox <ul>. Defaults to nil.

mention_input(assigns)

Renders the mention input widget.

The root div contains:

  1. A <textarea> wired to the PhiaMentionInput hook
  2. A hidden <input> that holds the CSV of mentioned user IDs
  3. A mention_dropdown/1 suggestion panel controlled by the open assign

The LiveView controls open/close state and the suggestion list. The hook handles @ detection in the textarea and DOM insertion of @name chips.

Attributes

  • id (:string) (required) - DOM id for the textarea element. The PhiaMentionInput hook is anchored to this element and uses it to identify the instance.

  • name (:string) (required) - Form field name for the textarea content. The hidden input for mentioned IDs will be named #{name}_ids.

  • suggestions (:list) - List of %{id: string, name: string, avatar: string | nil} suggestion maps. Updated by the LiveView in response to on_mention events. An empty list renders "No results" inside the open dropdown.

    Defaults to [].

  • open (:boolean) - Whether the suggestion dropdown is currently visible. Set to true in handle_event("mention_search", ...) and false in handle_event("mention_select", ...).

    Defaults to false.

  • search (:string) - Current @mention search query typed by the user. Defaults to "".

  • value (:string) - Current textarea text value (for server-rendered initial content). Defaults to "".

  • mentioned_ids (:list) - List of already-selected user ID strings. Serialised as a comma-separated value in the hidden name_ids input for form submission and changeset processing.

    Defaults to [].

  • on_mention (:string) - Event name that the hook emits via push_event when the user types after @. The LiveView receives %{"query" => query}.

    Defaults to nil.

  • on_select (:string) - phx-click event name fired when a suggestion is selected. The LiveView receives %{"id" => user_id, "name" => user_name}.

    Defaults to "mention_select".

  • placeholder (:string) - Textarea placeholder text. Defaults to "Type a message… use @ to mention".

  • class (:string) - Additional CSS classes for the root wrapper div. Defaults to nil.

  • Global attributes are accepted. HTML attributes forwarded to the root div.