Can I render changeset errors in a modal without navigating?

Background: I have a login form with a register button, the register button opens a modal where someone can register for an account (you don’t say :105:). I use a session controller for logging in, and the view for that controller houses the login form as well as the registration modal.

If I open the registration modal, enter invalid data, then click register; theuser_controller’s :create action is picking up this request then rendering the session_controller's new_view with the invalid changeset, my problem here is that it re-renders the whole page, changes the url, and nukes my modal, is there a way that I can avoid the url change, and the re-rendering of the entire page, like some sort of data refresh? If I click register again, I can see the validation errors but this is obviously not how it should be. Any help is appreciated.

My code is as follows:

“new.html.eex” for my session view looks like this, I realise that this is a mix of ideas on a single page but I’m not sure how I can make this better so suggestions are welcome.

<div class="login-container">
  <div class="login-form">
    <div class="d-logo"></div>
    <%= form_for @conn, page_path(@conn, :index), [as: :session, class: "login-form"], fn f ->%>
      <div class="input-group">
        <span class="input-group-addon">
          <i class="glyphicon glyphicon-user"></i>
        </span>
        <%= text_input f, :username, placeholder: "Username", class: "form-control" %>
      </div>
      <br>
      <div class="input-group">
        <span class="input-group-addon">
          <i class="glyphicon glyphicon-lock"></i>
        </span>
        <%= password_input f, :password, placeholder: "Password", class: "form-control" %>
      </div>
      <br>
      <%= submit "LOGIN", class: "btn btn-primary" %>
      <button type="button" class="btn btn-secondary" data-toggle="modal" data-target="#registrationModal">
        REGISTER
      </button>
    <% end %>
  </div>
</dv>

<div class="modal fade" id="registrationModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLongTitle" aria-hidden="true">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <h2 class="modal-title" id="exampleModalLongTitle">Register</h2>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <%= form_for @changeset, user_path(@conn, :create), fn f -> %>
        <div class="modal-body">
          <div class="form-group">
            <%= text_input f, :name, placeholder: "Name", class: "form-control" %>
            <%= error_tag f, :name %>
          </div>
          <div class="form-group">
            <%= text_input f, :username, placeholder: "Username", class: "form-control" %>
            <%= error_tag f, :username %>
          </div>
          <div class="form-group">
            <%= password_input f, :password, placeholder: "Password", class: "form-control" %>
            <%= error_tag f, :password %>
          </div>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
          <%= submit "Register", class: "btn btn-primary" %>
        </div>
      <% end %>
    </div>
  </div>
</div>

and when I render the above template from the session controller, it’s like this

defmodule CandidateTrackerWeb.SessionController do
  use CandidateTrackerWeb, :controller
  alias CandidateTracker.Accounts
  alias CandidateTracker.Accounts.User

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

and in the user controller that receives the changeset, upon pressing register I’m doing this

defmodule CandidateTrackerWeb.UserController do
  use CandidateTrackerWeb, :controller
  alias CandidateTracker.Accounts

  def create(conn, %{"user" => user_params}) do
    result = Accounts.register(user_params)

    case result do
      {:ok, user} ->
        conn
        |> CandidateTracker.Auth.login(user)
        |> redirect(to: page_path(conn, :index)) # work in progress, this is a placeholder

      {:error, changeset} ->
        render(conn, CandidateTrackerWeb.SessionView, "new.html", changeset: changeset)
    end
  end
end
1 Like

You can make an xhr request, and render the errors through js.

1 Like

I hadn’t really thought about that, thank you. I hope this will be as simple as it sounds; I will report back

1 Like

I’ve managed to validate my form using an XHR request but now I have an issue where the redirect doesn’t happen. Chrome is receiving the redirect but it won’t navigate from the page and logging the resultant xhr response shows the responseText as the page it should be going to. Am I doing something wrong?

EDIT: I’ve just tried to render the page_controller’s index page and nothing happens.

1 Like

XHR behave differently for redirects than submitting a form. The xhr will transparently follow the redirect without any way for the client to be notified of the redirect. See here for how this can handled with turbolinks on the frontend: https://github.com/thechangelog/changelog.com/blob/master/lib/changelog_web/plugs/turbolinks.ex

2 Likes

I meant that you can post to an “api” endpoint which would return json containing changeset errors.

2 Likes

I’m going to try this

1 Like

That’s exactly what I did. I just needed to redirect if there were no errors, but I’m going to try what @LostKobrakai suggested to manage the redirect

1 Like

Hm, I thought you returned html and not json for some reason. Anyway, you can use window.location to redirect with js.

1 Like

I just tried to do it this way and it works :smiley:

EDIT: Thank you or your help

1 Like