thiagomajesk

thiagomajesk

Is PRG a valid technique in Phoenix?

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.

First 10 of 32 Posts Switch mode

Nicd

Nicd

To me the PRG way you described seems like a lot of hassle for little benefit. I’ve used the way you first mentioned: put the assigns in a function that you can use both in the empty form case and the “failed submit” case.

thiagomajesk

thiagomajesk OP

For building data, maybe yes. But still does not solve the form resubmission issue.

LostKobrakai

LostKobrakai

Why do you care about resubmissions of invalid forms? This is usually only a problem for successful form submissions, which likely have side-effects.

thiagomajesk

thiagomajesk OP

I guess that if the server is always responding with a 200, it’s still a “valid form submission” for the browser’s point fo view (usually, frameworks don’t return a 500 when there’s a validation problem). Then, if you want to prevent that you should use PRG - which states that a post request is always followed by a redirect.

LostKobrakai

LostKobrakai

To me the redirect is meant to protect against side effects happening more than once. Invalid forms should not cause side effects. If they really do, then a redirect is certainly useful.

peerreynders

peerreynders

Are you certain of the bolded statements?

Gets or sets the client validation mode for the application.

A quick scan of various web articles seems to suggest that unobtrusive validation is entirely client based - historically using https://jqueryvalidation.org/.

HTML5 (no-JS) form validation has improved quite a bit in recent years

And nothing is stopping you from using your own HTML5-based, JS-based, or LiveView-based validation in Phoenix.

thiagomajesk

thiagomajesk OP

There’s also that annoying form resubmission popup browsers show when you refresh or navigate back and forth, this behavior might be undesirable if the person, for instance, wants to start fresh in the form.

One thing that I noticed is that Phoenix changes the url when there’s an error. If I’m at /car/new and the submit yields an error, I’m redirected to /car - iterestanly actually makes a post request to /car. How do you guys manage this?

thiagomajesk

thiagomajesk OP

@peerreynders Yes, it is client-based validation.
In the case of aspnet specifically, it reuses the validation from the server so you don’t have to write client code (there’s a “plugin” that acts on top of that jquery validation you’ve mentioned).
About the second statement: I meant that reloading form data from the server would be a fallback in case the user has disabled javascript - should’ve made myself clearer.

Yes, I wanted to know what is the approach people normally use and if there’s something equivalent.
I’ve seen something about that in the past, but I wasn’t sure if LiveView does that or there’s another built-in form of “unobtrusive validation” (I saw a live WebSocket connection on the console and thought this could be the case).

peerreynders

peerreynders

<form accept-charset="UTF-8" action="/car" method="post">
  ...
</form>

It’s going to /car regardless based on the action set in the form element. It’s new.html.eex that is reused.

From Programming Phoenix 1.4:

User Controller
https://media.pragprog.com/titles/phoenix14/code/ecto/listings/rumbl/lib/rumbl_web/controllers/user_controller.change3.ex

new.html.eex
https://media.pragprog.com/titles/phoenix14/code/ecto/listings/rumbl/lib/rumbl_web/templates/user/new.change1.html.eex

Note how new renders with new.html while create renders with new.html in case there is an error (in case of success it redirects to index which renders with index.html).

But your code does the same thing.

thiagomajesk

thiagomajesk OP

That’s very interesting because in aspnet the action of the form would be just “create”, so if there’s an error it wouldn’t change the url (which I find weird).

I know this is a bit off-topic, but now I’m curious… Is this a Phoenix convention?
Seems to me it’s telling the form to look for the resource instead of the specific action and then uses a convention to select the “create” action, is that right? Do you know why’s done that way instead of just using the action itself? (Besides to avoid hardcoding the convention in the form)

Where Next?

Trending in Discussions Top

AstonJ
As the title says, please share what you’ve been up to with Elixir. Whether that’s been learning it, looking into it, making stuff with i...
2977 91561 914
New
byu
@chrismccord : I just saw the Extract AGENTS.md from Phoenix.new into phx.new generator commit to the phoenix project. My initial shotgu...
New
arcanemachine
I was working on an Ecto migration and I needed a timestamp. So, for the nth time, I looked up the different data types for timestamps, a...
New
AstonJ
Just a general thread to post chat/news/info relating to AI/ML stuff that may be relevant for Nx now or in the future. Got anything to sh...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New
alexslade
Fly’s CEO posted this recently - Turn And Face The Strange · The Fly Blog It says that Fly is going all-in on sprites, which is a worry ...
New

Other Trending Topics Top

kraleppa
If you’ve ever had to debug a BEAM node you know it can be a painful process. Parsing raw data, or dealing with clutter often gets in the...
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement