Phoenix why not show changeset errors?

changeset

  def changeset(struct, params, :signin) do
    struct
    |> cast(params, [:email, :password])
    |> validate_required([:email, :password])
    |> validate_format(:email, ~r/@/)
    |> validate_length(:password, min: 6, max: 128)
  end

controller

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

    if changeset.valid? do
      redirect conn, to: page_path(conn, :index)
    else
      render(conn, "new.html", changeset: changeset)
    end

html form

<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <div class="form-group">
    <%= label f, :email, class: "control-label" %>
    <%= text_input f, :email, class: "form-control" %>
    <%= error_tag f, :email %>
  </div>

  <div class="form-group">
    <%= label f, :password, class: "control-label" %>
    <%= password_input f, :password, class: "form-control" %>
    <%= error_tag f, :password %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

I don’t understand why not show errors. Plaese help me, thank you!

2 Likes

You usually don’t just check that the changeset is valid, you do something with it.

The expected usage is something like this:

case Repo.insert(changeset) do
  {:ok, _model} ->
    conn
    |> redirect(to: page_path(conn, :index))
  {:error, changeset} ->
    # Here the changeset has had an action attribute set
    conn
    |> render("new.html", changeset: changeset)
end

The form helper checks that an action was attempted on the changeset.
You can force errors to be shown by adding an action before sending it to the page.

changeset = %{changeset | action: :insert}
15 Likes

It’s work! Thank you very much! :smile:

1 Like

Sorry to bump up an old thread but can someone explain why it’s not working though? I need to manually validate and show the errors because my form is a little complex (I use external api to map some fields).

The following didn’t work.

  • render(conn, "new.html", changeset: changeset)
  • render(conn, "new.html", changeset: changeset, errors: changeset.errors)
  • render(conn, "new.html", changeset: %{changeset | errors: changeset.errors})

I got it. I couldn’t find it in the documentation so I started debugging it myself using IO.puts and finding the differences. (Let me know if you have better debugging techniques you would like to share)

Here’s my solution

changeset = %{changeset | action: :insert, errors: changeset.errors}
render(conn, "new.html", changeset: changeset)

1 Like

I’m not really sure why a changeset is being used here at all. It appears that you’re using a changeset to verify whether a user can be logged in? Changesets are for database operations and that’s all.

Forcing on an action key on to the struct is very bad practice. When used properly, changesets will configure action themselves.

If you’re being RESTful, you should define a SessionController and point your login form there.

Router

resources "/sessions", SessionController, only: [:new, :create, :delete],
  singleton: true

Controller

defmodule YourApp.SessionController do
  use YourApp.Web, :controller

  # Render the login form in new.html
  def new(conn, _params) do
    render(conn, "new.html")
  end

  # Handle a login attempt
  # POST /sessions
  def create(conn, %{"user" => user_params}) do
    case perform_your_password_check_here do
      {:ok, user} ->
        conn
        |> Guardian.Plug.sign_in(user) # Or the equivalent of whatever auth lib you're using
        |> put_flash(:info, "Signed in successfully")
        |> redirect(to: "/")
      :error ->
        conn
        |> put_flash(:info, "Email or password incorrect")
        |> redirect(to: session_path(conn, :new))
    end
  end

  # Log the user out
  # DELETE /session
  def delete(conn, _params) do
    conn
    |> Guardian.Plug.sign_out
    |> put_flash(:info, "Signed out successfully")
    |> redirect(to: session_path(conn, :new))
  end
end

Changesets are not only for database operations. You can use Ecto schemas to map data coming from any source, not only the database, and consequently use changesets to cast and validate changes you want to apply to any of those data sources. Ecto 2.0 even generalized things a bit to allow us to map and cast data even without having a schema. There is a bit of info here: Ecto’s insert_all and schemaless queries « Plataformatec Blog

We probably can improve the docs surrounding this area quite a lot. :slight_smile:

3 Likes

You should need only: %{changeset | action: :insert}.

1 Like

Seems my wording was way too strict, which happens from time to time… thanks @josevalim!

1 Like

Hah, I’d actually played with putting my permission system in a Ecto Changeset compatible thing but decided against it as it seemed I was putting different ideas together that should not be. Glad to see it is actually designed that way. ^.^

Be explicit:
changeset = %{changeset | action: :check_errors} :sweat_smile:
It works as well ~

2 Likes

The downside to using an action which is outside of the typespec is that it will cause (obscure) dialyzer errors.

action() :: nil | :insert | :update | :delete | :replace | :ignore
1 Like

I tried to follow this (and the docs) and I get response tellimg me that:
non-struct data in changeset requires the :as option given. I have params like: Parameters: %{"_csrf_token" => "[FILTERED]", "session" => %{"email" => "", "password" => "[FILTERED]"}} actually both fields are empty there.
I create the changeset:

data  = %{}
types = %{email: :string, password: :string}

changeset =
	{data, types} # The data+types tuple is equivalent to [...]
	|> Ecto.Changeset.cast(params["session"], Map.keys(types))
	|> Ecto.Changeset.validate_required([:email, :password])

and pass it back to a form:

render("new.html", changeset: changeset)

Which obvious part I am missing here?

Changing

= form_for @changeset, @action, fn f ->

to

= form_for @changeset, @action, [as: :session], fn f ->

seems to make it work. Looks like this is caused by lack of “name” of the struct or so.

1 Like