НейроАгент

Orchid Laravel: модальное окно не закрывается при ошибках

Узнайте, как предотвратить закрытие модального окна в Orchid Laravel при ошибках валидации. Используйте AJAX и JavaScript для отображения ошибок прямо в модальном окне без перезагрузки страницы.

Вопрос

Как предотвратить закрытие модального окна в Orchid Laravel при ошибках валидации и вывести ошибки непосредственно в модальном окне?

НейроАгент

Проблема закрытия модального окна Orchid Laravel при ошибках валидации возникает из-за того, что стандартная валидация Laravel вызывает перезагрузку страницы. Чтобы предотвратить это и отображать ошибки непосредственно в модальном окне, необходимо использовать AJAX-запросы для отправки формы и JavaScript для управления состоянием модального окна.

Содержание

Основная проблема

Модальные окна в Orchid Laravel закрываются при возникновении ошибок валидации, потому что стандартная отправка формы через HTTP-запрос приводит к перезагрузке страницы. Это создает плохой пользовательский опыт, так как пользователю приходится повторно открывать модальное окно и вводить данные заново.

Проблема связана с тем, что Orchid использует собственный JavaScript контроллер для управления модальными окнами, который при обнаружении ошибок валидации (элементов с классом .invalid-feedback) может блокировать дальнейшую работу с модальными окнами.

Причины возникновения

Основные причины, по которым модальное окно закрывается при ошибках валидации:

  1. Стандартная форма отправки: При отправке формы через метод POST или GET происходит перезагрузка страницы

  2. JavaScript контроллер Orchid: Как указано в GitHub issue #2801, контроллер modal_controller.js проверяет наличие элементов с классом .invalid-feedback и может блокировать работу модальных окон

  3. Обработка ошибок Laravel: По умолчанию Laravel перенаправляет пользователя обратно на страницу с ошибками в сессии, что приводит к закрытию модального окна

  4. Отсутствие AJAX-обработки: Без асинхронной отправки формы страница перезагружается, уничтожая состояние модального окна

Решение с использованием AJAX

Наиболее эффективным решением является использование AJAX для отправки формы без перезагрузки страницы.

Шаг 1: Создание роута для AJAX-обработки

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

Шаг 2: Метод контроллера для обработки AJAX

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',
        // другие правила валидации
    ]);
    
    // Если валидация прошла успешно
    // Ваша логика сохранения данных
    
    return response()->json([
        'success' => true,
        'message' => 'Данные успешно сохранены'
    ]);
}

Шаг 3: JavaScript обработка формы

javascript
// В вашем Blade шаблоне
<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"]');
            
            // Блокируем кнопку на время отправки
            submitButton.disabled = true;
            submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Сохранение...';
            
            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) {
                    // Закрываем модальное окно при успешном сохранении
                    const modal = bootstrap.Modal.getInstance(document.querySelector('#yourModal'));
                    modal.hide();
                    
                    // Показываем уведомление об успехе
                    alert(data.message);
                } else {
                    // Обработка ошибок
                    if (data.errors) {
                        displayValidationErrors(data.errors);
                    }
                }
            })
            .catch(error => {
                console.error('Error:', error);
                alert('Произошла ошибка при отправке формы');
            })
            .finally(() => {
                // Восстанавливаем кнопку
                submitButton.disabled = false;
                submitButton.innerHTML = 'Сохранить';
            });
        });
    }
});

// Функция для отображения ошибок валидации
function displayValidationErrors(errors) {
    // Очищаем предыдущие ошибки
    document.querySelectorAll('.invalid-feedback').forEach(el => el.remove());
    document.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
    
    // Отображаем новые ошибки
    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>

Решение с JavaScript контролем

Если вы предпочитаете использовать стандартную валидацию Laravel, можно добавить JavaScript для автоматического открытия модального окна при наличии ошибок.

Шаг 1: Модификация контроллера Orchid

Создайте кастомный JavaScript контроллер для управления модальными окнами:

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() {
        // Проверяем наличие ошибок валидации
        const hasErrors = this.element.querySelectorAll('.invalid-feedback').length > 0;
        
        if (hasErrors) {
            // Предотвращаем закрытие модального окна при клике на фон
            this.element.addEventListener('click', this.handleBackdropClick.bind(this));
            
            // Предотвращаем закрытие по 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();
        }
    }
}

Шаг 2: Автоматическое открытие модального окна при ошибках

php
// В вашем Blade шаблоне
<script>
@if (count($errors) > 0)
    document.addEventListener('DOMContentLoaded', function() {
        const modal = new bootstrap.Modal(document.querySelector('#yourModal'));
        modal.show();
        
        // Добавляем обработчик для предотвращения закрытия
        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>

Настройка валидации в Orchid

Для правильной работы валидации в модальных окнах Orchid необходимо правильно настроить валидацию и отображение ошибок.

Шаг 1: Создание Screen с валидацией

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('Имя')
                ->placeholder('Введите имя')
                ->required(),
            
            Input::make('email')
                ->title('Email')
                ->type('email')
                ->placeholder('user@example.com')
                ->required(),
        ];
    }
}

Шаг 2: Обработка валидации в методе

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',
    ]);
    
    // Логика сохранения
    // ...
    
    return redirect()->route('dashboard')
        ->with('success', 'Данные успешно сохранены');
}

Шаг 3: Blade шаблон модального окна

blade
<!-- resources/views/platform/layouts/modal.blade.php -->
<div class="modal-dialog">
    <div class="modal-content">
        <div class="modal-header">
            <h5 class="modal-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">Отмена</button>
            <button type="submit" class="btn btn-primary">Сохранить</button>
        </div>
    </div>
</div>

Полный пример реализации

Вот полный пример реализации модального окна с AJAX-обработкой и валидацией в Orchid:

Шаг 1: Создание 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 = 'Пользователи';
    public $description = 'Управление пользователями';

    public function commandBar(): array
    {
        return [
            ModalCommand::make('Добавить пользователя')
                ->modal('userModal')
                ->method('saveUser')
                ->icon('bs plus-circle'),
        ];
    }

    public function layout(): array
    {
        return [
            Layout::modal('userModal', [
                Layout::rows([
                    Input::make('user.name')
                        ->title('Имя')
                        ->placeholder('Введите имя')
                        ->required(),
                    
                    Input::make('user.email')
                        ->title('Email')
                        ->type('email')
                        ->placeholder('user@example.com')
                        ->required(),
                ]),
            ])->title('Добавить пользователя'),
        ];
    }

    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', 'Пользователь успешно создан');
    }
}

Шаг 2: JavaScript для AJAX

blade
<!-- В вашем главном шаблоне -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    // Обработка всех форм в модальных окнах Orchid
    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');
            
            // Блокируем кнопку
            submitButton.disabled = true;
            submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Сохранение...';
            
            // Определяем 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) {
                    // Закрываем модальное окно
                    const modal = bootstrap.Modal.getInstance(modalElement);
                    modal.hide();
                    
                    // Показываем уведомление
                    showNotification(data.message, 'success');
                    
                    // Обновляем таблицу или список
                    location.reload();
                } else {
                    // Показываем ошибки
                    if (data.errors) {
                        displayValidationErrors(data.errors, this);
                    } else {
                        showNotification(data.message || 'Произошла ошибка', 'error');
                    }
                }
            })
            .catch(error => {
                console.error('Error:', error);
                showNotification('Произошла ошибка при отправке формы', 'error');
            })
            .finally(() => {
                submitButton.disabled = false;
                submitButton.innerHTML = 'Сохранить';
            });
        });
    });
});

function displayValidationErrors(errors, form) {
    // Очищаем предыдущие ошибки
    form.querySelectorAll('.invalid-feedback').forEach(el => el.remove());
    form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
    
    // Отображаем новые ошибки
    Object.keys(errors).forEach(field => {
        // Преобразуем поле в формат Orchid
        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);
    
    // Автоматическое закрытие через 5 секунд
    setTimeout(() => {
        notification.remove();
    }, 5000);
}
</script>

Лучшие практики

При работе с модальными окнами и валидацией в Orchid Laravel придерживайтесь следующих рекомендаций:

  1. Используйте AJAX для отправки форм: Это prevents перезагрузку страницы и сохраняет состояние модального окна

  2. Обрабатывайте ошибки на стороне клиента: Отображайте ошибки валидации непосредственно в модальном окне для лучшего пользовательского опыта

  3. Реализуйте индикаторы загрузки: Показывайте пользователю, что форма обрабатывается

  4. Добавляйте валидацию на стороне клиента: Предотвращайте отправку форм с неверными данными до отправки на сервер

  5. Используйте уведомления: Информируйте пользователей об успешном выполнении или ошибках

  6. Обрабатывайте все возможные сценарии: Учитывайте сетевые ошибки, ошибки валидации и другие исключения

  7. Следите за производительностью: Оптимизируйте JavaScript код для быстрой работы модальных окон

  8. Тестируйте в разных браузерах: Убедитесь, что решение работает корректно во всех браузерах

  9. Документируйте код: Описывайте особенности работы с модальными окнами для других разработчиков

  10. Используйте последние версии библиотек: Следите за обновлениями Orchid и Bootstrap для использования последних исправлений и возможностей

Источники

  1. Modal with validation errors block another modal to show content · GitHub Issue #2801

  2. Keeping modal dialog open after validation error laravel - Stack Overflow

  3. How to prevent modal from closing when validation error - Laravel 8 + bootstrap - Stack Overflow

  4. Modal Dialog | Orchid - Laravel Admin Panel

  5. Screens | Orchid - Laravel Admin Panel

  6. Quick Start for Beginners | Orchid - Laravel Admin Panel

  7. Laravel Orchid Editing an entry in a list - Stack Overflow

Заключение

Предотвращение закрытия модального окна при ошибках валидации в Orchid Laravel требует комплексного подхода, включающего:

  • Использование AJAX для асинхронной отправки форм
  • Правильную обработку и отображение ошибок валидации
  • Настройку JavaScript контроллеров для управления состоянием модальных окон
  • Следование лучшим практикам для обеспечения хорошего пользовательского опыта

Основные решения включают:

  1. Полная AJAX-реализация с обработкой ошибок на клиентской стороне
  2. Модификация JavaScript контроллеров Orchid для предотвращения закрытия окна при ошибках
  3. Комбинирование серверной и клиентской валидации для максимальной надежности

Эти подходы позволят создать удобный интерфейс, где пользователи могут исправлять ошибки прямо в модальном окне без необходимости его повторного открытия.