I’ve been using phoenix for side projects last year or two, mostly small CRUD apps. I find when I try to do things in forms outside of regular data entry I always seem to stumble along. A big one recently is settings fields based on the values of other fields. Or responding to a field value and conditionally rendering something else. What are the recommended ways of working with forms like this? Do i use hooks and set it all up client side (to me this seems easiest since I’m more familiar with JS, but I don’t really like it)? Do I run a function in the validation event? If so, do i operate on the params, or on the changeset, or on the form itself? What if i need to do a database call for one of the operations?
I guess as an example, I have a work order, has many work items, each work item has a quantity and a unit price. I want to calculate the total price of each item based on the quantity and unit price set for each, as well as the total work order price. And then lets also add tax based on the selected customer.
What I’ve done is:
defmodule ....WorkItem.Item do
def changeset(item, attrs) do
item
|> cast(attrs, [
...
])
...
|> calculate_total_price()
end
defp calculate_total_price(changeset) do
quantity = get_field(changeset, :quantity, 0)
unit_price = get_field(changeset, :unit_price, 0)
total =
if quantity && unit_price do
Decimal.mult(Decimal.new(quantity), Decimal.new(unit_price))
else
Decimal.new("0")
end
put_change(changeset, :total_price, total)
end
end
defmodule ....WorkOrders.WorkOrder do
def changeset(work_order, attrs) do
work_order
|> cast(attrs, [
...
])
...
|> Calculations.calculate_totals()
end
end
defmodule HighImpactSigns.WorkOrders.Calculations do
import Ecto.Changeset
@gst_rate Decimal.new("0.05")
@pst_rate Decimal.new("0.07")
@combined_rate Decimal.new("0.12")
def calculate_totals(changeset) do
items =
case get_change(changeset, :items) do
nil -> get_field(changeset, :items) || []
items -> filter_deleted_items(items)
end
sub_total =
Enum.reduce(items, Decimal.new("0.00"), fn item, acc ->
item_total =
case item do
%Ecto.Changeset{} -> get_field(item, :total_price) || Decimal.new("0")
%WorkItem.Item{} -> item.total_price || Decimal.new("0")
end
Decimal.add(acc, item_total)
end)
|> Decimal.round(2)
tax_type = get_field(changeset, :tax_type, :None) || :None
tax_total =
case tax_type do
:None -> Decimal.new("0.00")
:GST -> Decimal.mult(sub_total, @gst_rate)
:PST -> Decimal.mult(sub_total, @pst_rate)
:"GST/PST" -> Decimal.mult(sub_total, @combined_rate)
:Exempt -> Decimal.new("0.00")
end
|> Decimal.round(2)
total_price = Decimal.add(sub_total, tax_total) |> Decimal.round(2)
put_change(changeset, :sub_total, sub_total)
|> put_change(:tax_total, tax_total)
|> put_change(:total_price, total_price)
end
defp filter_deleted_items(items) do
Enum.reject(items, fn
%Ecto.Changeset{action: action} when action in [:replace, :delete] -> true
_ -> false
end)
end
end
I’ve moved setting the tax type based on the customer somewhere else so i wasn’t spamming the database.
In my work order form (I have a custom select element that emits this event when selecting a customer):
@impl true
def handle_event("selection_change", %{"selection" => selection}, socket) do
customer = CustomerInformation.get_customer!(selection)
form = socket.assigns.form
changeset =
form.source
|> Ecto.Changeset.put_change(:tax_type, customer.tax_type)
|> Ecto.Changeset.put_change(:customer_id, selection)
|> WorkOrders.Calculations.calculate_totals()
{:noreply, socket |> assign_form(changeset)}
end
I have to add another feature, where I’m calculating the price based on the square footage of the item and a price per square footage set on the type of item it is. This would mean conditionally rendering some inputs based on if i am calculating based on the square footage, and then also modifying values in the form again.
i found myself doing things like
item[:substrate_id].value
Which just seems wrong. And then maybe reaching for input_value/2
?
Does anyone have any tips on a pattern i can reach for or some documentation i can look at to kind of solidify the “good” ways of doing this sort of stuff?