I’m interested in people’s opinions on how I approached a particular problem. I’m using LiveView, and I have a complex form with 4 total associations, one of those nested 2 levels in the base struct. I needed the entire changeset because I use the actions to broadcast the results (for example, if I update the parent, and create 2 children, and update 1 child, I need to broadcast an update on the parent, and 2 children, and broadcast a create on the other child). I also needed to know the current_change
AKA only the thing or thing that changed last, so that I could perform some actions only once when a field changed. Specifically to make this work with LiveView, I needed to update the params when the x_id changed`
My solution, at a high level is use the same changeset to validate changes, and to update the record (I know that’s normal, but bear with me). I had to actually change the struct and re-validate, so I use apply_action
during the form validation and Repo.update
when I update the record (you’ll see why in a moment). Generally, the parent record has belongs_to
relationships to the child structs, so when the x_id
field changes on the parent, I want to present the fields of the new record to the user. I do this by changing the params
when the x_id changes. Similarly, if I change the x_id
, and make changes to x
I need to change the associated record before validating the changes.
Basically handle_page_id_change
manages updating the data in the changeset to the new record if the parent x_id
changes. maybe_replace_page_params
handles updating the params when the x_id
changes so that the form changes when the user selects a new x
. Note that I have to recast the x_id
's after changing the associated records because I used apply_action
/ Repo.update
.
In the case of cast_assoc(:annotation ...
and handle_content_id_change
I’m doing the same thing for the 2nd level nested change. I could potentially clean that up and refactor it inside the annotation changeset, but since there was only one of them, and I don’t do this anywhere else (yet), I left it here.
Once the associated data and params are updated, It’s business as usual. I cast the remaining params/foreign keys/assocs, and automatically name some records.
Any questions or comments? I’m curious if anyone else has had the same problem, or has any other ways of doing this that work better. I don’t fully understand the performance cost of doing all this work every time the user hits a key, but it’s been pretty performant in testing. From a “database correctness” standpoint, this seems like the right thing to do (change the foreign key on the parent first, make any changes to the associated record second).
def nested_changeset(step, last_step, attrs, state, action) do
last_change = changeset(last_step, attrs)
step
# |> change page
|> cast(attrs, [ :page_id ])
|> foreign_key_constraint(:page)
|> Changeset.handle_page_id_change(state)
|> Changeset.maybe_replace_page_params(last_change, state)
# |> same_thing_for_annotation_and_element
# |> update step fk's
|> Changeset.cast_changeset_params([ :page_id, :annotation_id, :element_id ])
|> Changeset.update_foreign_keys(action)
# |> change content
|> cast_assoc(:annotation, with: &Annotation.content_id_changeset/2)
|> Changeset.handle_content_id_change(state)
|> Changeset.maybe_replace_content_params(last_change, state)
# |> update annotation fk's
|> cast_assoc(:annotation, with: &Annotation.content_id_changeset/2)
|> Changeset.update_foreign_keys(action)
# |> final changes
|> Changeset.cast_changeset_params([ :process_id, :step_type_id ])
|> Changeset.cast_changeset_params([ :name, :order, :url, :text, :width, :height, :page_reference ])
|> foreign_key_constraint(:process)
|> foreign_key_constraint(:step_type)
|> assoc_changeset()
|> names_changeset()
|> validate_required([:order])
end
def handle_page_id_change(%{ changes: %{ page_id: page_id }} = changeset, state) do
Logger.debug("Page id changed to #{page_id}")
page = Web.get_page!(page_id, state, state.assigns.state_opts)
Map.put(changeset.data, :page, page)
|> cast(changeset.params, [ ])
end
def handle_page_id_change(changeset, _state), do: changeset
def maybe_replace_page_params(changeset, %{ changes: %{ page_id: page_id }}, state) do
Logger.debug("Replacing Page #{page_id} Params")
page = Web.get_page!(page_id, state, state.assigns.state_opts)
page_params = replace_params_with_fields(changeset.params["page"], page, Page)
params =
changeset.params
|> Map.put("page", page_params)
|> Map.put("page_id", Integer.to_string(page_id))
Map.put(changeset, :params, params)
end
def maybe_replace_page_params(changeset, _last_changeset, _state), do: changeset
def update_foreign_keys(changeset, action) do
case action do
:validate ->
{ :ok, step } = apply_action(changeset, :update)
step
|> cast(changeset.params, [ ])
_ ->
{ :ok, step } = UserDocs.Repo.update(changeset)
step
|> cast(changeset.params, [ ])
end
end