With Livewire v3’s introduction, Form Objects now offer a streamlined way to separate field logic from Components.
7 min read · Nov 25, 2023
--
Setting Up the Laravel Project
Our ‘Product’ model will encompass two database fields: ‘name’ and ‘description’. Here’s a glimpse of the migration for creating the products table:
// database/migrations/xxx_create_products_table.php:
public function up(): void{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description');
$table->timestamps();
});
}
And, here’s the essence of our Product model:
app/Models/Product.php:
class Product extends Model
{
protected $fillable = [
'name',
'description',
];
}
Setting up Livewire and Tailoring Breeze
Before diving in, the first step on our to-do list is installing Livewire, which you can accomplish with the following Composer command:
composer require livewire/livewire
Open your resources/js/app.js
and remove or comment out the Alpine.js lines:
import './bootstrap';
import Alpine from 'alpinejs';
window.Alpine = Alpine;
Alpine.start();
Once these changes are made, recompile your assets:
npm run prod
By default, Livewire searches for a layout atresources/views/components/layouts/app.blade.php
. We'll need to point Livewire to the right location through its configuration settings.
First, publish the Livewire configuration file:
php artisan livewire:publish --config
Then, within config/livewire.php
, modify the layout paths:
return [
// ...
'layout' => 'components.layouts.app',
'layout' => 'layouts.app',
// ...
];
There you have it! Livewire is now integrated into our project. Next, let’s craft a Livewire component to display our list of products.
Crafting the Livewire Component for Product Listing
Start by crafting the component:
php artisan make:livewire ProductList
Navigate to app/Livewire/ProductList.php and set it up as follows:
use App\Models\Product;
use Illuminate\Contracts\View\View;
class ProductList extends Component
{
public function render(): View
{
return view('livewire.product-list', [
'products' => Product::all(),
]);
}
}
To make our products accessible from the main navigation:
Open resources/views/layouts/navigation.blade.php
:
// ...
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<x-nav-link :href="route('users.index')" :active="request()->routeIs('users.index')">
{{ __('Users') }}
</x-nav-link>
<x-nav-link :href="route('products')" :active="request()->routeIs('products')">
{{ __('Products') }}
</x-nav-link>
</div>
// ...
Finally, design the product listing:
resources/views/livewire/product-list.blade.php:
<div>
<x-slot name="header">
<h2 class="text-xl font-semibold leading-tight text-gray-800">
{{ __('Products') }}
</h2>
</x-slot>
<div class="py-12">
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg">
<div class="p-6 overflow-hidden overflow-x-auto bg-white border-b border-gray-200">
<div class="min-w-full align-middle">
<table class="min-w-full border divide-y divide-gray-200">
<!-- Table Header -->
<thead>
<tr>
<th class="px-6 py-3 text-left bg-gray-50">
<span class="text-xs font-medium leading-4 tracking-wider text-gray-500 uppercase">Name</span>
</th>
<th class="px-6 py-3 text-left bg-gray-50">
<span class="text-xs font-medium leading-4 tracking-wider text-gray-500 uppercase">Description</span>
</th>
<th class="px-6 py-3 text-left bg-gray-50"></th>
</tr>
</thead>
<!-- Table Body -->
<tbody class="bg-white divide-y divide-gray-200 divide-solid">
@forelse($products as $product)
<tr>
<td class="px-6 py-4 text-sm leading-5 text-gray-900">
{{ $product->name }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900">
{{ $product->description }}
</td>
<td class="px-6 py-4 text-sm leading-5 text-gray-900">
{{-- Placeholder for Edit action --}}
</td>
</tr>
@empty
<tr>
<td colspan="3" class="px-6 py-4 text-sm leading-5 text-gray-900">
No products available.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
Now, your Livewire component will showcase a sleek product listing.
Crafting a Modal using Form Object
First, generate a Livewire component and a Form Object that will handle our modal and data validations.
php artisan make:livewire ProductModal php artisan livewire:form ProductForm
To smoothly implement modals, let’s integrate the wire-elements/modal
package:
composer require wire-elements/modal:^2.0
For the Livewire component to function as a modal, instead of the typical Livewire\Component
we have to extend it to LivewireUI\Modal\ModalComponentModal
Update your app/Livewire/ProductModal.php
:
use Livewire\Component;
use Illuminate\Contracts\View\View;
use LivewireUI\Modal\ModalComponent;
class ProductModal extends ModalComponent
{
public function render(): View
{
return view('livewire.product-form');
}
}
Craft a corresponding form within the Blade file,
resources/views/livewire/product-form.blade.php:
<div class="p-6">
<form wire:submit="save">
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="mt-1 block w-full" type="text" />
</div>
<div class="mt-4">
<x-input-label for="description" :value="__('Description')" />
<textarea id="description" class="mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"></textarea>
</div>
<div class="mt-4">
<x-primary-button>
Save
</x-primary-button>
</div>
</form>
</div>
Lastly, place buttons to invoke the modal in resources/views/livewire/product-list.blade.php
:
// ...
<x-primary-button wire:click="$dispatch('openModal', { component: 'product-modal' })" class="mb-4">
New Product
</x-primary-button>
// ...
@forelse($products as $product)
<tr class="bg-white">
<td class="px-6 py-4 text-sm">
{{ $product->name }}
</td>
<td class="px-6 py-4 text-sm">
{{ $product->description }}
</td>
<td class="px-6 py-4 text-sm">
<x-secondary-button wire:click="$dispatch('openModal', { component: 'product-modal', arguments: { product: {{ $product }} }})">
Edit
</x-secondary-button>
</td>
</tr>
@empty
// ...
After clicking the New Product button, the form-containing modal appears.
Product Creation: A Modal Approach
Begin by defining the required properties within our Form Object, and subsequently binding them to input fields.
app/Livewire/Forms/ProductForm.php:
class ProductForm extends Form
{
public string $name = '';
public string $description = '';
}
In app/Livewire/ProductModal.php, inject the form:
class ProductModal extends ModalComponent
{
public Forms\ProductForm $form;
// ...
}
Update your resources/views/livewire/product-modal.blade.php:
<div class="p-6">
<form wire:submit="save">
<!-- Name input -->
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input wire:model="form.name" id="name" class="mt-1 block w-full" type="text" />
<x-input-error :messages="$errors->get('form.name')" class="mt-2" />
</div>
<!-- Description input -->
<div class="mt-4">
<x-input-label for="description" :value="__('Description')" />
<textarea wire:model="form.description" id="description" class="mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"></textarea>
<x-input-error :messages="$errors->get('form.description')" class="mt-2" />
</div>
<!-- Save button -->
<div class="mt-4">
<x-primary-button>
Save
</x-primary-button>
</div>
</form>
</div>
For the saving part, the wire:submit
directive links to the save
method, which we'll define in the Form Object. It'll house all the logic for creating and updating the product.
In app/Livewire/ProductForm.php
:
class ProductForm extends Form
{
public function save(): void
{
$this->validate();
Product::create($this->only(['name', 'description']));
$this->reset();
}
public function rules(): array
{
return [
'name' => ['required'],
'description' => ['required']
];
}
}
Upon saving a product, the modal should close and the table should refresh to display the new entries.
In app/Livewire/ProductModal.php:
class ProductModal extends ModalComponent
{
public Forms\ProductForm $form;
public function save(): void
{
$this->form->save();
$this->closeModal();
$this->dispatch('refresh-list');
}
// ...
}
To ensure the table gets automatically updated after a product is saved, add a refresh method in app/Livewire/ProductList.php
:
use Livewire\Attributes\On;
class ProductList extends Component
{
#[On('refresh-list')]
public function refresh() {}
// ...
}
After successfully saving a product, the modal will close and the table will update to reflect the new information.
Product Editing: Setting Up the Form and Data Persistence
To refine our product editing capability, begin by setting up properties when a product is provided.
In app/Livewire/ProductModal.php
, incorporate the product model:
use App\Models\Product;
class ProductModal extends ModalComponent
{
public ?Product $product = null;
public Forms\ProductForm $form;
public function mount(Product $product = null): void
{
if ($product && $product->exists) {
$this->form->setProduct($product);
}
}
// ...
}
Next, navigate to app/Livewire/Forms/ProductForm.php
and include these adjustments:
use App\Models\Product;
class ProductForm extends Form
{
public ?Product $product = null;
public string $name = '';
public string $description = '';
public function setProduct(?Product $product = null): void
{
$this->product = $product;
$this->name = $product->name;
$this->description = $product->description;
}
// ...
}
To ensure our database is updated properly, we’ll include a check within our saving logic. Depending on the result, we’ll either add a new product or update an existing one.
Update app/Livewire/Forms/ProductForm.php
:
class ProductForm extends Form
{
// Existing properties and methods...
public function save(): void
{
$this->validate();
if (!$this->product) {
Product::create($this->only(['name', 'description']));
} else {
$this->product->update($this->only(['name', 'description']));
}
$this->reset();
}
// Other methods...
}
Now, when you edit a product, your system will check if the product exists. If it does, the product’s details will be updated, otherwise, a new product will be created.
Ensuring the Uniqueness of Product Names
To maintain the distinctness of each product, we’ll institute a unique rule for product names.
Within app/Livewire/Forms/ProductForm.php
:
use Illuminate\Validation\Rule;
class ProductForm extends Form
{
// Existing properties...
public function rules(): array
{
return [
'name' => [
'required',
Rule::unique('products', 'name')->ignore($this->component->product),
],
'description' => ['required'],
];
}
}
An essential aspect of this implementation is to ensure we exclude the current product (if editing) from the uniqueness check. With Livewire, properties from the main component can be fetched using $this->component
.
This addition ensures that while creating or updating, each product name remains distinct in the database.
Additional Guidance on Validation: Handling the “form.” Prefix
Upon implementing Form Objects, you might notice that the attribute names referenced in validation messages come prefixed with “form.” This could potentially lead to unclear or inaccurate validation feedback.
To bring clarity to this, we can simply add a validationAttributes
method to our ProductForm
.
In app/Livewire/Forms/ProductForm.php
:
class ProductForm extends Form
{
// Previous properties and methods...
public function validationAttributes(): array
{
return [
'name' => 'name',
'description' => 'description',
];
}
}
With this modest yet impactful change, the attribute names are presented as expected, enhancing the user experience.
Unlock the secrets to mastering Laravel and supercharge your development skills! This guide, crafted by seasoned experts, is your golden ticket to bypass common roadblocks and accelerate your career growth. With this treasure trove of Laravel wisdom, you’re not just buying an eBook; you’re investing in a leap forward. Two years of professional advancement await at your fingertips. Make the smart move — transform your Laravel expertise with just one click. Get Your Copy Now!