On validation error, redirect back to :new instead of rendering template from :create


First question so I hope I don’t bungle it too much. Happy to be learning Elixir and Phoenix :slight_smile:

So, for context, I come from a Laravel background, and while I think Rails and Laravel share a lot of conventions, one thing they don’t share that Phoenix inherited from Rails(?) is rendering the new form when there is an error during resource creation.

In Laravel, the flow goes like this:

  • GET /posts/new
  • POST /posts
  • ERROR in submitted data
  • Redirect to GET /posts/new but pass along the invalid changeset to prefill the form.

I realize that this doesn’t really matter, but I’d like to learn how to implement this behavior. I toyed around with a barebones Phoenix install using phx.gen.html and got the redirect back to :new but passing the %Ecto.Changeset{} as a param was blowing up when the request was being processed by the framework.

I tried a few different function signatures to match on the changeset as a param, but no dice.

Any help is appreciated and I look forward to chatting with you all in the future!

This is the tricky part: you’ll need to transform the changeset into something that can be passed along in a GET:

  • URL parameters. Beware that browsers and middleboxes all have a maximum URL length, so this will not work if there are too many parameters or the values are too long - or at all, if there are file parameters
  • session data. This can be too shared, though, and cause “leftover data” to show up unexpectedly

Then in your new action, you’d pass those parameters instead of %{} to create a changeset.

1 Like

Thanks @al2o3cr ! I could have sworn I tried that combination but it wasn’t working. Maybe I was trying to cast something to a different type…

Here’s what my final solution looks like

# posts_controller.ex

def new(conn, params) do
    changeset = Blog.change_post(%Post{}, params)
    render(conn, "new.html", changeset: changeset)

def create(conn, %{"post" => post_params}) do
    case Blog.create_post(post_params) do
      {:ok, post} ->
        |> put_flash(:info, "Post created successfully.")
        |> redirect(to: Routes.post_path(conn, :show, post))

      {:error, %Ecto.Changeset{} = changeset} ->
        redirect(conn, to: Routes.post_path(conn, :new, post_params))

This adds them as GET params which is fine for this use case. I did some digging in Laravel and they use a flash to the session to store them, so I might look at implementing that if I get bored.

Cheers :beers:

Actually, crap, now that I’m writing this, I remember why I started trying to change things.

With this approach, there is no visible error message for the user. The request is redirected with no feedback. Is there a way to always validate the passed in params?

Ok. I figured it out. Here is one of my new functions

def new(conn, %{"_action" => "insert" } = params) do
    changeset = Pets.change_dog(%Dog{}, params)
    {:error, changeset} = Changeset.apply_action(changeset, :insert)
    render(conn, "new.html", changeset: changeset)

And the return from the create error

redirect(conn, to: Routes.dog_path(conn, :new, Map.put(dog_params, "_action", :insert)))

Also, I hope this isn’t confusing for someone coming back later and now I’ve switched to dogs :sweat_smile: