Looking at the previous example of country/city form, let's talk about binding the inputs to properties and saving their data.
We have three goals in this lesson:
- Submit the form to save data to DB
- Show the success alert
- Clear the form inputs
All without the page refresh.
Simple Tailwind Design
To demonstrate the form refresh without the full page refresh, I've added a very simple Tailwind design (thanks ChatGPT):
Here's the updated HTML of the Blade files.
resources/views/companies/create.blade.php:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Simple Form</title> <script src="https://cdn.tailwindcss.com"></script></head><body class="flex items-center justify-center min-h-screen bg-gray-100"> <livewire:company-create /></body></html>
And then the dynamic Livewire blade:
resources/views/livewire/company-create.blade.php:
<div class="w-full max-w-md p-6 bg-white rounded-lg shadow-md"> <form> <div class="mb-4"> <label for="name" class="block text-gray-700">Company name</label> <input wire:model="name" type="text" required id="name" class="w-full p-2 mt-1 border rounded border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"> </div> <div class="mb-4"> <label for="country" class="block text-gray-700">Country</label> <select wire:model.live="country" required id="country" class="w-full p-2 mt-1 border rounded border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="">-- choose country --</option> @foreach ($countries as $country) <option value="{{ $country->id }}">{{ $country->name }}</option> @endforeach </select> </div> <div class="mb-4"> <label for="city" class="block text-gray-700">City</label> <select wire:model="city" required id="city" class="w-full p-2 mt-1 border rounded border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"> @forelse($cities as $city) <option value="{{ $city->id }}">{{ $city->name }}</option> @empty <option value="">-- choose country first --</option> @endforelse </select> </div> <button type="submit" class="w-full p-2 text-white bg-blue-500 rounded hover:bg-blue-600">Submit</button> </form></div>
The important bits for us are the Livewire bindings for three fields:
-
input wire:model="name"
-
select wire:model.live="country"
(why ".live?" - we'll discuss in a minute) -
select wire:model="city"
With those wire:model
pieces, we bind the Blade/HTML inputs to the Livewire component properties:
app/Livewire/CompanyCreate.php:
class CompanyCreate extends Component{ public $countries; public $name; public $country; public $city; public $cities = []; // ...
It means that whenever something changes in the form, we can get those values in the Livewire component and perform the back-end changes as we did in the first lesson.
Including submitting the forms with them. Let me show you.
Here's the GitHub commit for the changes so far in this lesson.
Submitting the Form
As you may have noticed, those wire:model
above come instead of input name="..."
. So, we don't use typical name
properties and will not submit the form in the usual way.
Instead, we will call a Livewire component method.
resources/views/livewire/company-create.blade.php:
<form> <form wire:submit="save">
That save
is the name of the function we define in the Livewire component class.
app/Livewire/CompanyCreate.php:
class CompanyCreate extends Component{ public $name; public $country; public $city; // ... public function save(): void { // ... Company::create() will be here }
Now, let's add a Model/Migration for the companies
DB table.
php artisan make:model Company -m
Migration:
Schema::create('companies', function (Blueprint $table) { $table->id(); $table->string('name'); $table->foreignId('country_id')->constrained(); $table->foreignId('city_id')->constrained(); $table->timestamps();});
We run the migrations:
php artisan migrate
app/Models/Company.php:
namespace App\Models; use Illuminate\Database\Eloquent\Model; class Company extends Model{ protected $fillable = ['name', 'country_id', 'city_id'];}
And let's fill in our save()
method:
app/Livewire/CompanyCreate.php:
class CompanyCreate extends Component{ public $countries; public $name; public $country; public $city; public $cities = []; // ... public function save(): void { Company::create([ 'name' => $this->name, 'country_id' => $this->country, 'city_id' => $this->city, ]); $this->reset('name', 'country', 'city', 'cities'); }
So, what do we have here?
- We use
$this->name
,$this->country
, and$this->city
property values. I deliberately show them again in this snippet. - The method
$this->reset()
will set those properties in the original state and re-render the page.
So, let's try to fill in the form with these values:
As a result, after the submission, our form is empty again:
But in the database, we have a new record:
Great! Here's the GitHub commit for this first successful form submission.
This wire:submit
is only one example of calling the Livewire component method from Blade. You can call methods from other so-called "listeners" like wire:click
on a button or wire:mouseenter
on the element, see the full list here in the docs.
Success Alert
Now, as visual proof that the submission was actually successful, let's show a success alert to the user, including the name of the company we just saved.
For that, we define another property in the Livewire component class: empty by default, but we assign a value after we save the data.
app/Livewire/CompanyCreate.php:
class CompanyCreate extends Component{ public $countries; public $name; public $country; public $city; public $cities = []; public $savedName = ''; public function save(): void { Company::create([ 'name' => $this->name, 'country_id' => $this->country, 'city_id' => $this->city, ]); $this->savedName = $this->name; $this->reset('name', 'country', 'city', 'cities'); }}
Then, in the Livewire Blade file, we add an @if
statement to show that alert if the company name is not empty.
resources/views/livewire/company-create.blade.php:
<div class="w-full max-w-md p-6 bg-white rounded-lg shadow-md"> <!-- @if ($savedName != '') <div class="mb-4 p-4 text-green-700 bg-green-100 border border-green-400 rounded"> Company "{{ $savedName }}" saved successfully </div> @endif <!-- Form --> <form wire:submit="save"> <div class="mb-4"> // ...
And that's it! Now, our users would see not only the reset form with empty values but also the visual notification that it worked.
Bug Fix: city_id cannot be null?
While working on the course, I noticed a minor bug from the previous lesson that we can fix immediately. It will be a great example to explain how Livewire works.
If you're trying to code along with this tutorial, after submission, you may get an error "SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'city_id' cannot be null".
This happens if you just accept the default first city from the refreshed list.
It actually means that you haven't chosen the city.
And here's where you need to understand an important thing about how Livewire works internally.
Look at our updated()
method from the previous lesson:
app/Livewire/CompanyCreate.php:
public function updated($property){ if ($property == 'country') { $this->cities = City::where('country_id', $this->country)->get(); }}
So, if the user chooses the country, we're updating the list of cities, right?
Then, Livewire automatically calls the render()
method again to show those refreshed cities:
resources/views/livewire/company-create.blade.php:
<select wire:model="city" required id="city" class="w-full p-2 mt-1 border rounded border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500"> @forelse($cities as $city) <option value="{{ $city->id }}">{{ $city->name }}</option> @empty <option value="">-- choose country first --</option> @endforelse</select>
But the problem is that we never set the value of $this->city.
You would think that the refreshed list of cities means that the "New York" value in the browser would also auto-set the property in the Livewire component. But that's not how it works. We need to add this line.
app/Livewire/CompanyCreate.php:
public function updated($property){ if ($property == 'country') { $this->cities = City::where('country_id', $this->country)->get(); $this->city = $this->cities->first()->id; }}
Great, we've fixed the bug.
This is the final GitHub commit for this lesson: for the success alert and the bug fix.
Ok, this lesson was a practical demonstration without too much theory.
In the next lesson, I will explain how that Livewire process actually works under the hood, with all those wire:model
, wire:model.live
, and wire:submit
statements.
No comments yet…