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

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

@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?

Popular in Questions Top

9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New

Other popular topics Top

AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39523 209
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement