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.

Marked As Solved

thiagomajesk

thiagomajesk

I thought it would be a good idea to extract this little functionality in its own specific package.
So, here it is: briefcase - It’s just a simple plug to scratch this little itch. If anybody is searching for a similar solution, feel free to take a look and to contribute :relaxed:

PS.: Since I’m not yet well versed in everything elixir has to offer, any suggestions are extremely welcome.

Also Liked

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.

Kurisu

Kurisu

I just want to add that when you don’t want the default urls generated by the resources/4 macro, you could always use directly get/4 and post/4 like below:

# Registration
  scope "/registration", DemoWeb do
    pipe_through :browser
    get "/", RegistrationController, :new
    post "/", RegistrationController, :create
  end

This way the url won’t change between empty new form and create_error form. In my case I just do that for SEO purpose (for example I don’t want the url in English). And if someone shared the url, clicking on it will always result on the GET new form.

If you still want to redirect on invalid submit, you could assign the invalid changeset to the conn passed to redirect/2. Then you could have two clauses for your new action, one for empty form and the other for invalid form:

 # invalid form
  def new(%{error_changeset: changeset} = conn, _params) do
    render(conn, "new.html", changeset: changeset)
  end

  # empty form 
  def new(conn, _params) do
    changeset = Context.change_resource()
    render(conn, "new.html", changeset: changeset)
  end
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.

Where Next?

Popular in Discussions Top

scouten
I’m looking for a host for the server part of a small (personal) side project that I’m working on. It’s currently written in Node.js and ...
New
CharlesO
Erlang :list.nth simple, but 1 - based nth(1, [H|_]) -> H; nth(N, [_|T]) when N > 1 -> nth(N - 1, T). Elixir Enum.at … coo...
New
Qqwy
I would like to spark a discussion about the static access operator: .. For whom does not know: it is used in Elixir to access fields of...
New
sergio
There’s a new TIOBE index report that came out that shows Elixir is still not in the top 50 used languages. It also goes on to call Elix...
New
WildYorkies
It seems that the more I read, the more I find Elixir users speaking about all the ways that Elixir is not good for x, y, and z use cases...
New
nburkley
AWS re:Invent is on at the moment with some interesting announcements. One new feature in particular is the Lambda Runtime API for AWS La...
New
PragTob
Hello everyone, I know we had quite some threads (read through lots of them) about background job processing but it remains a hotly deba...
New
crispinb
On reading dhh’s latest The One Person Framework it strikes me that Phoenix with LiveView is already pretty much this. However, never hav...
New
laiboonh
Hi all, I am trying to convince my team to use liveview over the current react. What are some of the points where one should consider us...
New
matthias_toepp
I’d love to hear what people think about Wisp, the new Gleam web framework started by Gleam’s primary creator Louis Pilfold. Gleam, alon...
New

Other popular topics Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31194 112
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement