Hello @mmmrrr
After some trail and error i came to the following but i feel like the multiple form way is a bit weird.
Currently i am doing for general master data. like units or sizes.
def render(assigns) do
~H"""
<div class="mx-auto max-w-2xl">
<.header class="text-center">Sizes</.header>
<div class="flex justify-between">
<.button phx-click="add_row" class="mb-2 mt-2">Add Row</.button>
<.button phx-click="saveall" class="mb-2 mt-2">Save</.button>
</div>
<div class="flex w-full">
<div class="w-[30%] border-b pb-1 ps-3">Sort</div>
<div class="w-[30%] border-b pb-1 ps-3">Code</div>
<div class="w-[30%] border-b pb-1 ps-3">Name</div>
<div class="w-[9%] border-b pb-1"></div>
</div>
<%= for row <- @data do %>
<div class="flex w-full gap-2 p-2 border-b">
<form phx-change="update_row" class="w-[30%]">
<input type="hidden" name="row_id" value={row.hiddenId} />
<input type="hidden" name="field" value="sort" />
<.input type="text" name="value" value={row.sort}/>
<%= if row.errors[:sort] do %>
<span class="text-red-500"><%= row.errors[:sort] %></span>
<% end %>
</form>
<form phx-change="update_row" class="w-[30%]">
<input type="hidden" name="row_id" value={row.hiddenId} />
<input type="hidden" name="field" value="code" />
<.input type="text" name="value" value={row.code}/>
<%= if row.errors[:code] do %>
<span class="text-red-500"><%= row.errors[:code] %></span>
<% end %>
</form>
<form phx-change="update_row" class="w-[30%]">
<input type="hidden" name="row_id" value={row.hiddenId} />
<input type="hidden" name="field" value="name" />
<.input type="text" name="value" value={row.name}/>
<%= if row.errors[:name] do %>
<span class="text-red-500"><%= row.errors[:name] %></span>
<% end %>
</form>
<button phx-click="delete_row" phx-value-row_id={row.hiddenId} class="w-[9%] border border-red-500 rounded-lg text-red-500 flex mt-2 justify-center items-center hover:text-white hover:bg-red-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
</button>
</div>
<% end %>
<div class="flex justify-between">
<.button phx-click="add_row" class="mb-2 mt-2">Add Row</.button>
<.button phx-click="saveall" class="mb-2 mt-2">Save</.button>
</div>
</div>
"""
end
then i handle all the events for a given “row” as follows
def mount(_params, _session, socket) do
master_data_sizes = load_data()
# Assign columns and formatted data to the socket
socket = assign(socket, data: master_data_sizes)
{:ok, socket}
end
def handle_event("add_row", _params, socket) do
updated_data = add_row(socket.assigns.data)
{:noreply, assign(socket, :data, updated_data)}
end
def handle_event("update_row", %{"row_id" => row_id, "field" => field, "value" => value}, socket) do
updated_data = update_data(socket.assigns.data, row_id, field, value)
{:noreply, assign(socket, :data, updated_data)}
end
def handle_event("delete_row", %{"row_id" => row_id}, socket) do
updated_data = delete_row(socket.assigns.data, row_id)
{:noreply, assign(socket, :data, updated_data)}
end
def handle_event("saveall", _params, socket) do
case save_all(socket.assigns.data) do
:ok ->
socket = socket |> assign(:data, load_data()) |> put_flash(:info, "Masterdata list saved!")
{:noreply, socket}
{:error, updated_data} ->
socket = socket |> assign(:data, updated_data) |> put_flash(:error, "One or more entries are invalid.")
{:noreply, socket}
end
end
defp add_row(data) do
max_hidden_id =
data
|> Enum.map(&Map.get(&1, :hiddenId))
|> Enum.max(fn -> 0 end) # Use 0 as the default value if there are no rows
new_row = %{
id: nil,
hiddenId: max_hidden_id + 1,
sort: "",
code: "",
name: "",
errors: %{}
}
data ++ [new_row]
end
defp update_data(data, row_id, field, value) do
Enum.map(data, fn row ->
if Integer.to_string(row.hiddenId) == row_id do
updated_row = Map.put(row, String.to_existing_atom(field), value)
changeset = if updated_row.id do MasterDataSizes.changeset(%MasterDataSizes{}, Map.from_struct(updated_row)) else MasterDataSizes.changeset(%MasterDataSizes{}, updated_row) end
errors = if changeset.valid?, do: %{}, else: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
Map.put(updated_row, :errors, errors)
else
row
end
end)
end
defp delete_row(data, row_id) do
Enum.reject(data, fn row -> row.id == String.to_integer(row_id) end)
end
defp save_all(data) do
db_data = Repo.all(MasterDataSizes)
data_map = Map.new(data, fn row -> {row.id, row} end)
db_map = Map.new(db_data, fn row -> {row.id, row} end)
updated_data = Enum.map(data, fn row ->
changeset = if Map.has_key?(db_map, row.id) do
existing_record = Map.get(db_map, row.id)
MasterDataSizes.changeset(existing_record, Map.from_struct(row))
else
MasterDataSizes.changeset(%MasterDataSizes{}, row)
end
if changeset.valid? do
if Map.has_key?(db_map, row.id) do
Repo.update!(changeset)
else
Repo.insert!(changeset)
end
Map.put(row, :errors, %{})
else
errors = Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
Map.put(row, :errors, errors)
end
end)
# Delete rows not present in data
Enum.each(db_data, fn row ->
unless Map.has_key?(data_map, row.id) do
Repo.delete!(row)
end
end)
if Enum.any?(updated_data, &(&1.errors != %{})) do
{:error, updated_data}
else
:ok
end
end
defp load_data do
MasterDataSizesService.list()
|> Enum.map(fn item ->
item
|> Map.put(:errors, %{})
|> Map.put(:hiddenId, item.id)
end)
end
This worked but i feel like I am doing things wrong.
In the future i will have a structure as follows:
{
PurchaseOrderNumber: "PO0001",
etc...
items: [
{
productno: "Product1"
}
]
}
I would like to have a table to handle the items on my page to be done in a table like format:
I hope that clarifies what i am trying to do. I was hoping to do something similar to what i achieved with my master data page on a purchase order page.