Hi everyone,
I finally had a bit of time to followup on this post: Best way to handle associations in single html form
In my previous post I just wanted to create a handle a nested association in the same html form. Specifically to create the parent and child in one go.
In this post I want to go a little bit further and try to explain to new comers how I manage to implement a full CRUD for this scenarios.
Please feel free to comment in this post how did you handle this scenario.
You can find all the code for this implementation in bitbucket here.
Scenario:
Handle a nested (has_many) association in the same html form.
Schema: Invoice --> has_many --> details
Key points:
- You are using one single page form to handle both parent and child associations.
- When creating the association for the first time you don’t have the parent’s id yet so you have to save the parent’s part fist then use its id to associate each child.
- I will be using drab library to add fields to the html form dynamically. So you will have to add it to your project.
- I will use cast_assoc to handle the update and delete of the nested association.
Create
To keep this post as short as possible, please refer to my previous post where I coverd the create action only. You can find it here.
Update
Please refer to the cast_assoc official documentation for further reference but I will enumerate some important points:
-
cast_assoc requires you to preload the nested associations records
def get_invoice!(id) do Repo.get!(Invoice, id) |> Repo.preload(:details) end
-
Based in the id number cast_assoc will set the changeset action accordingly (insert, update, delete, replace) and the database will follow.
-
cast_assoc will handle all the database insert, update and delete actions.
So how does cast_assoc works. Lets see the code:
def update_changeset(%Invoice{} = invoice, attrs) do
invoice
|> cast(attrs, [:provider])
|> validate_required([:provider])
|> cast_assoc(:details, with: &Multipartform.Documents.Detail.update_changeset(&1, &2, invoice))
end
Note that cast_assoc/3 takes 3 arguments: the changeset, the nested association as atom and a function. cast_assoc will invoque the function for each child record. The function must return a changeset of Details for each child record that is received in the attrs[“details”].
Also note that this function has three arguments. The first is the %Details{} struct and the second is the corresponding attributes in attrs[“details”] based on the id. So cast_assoc matches the id’s and calls the function with the arguments. Lets see the code of this function:
@doc false
def update_changeset(%Detail{invoice_id: nil}, attrs, invoice) do
Ecto.build_assoc(invoice, :details)
|> cast(attrs, [:tax, :code, :desc, :up, :invoice_id])
|> validate_required([:tax, :code, :desc, :up, :invoice_id])
end
@doc false
def update_changeset(%Detail{} = detail, attrs, _invoice) do
detail
|> cast(attrs, [:tax, :code, :desc, :up, :invoice_id, :delete])
|> validate_required([:tax, :code, :desc, :up, :invoice_id])
|> mark_for_delete()
end
@doc false
defp mark_for_delete(changeset) do
if get_change(changeset, :delete) do
%{ changeset | action: :delete }
else
changeset
end
end
Two important thing to note:
- We are pattern matching on the first argument of the function header (Detail Struct). This argument is the record we preloaded. If there is no matching id means that the record found is new and this argument is initialized with nil values.
- When the invoice_id is nil in the struct means that the record is new and we have to build the association between the invoice and the detail.
Delete
To delete a nested association you just have to delete the entire record in the html form so it won’t be part of the attrs[“details”]. cast_assoc will delete the record form the database. This is a straight forward operation but because it is a destructive operation the recommended way is to mark the record for delete and change the changeset action to delete. This lets you do something else with the data insted of just deleting it.
Note that for each record we check the changeset for the key “delete” in the changes and if so we change the changeset action to delete.
I hope this helps explanation helps.
Best regards