Show live changes depending on nested data

I have an Invoice resource that has_many InvoiceItems. Every InvoiceItem gets some data (quantity, tax_rate, unit_price) and calculates the item's total amounts in a change.
# Invoice Resource
change manage_relationship(:items, type: :create)

change __MODULE__.Changes.SetIdentifier
# Runs the after_action hook
change __MODULE__.Changes.AggregateTaxes
# Invoice Resource
change manage_relationship(:items, type: :create)

change __MODULE__.Changes.SetIdentifier
# Runs the after_action hook
change __MODULE__.Changes.AggregateTaxes
In the Invoice-create_invoice action I want to aggregate all the taxes, this means iterating all items and building sums. The way I currently do this is by running the action and then updating the invoice in an after_action hook, as I could not find a way to access the InvoiceItem's data before. This works fine, the problem is that it breaks live updating forms – I'd like to show the calculated amounts immediately using the after_action hook breaks this. Is there a way to work around this? I think I need a way to run the nested items' changes first or at least make sure the inputs to my nested resource are valid, in this case I could calculate the value in the Invoice's changeset. I feel like there must be a way to do this, I just could not figure it out yet 🙂
Solution:
I couldn't really find out how to use transform_params here. As far as I can understand it's helpful for updating the parameters that get passed to the form, but my "calculated" fields are not actually part of the form. I was able to figure out another way – maybe it's helpful for someone else, so I'll share it. The solution was quite easy: 1) Run the necessary calculations in the top level changeset (also):...
Jump to solution
3 Replies
ZachDaniel
ZachDaniel2mo ago
I think for a lot of these complex UI stuff currently folks are just doing it in the UI using things like transform_params and Ash.calculate etc.
zimt28
zimt28OP2mo ago
I see. Could this be improved in the future? I think it would be nice and follow the Ash paradigm if this could be handled in one place
Solution
zimt28
zimt282mo ago
I couldn't really find out how to use transform_params here. As far as I can understand it's helpful for updating the parameters that get passed to the form, but my "calculated" fields are not actually part of the form. I was able to figure out another way – maybe it's helpful for someone else, so I'll share it. The solution was quite easy: 1) Run the necessary calculations in the top level changeset (also): From this
@impl true
def change(changeset, _opts, context) do
Ash.Changeset.after_action(changeset, fn _changeset, record ->
calculate_totals(record, context)
end)
end

defp calculate_totals(record, context) do
tax_items =
record.items
|> ...

record =
record
|> Ash.Changeset.for_update(:update, %{}, tenant: context.tenant)
|> Ash.Changeset.force_change_attributes(%{
tax_items: tax_items
})
|> Ash.update!()

{:ok, record}
end
@impl true
def change(changeset, _opts, context) do
Ash.Changeset.after_action(changeset, fn _changeset, record ->
calculate_totals(record, context)
end)
end

defp calculate_totals(record, context) do
tax_items =
record.items
|> ...

record =
record
|> Ash.Changeset.for_update(:update, %{}, tenant: context.tenant)
|> Ash.Changeset.force_change_attributes(%{
tax_items: tax_items
})
|> Ash.update!()

{:ok, record}
end
to this
@impl true
def change(changeset, _opts, _context) do
items = Ash.Changeset.get_argument(changeset, :items) || []

valid_items =
for item <- items,
{:ok, record} <- [maybe_invoice_item(item)] do
record
end

{subtotal, tax_total, total} =
Enum.reduce(valid_items, {0, 0, 0}, fn item, {subtotal, tax_total, total} ->
{
Decimal.add(subtotal, item.subtotal),
Decimal.add(tax_total, item.tax_amount),
Decimal.add(total, item.total)
}
end)

Ash.Changeset.force_change_attributes(changeset, %{
subtotal: subtotal,
tax_total: tax_total,
total: total
})
end

defp maybe_invoice_item(item) do
InvoiceItem
|> Ash.Changeset.for_create(:create_invoice_item, item)
|> Ash.Changeset.apply_attributes()
end
@impl true
def change(changeset, _opts, _context) do
items = Ash.Changeset.get_argument(changeset, :items) || []

valid_items =
for item <- items,
{:ok, record} <- [maybe_invoice_item(item)] do
record
end

{subtotal, tax_total, total} =
Enum.reduce(valid_items, {0, 0, 0}, fn item, {subtotal, tax_total, total} ->
{
Decimal.add(subtotal, item.subtotal),
Decimal.add(tax_total, item.tax_amount),
Decimal.add(total, item.total)
}
end)

Ash.Changeset.force_change_attributes(changeset, %{
subtotal: subtotal,
tax_total: tax_total,
total: total
})
end

defp maybe_invoice_item(item) do
InvoiceItem
|> Ash.Changeset.for_create(:create_invoice_item, item)
|> Ash.Changeset.apply_attributes()
end
The "trick" here is not to wait for the actual results but calculate them using the children's changeset. 2) Use @form.source.attributes in the LiveView to get the updated values:
Total: {@form.source.attributes.total}
Total: {@form.source.attributes.total}

Did you find this page helpful?