dwyl / learn-phoenix

:fire: Phoenix is the web framework without compromise on speed, reliability or maintainability! Don't settle for less. :rocket:
649 stars 45 forks source link

Passing action along in form with multiple pages #25

Open katbow opened 7 years ago

katbow commented 7 years ago

I have a form with name and email (form1) on one page, which is inserted into the db, then goes to a second form (/signup2) to fill in password and security Q&A. The first page was loading as expected and the name & email was being properly inserted into the database upon clicking "Next".

However, when it was redirected to /signup2 there would be an error:

assign @action not available in eex template.
Please make sure all proper assigns have been set. If this
is a child template, ensure assigns are given explicitly by
the parent template as they are not automatically forwarded.
Available assigns: [:changeset, :conn, :current_user, :view_module, :view_template]

So the trouble seemed to be that in /signup2 there was no action. The create controller that inserted the input from form1 and redirected to /signup2 was as follows when the error was occurring:

  def create(conn, %{"user" => user_params}) do
    changeset = User.changeset(%User{}, user_params)

    case Repo.insert(changeset) do
      {:ok, _post} ->
        conn
        |> redirect(to: user_path(conn, :signup2, changeset: changeset))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

When the changeset was inspected, the action was set to nil.

We tried setting this by adding assign to the pipe, but assign kept coming back as an undefined function:

{:ok, _post} ->
        conn
        |> assign(action: :some_action)
        |> redirect(to: user_path(conn, :signup2, changeset: changeset))

The solution was to render the signup2.html and pass in the action directly.

{:ok, _user} ->
        render(conn, "signup2.html", changeset: changeset, action: :some_action)
katbow commented 7 years ago

This had a bit more of a rework. I realised that signup2 would be similar to the :edit resource that is a 'default' in phoenix, and create2 that followed would be similar to :update. I modelled the routes & controllers after this.

in router.ex

    get "/users/:id/signup2", UserController, :signup2
    post "/users/:id/create2", UserController, :create2 

and in user_controller.ex

def create(conn, %{"user" => user_params}) do
    changeset = User.changeset(%User{}, user_params)
    case Repo.insert(changeset) do
      {:ok, user} ->
        conn
        |> redirect(to: "/users/#{user.id}/signup2", action: :signup2, user: user)
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def signup2(conn, %{"id" => id}) do
    user = Repo.get!(User, id)
    changeset = User.security_question(%User{})
    render(conn, "signup2.html", changeset: changeset, action: "/users/#{user.id}/#{:create2}", user: user)
  end

  def create2(conn, %{"user" => user_params, "id" => id}) do
    user = Repo.get!(User, id)
    changeset = User.registration_changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, user} ->
        conn
        |> Healthlocker.Auth.login(user)
        |> put_flash(:info, "User created successfully.")
        |> redirect(to: user_path(conn, :index))
      {:error, changeset} ->
        render(conn, "signup2.html", changeset: changeset)
    end
  end

Since signup2 takes an id, I passed it the user (as seems to usually be the case when linking to edit). The part I had the most difficulty with was figuring out what needed to be passed to the action. I modelled this after what the update and edit urls would look like also by inputting the id into the url.

The last problem I need to fix is if the changeset is invalid (for example if password is not the min length), then I get the error I had in the beginning with assign @action not available in eex template. instead of an error_tag rendering for the password field.

I think the problem lies in here:

def put_pass_hash(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{password: pass}} ->
        put_change(changeset, :password_hash, Bcrypt.hashpwsalt(pass))
      _ ->
        changeset
    end
  end

The changeset that is being returned when the changeset is invalid does not contain an action. I'm not certain at this point what the action needs to be though. Thoughts?

katbow commented 7 years ago

Ok got it now. It wasn't in put_pass_hash, but in create2. The render there also needed the action & user passed along which makes sense since it's rendering the same form as in signup2!

Actions can also be set in the html.eex instead of passing in @action. However, since we need the user.id, it make more sense to grab this in the controller and assign it there.