Is my has_many and many_many relationship implementation (changeset, and handle_event) ok?

To input multiple items into my sale_form, I directly add them into the socket.assigns.sale_form.data.items in one of my handle_events for selecting items. In the code below, I hard-coded how I would input a single staff into the sale_form. I then pass that changeset together with params from my final handle_event when I submit the form and pass them all to my query and changeset. This is a working example for CREATE thus far. Havent implemented EDIT or DELETE to see what happens. After struggling for a very long time, i’m unsure if this is the correct way to do so but it works for now. ECTO EXPERTS out there, please drop some wisdom :pray:

Here’s my changeset:

@doc false
  def changeset(sale, attrs) do
    sale
    |> cast(attrs, [:revenue])
    |> cast_assoc(:items, required: true, with: &Item.changeset/2)
    |> cast_assoc(:customer, required: true, with: &Customer.changeset/2)
    |> validate_required([
      :revenue
    ])
  end

Here’s my context query:

def create_sale(%Sale{} = sale, attrs \\ %{}) do
    sale
    |> Sale.changeset(attrs)
    |> Repo.insert()
  end

Here’s my handle_event:

def handle_event("new-sale", %{"sale" => params}, socket) do
    updated_sale_form = %{
      socket.assigns.sale_form
      | data: %{
          socket.assigns.sale_form.data
          | staff: Staffs.get_staff!(1),
            staff_id: 1 #hard-coded for now
        }
    }

    sale_struct = updated_sale_form.data

    full_params =
      Map.merge(
        %{
          "revenue" => socket.assigns.total_revenue
        },
        params
      )

    case Sales.create_sale(sale_struct, full_params) do

Project deps:

{:phoenix, "~> 1.7.2"},
{:phoenix_ecto, "~> 4.4"},
{:ecto_sql, "~> 3.6"},

Hey, it’s not very clear what you’re trying to achieve based on your initial post. For better feedback, it’d help if you could describe the specific use case you’re trying to support. It’d also be good to explicitly outline the different models/schemas and the relationships between them e.g. Sale has many Items (or is it many to many?), Sale belongs to Customer, Sale belongs to Staff.

Generally speaking, you shouldn’t need to manually change the form data like you’re doing in handle_event callback. I’d suggest taking a look at the inputs_for/1 and incorporate it into your sales form to handle associations like items or staff via nested form inputs.

1 Like

Ah i see, let me provide clearer information. With my implementation above, I’m unable to update the items in my Sale. I can update the non-assoc fields but i’m unable to update the assoc-fields like items or staff or customer. This is likely because i’m passing the new items information NOT through the attrs in the changeset(), but the sale instead.

My question: How do I edit my items in my sale? (which is an assoc-field for my sale struct)

Here’s my schema:

schema "sales" do
    field :revenue, :float

    belongs_to :customer, Pfbooks.Customers.Customer
    belongs_to :staff, Pfbooks.Staffs.Staff
    many_to_many :items, Pfbooks.Items.Item, join_through: "sales_items"

    timestamps()
  end

This should be a good reference for you. It’ll walk you through using inputs_for/1 to add, reorder, delete through a many-to-many association.

In this post, there’s a many-to-many relationship between Author and Book which should map to your many-to-many relationship between Sale and Item.

1 Like

Thank you for your reply!
I still dont fully understand it because I’ve not implemented it. The reason is the example uses <.input type =“select” …> while my usecase is <.input type =“search” …> which i dont know how to. Hence I just use a regular <input> to do so. I have yet to figure out how to do the proper mapping of name to make my <input> function like <.input> from core_components.ex. Do you know any guide / examples for this?

However, from the fly guide you sent, I did figure out how to update my many_many relationship between “sale” and “items” for me. Now, I’m facing the issue of trying to update my customer, which each “customer” has_many “sale”. So each “sale” belongs_to a “customer”. When I implement the same thing as that for items (which now works), i’m getting this error:

** (Ecto.ConstraintError) constraint error when attempting to delete struct:

    * "sales_customer_id_fkey" (foreign_key_constraint)

This means I cannot edit the customer in my sale. This is suggesting that the new customer I put into the params is conflicting (weirdly) because that customer already exists in the db ._. I dont understand this. Please help :pray:

Allow me to elaborate on this.

For my items field, i need to search through my items list from the DB and choose the right one. This means I need to implement a search function. It will display the search query results. I then click the correct item. The act of clicking to select the correct item (which is just a div) triggers a handle_event. I manually add this item into a list in my socket, NOT into the form struct. This causes the changes to not be captured by the changeset as well. I can see that inputs_for allows me to put nested fields into the form to allow change tracking. HOWEVER, i dont know how to do it for this case where I need to SEARCH for the items, not SELECT them from a limited list like in the example you gave.

Am I making sense? Hope i’m articulating my problem and frustrations better :')) @codeanpeace

maybe I can dynamically insert a hidden input tag to have the items_id show up in the form struct

That’s the way to go. Essentially all the searching you’re doing is completely unrelated the form as in the form doesn’t care about what you need to do to search for a record. All it cares for is having the result be the value of an input of the form.

If you do that client side you can use Form bindings — Phoenix LiveView v0.20.4 to trigger a form change event from js whenever a records was selected and populated on an input.

1 Like

Ah i see. Thanks for the response. I’ll give it a go. Though I gotta say, this feels very duct taped together. I’ve not used other web frameworks and i’m curious if they are this complicated as well to deal with nested data.

The complication is likely not in the nesting of data, but in the conflation of usecases. You need a form to do the search, but you also need a form to submit the looked up data. But with html not allowing nested forms you’re likely trying to make both things work as part of a single form, which makes things more complicated.

You can work around nested forms with form="form-id" attribute on inputs, which allows the inputs of different forms to be composed together freely without needing to nest the <form> elements.

Many js/client side frameworks just don’t use any form level semantics to begin with and work only on a per input level, which comes with a whole different set of tradeoffs.

2 Likes