How to prevent a modal window from closing in Orchid Laravel when validation errors occur and display the errors directly in the modal window?
Problem: Modal Window Closing During Validation Errors in Orchid Laravel
The issue of modal windows closing in Orchid Laravel when validation errors occur stems from the fact that standard Laravel validation causes page reload. To prevent this and display errors directly within the modal window, you need to use AJAX requests to submit the form and JavaScript to manage the modal state.
Table of Contents
- Main Problem
- Causes
- Solution Using AJAX
- Solution with JavaScript Control
- Validation Setup in Orchid
- Complete Implementation Example
- Best Practices
Main Problem
Modal windows in Orchid Laravel close when validation errors occur because standard form submission through HTTP request causes page reload. This creates a poor user experience as users have to reopen the modal window and re-enter data.
The issue is related to the fact that Orchid uses its own JavaScript controller to manage modal windows, which, upon detecting validation errors (elements with the .invalid-feedback class), can block further work with modal windows.
Causes
The main reasons why a modal window closes during validation errors:
-
Standard form submission: When submitting a form via
POSTorGETmethod, the page reloads -
Orchid JavaScript controller: As mentioned in GitHub issue #2801, the
modal_controller.jscontroller checks for the presence of elements with the.invalid-feedbackclass and can block modal window operations -
Laravel error handling: By default, Laravel redirects the user back to the page with errors in the session, which causes the modal window to close
-
Lack of AJAX handling: Without asynchronous form submission, the page reloads, destroying the modal window state
Solution Using AJAX
The most effective solution is to use AJAX to submit the form without page reload.
Step 1: Creating a route for AJAX handling
// routes/web.php
Route::post('/modal-submit', [YourController::class, 'modalSubmit'])->name('modal.submit');
Step 2: Controller method for AJAX handling
// app/Http/Controllers/YourController.php
public function modalSubmit(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
// other validation rules
]);
// If validation passed
// Your data saving logic
return response()->json([
'success' => true,
'message' => 'Data saved successfully'
]);
}
Step 3: JavaScript form handling
// In your Blade template
<script>
document.addEventListener('DOMContentLoaded', function() {
const modalForm = document.querySelector('#your-modal-form');
if (modalForm) {
modalForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const submitButton = this.querySelector('button[type="submit"]');
// Disable button during submission
submitButton.disabled = true;
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Saving...';
fetch('{{ route("modal.submit") }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Close modal on successful save
const modal = bootstrap.Modal.getInstance(document.querySelector('#yourModal'));
modal.hide();
// Show success notification
alert(data.message);
} else {
// Handle errors
if (data.errors) {
displayValidationErrors(data.errors);
}
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while submitting the form');
})
.finally(() => {
// Restore button
submitButton.disabled = false;
submitButton.innerHTML = 'Save';
});
});
}
});
// Function to display validation errors
function displayValidationErrors(errors) {
// Clear previous errors
document.querySelectorAll('.invalid-feedback').forEach(el => el.remove());
document.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
// Display new errors
Object.keys(errors).forEach(field => {
const input = document.querySelector(`[name="${field}"]`);
if (input) {
input.classList.add('is-invalid');
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.textContent = errors[field][0];
input.parentNode.appendChild(feedback);
}
});
}
</script>
Solution with JavaScript Control
If you prefer to use standard Laravel validation, you can add JavaScript to automatically open the modal window when errors are present.
Step 1: Modifying Orchid Controller
Create a custom JavaScript controller to manage modal windows:
// resources/js/controllers/modal_controller.js
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
connect() {
this.element.addEventListener('shown.bs.modal', this.onModalShown.bind(this));
}
onModalShown() {
// Check for validation errors
const hasErrors = this.element.querySelectorAll('.invalid-feedback').length > 0;
if (hasErrors) {
// Prevent modal closing when clicking on backdrop
this.element.addEventListener('click', this.handleBackdropClick.bind(this));
// Prevent closing on Escape
this.element.addEventListener('keydown', this.handleEscapeKey.bind(this));
}
}
handleBackdropClick(e) {
if (e.target === this.element) {
e.preventDefault();
e.stopPropagation();
}
}
handleEscapeKey(e) {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
}
}
}
Step 2: Automatically open modal window on errors
// In your Blade template
<script>
@if (count($errors) > 0)
document.addEventListener('DOMContentLoaded', function() {
const modal = new bootstrap.Modal(document.querySelector('#yourModal'));
modal.show();
// Add handler to prevent closing
const modalElement = document.querySelector('#yourModal');
modalElement.addEventListener('hide.bs.modal', function(e) {
const hasErrors = modalElement.querySelectorAll('.invalid-feedback').length > 0;
if (hasErrors) {
e.preventDefault();
e.stopPropagation();
}
});
});
@endif
</script>
Validation Setup in Orchid
For proper validation functionality in Orchid modal windows, you need to correctly configure validation and error display.
Step 1: Creating a Screen with validation
// app/Orchid/Screens/YourScreen.php
namespace Orchid\Screen\Layouts;
use Orchid\Screen\Layout;
use Orchid\Screen\Fields\Input;
class YourModalLayout extends Layout
{
protected $template = 'platform::layouts.modal';
public function fields(): array
{
return [
Input::make('name')
->title('Name')
->placeholder('Enter name')
->required(),
Input::make('email')
->title('Email')
->type('email')
->placeholder('user@example.com')
->required(),
];
}
}
Step 2: Handling validation in method
// app/Orchid/Screens/YourScreen.php
public function saveData(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
]);
// Saving logic
// ...
return redirect()->route('dashboard')
->with('success', 'Data saved successfully');
}
Step 3: Modal window Blade template
<!-- resources/views/platform/layouts/modal.blade.php -->
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Modal Window Title</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
{{ csrf_field() }}
@if ($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
{{ $template }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>
</div>
Complete Implementation Example
Here’s a complete example implementation of a modal window with AJAX handling and validation in Orchid:
Step 1: Creating a Screen
// app/Orchid/Screens/UserScreen.php
namespace Orchid\Screen;
use App\Models\User;
use Illuminate\Http\Request;
use Orchid\Screen\Actions\ModalCommand;
use Orchid\Screen\Fields\Input;
use Orchid\Screen\Layouts\Modal;
use Orchid\Support\Facades\Layout;
class UserScreen extends Screen
{
public $name = 'Users';
public $description = 'User management';
public function commandBar(): array
{
return [
ModalCommand::make('Add User')
->modal('userModal')
->method('saveUser')
->icon('bs plus-circle'),
];
}
public function layout(): array
{
return [
Layout::modal('userModal', [
Layout::rows([
Input::make('user.name')
->title('Name')
->placeholder('Enter name')
->required(),
Input::make('user.email')
->title('Email')
->type('email')
->placeholder('user@example.com')
->required(),
]),
])->title('Add User'),
];
}
public function saveUser(Request $request)
{
$validated = $request->validate([
'user.name' => 'required|string|max:255',
'user.email' => 'required|email|unique:users,email',
]);
User::create($validated['user']);
return redirect()->route('platform.main')
->with('success', 'User created successfully');
}
}
Step 2: JavaScript for AJAX
<!-- In your main template -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle all forms in Orchid modal windows
document.querySelectorAll('#userModal form, #userModal .modal-content form').forEach(form => {
form.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const submitButton = this.querySelector('button[type="submit"]');
const modalElement = this.closest('.modal');
// Disable button
submitButton.disabled = true;
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Saving...';
// Determine submission URL
let url;
if (modalElement.id === 'userModal') {
url = '{{ route("platform.systems.users.store") }}';
} else {
url = this.action;
}
fetch(url, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Close modal
const modal = bootstrap.Modal.getInstance(modalElement);
modal.hide();
// Show notification
showNotification(data.message, 'success');
// Refresh table or list
location.reload();
} else {
// Show errors
if (data.errors) {
displayValidationErrors(data.errors, this);
} else {
showNotification(data.message || 'An error occurred', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
showNotification('An error occurred while submitting the form', 'error');
})
.finally(() => {
submitButton.disabled = false;
submitButton.innerHTML = 'Save';
});
});
});
});
function displayValidationErrors(errors, form) {
// Clear previous errors
form.querySelectorAll('.invalid-feedback').forEach(el => el.remove());
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
// Display new errors
Object.keys(errors).forEach(field => {
// Convert field to Orchid format
const orchidField = field.replace('user.', '');
const input = form.querySelector(`[name*="${orchidField}"], [name="${field}"]`);
if (input) {
input.classList.add('is-invalid');
const feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
feedback.textContent = errors[field][0];
input.parentNode.appendChild(feedback);
}
});
}
function showNotification(message, type) {
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const notification = document.createElement('div');
notification.className = `alert ${alertClass} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
document.body.appendChild(notification);
// Auto-close after 5 seconds
setTimeout(() => {
notification.remove();
}, 5000);
}
</script>
Best Practices
When working with modal windows and validation in Orchid Laravel, follow these recommendations:
-
Use AJAX for form submission: This prevents page reload and preserves the modal window state
-
Handle errors on the client side: Display validation errors directly in the modal window for better user experience
-
Implement loading indicators: Show users that the form is being processed
-
Add client-side validation: Prevent form submission with invalid data before sending to the server
-
Use notifications: Inform users about successful completion or errors
-
Handle all possible scenarios: Consider network errors, validation errors, and other exceptions
-
Monitor performance: Optimize JavaScript code for fast modal window operation
-
Test in different browsers: Ensure the solution works correctly in all browsers
-
Document your code: Describe modal window usage features for other developers
-
Use the latest library versions: Keep up with Orchid and Bootstrap updates to use the latest fixes and features
Conclusion
Preventing modal window closure during validation errors in Orchid Laravel requires a comprehensive approach, including:
- Using AJAX for asynchronous form submission
- Proper handling and display of validation errors
- Configuring JavaScript controllers to manage modal window states
- Following best practices for good user experience
The main solutions include:
- Full AJAX implementation with client-side error handling
- Modifying Orchid JavaScript controllers to prevent window closure on errors
- Combining server-side and client-side validation for maximum reliability
These approaches will allow you to create a user-friendly interface where users can fix errors directly in the modal window without needing to reopen it.