Skip to content

Instantly share code, notes, and snippets.

@srcrip
Last active May 3, 2024 14:30
Show Gist options
  • Save srcrip/a73f4134e29c261acaa86df1444cbac3 to your computer and use it in GitHub Desktop.
Save srcrip/a73f4134e29c261acaa86df1444cbac3 to your computer and use it in GitHub Desktop.
Some helper functions for making nested forms in LiveView
<.form for={@form} class="max-w-xs">
<NestedFormComponents.inputs_for_embeds
:let={embed_form}
parent_form={@form}
field={@form[:items]}
id={"#{@form[:items].id}_embeds"}
sort_param={:items_sort}
drop_param={:items_drop}
>
<div class="grow">
<.input field={embed_form[:text]} aria-label="text" type="text" placeholder="List item" />
</div>
<:drop_button>
Remove
</:drop_button>
<:add_button class="w-full mt-2">
Add
</:add_button>
</NestedFormComponents.inputs_for_embeds>
</.form>
defmodule ExampleWeb.NestedFormComponents do
@moduledoc """
Helper components for driving forms from embedded schemas.
"""
use ExampleWeb, :html
attr :parent_form, Phoenix.HTML.Form, required: true
attr :field, Phoenix.HTML.FormField, required: true
attr :id, :string, required: true
attr :sort_param, :any, required: true
attr :drop_param, :any, required: true
attr :rest, :global, include: ~w(as default prepend append skip_hidden options)
slot :inner_block, required: true
slot :add_button do
attr :class, :any
end
slot :drop_button do
attr :class, :any
end
def inputs_for_embeds(assigns) do
# slots can be multiple, but these two are only ever singletons
add_button_class =
case assigns.add_button do
[%{class: add_button_class}] ->
add_button_class
_ ->
nil
end
drop_button_class =
case assigns.drop_button do
[%{class: drop_button_class}] ->
drop_button_class
_ ->
nil
end
assigns =
assigns
|> assign(:add_button_class, add_button_class)
|> assign(:drop_button_class, drop_button_class)
~H"""
<.inputs_for :let={embed_form} field={@field}>
<.sort_field form={@parent_form} index={embed_form.index} sort_param={@sort_param} />
<div class="flex gap-2">
<%= render_slot(@inner_block, embed_form) %>
<.drop_button
form={@parent_form}
index={embed_form.index}
drop_param={@drop_param}
class={[
"place-self-end",
@drop_button_class
]}
>
<%= render_slot(@drop_button) %>
</.drop_button>
</div>
</.inputs_for>
<.drop_field form={@parent_form} drop_param={@drop_param} />
<.add_button form={@parent_form} sort_param={@sort_param} class={@add_button_class}>
<%= render_slot(@add_button) %>
</.add_button>
"""
end
attr :form, Phoenix.HTML.Form, required: true
attr :sort_param, :any, required: true
attr :index, :integer, required: true
def sort_field(assigns) do
~H"""
<input type="hidden" name={"#{@form.name}[#{@sort_param}][]"} value={@index} />
"""
end
attr :form, Phoenix.HTML.Form, required: true
attr :drop_param, :any, required: true
def drop_field(assigns) do
~H"""
<input type="hidden" name={"#{@form.name}[#{@drop_param}][]"} />
"""
end
attr :class, :any, default: nil
attr :form, Phoenix.HTML.Form, required: true
attr :sort_param, :any, required: true
attr :on_click, Phoenix.LiveView.JS, default: JS.dispatch("change")
slot :inner_block, required: true
def add_button(assigns) do
~H"""
<.button
class={@class}
type="button"
name={"#{@form.name}[#{@sort_param}][]"}
value="new"
phx-click={@on_click}
>
<%= render_slot(@inner_block) %>
</.button>
"""
end
attr :class, :any, default: nil
attr :form, Phoenix.HTML.Form, required: true
attr :index, :integer, required: true
attr :drop_param, :any, required: true
attr :on_click, Phoenix.LiveView.JS, default: JS.dispatch("change")
slot :inner_block, required: false
def drop_button(assigns) do
~H"""
<.button
class={@class}
type="button"
name={"#{@form.name}[#{@drop_param}][]"}
value={@index}
phx-click={@on_click}
>
<%= if @inner_block == [] do %>
<.icon name="hero-x-mark" class="w-6 h-6 relative top-2" />
<% else %>
<%= render_slot(@inner_block) %>
<% end %>
</.button>
"""
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment