Menus and dialogs
Menu and dialog components - action_menu, dropdown, select_menu, drawer, dialog - share common interaction and appearance.
To use these components, CSS and JavaScript hook must be installed - see Installation.
Usage
Menus are created with 3 elements: the component wrapper, a toggle button and the menu contents. For example for a dropdown menu:
<.dropdown> <:toggle>Menu</:toggle> <:item href="#url">Item 1</:item> <:item href="#url">Item 2</:item> </.dropdown>
<.dialog> <:header_title>Title</:header_title> <:body> Content </:body> </.dialog>
Prompt
The menu toggle
is an HTML label element that is connected to the checkbox that holds the state; clicking will toggle the checkbox state.
To control the open state from the outside, use the Prompt
hook with either of these functions:
-
Prompt.show(selectorOrElement)
-
Prompt.hide(selectorOrElement)
-
Prompt.toggle(selectorOrElement)
where selectorOrElement
is either an HTML element, this
(when used within the component), or a selector string.
<.button is_primary onclick="Prompt.show('#my-menu')"> Show menu from outside </.button> <.action_menu id="my-menu"> <:toggle>Show menu</:toggle> <.action_list> <.action_list_item> One </.action_list_item> <.action_list_item> Two </.action_list_item> <.action_list_item onclick="Prompt.hide(this)"> Hide from inside </.action_list_item> </.action_list> </.action_menu>
Status callbacks
Use attr prompt_options
to pass JavaScript state callback functions.
<.action_menu prompt_options="{ willShow: function(elements) { console.log('willShow', elements) }, didShow: function(elements) { console.log('didShow', elements) }, willHide: function(elements) { console.log('willHide', elements) }, didHide: function(elements) { console.log('didHide', elements) } }"> ... </.action_menu>
CSS
Prompt CSS mainly defines behavior and can be modified by providing different values to the default CSS Variables:
[data-prompt] { /* colors */ --prompt-background-color-backdrop: black; --prompt-background-opacity-backdrop-dark: 0.5; /* - default: */ --prompt-background-opacity-backdrop-medium: 0.2; --prompt-background-opacity-backdrop-light: 0.07; /* transitions */ --prompt-transition-timing-function-backdrop: ease-in-out; --prompt-transition-timing-function-content: ease-in-out; --prompt-transition-duration-content: 180ms; --prompt-fast-transition-duration-content: 140ms; --prompt-transition-duration-backdrop: var(--prompt-transition-duration-content); --prompt-fast-transition-duration-backdrop: var(--prompt-fast-transition-duration-content); /* drawer */ --drawer-width: 320px; /* z-index */ --prompt-z-index-backdrop: 98; --prompt-z-index-touch: 99; --prompt-z-index-menu-content: 100; --prompt-z-index-drawer-content: 200; --prompt-z-index-dialog-content: 300; }
App header
Take note of the z-index
values: if the application uses a fixed header, you may want to ensure that its z-index is below dialogs/drawers and above menus, so between 101
and 199
.
Maintaining state in forms
Under the hood, menu behavior is implemented through the Prompt hook that retrieves the open state and other relevant attributes from a hidden toggle checkbox. By using a checkbox, we are able to preserve the open state when they are used within a form and updated by user actions.
Normally after an update to the LiveView state, the menu/dialog/drawer is redrawn, resulting in it being rendered closed.
To preserve the open state of the menu, you can pass the form along with a fictitious and unique field name (not used in the data model). This field name is then used in event handlers.
Here we are adding field status_toggle
to the form:
<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save"> <.action_menu form={f} field={:status_toggle}> <:toggle>Menu</:toggle> <.action_list is_multiple_select> <%= for {label, value} <- @options do %> <.action_list_item form={f} field={:statuses} checked_value={value} is_multiple_select is_selected={value in @values} > <%= label %> </.action_list_item> <% end %> </.action_list> </.action_menu> </.form>
The same method can be used for dialogs and drawers:
<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save"> <.dialog form={f} field={:status_toggle} id="status-dialog"> <:body> <.action_list is_multiple_select> # See above </.action_list> </:body> </.dialog> </.form>
Menu behavior: when to update
The implementation of menu behavior from a user's perspective is determined by the event handler, where you have two options for updating the model state:
- Update the model state after each selection.
- Update the model state after closing the menu.
Approach 1: Update with each selection
This approach is usually preferred, because it allows for direct validation feedback.
Process the event as usual and re-insert the fictitious field value:
# user_live/show.ex def handle_event("save", %{"user" => params}, socket) do case User.update(socket.assigns.user, params) do {:ok, user} -> # Re-insert ui_prompt_user_job value ui_changeset = User.changeset( user, params |> Map.take(["ui_prompt_user_job"]) ) socket = socket |> assign(:user, user) |> assign(:changeset, ui_changeset) {:noreply, socket} {:error, %Ecto.Changeset{} = changeset} -> # Re-insert ui_prompt_user_job value ui_changeset = User.changeset( changeset.data, params |> Map.take(["ui_prompt_user_job"]) ) socket = socket |> assign(:changeset, changeset |> Map.merge(ui_changeset)) {:noreply, socket} end end
Approach 2: Update after closing the menu
Ignore the event while the the menu is open (the toggle checkbox value is "true"):
def handle_event("save", %{"user" => %{"status_toggle" => "true"}}, socket) do # Ignore {:noreply, socket} end def handle_event("save", %{"user" => params}, socket) do # status_toggle is "false", so process normally case User.update(socket.assigns.user, params) do ...
-
On this page
- Top of page
- Usage
- Prompt
- CSS
- Maintaining state in forms