Submitting to a controller that didn't render the form

I have a form which is rendered by AccountController#edit for a users convenience. However when submitting this form I want to call a different controller endpoint named AdvancedSettingsController#update as it doesn’t make sense to do the update within the account controller.

If this form should fail validation I’d like to re-render the original page AccountController#edit with the changeset errors but I don’t want to duplicate all the logic (ecto queries etc.) used to render than page in my AdvancedSettingsController#update function.

I tried using redirect for this but I ended up losing my changeset errors which I would like to display to the user. Setting a flash is ok but this is a complex form.

Another idea I had was to Conn.assign my error changeset in my update function and then check if it exists in the edit function after redirecting. Assign lives until the response is sent but I’m not sure if a redirect resets the conn. I’m assuming not as flash lives beyond a redirect but maybe that is in the session?

Is this the right approach for dealing with these “unrestful” forms?

Maybe you should consider client side validations in this case? You can redirect on success but prevent submission on validation failure.

You should be able to pattern match on the method int the conn struct. Something like this:

def edit(%Plug.Conn{method: "GET"} = conn, params) do
  ... edit form
end

def edit(%Plug.Conn{method: "POST"} = conn, params) do
  ... validate form
end

Don’t forget to update the router.

post("edit", AdvancedSettingsController, :edit)

https://hexdocs.pm/plug/Plug.Conn.html#content

Edit

I might have jumped the gun in my previous response. It’s probably not the best approach. I would just update the :update route path. Your route helpers should work the same this way too.

resources("settings", AdvancedSettingsController, except: :update)
# possible custom routes
put("/settings/:id/edit", AdvancedSettingsController, :update)
patch("/settings/:id/edit", AdvancedSettingsController, :update)

They are 2 separate controllers. Apologies if my question was unclear.

So I’m trying to logically separate controllers based on their business functions. The problem is that I’m validating a changeset sent from one controller in another. If it’s invalid that causes a bunch of duplicated logic as you have to fetch all the variables required by the other controllers’ template in both functions.

I don’t currently have JS on this page and I’m not really looking to introduce it but I’ll keep that in mind.

I’m thinking I could create an edit/3 function passing the changeset as the third argument and simply calling the other controller function. Defaulting to nil.

1 Like

Conn.assign my error changeset in my update function and then check if it exists in the edit function after redirecting

This won’t work because your conn will be new when you redirect. Every request in cowboy is it’s own isolated process.

If I follow I think you could do a couple of things.
Submit the form to the AdvancedSettingsController#update function and render the account view.

get("account/:id/edit", AccountController, :edit)
put("account/:id", AdvancedSettingsController, :update)

# phoenix 1.3
render(conn, AccontView, "edit.html", changeset: changeset, account: account)
#phoenix 1.4
conn |> put_view(AccountView) |> render("edit.html", changeset: changeset, account: account)

But, it’s probably best to abstract the functionality into a context and call it from wherever it’s needed.

Right. The 302 redirect actually gets sent to the browser so I’ve already lost the conn.

Good call. This is probably what I will end up doing. Thanks.

1 Like