I’ll paste what I think are the relevant parts:
<h1>New todo list</h1>
<.simple_form for={@form} action={@action} phx-change="validate" phx-submit="save">
<.input field={@form[:name]} type="text" label="Name" />
<h3>Todo items</h3>
<.inputs_for :let={todo} field={@form[:todos]} skip_hidden="true">
<div class="card mb-3">
<div class="card-body">
<input type="hidden"
name={todo[:_persistent_id].name}
value={todo.index} />
<input type="hidden"
name={todo[:id].name}
value={todo[:id].value} />
<input
type="hidden"
id={"#{@form[:todos_sort].id}_#{todo.index}"}
name={@form[:todos_sort].name <> "[]"}
value={todo.index}
/>
<.input field={todo[:name]} type="text" label="Name"/>
<label class="btn btn-sm btn-outline-danger" style="cursor:pointer">
<input
type="checkbox"
name={@form[:todos_drop].name <> "[]"}
value={todo.index}
hidden />
<div>Delete <.icon name="trash"/></div>
</label>
<.move_item
item={todo}
sort_param={@form[:todos_sort]}
nr_of_items={length(@form[:todos].value)}>
</.move_item>
</div>
</div>
</.inputs_for>
<.add_new_item sort_param={@form[:todos_sort]}>
Add new item
</.add_new_item>
<hr/>
<:actions>
<.button>Save Todo list</.button>
</:actions>
</.simple_form>
The <.add_new_item />
component works perfectly, and it’s based on what the docs suggest.
The <.move_item/>
, however is a bit more complex. It generates two buttons which when clicked swap the values of the following input value and changes a “dummy field” in order to re-submit the form, as in the code below.
<input
type="hidden"
id={"#{@form[:todos_sort].id}_#{todo.index}"}
name={@form[:todos_sort].name <> "[]"}
value={todo.index}
/>
The code for that component is this:
def move_item(assigns) do
~H"""
<input
type="hidden"
id={"#{@sort_param.id}__dummy__#{@item.index}"}
name={"__#{@sort_param.id}[__dummy__#{@item.index}]"}
value="dummy"/>
<div class="btn-group">
<button
type="button"
class="btn btn-sm btn-outline-dark"
phx-click={swap_with_element_above(@item, @sort_param, @nr_of_items)}
disabled={@item.index < 1}>
<.icon name="chevron-up" /> Move up
</button>
<button
type="button"
class="btn btn-sm btn-outline-dark"
phx-click={swap_with_element_below(@item, @sort_param, @nr_of_items)}
disabled={@item.index >= @nr_of_items - 1}>
<.icon name="chevron-down" /> Move down
</button>
</div>
"""
end
defp swap_with_element_above(item, sort_param, _nr_of_items) do
if item.index <= 0 do
%JS{}
else
# These selectors represent the sort_param input fields
# that control element ordering.
# We can depend on these names because these are detgerministically
# generated by the `<.nested_inputs_for/>` component.
selector_for_element = "##{sort_param.id}_#{item.index}"
selector_for_element_above = "##{sort_param.id}_#{item.index - 1}"
selector_for_dummy_input = "##{sort_param.id}__dummy__#{item.index}"
%JS{}
|> JS.set_attribute({"value", item.index - 1}, to: selector_for_element)
|> JS.set_attribute({"value", item.index}, to: selector_for_element_above)
|> JS.dispatch("change", to: selector_for_dummy_input)
end
end
defp swap_with_element_below(item, sort_param, nr_of_items) do
if item.index >= nr_of_items - 1 do
%JS{}
else
# These selectors represent the sort_param input fields
# that control element ordering.
# We can depend on these names because these are detgerministically
# generated by the `<.nested_inputs_for/>` component.
selector_for_element = "##{sort_param.id}_#{item.index}"
selector_for_element_below = "##{sort_param.id}_#{item.index + 1}"
selector_for_dummy_input = "##{sort_param.id}__dummy__#{item.index}"
%JS{}
|> JS.set_attribute({"value", item.index + 1}, to: selector_for_element)
|> JS.set_attribute({"value", item.index}, to: selector_for_element_below)
|> JS.dispatch("change", to: selector_for_dummy_input)
end
end
The :todos_sort
is passed in the liveview parameters correctly, and the resulting changeset and form is generted correctly. It’s the DOM that doesn’t seem to be passed correctly.
The parameters are passed into the following changeset:
defmodule Registo.FormDemo.TodoList do
use Ecto.Schema
import Ecto.Changeset
alias Registo.FormDemo.Todo
@type t :: %__MODULE__{}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "todo_lists" do
field :name, :string
has_many :todos, Todo,
preload_order: [asc: :position],
on_delete: :delete_all
timestamps(type: :utc_datetime)
end
@doc false
def changeset(todo_list, attrs) do
todo_list
|> cast(attrs, [:name])
|> cast_assoc(:todos,
with: &todo_changeset/3,
drop_param: :todos_drop,
sort_param: :todos_sort
)
|> validate_required([:name])
end
def todo_changeset(todo, changes, position) do
todo
|> Todo.changeset(changes)
|> put_change(:position, position)
end
end
This code is very integrated in the rest of the codebase, so it’s hard to show a self-contained example. I could generate anew app and build this form from scratch but that would require using tailwind components, which I’m not at all familiar with that.