Skip to content

Commit d9187f2

Browse files
committed
Greatly improve look and ease of use for devise form views
1 parent e6ac333 commit d9187f2

File tree

9 files changed

+123
-88
lines changed

9 files changed

+123
-88
lines changed

app/assets/stylesheets/better_together/application.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
// Import Bootstrap and Font Awesome
1919

2020
@import 'bootstrap';
21+
@import 'devise';
2122
@import 'font-awesome';
2223
@import 'actiontext';
2324
@import 'contact_details';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
#devise-links {
3+
padding-top: 1.5rem;
4+
margin-top: 1.5rem;
5+
border-top: var(--bs-card-border-width) solid var(--bs-card-border-color);
6+
}
7+
8+
.devise-link {
9+
text-decoration: none;
10+
margin-top: 1rem;
11+
}

app/javascript/controllers/better_together/form_validation_controller.js

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,93 @@
11
import { Controller } from "@hotwired/stimulus";
22

33
export default class extends Controller {
4-
static targets = ["input"]; // Add Trix as a target
4+
static targets = ["input"];
55

66
connect() {
77
this.element.setAttribute("novalidate", true); // Disable default HTML5 validation
88
this.element.addEventListener("input", this.checkValidity.bind(this));
99

10-
// Track whether the form has been changed
11-
this.isDirty = false;
12-
this.isSubmitting = false; // Flag to track form submission
10+
this.isSubmitting = false; // Track form submission
11+
this.originalValues = new Map(); // Store initial field values
12+
this.dirtyFields = new Set(); // Track which fields have actually changed
1313

14-
// Listen for changes in the form to track unsaved changes
15-
this.element.addEventListener("change", this.markAsDirty.bind(this));
14+
// Initialize original values for all fields
15+
this.storeInitialValues();
1616

17-
// Handle form submission to avoid triggering the dirty state warning
17+
// Listen for changes to mark fields dirty
18+
this.element.addEventListener("change", this.markFieldAsDirty.bind(this));
19+
20+
// Handle form submission
1821
this.element.addEventListener("submit", this.handleFormSubmit.bind(this));
1922

20-
// Handle Turbo navigation events
23+
// Handle Turbo navigation (unsaved changes warning)
2124
document.addEventListener("turbo:before-visit", this.handleTurboNavigation.bind(this));
2225
}
2326

2427
disconnect() {
25-
// Clean up the Turbo event listener
2628
document.removeEventListener("turbo:before-visit", this.handleTurboNavigation.bind(this));
2729
}
2830

29-
markAsDirty() {
30-
this.isDirty = true; // Mark the form as "dirty" (changed)
31+
storeInitialValues() {
32+
const fields = this.element.querySelectorAll("input, select, textarea");
33+
fields.forEach(field => {
34+
this.originalValues.set(field, field.value);
35+
});
36+
}
37+
38+
markFieldAsDirty(event) {
39+
const field = event.target;
40+
41+
if (this.originalValues.get(field) !== field.value) {
42+
this.dirtyFields.add(field);
43+
} else {
44+
this.dirtyFields.delete(field);
45+
}
46+
}
47+
48+
isFormDirty() {
49+
return this.dirtyFields.size > 0;
3150
}
3251

3352
handleFormSubmit(event) {
34-
// Check if the form is valid before submission
3553
if (!this.element.checkValidity()) {
36-
event.preventDefault(); // Prevent form submission
37-
this.checkAllFields(); // Manually validate all fields
54+
event.preventDefault();
55+
this.checkAllFields();
3856
return;
3957
}
40-
41-
this.isSubmitting = true; // Mark the form as being submitted to prevent warning
58+
59+
this.isSubmitting = true;
4260
}
4361

4462
handleTurboNavigation(event) {
45-
// Only show the unsaved changes warning if the form is dirty and not currently submitting
46-
if (this.isDirty && !this.isSubmitting) {
63+
if (this.isFormDirty() && !this.isSubmitting) {
4764
const confirmation = confirm("You have unsaved changes. Are you sure you want to leave?");
4865
if (!confirmation) {
49-
event.preventDefault(); // Prevent Turbo from navigating if the user cancels
66+
event.preventDefault();
5067
}
5168
}
5269
}
5370

54-
// Add a method to check all fields
5571
checkAllFields() {
5672
const fields = this.element.querySelectorAll("input, select, textarea");
57-
fields.forEach(field => {
58-
this.checkValidity({ target: field });
59-
});
73+
fields.forEach(field => this.checkValidity({ target: field }));
6074
}
6175

6276
checkValidity(event) {
6377
const field = event.target;
6478

65-
// Skip validation for Trix hidden input fields
66-
if (field.closest("trix-editor")) return this.checkTrixValidity(event);
79+
if (field.closest("trix-editor")) {
80+
return this.checkTrixValidity(event);
81+
}
6782

68-
// If field is valid but empty, remove validation classes
6983
if (field.checkValidity() && field.value.trim() === "") {
7084
field.classList.remove("is-valid", "is-invalid");
7185
this.hideErrorMessage(field);
72-
}
73-
// If field is valid and not empty, apply valid state
74-
else if (field.checkValidity()) {
86+
} else if (field.checkValidity()) {
7587
field.classList.remove("is-invalid");
7688
field.classList.add("is-valid");
7789
this.hideErrorMessage(field);
78-
}
79-
// If field is invalid, apply invalid state
80-
else {
90+
} else {
8191
field.classList.add("is-invalid");
8292
this.showErrorMessage(field);
8393
}
@@ -86,22 +96,16 @@ export default class extends Controller {
8696
checkTrixValidity(event) {
8797
const editor = event.target;
8898
const field = editor.closest("trix-editor");
89-
9099
const editorContent = editor.editor.getDocument().toString().trim();
91100

92-
// If Trix content is empty, remove validation classes
93101
if (editorContent === "") {
94102
field.classList.remove("is-valid", "is-invalid");
95103
this.hideErrorMessage(field);
96-
}
97-
// If Trix content is not empty, apply valid state
98-
else if (editorContent.length > 0) {
104+
} else if (editorContent.length > 0) {
99105
field.classList.remove("is-invalid");
100106
field.classList.add("is-valid");
101107
this.hideErrorMessage(field);
102-
}
103-
// If Trix content is considered invalid (you can define conditions here)
104-
else {
108+
} else {
105109
field.classList.add("is-invalid");
106110
this.showErrorMessage(field);
107111
}
@@ -113,18 +117,22 @@ export default class extends Controller {
113117
field.classList.remove("is-invalid", "is-valid");
114118
this.hideErrorMessage(field);
115119
});
120+
121+
// Reset dirty state
122+
this.dirtyFields.clear();
123+
this.storeInitialValues(); // Re-store current values as "original"
116124
}
117125

118126
showErrorMessage(field) {
119127
const errorMessage = field.nextElementSibling;
120-
if (errorMessage && errorMessage.classList.contains("invalid-feedback")) {
128+
if (errorMessage?.classList.contains("invalid-feedback")) {
121129
errorMessage.style.display = "block";
122130
}
123131
}
124132

125133
hideErrorMessage(field) {
126134
const errorMessage = field.nextElementSibling;
127-
if (errorMessage && errorMessage.classList.contains("invalid-feedback")) {
135+
if (errorMessage?.classList.contains("invalid-feedback")) {
128136
errorMessage.style.display = "none";
129137
}
130138
}

app/views/devise/confirmations/new.html.erb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div class="col-md-6">
44
<h2><%= t('.resend_confirmation_instructions') %></h2>
55

6-
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'needs-validation', novalidate: true }) do |f| %>
6+
<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'card card-body needs-validation', novalidate: true }, data: { controller: 'better_together--form-validation' }) do |f| %>
77
<%= render "devise/shared/error_messages", resource: resource %>
88

99
<!-- Email Field -->
@@ -15,10 +15,11 @@
1515
<!-- Resend Button -->
1616
<div class="text-center mb-4">
1717
<%= f.submit t('.resend_confirmation_instructions'), class: 'btn btn-primary' %>
18+
19+
<!-- Additional Links -->
20+
<%= render "devise/shared/links" %>
1821
</div>
1922
<% end %>
20-
21-
<%= render "devise/shared/links" %>
2223
</div>
2324
</div>
2425
</div>
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
<div class="container my-4">
1+
<div class="container mt-4">
22
<div class="row justify-content-center">
33
<div class="col-md-6">
44
<h2><%= t('.forgot_your_password') %></h2>
55

6-
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'needs-validation', novalidate: true }) do |f| %>
6+
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'card card-body needs-validation', novalidate: true }, data: { controller: 'better_together--form-validation' }) do |f| %>
77
<%= render "devise/shared/error_messages", resource: resource %>
88

99
<!-- Email Field -->
@@ -14,12 +14,13 @@
1414
</div>
1515

1616
<!-- Submit Button -->
17-
<div class="text-center mb-4">
17+
<div class="text-center">
1818
<%= f.submit t('.send_me_reset_password_instructions'), class: 'btn btn-primary' %>
19+
20+
<!-- Additional Links -->
21+
<%= render "devise/shared/links" %>
1922
</div>
2023
<% end %>
21-
22-
<%= render "devise/shared/links" %>
2324
</div>
2425
</div>
2526
</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<%= render template: 'devise/registrations/new', locals: { platform_invitation: @platform_invitation } if @platform_invitation %>

app/views/devise/registrations/new.html.erb

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<div class="col-md-6">
2121
<h2 class="mb-4"><%= t('.sign_up') %></h2>
2222

23-
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'needs-validation', novalidate: true }, data: { controller: 'better_together--form-validation' }) do |f| %>
23+
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: 'card card-body needs-validation', novalidate: true }, data: { controller: 'better_together--form-validation' }) do |f| %>
2424
<%= render "devise/shared/error_messages", resource: resource %>
2525

2626
<% if @platform_invitation %>
@@ -35,19 +35,30 @@
3535
</div>
3636

3737
<!-- Password Field -->
38-
<div class="mb-3">
38+
<div class="mb-3" data-controller="better_together--password-toggle">
3939
<%= f.label :password, t('.password.label'), class: 'form-label' %>
4040
<% if @minimum_password_length %>
4141
<em><%= t('devise.shared.minimum_password_length', count: @minimum_password_length) %></em>
4242
<% end %>
43-
<%= f.password_field :password, autocomplete: "new-password", class: 'form-control', required: true %>
43+
<div class="input-group">
44+
<%= f.password_field :password, autocomplete: "current-password", class: 'form-control', "data-target": "better_together--password-toggle.field", minlength: @minimum_password_length || 12 %>
45+
<button type="button" data-action="click->better_together--password-toggle#password" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="<%= t('devise.sessions.new.password.toggle') %>">
46+
<i class="password-field-icon-1 far fa-eye-slash" data-target="better_together--password-toggle.icon"></i>
47+
</button>
48+
</div>
4449
<small class="form-text text-muted"><%= t('.password.help') %></small>
4550
</div>
4651

4752
<!-- Password Confirmation Field -->
48-
<div class="mb-3">
53+
<div class="mb-3" data-controller="better_together--password-toggle">
4954
<%= f.label :password_confirmation, t('.password_confirmation.label'), class: 'form-label' %>
50-
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: 'form-control', required: true %>
55+
56+
<div class="input-group">
57+
<%= f.password_field :password_confirmation, autocomplete: "current-password", class: 'form-control', "data-target": "better_together--password-toggle.field", minlength: @minimum_password_length || 12 %>
58+
<button type="button" data-action="click->better_together--password-toggle#password" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="<%= t('devise.sessions.new.password.toggle') %>">
59+
<i class="password-field-icon-1 far fa-eye-slash" data-target="better_together--password-toggle.icon"></i>
60+
</button>
61+
</div>
5162
<small class="form-text text-muted"><%= t('.password_confirmation.help') %></small>
5263
</div>
5364

@@ -65,7 +76,7 @@
6576
<!-- Username Field -->
6677
<div class="mb-3">
6778
<%= person_form.label :identifier, t('.person.identifier'), class: 'form-label' %>
68-
<%= person_form.text_field :identifier, class: "form-control", required: true %>
79+
<%= person_form.text_field :identifier, class: "form-control", required: true, minlength: 3 %>
6980
<!-- Hint text for the Handle -->
7081
<small class="form-text text-muted"><%= t('.person.identifier_hint_html', platform: host_platform) %></small>
7182
</div>
@@ -82,10 +93,10 @@
8293
<!-- Submit Button -->
8394
<div class="text-center">
8495
<%= f.submit t('.sign_up'), class: 'btn btn-primary' %>
96+
<!-- Additional Links -->
97+
<%= render "devise/shared/links" %>
8598
</div>
8699
<% end %>
87-
88-
<%= render "devise/shared/links" %>
89100
</div>
90101
</div>
91102
<% end %>

app/views/devise/sessions/new.html.erb

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div class="col-md-6">
44
<h2 class="mb-4"><%= t('.sign_in') %></h2>
55

6-
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'card card-body' }) do |f| %>
6+
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'card card-body' }, data: { controller: 'better_together--form-validation' }) do |f| %>
77

88
<!-- Email Field -->
99
<div class="mb-3">
@@ -16,8 +16,8 @@
1616
<div class="mb-3" data-controller="better_together--password-toggle">
1717
<%= f.label :password, t('.password.label'), class: 'form-label' %>
1818
<div class="input-group">
19-
<%= f.password_field :password, autocomplete: "current-password", class: 'form-control', "data-target": "better_together--password-toggle.field" %>
20-
<button type="button" data-action="click->better_together--password-toggle#password" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="<%= t('.password.toggle') %>">
19+
<%= f.password_field :password, autocomplete: "current-password", class: 'form-control', "data-target": "better_together--password-toggle.field", minlength: @minimum_password_length || 12 %>
20+
<button type="button" data-action="click->better_together--password-toggle#password" class="btn btn-outline-secondary" data-bs-toggle="tooltip" title="<%= t('devise.sessions.new.password.toggle') %>">
2121
<i class="password-field-icon-1 far fa-eye-slash" data-target="better_together--password-toggle.icon"></i>
2222
</button>
2323
</div>
@@ -35,13 +35,11 @@
3535
<!-- Submit Button -->
3636
<div class="text-center">
3737
<%= f.submit t('.sign_in'), class: 'btn btn-primary' %>
38+
39+
<!-- Additional Links -->
40+
<%= render "devise/shared/links" %>
3841
</div>
3942
<% end %>
40-
41-
<!-- Additional Links -->
42-
<div class="mt-4">
43-
<%= render "devise/shared/links" %>
44-
</div>
4543
</div>
4644
</div>
4745
</div>

0 commit comments

Comments
 (0)