diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d48a345..95643aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ * enhancements * Add support for Ruby 3.1/3.2. + * Add support for Hotwire + Turbo, default in Rails 7+. + * `:turbo_stream` is now treated as a navigational format, so it works like HTML navigation when using Turbo. Note: if you relied on `:turbo_stream` to be treated as a non-navigational format before, you can reconfigure your `navigational_formats` in the Devise initializer file to exclude it. + * Devise requires the latest `responders` version, which allows configuring the status used for validation error responses (`error_status`) and for redirects after POST/PUT/PATCH/DELETE requests (`redirect_status`). For backwards compatibility, Devise keeps `error_status` as `:ok` which returns a `200 OK` response, and `redirect_status` to `:found` which returns a `302 Found` response, but you can configure it to return `422 Unprocessable Entity` and `303 See Other` to match the behavior expected by Hotwire/Turbo: + + ```ruby + # config/initializers/devise.rb + Devise.setup do |config| + # ... + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other + # ... + end + ``` + + These configs are already generated by default with new apps, and existing apps may opt-in as described above. Note that these defaults may change in future versions of Devise, to better match the Rails + Hotwire/Turbo defaults across the board. ### 4.8.1 - 2021-12-16 diff --git a/Gemfile b/Gemfile index 0b41fa0e..aac13712 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,7 @@ gem "rdoc" gem "rails-controller-testing", github: "rails/rails-controller-testing" -gem "responders", "~> 3.0" +gem "responders", github: "heartcombo/responders", branch: "main" group :test do gem "nokogiri", "< 1.13" diff --git a/Gemfile.lock b/Gemfile.lock index 704f50e9..a9a79c79 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/heartcombo/responders.git + revision: fb9f787055a7a842584ce351793b249676290090 + branch: main + specs: + responders (3.0.1) + actionpack (>= 5.2) + railties (>= 5.2) + GIT remote: https://github.com/rails/rails-controller-testing.git revision: 351c0162df0771c0c48e6a5a886c4c2f0a5d1a74 @@ -189,9 +198,6 @@ GEM rake (13.0.6) rdoc (6.5.0) psych (>= 4.0.0) - responders (3.0.1) - actionpack (>= 5.0) - railties (>= 5.0) rexml (3.2.5) ruby-openid (2.9.2) ruby2_keywords (0.0.5) @@ -231,7 +237,7 @@ DEPENDENCIES rails (~> 7.0.0) rails-controller-testing! rdoc - responders (~> 3.0) + responders! rexml sqlite3 (~> 1.4) timecop diff --git a/README.md b/README.md index 8fa3f64b..cd2e74a8 100644 --- a/README.md +++ b/README.md @@ -476,6 +476,25 @@ Please note: You will still need to add `devise_for` in your routes in order to devise_for :users, skip: :all ``` +### Hotwire/Turbo + +Devise integrates with Hotwire/Turbo by treating such requests as navigational, and configuring certain responses for errors and redirects to match the expected behavior. New apps are generated with the following response configuration by default, and existing apps may opt-in by adding the config to their Devise initializers: + +```ruby +Devise.setup do |config| + # ... + # When using Devise with Hotwire/Turbo, the http status for error responses + # and some redirects must match the following. The default in Devise for existing + # apps is `200 OK` and `302 Found respectively`, but new apps are generated with + # these new defaults that match Hotwire/Turbo behavior. + # Note: These might become the new default in future versions of Devise. + config.responder.error_status = :unprocessable_entity + config.responder.redirect_status = :see_other +end +``` + +_Note_: the above statuses configuration may become the default for Devise in a future release. + ### I18n Devise uses flash messages with I18n, in conjunction with the flash keys :notice and :alert. To customize your app, you can set up your locale file: diff --git a/app/controllers/devise/confirmations_controller.rb b/app/controllers/devise/confirmations_controller.rb index 3069c6ef..5e22079e 100644 --- a/app/controllers/devise/confirmations_controller.rb +++ b/app/controllers/devise/confirmations_controller.rb @@ -27,6 +27,7 @@ class Devise::ConfirmationsController < DeviseController set_flash_message!(:notice, :confirmed) respond_with_navigational(resource){ redirect_to after_confirmation_path_for(resource_name, resource) } else + # TODO: use `error_status` when the default changes to `:unprocessable_entity`. respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new } end end diff --git a/app/controllers/devise/registrations_controller.rb b/app/controllers/devise/registrations_controller.rb index 1b8a969f..f1292b4d 100644 --- a/app/controllers/devise/registrations_controller.rb +++ b/app/controllers/devise/registrations_controller.rb @@ -67,7 +67,7 @@ class Devise::RegistrationsController < DeviseController Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name) set_flash_message! :notice, :destroyed yield resource if block_given? - respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) } + respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name), status: Devise.responder.redirect_status } end # GET /resource/cancel diff --git a/app/controllers/devise/sessions_controller.rb b/app/controllers/devise/sessions_controller.rb index 9090b002..7c4ee7d4 100644 --- a/app/controllers/devise/sessions_controller.rb +++ b/app/controllers/devise/sessions_controller.rb @@ -77,7 +77,7 @@ class Devise::SessionsController < DeviseController # support returning empty response on GET request respond_to do |format| format.all { head :no_content } - format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name) } + format.any(*navigational_formats) { redirect_to after_sign_out_path_for(resource_name), status: Devise.responder.redirect_status } end end end diff --git a/app/controllers/devise/unlocks_controller.rb b/app/controllers/devise/unlocks_controller.rb index 1d04d622..b1487760 100644 --- a/app/controllers/devise/unlocks_controller.rb +++ b/app/controllers/devise/unlocks_controller.rb @@ -29,6 +29,7 @@ class Devise::UnlocksController < DeviseController set_flash_message! :notice, :unlocked respond_with_navigational(resource){ redirect_to after_unlock_path_for(resource) } else + # TODO: use `error_status` when the default changes to `:unprocessable_entity`. respond_with_navigational(resource.errors, status: :unprocessable_entity){ render :new } end end diff --git a/app/controllers/devise_controller.rb b/app/controllers/devise_controller.rb index 9911fa0b..4bf92a68 100644 --- a/app/controllers/devise_controller.rb +++ b/app/controllers/devise_controller.rb @@ -15,6 +15,7 @@ class DeviseController < Devise.parent_controller.constantize end prepend_before_action :assert_is_devise_resource! + self.responder = Devise.responder respond_to :html if mimes_for_respond_to.empty? # Override prefixes to consider the scoped view. diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 38d95b85..2a666e92 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -38,6 +38,6 @@
Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
+Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
<%= link_to "Back", :back %> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb index ba7ab887..cabfe307 100644 --- a/app/views/devise/shared/_error_messages.html.erb +++ b/app/views/devise/shared/_error_messages.html.erb @@ -1,5 +1,5 @@ <% if resource.errors.any? %> -Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
+Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
<%= link_to "Back", :back %> diff --git a/test/support/integration.rb b/test/support/integration.rb index 76d297a4..3ed0d85a 100644 --- a/test/support/integration.rb +++ b/test/support/integration.rb @@ -61,8 +61,8 @@ class ActionDispatch::IntegrationTest # account Middleware redirects. # def assert_redirected_to(url) - assert_includes [301, 302], @integration_session.status, - "Expected status to be 301 or 302, got #{@integration_session.status}" + assert_includes [301, 302, 303], @integration_session.status, + "Expected status to be 301, 302, or 303, got #{@integration_session.status}" assert_url url, @integration_session.headers["Location"] end