CessK

CessK

Datatables like data entry with LiveView

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!

Marked As Solved Switch mode

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.

Also Liked

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.

Where Next?

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
silverdr
Using Phoenix.LiveView.TagEngine as an EEx.Engine is deprecated! To compile HEEx, use Phoenix.LiveView.TagEngine.compile/2 instead. Sta...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
michallepicki
I am using Oban and occasionally, shortly after a deployment, a handful of jobs can fail because of dependency on other parts of the syst...
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
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement