# Livewire Modals
The Admin package uses Livewire components as full-page modals, providing a seamless admin interface without traditional page reloads.
## Overview
Livewire modals in Core PHP:
- Render as full-page routes
- Support direct URL access
- Maintain browser history
- Work with back/forward buttons
- No JavaScript modal libraries needed
## Creating a Modal
### Basic Modal
```php
'required|max:255',
'content' => 'required',
'status' => 'required|in:draft,published',
];
public function mount(?Post $post = null): void
{
$this->post = $post;
if ($post) {
$this->title = $post->title;
$this->content = $post->content;
$this->status = $post->status;
}
}
public function save(): void
{
$validated = $this->validate();
if ($this->post) {
$this->post->update($validated);
$message = 'Post updated successfully';
} else {
Post::create($validated);
$message = 'Post created successfully';
}
session()->flash('success', $message);
$this->redirect(route('admin.blog.posts'));
}
public function render()
{
return view('blog::admin.post-editor')
->layout('admin::layouts.modal');
}
}
```
### Modal View
```blade
{{-- resources/views/admin/post-editor.blade.php --}}
{{ $post ? 'Edit Post' : 'Create Post' }}
Publishing Tips
Write a clear, descriptive title
Use proper formatting in content
Save as draft to preview first
```
## Registering Modal Routes
```php
// Routes/admin.php
use Mod\Blog\View\Modal\Admin\PostEditor;
use Mod\Blog\View\Modal\Admin\PostsList;
Route::middleware(['web', 'auth', 'admin'])->prefix('admin/blog')->group(function () {
Route::get('/posts', PostsList::class)->name('admin.blog.posts');
Route::get('/posts/create', PostEditor::class)->name('admin.blog.posts.create');
Route::get('/posts/{post}/edit', PostEditor::class)->name('admin.blog.posts.edit');
});
```
## Opening Modals
### Via Link
```blade
New Post
```
### Via Livewire Navigate
```blade
```
### Via JavaScript
```blade
```
## Modal Layouts
### With HLCRF
```blade
Modal Header
Modal Content
Modal Footer
```
### Full-Width Modal
```blade
Full-width content
```
### With Sidebar
```blade
Main content
Sidebar
```
## Advanced Patterns
### Modal with Confirmation
```php
public bool $showDeleteConfirmation = false;
public function confirmDelete(): void
{
$this->showDeleteConfirmation = true;
}
public function delete(): void
{
$this->post->delete();
session()->flash('success', 'Post deleted');
$this->redirect(route('admin.blog.posts'));
}
public function cancelDelete(): void
{
$this->showDeleteConfirmation = false;
}
```
```blade
@if($showDeleteConfirmation)
Delete Post?
This action cannot be undone.
Delete
Cancel
@endif
```
### Modal with Steps
```php
public int $step = 1;
public function nextStep(): void
{
$this->validateOnly('step' . $this->step);
$this->step++;
}
public function previousStep(): void
{
$this->step--;
}
```
```blade
```
### Modal with Live Search
```php
public string $search = '';
public array $results = [];
public function updatedSearch(): void
{
$this->results = Post::where('title', 'like', "%{$this->search}%")
->limit(10)
->get()
->toArray();
}
```
```blade
@foreach($results as $result)
{{ $result['title'] }}
@endforeach
```
## File Uploads
### Single File
```php
use Livewire\WithFileUploads;
class PostEditor extends Component
{
use WithFileUploads;
public $image;
public function save(): void
{
$this->validate([
'image' => 'required|image|max:2048',
]);
$path = $this->image->store('posts', 'public');
Post::create([
'image_path' => $path,
]);
}
}
```
```blade
@if($image)
@endif
```
### Multiple Files
```php
public array $images = [];
public function save(): void
{
$this->validate([
'images.*' => 'image|max:2048',
]);
foreach ($this->images as $image) {
$path = $image->store('posts', 'public');
// Save path...
}
}
```
## Real-Time Validation
```php
protected array $rules = [
'title' => 'required|max:255',
'slug' => 'required|unique:posts,slug',
];
public function updated($propertyName): void
{
$this->validateOnly($propertyName);
}
```
```blade
@error('slug')
{{ $message }}
@enderror
```
## Loading States
```blade
{{-- Show loading on specific action --}}
SaveSaving...
{{-- Disable form during loading --}}
{{-- Spinner --}}
```
## Events
### Dispatch Events
```php
// From modal
public function save(): void
{
// Save logic...
$this->dispatch('post-saved', postId: $post->id);
}
```
### Listen to Events
```php
// In another component
protected $listeners = ['post-saved' => 'refreshPosts'];
public function refreshPosts(int $postId): void
{
$this->posts = Post::all();
}
```
```blade
{{-- In Blade --}}
```
## Best Practices
### 1. Use Route Model Binding
```php
// ✅ Good - automatic model resolution
Route::get('/posts/{post}/edit', PostEditor::class);
public function mount(?Post $post = null): void
{
$this->post = $post;
}
```
### 2. Flash Messages
```php
// ✅ Good - inform user of success
public function save(): void
{
// Save logic...
session()->flash('success', 'Post saved');
$this->redirect(route('admin.blog.posts'));
}
```
### 3. Validate Early
```php
// ✅ Good - real-time validation
public function updated($propertyName): void
{
$this->validateOnly($propertyName);
}
```
### 4. Use Loading States
```blade
{{-- ✅ Good - show loading feedback --}}
Save
```
## Testing
```php
set('title', 'Test Post')
->set('content', 'Test content')
->set('status', 'published')
->call('save')
->assertRedirect(route('admin.blog.posts'));
$this->assertDatabaseHas('posts', [
'title' => 'Test Post',
]);
}
public function test_validates_required_fields(): void
{
Livewire::test(PostEditor::class)
->set('title', '')
->call('save')
->assertHasErrors(['title' => 'required']);
}
public function test_updates_existing_post(): void
{
$post = Post::factory()->create();
Livewire::test(PostEditor::class, ['post' => $post])
->set('title', 'Updated Title')
->call('save')
->assertRedirect();
$this->assertEquals('Updated Title', $post->fresh()->title);
}
}
```
## Learn More
- [Form Components →](/packages/admin/forms)
- [HLCRF Layouts →](/packages/admin/hlcrf)
- [Livewire Documentation →](https://livewire.laravel.com)