📜 ⬆️ ⬇️

Devise: login and registration in modal windows

On the project, it was necessary to make a login through modal windows and “ordinary” pages for different types of devices. After the search, I realized that it is often described not exactly what is needed. So here they just put the form in a modal window (actually using a page from wiki devise ), and here ( login and registration ) override methods in devise controllers so that they constantly give only json and for “modeless” behavior you will need to write a lot of conditions with checking request format. Therefore, I decided to experiment in the new application and write support for 2 formats with a minimal amount of override and dirty hacks.

Create application


  1. Generate an application without tests and running bundle install : rails new devise_modal -B -T
  2. Add the necessary gems to the Gemfile :
    • gem 'therubyracer', platforms: :ruby
      gem "less-rails"
      gem 'twitter-bootstrap-rails', branch: 'bootstrap3' - for modal windows use bootstrap
    • gem 'devise' authentication will be through devise

    And install everything: bundle install
  3. We start the necessary generators
    rails g bootstrap:install static , "static" because nothing changes in the styles of bootstrap 'but we will not
    rails g devise:install; rails g devise User; rake db:migrate rails g devise:install; rails g devise User; rake db:migrate - install devise and create user

  4. Create a controller that will display the main page:
    rails g controller welcome index --no-helper --no-assets
    In the config / routes.rb bind index to the main page:
    root 'welcome#index'

At the end of this stage there is an application, with login / registration forms on standard devise links : users/sign_in and users/sign_up .

Modal windows for forms


There is nothing remarkable in the forms - we use standard devise 'ovskies by making them remote and changing the format to json . Next we make them modal, wrapping them in the appropriate bootstrap classes. As a result, we got the following partials:
app / views / shared / _sign_in.html.erb
 <div class="modal hide fade in" id="sign_in"> <div class="modal-header"> <button class="close" data-dismiss="modal">x</button> <h2>Sign in</h2> </div> <div class="modal-body"> <%= form_for(User.new, url: session_path(:user), html:{id: 'sign_in_user', :'data-type' => 'json'}, remote: true) do |f| %> <div> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true %> </div> <div> <%= f.label :password %><br /> <%= f.password_field :password, autocomplete: "off" %> </div> <% if Devise.mappings[:user].rememberable? -%> <div> <%= f.check_box :remember_me %> <%= f.label :remember_me %> </div> <% end -%> <div> <%= f.submit "Sign in" %> </div> <% end %> </div> <div class="modal-footer"> </div> </div> 

app / views / shared / _sign_up.html.erb
 <div class="modal hide fade in" id="sign_up"> <div class="modal-header"> <button class="close" data-dismiss="modal">x</button> <h2>Sign up</h2> </div> <div class="modal-body"> <%= form_for(User.new, url: registration_path(:user), html: {id: 'sign_up_user', :'data-type' => 'json'}, remote: true) do |f| %> <div> <%= f.label :email %><br /> <%= f.email_field :email, autofocus: true %> </div> <div> <%= f.label :password %><br /> <%= f.password_field :password, autocomplete: "off" %> </div> <div> <%= f.label :password_confirmation %><br /> <%= f.password_field :password_confirmation, autocomplete: "off" %> </div> <div><%= f.submit "Sign up" %></div> <% end %> </div> <div class="modal-footer"> </div> </div> 

Add a display of these files and links to call them in the layout :
 <%= link_to "Sign in", "#sign_in", "data-toggle" => "modal", :class => 'btn btn-small' %> <%= link_to "Sign up", "#sign_up", "data-toggle" => "modal", :class => 'btn btn-small' %> <%= render 'shared/sign_in' %> <%= render 'shared/sign_up' %> 

And after that we will improve a little, having made check on presence of the user:
app / views / layouts / application.html.erb
 <% if current_user %> <%= "Hello, #{current_user.email}" %> <%= link_to "Sign out", destroy_user_session_path, :method => :delete %> <% else %> <%= link_to "Sign in", "#sign_in", "data-toggle" => "modal", :class => 'btn btn-small' %> <%= link_to "Sign up", "#sign_up", "data-toggle" => "modal", :class => 'btn btn-small' %> <%= render 'shared/sign_in' %> <%= render 'shared/sign_up' %> <% end %> 

In order for all this to work, you need to add several methods to application_helper , which define the resource and related things for this context:
app / helpers / application_helper.rb
 def resource_name :user end def resource @resource ||= User.new end def devise_mapping @devise_mapping ||= Devise.mappings[:user] end 

As noted in the comments of printercu and DarthSim , it makes little sense to redefine global helpers for a resource , it is better to directly specify in forms instead of resource User.new , and instead of resource_name - :user . Also in app / views / shared / _sign_in.html.erb we Devise.mappings[:user] instead of devise_mapping . In general, you can generally get rid of this condition:
 <% if devise_mapping.rememberable? -%> 
based on whether we specify in the user’s model ( app / models / user.rb ) :rememberable . In addition, the app / views / shared / _sign_up.html.erb still had a helper devise_error_messages! which uses resource , but since the error text is taken from json 'and the answer, we simply remove from the form <%= devise_error_messages! %> <%= devise_error_messages! %> as unnecessary.
Now there are modal forms that are accessible from any page and allow you to enter and register. It remains only to ensure that the devise to these requests in response does not send html pages.
')

JSON responses from devise


In the devise gem, FailureApp is responsible for input errors . When an error occurs in SessionsController , that processes requests for input, it calls to respond , where with the help of http_auth? it is checked: you need to send 401 status or redirect to another page. Since the default devise 'a is:
config / initializers / devise.rb
 config.http_authenticatable_on_xhr = true 
then returns 401.
RegistrationsController, in response to an AJAX request, sends an html page to fix it. We will redefine it a bit - we will explicitly indicate which formats we are interested in:
rails g controller Registrations --no-helper --no-assets --no-views
config / routes.rb
 devise_for :users, controllers: {registrations: 'registrations'} 

app / controllers / registrations_controller.rb
 class RegistrationsController < Devise::RegistrationsController respond_to :html, :json end 

Now, if an unsuccessful registration attempt fails, 422 status will be given with error response text in responseJSON ['errors'] , and 201 if successful. Similarly for SessionsController ' and if successful, it is necessary to give the status, not the html page, therefore we will “teach” and Correctly respond to json requests:
rails g controller Sessions --no-helper --no-assets --no-views
config / routes.rb
 devise_for :users, controllers: {sessions: 'sessions', registrations: 'registrations'} 

app / controllers / sessions_controller.rb
 class SessionsController < Devise::SessionsController respond_to :html, :json end 

You can also write javascript that will process responses from modal forms, for example:
app / assets / javascripts / welcome.js.coffee
 $ -> $("form#sign_in_user, form#sign_up_user").bind("ajax:success", (event, xhr, settings) -> $(this).parents('.modal').modal('hide') ).bind("ajax:error", (event, xhr, settings, exceptions) -> error_messages = if xhr.responseJSON['error'] "<div class='alert alert-danger pull-left'>" + xhr.responseJSON['error'] + "</div>" else if xhr.responseJSON['errors'] $.map(xhr.responseJSON["errors"], (v, k) -> "<div class='alert alert-danger pull-left'>" + k + " " + v + "</div>" ).join "" else "<div class='alert alert-danger pull-left'>Unknown error</div>" $(this).parents('.modal').children('.modal-footer').html(error_messages) ) 

At the entrance, we wrap the error in alert , and at registration - errors for each parameter, after which we display the received message in footer 'e. If the request is successful, we simply remove the modal form (you can also update the block in layout 's, in which the user is checked to display the user data (they also come in the answer)).
Now controllers give answers in the correct format, as well as for modal forms - json , and for standard ( users / sign_in , users / sign_up ) - html . And all that was needed for this was needed - to redefine the controllers, expanding the set of formats:
 respond_to :html, :json 

Note

The application was written on rails 4, but the differences for 3.2 will be minimal: the bundle install will start when you create the application, you will need to delete public/index.html and the path to the main one will look a little different:
config / routes.rb
 root to: 'welcome#index' 

Source: https://habr.com/ru/post/216837/


All Articles