I have devise installed as authentication system in my Rails 7 app. I want to be able to create dynamic forms for organization and individual user types using hotwire. That is, when I click on individual option, a number of form fields should be displayed while others are hidden. I should be able to do this both on new and edit registration pages.
Since I am attempting to use hotwire to make this work, I have the a navigate stimulus controller
navigate_controller.js
import { Controller } from "@hotwired/stimulus";
/*
* Usage
* =====
*
* add data-controller="navigate" to the turbo frame you want to navigate
*
* Action (add to radio input):
* data-action="change->navigate#to"
* data-url="/new?input=yes"
*
*/
export default class extends Controller {
to(e) {
e.preventDefault();
const { url } = e.target.dataset;
this.element.src = url;
}
}
In my signup page, I have a conditional statement that wraps the organization fields and also turbo frame tag which points to the navigate controller.
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<%= turbo_frame_tag "account_types", data: { controller: "navigate" } do %>
<div class="form-check form-check-inline">
<%= f.label :account_type, value: 'individual', class: 'form-check form-check-inline' do %>
<%= f.radio_button :account_type, 'individual', class: 'form-check-input', data: { action: "change->navigate#to", url: new_account_registration_path({ account: { account_type: "individual" } }) } %>
<label class="form-check-label">Individual</label>
<% end %>
<%= f.label :account_type, value: 'organization', class: 'form-check form-check-inline' do %>
<%= f.radio_button :account_type, 'organization', class: 'form-check-input', data: { action: "change->navigate#to", url: new_account_registration_path({ account: { account_type: "company" } }) } %>
<label class="form-check-label">Organization</label>
<% end %>
</div>
<% if @account.organization? %>
<div class="field">
<%= f.label :organization_name %><br />
<%= f.text_field :organization_name, autocomplete: "Organization name" %>
</div>
<div class="form-check form-check-inline">
Organization Type
<%= f.label :organization_type, value: 'base', class: 'form-check form-check-inline' do %>
<%= f.radio_button :organization_type, 'base', class: 'form-check-input' %>
<label class="form-check-label">Base</label>
<% end %>
<%= f.label :organization_type, value: 'ministry', class: 'form-check form-check-inline' do %>
<%= f.radio_button :organization_type, 'ministry', class: 'form-check-input' %>
<label class="form-check-label">Ministry</label>
<% end %>
</div>
<% else %>
<div class="field">
<%= f.label :firstname %><br />
<%= f.text_field :firstname, autocomplete: "firstname" %>
</div>
<div class="field">
<%= f.label :surname %><br />
<%= f.text_field :surname, autocomplete: "surname" %>
</div>
<% end %>
<% end %>
<div class="field">
<%= f.label :email %><br />
<%= f.email_field :email, autocomplete: "email" %>
</div>
<div class="field">
<%= f.label :username %><br />
<%= f.text_field :username, autocomplete: "username" %>
</div>
<div class="field">
<%= f.label :state %><br />
<%= f.text_field :state, autocomplete: "state" %>
</div>
<div class="field">
<%= f.label :country %><br />
<%= f.text_field :country, autocomplete: "country" %>
</div>
<div class="field">
<%= f.label :bio %><br />
<%= f.text_area :bio, autocomplete: "bio" %>
</div>
<div class="field">
<%= f.label :password %>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em>
<% end %><br />
<%= f.password_field :password, autocomplete: "new-password" %>
</div>
<div class="field">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>
<br>
<div class="actions">
<%= f.submit "Sign up" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
I have also generated devise registrations controller, added params to the new action
# frozen_string_literal: true
class Accounts::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
def new
# super
@account = Account.new configure_sign_up_params
end
# POST /resource
# def create
# super
# end
# GET /resource/edit
# def edit
# super
# end
# PUT /resource
# def update
# super
# end
# DELETE /resource
# def destroy
# super
# end
# GET /resource/cancel
# Forces the session data which is usually expired after sign
# in to be expired now. This is useful if the user wants to
# cancel oauth signing in/up in the middle of the process,
# removing all OAuth session data.
# def cancel
# super
# end
protected
# If you have extra params to permit, append them to the sanitizer.
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:username, :firstname, :surname, :state, :country, :bio, :account_type, :organization_name, :organization_type])
rescue
{}
end
# If you have extra params to permit, append them to the sanitizer.
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: [:username, :firstname, :surname, :state, :country, :bio, :account_type, :organization_name, :organization_type])
end
# The path used after sign up.
# def after_sign_up_path_for(resource)
# super(resource)
# end
# The path used after sign up for inactive accounts.
# def after_inactive_sign_up_path_for(resource)
# super(resource)
# end
end
I guess I am not customizing the devise registration controller properly for it not to work
configure_sign_up_params
only adds extra parameters to permit, which devise then uses inside a corresponding action:
pp devise_parameter_sanitizer
#=>
<Devise::ParameterSanitizer:0x00007fd64171a420
@auth_keys=[:email],
@params=#<ActionController::Parameters {"controller"=>"accounts/registrations", "action"=>"new"} permitted: false>,
@permitted=
{:sign_in=>[:email, :password, :remember_me],
:sign_up=>[:email, :password, :password_confirmation, :account_type],
# ^ used in Devise::RegistrationsController#create action
:account_update=>[:email, :password, :password_confirmation, :current_password]},
@resource_name=:account>
sign_up_params
method is what you want to use to get permitted params when registering:
class Accounts::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create, :new]
def new
build_resource sign_up_params
end
protected
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:account_type])
end
end
You really have to look at devise source to understand what is going on. Basic rails also works for this situation:
class Accounts::RegistrationsController < Devise::RegistrationsController
def new
@account = Account.new(params.fetch(:account, {}).permit(:account_type))
end
end
Also you're unnecessarily nesting <label>
tag:
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= turbo_frame_tag "account_types", data: {controller: "navigate"} do %>
<%= f.label :account_type_individual do %>
<%= f.radio_button :account_type, "individual",
data: {
action: "change->navigate#to",
url: new_account_registration_path(account: {account_type: "individual"})
}
%>
Individual
<% end %>
<%= f.label :account_type_organization do %>
<%= f.radio_button :account_type, "organization",
data: {
action: "change->navigate#to",
url: new_account_registration_path(account: {account_type: "organization"})
}
%>
Organization
<% end %>
<% if resource.account_type == "organization" %>
# TODO: company fields
<% end %>
<% end %>
<%= f.submit "Sign up" %>
<% end %>