Search code examples
elixirphoenix-framework

Elixir Phoenix: Using `for` and `as` on a login form element using new heex syntax?


Versions:

  • Elixir 1.14.4
  • Phoenix 1.7.2

Noob here on both Elixir and Phoenix. I am really struggling to learn that framework.

I am trying to go through a user authentication tutorial with a newer version of phoenix. The tutorial suggests to implement a login user form of (the source can be found in Github):

<h1>Sign in</h1>
<%= form_for @conn, session_path(@conn, :new), [as: :session], fn f -> %>
  <%= text_input f, :username, placeholder: "username" %>
  <%= password_input f, :password, placeholder: "password" %>
  <%= submit "Sign in" %>
<% end %>

As I struggled using the <%= form_for syntax (not working), I am trying to use something like below which I copy-pasted from the auto-generated user_html/user_form.html.heex (my sample uses a little different name attribute):

<.simple_form :let={f} for={???} action={~p"/login"}>
  <.input field={f[:name]} type="text" label="Username" />
  <.input field={f[:password]} type="password" label="Password" />
  <:actions>
    <.button>Sign In</.button>
  </:actions>
</.simple_form>

In the auto-generated components/core_components.ex I can see simple_form implemented with a .form-tag. However, I have no idea how to use the for (is that @conn?) and as (in the original code [as: :session]) attributes to the form.

My "project" is pretty much mix phx.new project plus the tutorial.

Does anyone have an idea how to implement the code to the new form?


Solution

  • I'm still confused a lot. But here is that login form working. As mentioned in the question there are some changes to the original tutorial.

    The original login form

    <%= form_for @conn, session_path(@conn, :new), [as: :session], fn f -> %>
      <%= text_input f, :username, placeholder: "username" %>
      <%= password_input f, :password, placeholder: "password" %>
      <%= submit "Sign in" %>
    <% end %>
    

    changed to:

    <.simple_form :let={f} for={@changeset} as={:session} method={"post"}  action={~p"/login"}>
      <.input field={f[:name]} type="text" label="Username" />
      <.input field={f[:password]} type="password" label="Password" />
      <:actions>
        <.button>Sign In</.button>
      </:actions>
    </.simple_form>
    

    I guess, since I pass back a changeset that contains the name and then retry the log in, submitting makes the system look for a PUT path, which I don't provide. Hence, I added the method={"post"}. Is this the right way to do it? I'm not so sure.

    Below is the original SessionController from the tutorial (a little shortened)

    defmodule TeacherWeb.SessionController do
      #... 
    
      def new(conn, _params) do
        render(conn, "new.html")
      end
    
      def create(conn, %{"session" => auth_params}) do
        user = Accounts.get_by_username(auth_params["username"])
        case Comeonin.Bcrypt.check_pass(user, auth_params["password"]) do
        {:ok, user} ->
          conn
          |> put_session(:current_user_id, user.id)
          |> put_flash(:info, "Signed in successfully.")
          |> redirect(to: movie_path(conn, :index))
        {:error, _} ->
          conn
          |> put_flash(:error, "There was a problem with your username/password")
          |> render("new.html")
        end
      end
    
      def delete(conn, _params) do
        # ...
      end
    end
    

    I guess the changeset is now mandatory in the form. So, I added that:

      def new(conn, _params) do
        changeset = Accounts.change_user(%User{})
        render(conn, :new, changeset: changeset)
      end
    

    and

    def create(conn, %{"session" => auth_params}) do
      user = Accounts.get_by_name(auth_params["name"])
      { res_creds, user } = if user do
        { Comeonin.Bcrypt.check_pass(user, auth_params["password"]), user }
      else
        { {:error, "User does not exist"}, %User{} }
      end
      case res_creds do
        {:ok, user} ->
          conn
          |> put_session(:current_user_id, user.id)
          |> put_flash(:info, "Signed in successfully.")
          |> redirect(to: ~p"/")
        {:error, _} ->
          cs = User.invalidate_password(user) # This is just to create dummy changeset, so that the form works
          conn
          |> put_flash(:error, "There was a problem with your username/password")
          |> render("new.html", changeset: cs)
      end
    end
    

    It feels like the new version works quite a bit differently which is somewhat troublesome (unless I handled it not in the right way). But for now I'm happy that that works.