Hi! I’ve been experiencing with Phoenix lately and was wondering if the PRG (Post Redirect Get) technique is relevant or there’s a better way to approach complex form building and validation.
Apart from its original purpose, the PRG pattern is very useful when you need to consistently build form data. Consider something like the code below:
def new(conn, _params) do
changeset = ProductionLine.change_car(%Car{})
# Loads the data necessary to render the form
car_colors = ProductionLine.list_car_colors()
car_optionals = ProductionLine.list_car_optionals()
render(conn, "new.html",
changeset: changeset,
car_colors: car_colors,
car_optionals: car_optionals)
end
def create(conn, %{"car" => car_params}) do
case ProductionLine.create_car(car_params) do
{:ok, car} ->
conn
|> put_flash(:info, "Car created successfully.")
|> redirect(to: Routes.car_path(conn, :show, car))
{:error, %Ecto.Changeset{} = changeset} ->
# Ops, error! Don't have the assigns to build the template
# Won't even show the error messages because of missing data
render(conn, "new.html", changeset: changeset)
end
end
In the first scenario, there are traditionally two options I’ve seen to solve the problem of re-populating the form:
- Extracting the logic to another function (which does not solve the form resubmission problem)
- Using the PRG pattern to separate responsibilities and centralizing the form initialization logic
With PRG, we would make the “new” action always responsible for knowing how to build the form. Then, when you submit your form, the resulting action of the post is always a redirect…
If there are any problems, you should redirect to the “new” action passing the current state of the form so it knows how to display the errors properly.
def new(conn, _params) do
changeset = ProductionLine.change_car(%Car{})
# Loads necessary data
car_colors = ProductionLine.list_car_colors()
car_optionals = ProductionLine.list_car_optinals()
render(conn, "new.html",
changeset: changeset,
car_colors: car_colors,
car_optionals: car_optionals)
end
def create(conn, %{"car" => car_params}) do
case ProductionLine.create_car(car_params) do
{:ok, car} ->
conn
|> put_flash(:info, "Car created successfully.")
|> redirect(to: Routes.car_path(conn, :show, car))
{:error, %Ecto.Changeset{} = changeset} ->
# render(conn, "new.html", changeset: changeset)
redirect(conn, to: Routes.car_path(conn, :new), changeset: changeset)
end
end
For this second example, it would still be necessary to pass the state of the form (which contains the errors) to the “new” action to be able to display it (I don’t know how this would be done in Phoenix though)
This is a very common approach that I’ve been using with aspnet, so I was wondering how it was solved over here. However, with aspnet, there’s a lot of “smoke and mirrors” to make this work…
After a post (on error), I would normally serialize the “model-state” that contains the errors, place it in a “temp-data” container that is short-lived, redirect to the “new” action, import the “model-state”, load the form data and then, call the view to display the form with the errors.
To avoid this whole processing there’s also another technique called “unobtrusive validation”, which prevents having to collect data from the database every time there’s an error on the form. It consists of making a post request to the controller and if the state of the form is invalid, it prevents reloading the page and instead displays the errors. Besides the obvious advantage of not having to hit the database every time to repopulate the form, you’ll still have a fallback rendering mechanism if the user has disabled javascript in the browser.