assets_js_controllers_gathering-type-form-controller.js
import { Controller } from "@hotwired/stimulus";
/**
* Gathering Type Form Controller
*
* Handles real-time validation and user feedback for gathering type forms.
* Provides immediate feedback on name availability and description length.
*/
class GatheringTypeFormController extends Controller {
static targets = ["name", "description", "nameError", "descriptionCount", "descriptionError", "submitButton"]
static values = {
maxDescriptionLength: { type: Number, default: 500 },
checkNameUrl: String
}
/**
* Initialize the controller
*/
connect() {
if (this.hasDescriptionTarget) {
this.updateDescriptionCount();
// Set maxLength attribute to prevent typing beyond limit
this.descriptionTarget.setAttribute('maxlength', this.maxDescriptionLengthValue);
}
}
/**
* Validate name field on blur
*/
validateName() {
if (!this.hasNameTarget) return;
const name = this.nameTarget.value.trim();
if (name.length === 0) {
this.showNameError("Name is required");
return false;
}
if (name.length < 3) {
this.showNameError("Name must be at least 3 characters");
return false;
}
if (name.length > 128) {
this.showNameError("Name must be less than 128 characters");
return false;
}
this.clearNameError();
return true;
}
/**
* Update description character count
*/
updateDescriptionCount() {
if (!this.hasDescriptionTarget || !this.hasDescriptionCountTarget) return true;
let length = this.descriptionTarget.value.length;
// Prevent typing beyond max length
if (length > this.maxDescriptionLengthValue) {
this.descriptionTarget.value = this.descriptionTarget.value.substring(0, this.maxDescriptionLengthValue);
length = this.maxDescriptionLengthValue;
}
const remaining = this.maxDescriptionLengthValue - length;
this.descriptionCountTarget.textContent =
`${length} / ${this.maxDescriptionLengthValue} characters`;
if (remaining < 50) {
this.descriptionCountTarget.classList.add('text-warning');
this.descriptionCountTarget.classList.remove('text-muted');
} else {
this.descriptionCountTarget.classList.remove('text-warning');
this.descriptionCountTarget.classList.add('text-muted');
}
if (length > this.maxDescriptionLengthValue) {
this.descriptionCountTarget.classList.add('text-danger');
this.descriptionCountTarget.classList.remove('text-warning');
this.showDescriptionError(`Description cannot exceed ${this.maxDescriptionLengthValue} characters`);
this.disableSubmit();
return false;
} else {
this.descriptionCountTarget.classList.remove('text-danger');
this.clearDescriptionError();
this.enableSubmit();
return true;
}
}
/**
* Show description error message
*/
showDescriptionError(message) {
if (this.hasDescriptionTarget) {
this.descriptionTarget.classList.add('is-invalid');
this.descriptionTarget.setAttribute('aria-invalid', 'true');
}
// Create or update error element if it doesn't exist
let errorElement;
if (this.hasDescriptionErrorTarget) {
errorElement = this.descriptionErrorTarget;
} else {
// Create error element dynamically
errorElement = document.createElement('div');
errorElement.className = 'invalid-feedback';
errorElement.id = 'description-error';
errorElement.setAttribute('data-gathering-type-form-target', 'descriptionError');
this.descriptionTarget.parentElement.appendChild(errorElement);
}
errorElement.textContent = message;
errorElement.classList.remove('d-none');
errorElement.style.display = 'block';
if (this.hasDescriptionTarget) {
this.descriptionTarget.setAttribute('aria-describedby', 'description-error');
}
}
/**
* Clear description error message
*/
clearDescriptionError() {
if (this.hasDescriptionTarget) {
this.descriptionTarget.classList.remove('is-invalid');
this.descriptionTarget.removeAttribute('aria-invalid');
this.descriptionTarget.removeAttribute('aria-describedby');
}
if (this.hasDescriptionErrorTarget) {
this.descriptionErrorTarget.classList.add('d-none');
this.descriptionErrorTarget.style.display = 'none';
}
}
/**
* Disable submit button
*/
disableSubmit() {
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.disabled = true;
}
}
/**
* Enable submit button
*/
enableSubmit() {
if (this.hasSubmitButtonTarget) {
this.submitButtonTarget.disabled = false;
}
}
/**
* Show name error message
*/
showNameError(message) {
if (this.hasNameErrorTarget) {
this.nameErrorTarget.textContent = message;
this.nameErrorTarget.classList.remove('d-none');
}
if (this.hasNameTarget) {
this.nameTarget.classList.add('is-invalid');
}
}
/**
* Clear name error message
*/
clearNameError() {
if (this.hasNameErrorTarget) {
this.nameErrorTarget.classList.add('d-none');
}
if (this.hasNameTarget) {
this.nameTarget.classList.remove('is-invalid');
this.nameTarget.classList.add('is-valid');
}
}
/**
* Validate entire form before submission
*/
validateForm(event) {
let isValid = true;
if (this.hasNameTarget) {
isValid = this.validateName() && isValid;
}
if (this.hasDescriptionTarget) {
isValid = this.updateDescriptionCount() && isValid;
}
if (!isValid) {
event.preventDefault();
this.showValidationSummary();
}
return isValid;
}
/**
* Show validation summary
*/
showValidationSummary() {
// Check for existing alert and clean it up
const existingAlert = this.element.querySelector('.alert.alert-danger.validation-summary');
if (existingAlert) {
// Clear any pending timeout
if (existingAlert.dataset.timeoutId) {
clearTimeout(parseInt(existingAlert.dataset.timeoutId));
}
existingAlert.remove();
}
// Flash a message at the top of the form
const alert = document.createElement('div');
alert.className = 'alert alert-danger alert-dismissible fade show validation-summary';
alert.role = 'alert';
alert.innerHTML = `
<strong>Validation Error:</strong> Please correct the errors below.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
this.element.prepend(alert);
// Store timeout ID for cleanup
const timeoutId = setTimeout(() => {
alert.remove();
}, 5000);
alert.dataset.timeoutId = timeoutId.toString();
// Handle manual close button click
const closeButton = alert.querySelector('.btn-close');
if (closeButton) {
closeButton.addEventListener('click', () => {
if (alert.dataset.timeoutId) {
clearTimeout(parseInt(alert.dataset.timeoutId));
}
});
}
// Handle Bootstrap dismissal event
alert.addEventListener('closed.bs.alert', () => {
if (alert.dataset.timeoutId) {
clearTimeout(parseInt(alert.dataset.timeoutId));
}
});
}
}
// Add to global controllers registry
if (!window.Controllers) {
window.Controllers = {};
}
window.Controllers["gathering-type-form"] = GatheringTypeFormController;
// Export as default for ES6 import
export default GatheringTypeFormController;