NeuroAgent

Orchid Laravel: Modal Window Doesn't Close on Errors

Learn how to prevent modal window closure in Orchid Laravel during validation errors. Use AJAX and JavaScript to display errors directly in the modal window without page reload.

Question

How to prevent a modal window from closing in Orchid Laravel when validation errors occur and display the errors directly in the modal window?

NeuroAgent

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

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:

  1. Standard form submission: When submitting a form via POST or GET method, the page reloads

  2. Orchid JavaScript controller: As mentioned in GitHub issue #2801, the modal_controller.js controller checks for the presence of elements with the .invalid-feedback class and can block modal window operations

  3. 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

  4. 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

php
// routes/web.php
Route::post('/modal-submit', [YourController::class, 'modalSubmit'])->name('modal.submit');

Step 2: Controller method for AJAX handling

php
// 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

javascript
// 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:

javascript
// 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

php
// 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

php
// 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

php
// 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

blade
<!-- 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

php
// 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

blade
<!-- 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:

  1. Use AJAX for form submission: This prevents page reload and preserves the modal window state

  2. Handle errors on the client side: Display validation errors directly in the modal window for better user experience

  3. Implement loading indicators: Show users that the form is being processed

  4. Add client-side validation: Prevent form submission with invalid data before sending to the server

  5. Use notifications: Inform users about successful completion or errors

  6. Handle all possible scenarios: Consider network errors, validation errors, and other exceptions

  7. Monitor performance: Optimize JavaScript code for fast modal window operation

  8. Test in different browsers: Ensure the solution works correctly in all browsers

  9. Document your code: Describe modal window usage features for other developers

  10. 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:

  1. Full AJAX implementation with client-side error handling
  2. Modifying Orchid JavaScript controllers to prevent window closure on errors
  3. 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.