CessK

CessK

Hello Everyone,

I have a question about table like data entry. And to make it as smooth as possible for the end user to add, edit and remove rows.

In previous projects i have done i have used Jquery data tables with a C# backed.

This worked very well. However i am unable to find something similar.

I have googled around and the best i could find was a 2 year old library:

I think Live View is the way to go here to implement something similar.

The use case for this would be something like a Purchase Order.

I have the main Purchase Order information and a table for the items. I would like to be able to add, update or remove items from this table.

And then able to submit a form to the _live.ex to save them as 1 object.

If there is something like this I would love to hear it as google has failed me on this.

Thanks for any information in advance!

Showing Posts 1 to 7

mmmrrr

mmmrrr

Hey @CessK!

I’m still a little confused as to what you want to achieve: do you simply have a “form” that contains a table with item information that can be updated? Then I think this should be trivial in LiveView.

Exzeitable is, as far as I understand it, for another usecase though: you have a long list of stuff you want the user to filter, sort and search. For this exzeitable looks fantastic. It doesn’t seem to be intended for editing data.

So in the end: the implementation depends on your exact set of requirements. You could also simply use the mentioned jquery library and implement the process without LiveView if you wanted to, but again: it depends.

Could you give us a little more details of what exactly you want to achieve? Then it would be simpler to formulate specific advice.


As an aside: In Elixir it often doesn’t matter that libraries seem to be “abandoned” since they are simply done. Additionally, in this particular case, the last commit is from two days ago :slight_smile:

CessK

CessK OP

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.

mmmrrr

mmmrrr

Understood! So your datastructure has the order information embedded as an array, so you’ll only have one changeset and the information is ephemeral and shouldn’t be persisted, correct?

In that case, I would probably use a single form around the table, so that I can still use the error tracking provided by the Ecto/LiveView integration. Then it comes down to something like this: How to use a list of items (array) with LiveView form

But in principle your implementation would also work :slight_smile:

CessK

CessK OP

How would that work for table and in my example a list of change sets on a table?

That part is not very clear to me yet and the key point that I am missing.

mmmrrr

mmmrrr

I’ll try to throw together a demo later today, if no one beats me to it :wink:

The key point is: you would only have one changeset. Your schema would contain an embedded schema which can also be used for validation.

The other option would be to persist every entry of the table to the database in which case every row would be a form (something like the edit form popover that gets generated when using the phx.gen.live tool, just inside of the table listing). But this is not what you want to achieve, if I’m understanding it correctly.

mmmrrr

mmmrrr

So I just bootstrapped a new project called Example and generated a new resource: mix phx.gen.live Billing Order orders title:string positions:array:map based on the assumption, that you want an embedded list of positions inside of your data schema.

Next you need to adjust the data model. This should always be your guiding principle: Model first! :wink:

# This is the generated model
defmodule Example.Billing.Order do
  use Ecto.Schema
  import Ecto.Changeset

  schema "orders" do
    # This has been changed from the generated schema
    embeds_many :positions, Example.Billing.Position, on_replace: :delete
    field :title, :string

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(order, attrs) do
    order
    |> cast(attrs, [:title])
    # This is important for ecto to know how to cast the embed
    |> cast_embed(:positions, with: &Example.Billing.Position.changeset/2)
    |> validate_required([:title])
  end
end

# This is something I added. This should contain the fields of your "table row".
defmodule Example.Billing.Position do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :product_name, :string
    field :quantity, :integer
  end

  # The embedded schema can have a separate changeset definition
  # This is cool, since it enables you to still get the error highlights
  # inside the live view form
  def changeset(schema, attrs) do
    schema
    |> cast(attrs, [:product_name, :quantity])
    |> validate_required([:product_name, :quantity])
    |> validate_number(:quantity, greater_than: 0)
  end
end

Then you’ll need to adjust the generated form component (of course this doesn’t have to be in a modal, I simply kept it that way for ease of implementation:

defmodule ExampleWeb.OrderLive.FormComponent do
  use ExampleWeb, :live_component

  alias Example.Billing

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.header>
        <%= @title %>
        <:subtitle>Use this form to manage order records in your database.</:subtitle>
      </.header>

      <.simple_form
        for={@form}
        id="order-form"
        phx-target={@myself}
        phx-change="validate"
        phx-submit="save"
      >
        <.input field={@form[:title]} type="text" label="Title" />

        <%!-- This  is the important bit --%>
        <fieldset class="flex flex-col gap-2">
          <legend class="font-bold">Order positions</legend>
          <%!-- Now we loop over the embedded positions. --%>
          <.inputs_for :let={f_line} field={@form[:positions]}>
            <.order_position f_line={f_line} />
          </.inputs_for>

          <.button class="mt-2" type="button" phx-click="add-position" phx-target={@myself}>
            Add
          </.button>
        </fieldset>

        <:actions>
          <.button phx-disable-with="Saving...">Save Order</.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  def order_position(assigns) do
    ~H"""
    <div>
      <div class="flex gap-5 items-end">
        <div class="grow">
          <.input class="mt-0" field={@f_line[:product_name]} label="Product name" />
        </div>
        <div class="grow">
          <.input class="mt-0" field={@f_line[:quantity]} type="number" label="Quantity" />
        </div>
      </div>
    </div>
    """
  end

  @impl true
  def update(%{order: order} = assigns, socket) do
    {:ok,
     socket
     |> assign(assigns)
     |> assign_new(:form, fn ->
       to_form(Billing.change_order(order))
     end)}
  end

  @impl true
  def handle_event("validate", %{"order" => order_params}, socket) do
    changeset = Billing.change_order(socket.assigns.order, order_params)
    {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
  end

  def handle_event("save", %{"order" => order_params}, socket) do
    save_order(socket, socket.assigns.action, order_params)
  end

  #
  # Add the event handler to update the form with the embedded
  #
  @impl true
  def handle_event("add-position", _, socket) do
    socket =
      update(socket, :form, fn %{source: changeset} ->
        existing = Ecto.Changeset.get_embed(changeset, :positions)
        changeset = Ecto.Changeset.put_embed(changeset, :positions, existing ++ [%{}])
        to_form(changeset)
      end)

    {:noreply, socket}
  end

  defp save_order(socket, :edit, order_params) do
    case Billing.update_order(socket.assigns.order, order_params) do
      {:ok, order} ->
        notify_parent({:saved, order})

        {:noreply,
         socket
         |> put_flash(:info, "Order updated successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  defp save_order(socket, :new, order_params) do
    case Billing.create_order(order_params) do
      {:ok, order} ->
        notify_parent({:saved, order})

        {:noreply,
         socket
         |> put_flash(:info, "Order created successfully")
         |> push_patch(to: socket.assigns.patch)}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
end

I hope this helps you to grok the connection between schema, changeset and form a little better.

CessK

CessK OP

@mmmrrr Ahh yes now i understand how it all fits together

<%!-- This  is the important bit --%>
<fieldset class="flex flex-col gap-2">

I see now how you used field set to do the same thing i did with multiple forms.

This will help me out greatly. Thank you for the time to write this out.

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New
apz
I’m new to elixir and just tried to install the elixirLS extension for VScode(ium) and it is throwing some errors that I would like help ...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews