...

понедельник, 24 марта 2014 г.

[Из песочницы] Devise: вход и регистрация в модальных окнах

На проекте необходимо было сделать логин через модальные окна и «обычные» страницы для разных типов устройств. После поиска понял, что зачастую описывается не совсем то, что нужно. Так здесь просто помещают форму в модальное окно (фактически пользуясь страницей из wiki devise), а тут (вход и регистрация) переопределяют методы в контроллерах devise так, что они постоянно отдают только json и для «немодального» поведения нужно будет писать много условий с проверкой формата запроса. Поэтому я решил поэкспериментировать в новом приложении и написать поддержку 2 форматов с минимальным количеством переопределения и грязных хаков.



Создание приложения





  1. Генерим приложение без тестов и запуска bundle install: rails new devise_modal -B -T

  2. Добавляем нужные гемы в Gemfile:


    • gem 'therubyracer', platforms: :ruby

      gem "less-rails"

      gem 'twitter-bootstrap-rails', branch: 'bootstrap3' — для модальных окон используем bootstrap

    • gem 'devise' авторизация будет через devise




    И устанавливаем всё: bundle install

  3. Запускаем нужные генераторы

    rails g bootstrap:install static, «static» так как ничего менять в стилях bootstrap'а не будем

    rails g devise:install; rails g devise User; rake db:migrate — устанавливаем devise и создаём пользователя

  4. Создаём контроллер, который будет отображать главную страницу:

    rails g controller welcome index --no-helper --no-assets

    В config/routes.rb привязываем index к главной странице:

    root 'welcome#index'




В конце этого этапа есть приложение, с формами входа/регистрации на стандартных ссылках для devise: users/sign_in и users/sign_up.

Модальные окна для форм




В формах нету ничего примечательного — используем стандартные devise'овские сделав их remote и поменяв формат на json. Дальше делаем их модальными, обернув в соответствующие классы bootstrap'а. В итоге получились такие partial'ы:

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(resource, as: resource_name, url: session_path(resource_name), 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_mapping.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(resource, as: resource_name, url: registration_path(resource_name), html: {id: 'sign_up_user', :'data-type' => 'json'}, remote: true) do |f| %>
<%= devise_error_messages! %>

<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>




Добавим отображение этих файлов и ссылок для их вызова в 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' %>




А после этого облагородим немного, сделав проверку на наличие юзера:

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 %>




Чтобы всё это работало нужно добавить несколько методов в application_helper, которые определяют resource и связанные с ним вещи для данного контекста:

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




Теперь есть модальные формы, которые доступны с любой страницы и позволяют входить и регистрироваться. Осталось только сделать, чтобы devise на эти запросы в ответ не отправлял html страницы.

JSON ответы от devise




В геме devise за ошибки связанные со входом отвечает FailureApp. При возникновении ошибки в SessionsController'е, который отрабатывает запросы на вход, вызывается respond, где с помощью http_auth? проверяется: нужно слать 401 статус или же переадресовывать на другую страницу. Так как по умолчанию у devise'а:

config/initializers/devise.rb

config.http_authenticatable_on_xhr = true


то и возвращается 401.

RegistrationsController же в ответ на AJAX запрос присылает html страницу, чтобы это исправить переопределим его немного — укажем явно, какие форматы нас интересуют:

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




Теперь при неудачной попытке регистрации будет отдаваться 422 статус с текстами ошибок в responseJSON['errors'], а при удачной — 201. Аналогично для SessionsController'а при удачном входе нужно отдавать статус, а не html-страницу, поэтому «научим» и его правильно реагировать на json запросы:

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




Также можно написать javascript, который будет обрабатывать ответы от модальных форм, например такой:

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)
)




При входе оборачиваем ошибку в alert, а при регистрации — ошибки по каждому параметру, после чего выводим полученное сообщение в footer'е. При успешном запросе просто убираем модальную форму (можно ещё обновлять блок в layout'е, в котором проверяется наличие пользователя, чтобы отображать данные пользователя (они также приходят в ответе)).

Теперь контроллеры отдают ответы в правильном формате, как и для модальных форм — json, так и для стандартных(users/sign_in, users/sign_up) — html. И всё, что понадобилось для этого понадобилось — переопределить контроллеры, расширив набор форматов:

respond_to :html, :json




Примечание



Приложение писалось на rails 4, но отличия для 3.2 будут минимальны: запустится bundle install при создании приложения, нужно будет удалить public/index.html а также путь на главную будет выглядеть чуть иначе:

config/routes.rb

root to: 'welcome#index'


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий