How to get selected value of HTML options element on phx-click event

Hi,

Basically I’m coding an order system. There’re Products, Customer, Order and OrderItems tables.

My Order form is like attached capture.

Here’s my codes:
lib/myapp_web/live/admin/form_component.html.leex

<h2><%= @title %></h2>

<%= f = form_for @changeset, "#",
  id: "order-request-form",
  phx_target: @myself,
  phx_submit: "save" %>

  <%= label f, :customer_id %>
  <%= text_input f, :customer_id %>
  <%= error_tag f, :customer_id %>

  <%= label f, :order_id %>
  <%= text_input f, :order_id %>
  <%= error_tag f, :order_id %>

  <h2>Order Items</h2>
  <table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Count</th>
      <th></th>
    </tr>
  </thead>
  <tbody id="products">
  <tr>
    <td>
      <%= select f, :product_id, Enum.map(@products, &{&1.name, &1.id })%>
    </td>
    <td>
      <%= select f, :count, 1..50 %>
      <%= error_tag f, :count %>
    </td>
    <td>
      <button type="button" phx-click="add">+</button>
    </td>
  </tr>
  </tbody>
</table>

<h2>Cart</h2>
  <table id="cart" phx-update="append">
  <thead>
    <tr>
      <th>Name</th>
      <th>Price</th>
      <th>Count</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <%= for item <- @cart_items do %>
      <tr id="item-<%= item.id %>">
        <td><%= item.name %></td>
        <td><%= item.price %></td>
        <td></td>
        <td>
          <button type="button" phx-click="remove">-</button>
        </td>
      </tr>
    <% end %>
  </tbody>
</table>

<%= submit "Save", phx_disable_with: "Saving..." %>

</form>

lib/myapp_web/live/admin/index.ex

defmodule MyAppWeb.AdminLive.Index do
  use MyAppWeb, :live_view

  alias MyApp.{ Products, Orders }

  @impl true
  def mount(_params, _session, socket) do
    socket = socket |> assign(:pending_orders, load_pending_orders())
    {:ok, socket, temporary_assigns: [cart_items: []]}
  end

  @impl true
  def handle_params(params, _url, socket) do
    IO.inspect(params)
    IO.inspect(socket.assigns)
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  @impl true
   def handle_event("add", params, socket) do
      IO.puts("+++++++++++++++++ADDING+++++++++++++++++++")
      IO.inspect(params)
      IO.inspect(socket.assigns.cart_items)
      IO.puts("+++++++++++++++++ADDED+++++++++++++++++++")
      cart_items = socket.assigns.cart_items
      socket = assign(socket, :cart_items, cart_items)
		{:noreply, socket}
  end

  @impl true
  def handle_event("remove", params, socket) do
    IO.puts("+++++++++++++++++REMOVING+++++++++++++++++++")
    IO.inspect(params)
    IO.inspect(socket.assigns.cart_items)
    IO.puts("+++++++++++++++++REMOVED+++++++++++++++++++")
    cart_items = socket.assigns.cart_items
    socket = assign(socket, :cart_items, cart_items)
		{:noreply, socket}
   end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Order")
    |> assign(:order_request, nil)
    |> assign(:products, load_avaliable_products())
    |> assign(:cart_items, [%Products.Product{}])
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Admin")
    |> assign(:pending_orders, load_pending_orders())
  end

  defp load_pending_orders do
     Orders.list_pending_orders()
  end

  defp load_avaliable_products do
	Products.list_avaliable_products()
  end

end

What I need is, getting selected values of ordered products when click the “+” button on server side.

How can I achieve this?

Thank you!

1 Like

Have you looked into phx-value- attributes that you can use with click events? Docs.

yes but I couldn’t figure out a way to do with it.

I’m hoping someone has a better answer because this would be useful but I’m afraid you’re probably going to have to address that with hooks

I tired ugly stuff like putting a form inside the form itself which incorrect I know, when clicked the “+” button it submits both form so both events occurs along with data actually.

Another way could be separate forms and move submit button out of the forms and submit the form via javascript.

But I’m looking a proper way than a hacky way.

Add :phx_change to your <form> to get every input value update. Then assign selected input values into phx-value attributes.

<%= f = form_for @changeset, "#",
  id: "order-request-form",
  phx_target: @myself,
  phx_change: "change",
  phx_submit: "save" %>

<button type="button" phx-click="add"
  phx-value-product-id="<%= @add_product_id %>"
  phx-value-count="<%= @add_count %>">+</button>
def handle_event("change", params, socket) do
  # find product_id and count in params
  socket =
    socket
    |> assign(socket, :add_product_id, product_id)
    |> assign(socket, :add_count, count)

  {:noreply, socket}
end

def handle_event("add", %{"product_id" => product_id, "count" => count}, socket) do
  ...
end
3 Likes

that’s kind of worked but had to dealt with a lot of bugs around, one left I guess :slight_smile:

for helping other people, I’d like to share whole codes and also ask a few things about Elixir, I think I’m not getting benefits of functional programming what I do in “add” event.

first I got rid of the modal popup, one have a single page. Here’s a gif for the latest the version.

Order Request

When I click on I remove it crashes due to value is nil.

Here’s html part of remove button, and yes when I check html I can see phx-value is set correctly.

            <%= for item <- @cart_items do %>
              <tr>
                <td><%= item.name %></td>
                <td><%= item.price %></td>
                <td><%= item.count %></td>
                <td>
                  <button type="button" phx-click="remove" phx-value="<%= item.product_id %>">-</button>
                </td>
              </tr>
            <% end %>

and here’s the log of remove action (during screen recording)

%{count: 1, id: 1, name: "Water", price: "12.5 TRY", product_id: 1}
%{count: 2, id: 2, name: "Water", price: "12.5 TRY", product_id: 1}
%{count: 3, id: 2, name: "Water", price: "12.5 TRY", product_id: 1}
%{count: 4, id: 2, name: "Water", price: "12.5 TRY", product_id: 1}
%{count: 18, id: 2, name: "Water", price: "12.5 TRY", product_id: 1}
%{count: 3, id: 2, name: "Milk", price: "20.0 TRY", product_id: 2}
%{count: 6, id: 3, name: "Milk", price: "20.0 TRY", product_id: 2}
%{count: 7, id: 3, name: "Milk", price: "20.0 TRY", product_id: 2}
%{count: 8, id: 3, name: "Milk", price: "20.0 TRY", product_id: 2}
%{count: 9, id: 3, name: "Milk", price: "20.0 TRY", product_id: 2}
%{count: 30, id: 3, name: "Juice", price: "20.0 TRY", product_id: 3}
%{count: 60, id: 4, name: "Juice", price: "20.0 TRY", product_id: 3}
%{count: 90, id: 4, name: "Juice", price: "20.0 TRY", product_id: 3}
%{count: 120, id: 4, name: "Juice", price: "20.0 TRY", product_id: 3}
%{count: 150, id: 4, name: "Juice", price: "20.0 TRY", product_id: 3}
++++ removing ++++
""
%{"value" => ""}
++++ removed ++++

value is empty?

backend-code:

  @impl true
  def handle_event("remove", params, socket) do
    %{"value" => value} = params
    IO.puts("++++ removing ++++")
    IO.inspect(value)
    IO.inspect(params)
    IO.puts("++++ removed ++++")
    request = params["request"]
    product_id = to_integer(request["value"])
    cart_items = socket.assigns.cart_items
    existing_product = Enum.find(socket.assigns.products, fn p -> p.id == product_id end)
    cart_items = List.delete(cart_items, existing_product)
    socket = assign(socket, :cart_items, cart_items)
    {:noreply, socket}
  end

Whole Source Code:

lib/myapp_web/live/admin/index.html.leex

<section class="row">
  <article class="column order-form">

    <h2> Order Request </h2>

    <%= f = form_for @changeset, "#",
      id: "order-request-form",
      phx_change: "change",
      phx_submit: "save" %>

      <%= label f, :phone_number %>
      <%= text_input f, :phone_number, value: @phone_number %>
      <%= error_tag f, :phone_number %>

      <h2>Avaliable Products</h2>
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Count</th>
            <th></th>
          </tr>
        </thead>
        <tbody id="products">
          <tr>
            <td>
              <%= select f, :product_id, Enum.map(@products, &{&1.name, &1.id }), selected: @add_product_id %>
            </td>
            <td>
              <%= select f, :count, 1..50, selected: @add_count %>
              <%= error_tag f, :count %>
            </td>
            <td>
              <button type="button" phx-click="add"
              phx-value-product-id="<%= @add_product_id %>"
              phx-value-count="<%= @add_count %>">+</button>
            </td>
          </tr>
        </tbody>
      </table>

      <h2>Cart</h2>

      <table id="cart">
        <thead>
          <tr>
            <th>Name</th>
            <th>Price</th>
            <th>Count</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          <%= if @cart_items do %>
            <%= for item <- @cart_items do %>
              <tr>
                <td><%= item.name %></td>
                <td><%= item.price %></td>
                <td><%= item.count %></td>
                <td>
                  <button type="button" phx-click="remove" phx-value="<%= item.product_id %>">-</button>
                </td>
              </tr>
            <% end %>
          <% else  %>
            <tr>
              <td colspan="4"> No Item added yet </td>
            </tr
          <% end %>

        </tbody>
      </table>

      <%= submit "Save", phx_disable_with: "Saving..." %>

  </form>

  </article>

</section>

<section class="row">
  <article class="column">
    <h2> Pending Orders </h2>
    <table class="pending-orders">
      <thead>
        <th>Order ID</th>
        <th>Order Date</th>
        <th style="text-align:center">Actions</th>
      </thead>
      <tbody>
        <%= for order <- @pending_orders do %>
          <tr id="order-<%= order.id %>">
            <td><%= order.id %></td>
            <td><%= order.inserted_at %></td>
            <td>
                <button>Delivered</button>
                <button>Cancelled</button>
            </td>
          </tr>
        <% end %>
      </tbody>
    </table>
  </article>
</section>

lib/myapp_web/live/admin/index.ex

defmodule MyAppWeb.AdminLive.Index do
  use MyAppWeb, :live_view

  alias MyAppWeb.{Products, Orders}

  @impl true
  def mount(_params, _session, socket) do
    changeset = Orders.Request.changeset(%Orders.Request{}, %{})

    socket =
      socket
      |> assign(:pending_orders, load_pending_orders())
      |> assign(:changeset, changeset)
      |> assign(:order_request, nil)
      |> assign(:products, load_avaliable_products())
      |> assign(:add_product_id, nil)
      |> assign(:add_count, 0)
      |> assign(:cart_items, nil)
      |> assign(:phone_number, nil)
      |> assign(:cart_items, nil)

    {:ok, socket}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  @impl true
  def handle_event("change", params, socket) do
    request = params["request"]

    socket =
      socket
      |> assign(:phone_number, request["phone_number"])
      |> assign(:add_product_id, request["product_id"])
      |> assign(:add_count, request["count"])
      |> assign(:cart_items, socket.assigns.cart_items)

    {:noreply, socket}
  end

  @impl true
  def handle_event("save", _params, socket) do
    # not implemented yet
    {:noreply, socket}
  end

  @impl true
  def handle_event("add", params, socket) do
    %{"count" => count, "product-id" => product_id, "value" => _value} = params

    product_id = to_integer(product_id)
    count = to_integer(count)
    cart_items = socket.assigns.cart_items

    product = Enum.find(socket.assigns.products, fn p -> p.id == product_id end)
    id = if cart_items == nil, do: 1, else: Enum.count(cart_items) + 1
    order_item = %{
      :id => id,
      :product_id => product_id,
      :name => product.name,
      :count => count,
      :price => "#{product.price} #{product.currency}"
    }

    cart_items =
      if cart_items == nil do
        [order_item]
      else
        cart_items = if existing_product = Enum.find(cart_items, fn i -> i.product_id == product_id end) do
          IO.inspect(existing_product)
          new_count = existing_product.count + count
          cart_items = List.delete(cart_items, existing_product)
          order_item = %{order_item | count: new_count}
          [cart_items] ++ [order_item]
        else
          [cart_items] ++ [order_item]
        end
        cart_items
      end

    socket = socket |> assign(:cart_items, List.flatten(cart_items))

    {:noreply, socket}
  end

  @impl true
  def handle_event("remove", params, socket) do
    %{"value" => value} = params
    IO.puts("++++ removing ++++")
    IO.inspect(value)
    IO.inspect(params)
    IO.puts("++++ removed ++++")
    request = params["request"]
    product_id = to_integer(request["value"])
    cart_items = socket.assigns.cart_items
    existing_product = Enum.find(socket.assigns.products, fn p -> p.id == product_id end)
    cart_items = List.delete(cart_items, existing_product)
    socket = assign(socket, :cart_items, cart_items)
    {:noreply, socket}
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Admin")
    |> assign(:pending_orders, load_pending_orders())
  end

  def format_product_name(product) do
    "#{product.name} - #{product.price} #{product.currency}"
  end

  defp load_pending_orders do
    Orders.list_pending_orders()
  end

  defp load_avaliable_products do
    Products.list_avaliable_products()
  end

  defp to_integer(s) do
    {i, _s} = Integer.parse(s)
    i
  end
end

PS: It didn’t work with temporary_assigns like in the first initial.

I think the syntax is supposed to be phx-value-value if you later on want to pattern match value out of it. Check how you did it for the ‘adding’ part: phx-value-product-id, phx-value-count

Anyway, looking at your demo gif, I don’t see why you couldn’t have 2 separate forms and simplify this a lot: one for adding and one for your cart.

oh you’re right! now it works! and about two forms, it’s because of tables I guess.

Customers
Products
Orders
OrderItems

with one action I have to save order request.

I improved the backend part little bit.

I don’t know why the form doesn’t show the validation error on the form.

    <%= f = form_for @changeset, "#",
      id: "order-request-form",
      phx_change: "change",
      phx_submit: "save" %>

      <%= label f, :phone_number %>
      <%= text_input f, :phone_number, value: @phone_number %>
      <%= error_tag f, :phone_number %>

      <%= submit "Save", phx_disable_with: "Saving..." %>
    </form>

Backend:

  @impl true
  def handle_event("change", params, socket) do
    request       = params["request"]
    phone_number  = request["phone_number"]
    product_id    = request["product_id"]
    count         = request["count"]
    cart_items    = socket.assigns.cart_items

    changeset = Orders.Request.changeset(%Orders.Request{}, %{
        :phone_number => phone_number,
        :cart_items => cart_items
      })

      IO.inspect(changeset)

    socket =
      socket
      |> assign(:phone_number, phone_number)
      |> assign(:add_product_id, product_id)
      |> assign(:add_count, count)
      |> assign(:cart_items, cart_items)
      |> assign(:changeset, changeset)

    {:noreply, socket}
  end

I can see the errors in console logs however it doesn’t show on the form, like regular form validation.

#Ecto.Changeset<
  action: nil,
  changes: %{phone_number: "0532366097"},
  errors: [
    phone_number: {"should be at least %{count} character(s)",
     [count: 11, validation: :length, kind: :min, type: :string]},
    cart_items: {"can't be blank", [validation: :required]}
  ],
  data: #Layvo.Orders.Request<>,
  valid?: false
>
#Ecto.Changeset<
  action: nil,
  changes: %{
    cart_items: [
      %{count: 5, id: 1, name: "Water", price: "12.5 TRY", product_id: 1}
    ],
    phone_number: "0532366097"
  },
  errors: [
    phone_number: {"should be at least %{count} character(s)",
     [count: 11, validation: :length, kind: :min, type: :string]}
  ],
  data: #Layvo.Orders.Request<>,
  valid?: false
>

Maybe it’s just a css issue?

Use the browser’s developer tools inspector and see if the error element exists and is just hidden.

Form Events section of the LiveView docs has some pointers on what to do if this is the case (add some css styles and maybe update your error_tag helper).

No, it doesn’t create an error tag when I inspected it. I disabled the “Save” button until conditions met.

 <%= submit "Save", phx_disable_with: "Saving...",
      disabled: (@cart_items == nil || Enum.count(@cart_items) == 0) ||
      (@phone_number == nil || String.length(@phone_number) < 11) %>

Can you post the code for the error_tag function? (my_app_web/views/error_helpers.ex)

It works in other forms. There must be something very wrong with my structure in this page but it’s almost same with others, except table part, and phx-target is missing. When enableDebug, in console I see all forms goes to server, the diff should be the only changes, right? That makes me think there might be something wrong with my implementation. Not sure, I’ll look into later to understand the diff logs.

Here it is. Thank you!

defmodule MyAppWeb.ErrorHelpers do
  @moduledoc """
  Conveniences for translating and building error messages.
  """

  use Phoenix.HTML

  @doc """
  Generates tag for inlined form input errors.
  """
  def error_tag(form, field) do
    Enum.map(Keyword.get_values(form.errors, field), fn error ->
      content_tag(:span, translate_error(error),
        class: "invalid-feedback",
        phx_feedback_for: input_id(form, field)
      )
    end)
  end

  @doc """
  Translates an error message using gettext.
  """
  def translate_error({msg, opts}) do
    # When using gettext, we typically pass the strings we want
    # to translate as a static argument:
    #
    #     # Translate "is invalid" in the "errors" domain
    #     dgettext("errors", "is invalid")
    #
    #     # Translate the number of files with plural rules
    #     dngettext("errors", "1 file", "%{count} files", count)
    #
    # Because the error messages we show in our forms and APIs
    # are defined inside Ecto, we need to translate them dynamically.
    # This requires us to call the Gettext module passing our gettext
    # backend as first argument.
    #
    # Note we use the "errors" domain, which means translations
    # should be written to the errors.po file. The :count option is
    # set by Ecto and indicates we should also apply plural rules.
    if count = opts[:count] do
      Gettext.dngettext(MyAppWeb.Gettext, "errors", msg, msg, count, opts)
    else
      Gettext.dgettext(MyAppWeb.Gettext, "errors", msg, opts)
    end
  end
end

Your changeset is missing an :action. I think that’s the issue here. Do this:

changeset = Orders.Request.changeset(%Orders.Request{}, %{
    :phone_number => phone_number,
    :cart_items => cart_items
    })
    |> Map.put(:action, :validate)

Your error_tag function is fine, btw. It’s the default Phoenix comes with.

wow! great catch! it works perfect! thanks a lot @sfusato!