Courses

Livewire 3 From Scratch: Practical Course

Dependent Dropdowns: Country and Cities

Summary of this lesson:
- Implementing country-city relationship in database
- Creating dynamic dropdown components
- Using wire:model.live for instant updates
- Handling dependent select fields

In this course section, we will build four small demo projects to practice Livewire more. The first project is a dependent dropdown, like a parent and a child.

When we will choose a country, the cities list will be refreshed.

dependent dropdowns


DB Structure

First, let's quickly see what the Migrations and Models look like.

database/migrations/xxx_create_countries_table.php:

Schema::create('countries', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});

app/Models/Country.php:

use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Country extends Model
{
protected $fillable = [
'name',
];
 
public function cities(): HasMany
{
return $this->hasMany(City::class);
}
}

database/migrations/xxx_create_cities_table.php:

Schema::create('cities', function (Blueprint $table) {
$table->id();
$table->foreignId('country_id');
$table->string('name');
$table->timestamps();
});

app/Models/City.php:

class City extends Model
{
protected $fillable = [
'country_id',
'name',
];
}

And a simple seeder to have some data in the DB:

$country = Country::create(['name' => 'United Kingdom']);
$country->cities()->create(['name' => 'London']);
$country->cities()->create(['name' => 'Manchester']);
$country->cities()->create(['name' => 'Liverpool']);
 
$country = Country::create(['name' => 'United States']);
$country->cities()->create(['name' => 'Washington']);
$country->cities()->create(['name' => 'New York City']);
$country->cities()->create(['name' => 'Denver']);

Livewire Component

For the Livewire component, we will name it just Dropdowns.

php artisan make:livewire Dropdowns

In the component, we first must get the list of countries in the mount method and set the cities list to an empty collection.

app/Livewire/Dropdowns.php:

use App\Models\Country;
use Illuminate\Support\Collection;
 
class Dropdowns extends Component
{
public Collection $countries;
public Collection $cities;
 
public function mount(): void
{
$this->countries = Country::pluck('name', 'id');
$this->cities = collect();
}
 
// ...
}

Now let's add inputs in the Blade file.

resources/views/livewire/dropdowns.blade.php:

<form method="POST" wire:submit="save">
<div>
<label for="country">Country</label>
<select wire:model.live="country" id="country" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm">
<option value="0">-- choose country --</option>
@foreach($countries as $id => $country)
<option value="{{ $id }}">{{ $country }}</option>
@endforeach
</select>
</div>
 
<div class="mt-4">
<label for="city">City</label>
<select wire:model="city" id="city" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm">
@if ($cities->count() == 0)
<option value="">-- choose country first --</option>
@endif
@foreach($cities as $city)
<option value="{{ $city->id }}">{{ $city->name }}</option>
@endforeach
</select>
</div>
 
<button class="mt-4 px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700">
Save
</button>
</form>

The important part here is to use live for the country input. Otherwise, it won't update the cities list after selecting a country.

empty dropdowns form

Next, we need public properties for country and city, which we bind to inputs using wire:model directive.

app/Livewire/Dropdowns.php:

class Dropdowns extends Component
{
public Collection $countries;
public Collection $cities;
 
public int $country;
public int $city;
 
public function mount(): void
{
$this->countries = Country::pluck('name', 'id');
$this->cities = collect();
}
 
// ...
}

All that is left is to update cities when country public property is updated. We can use Lifecycle Hook updated.

app/Livewire/Dropdowns.php:

use App\Models\City;
 
class Dropdowns extends Component
{
public Collection $countries;
public Collection $cities;
 
public int $country;
public int $city;
 
public function mount(): void
{
$this->countries = Country::pluck('name', 'id');
$this->cities = collect();
}
 
public function updatedCountry($value): void
{
$this->cities = City::where('country_id', $value)->get();
$this->city = $this->cities->first()->id;
}
 
// ..
}

When a user selects a country, the lifecycle hook updatedCountry gets triggered. In this method, we set the cities list and the city property to the first from the cities list.

Previous: File Uploads in Livewire
avatar

When using this type of technique, what would be the best way to provide useful feedback to the user in case of a communication disruption when they're using the select?

I know we can set the select (or even the whole form or page) to be disabled using wire:offline but consider a scenario where "United States" and "Washington" was already selected and then the user selected "United Kingdom" but at the exact moment they did that, the connection was disrupted, so the city stayed populated with the United States cities even though the country now shows United Kingdom. It might be a bit confusing for the user. So is there a way that we could somehow clear/reset the city dropdown when the country is clicked, before the response comes back, just in case the response never comes in order to improve UX?

avatar

If your users have connectivity problems and this is very serious issue for you i'd suggest implementing this only client side.

But there's a catch, is it worth it if they can't submit the form anyway, due to... well... connectivity issues?

I'd stay with something simple, and move on, unless there's some good reason to do otherwise.

<div wire:offline>
    You are now offline. Press this button to reload the page.
</div>
avatar

I was thinking more in terms of intermittent connectivity issues, such as using a mobile on a train for example. In such a case it's very likely that someone could choose a country and have it fail silently due to the connection dropping for 1 second but still be able to do everything else because the connection is back. And my concern would be that their overall perception of the app would be that it's buggy because they wouldn't understand why just that one thing failed, especially as there's no feedback to the user.

avatar

In my opinion, if someone tries to use any app on the unstable train connection, they KNOW that connection is unstable and totally wouldn't blame the app.

I would personally just validate the data on the back end.

avatar

I understand, and my answer won't change at all. Let me explain my point of view:

  1. If you want to minimize chances packets being dropped you might want to reduce round trips as much as possible and have complete control what happens if the connection fails. Livewire is opposite of that.
  2. If there's no connection user cannot do anything, your application can only hope and retry request if connection comes back. Thus wire:offline with check your connection message seems sufficient for that.
  3. I'm not sure right now how Livewire handles retries because I didn't test this specific scenario, you should test that yourself. Or just have a simple blade form with a single POST request.
  4. Optionally you could do REST implementation and handle UI and Networking to cover this case and have complete control what you want to do.
  5. So now we have 3 ways (at least) to aproach it, Livewire wire:offline, http POST, and something in between with REST API. And yet there's no guarantee something won't fail due to connection, and the actual form is not the focus at this point, it can happen on any page load or action, and window can be 1 second to N.
  6. This would be my reasoning why I wouldn't focus on this problem a lot and just minimize impact by telling user that there's a problem on HIS side, and not a BUG with our application, especially it is ONLINE app, not offline app.
avatar

Thank you for your detailed reply. Nothing you have said is technically wrong, but I'm thinking that maybe I haven't really explained myself properly.

Looking into this in more detail, the problem seems to be that if the user performs any Livewire request on a component at the split-second when the network is unavailable (because they are on a train, or for whatever other reason) the result is that the page breaks. And it doesn't break just while the network is offline, it is broken forever, until the page is refreshed.

There's no concept of a retry as far as I can tell. Livewire completely forgets that the request was attempted. If you look in the network tab of dev tools, it just shows as "(failed)". And you can't show a wire:offline message to tell the user to refresh the page, because that message would only display for the split second that the network connection was unavailable. As soon as the network is available again the message will disappear. But our page is still broken.

The point, though, is that the page breaks because of the user performing an action during that split second when the network is offline.

For a simple demonstration of what I mean, just go to the Filament Demo, click Customers and then click Edit for any customer until you find one with more than one address attached. Now, take your browser offline and press one of those "Detach" buttons. It will fail and show as a network error in dev tools. Now, bring your browser back online and try the "Detach" button again. It still doesn't work, but now there is no network error, or even an error in the console. It's completely silent and it will never work again unless you refresh the page.

The dropdown example of this lesson breaks in the same way. If the user happens to select the dropdown during a split second when the network is unavailable then the entire component is broken until the page is refreshed.

My apolgoies, perhaps this wasn't really the correct place to bring up this issue. I started by honestly thinking it was just an issue with the way this lesson instructed us to make the dropdown component, but I can see now that it's bascially an issue that affects Livewire globally and there's nothing specifically wrong with the tutorial on how to make this component.

avatar

you're right

avatar

Yes that's how Livewire works. nothing we can do about it.

avatar

I’m trying to get the value of select onchange with livewire and select2 jquery but when I start select2 I can’t get the value, without select2 the value gets normally.

avatar

This course was made with livewire 2. If you are using livewire 3 there might be some differences how to make it

avatar
You can use Markdown
avatar

Thank you guys! I'm really enjoying working through these courses and getting such detailed feedback on my thoughts and concerns like this is hugely valuable to my . I really appreciate it!

🥳 1
avatar
You can use Markdown
avatar

In case anyone select the "choose country" after already selected any city the app will break, to fix that you can put that in the updatedCountry method: "public function updatedCountry($value): void { $this->cities = City::where("country_id", $value)->get(); $this->city = $this->cities->first()?->id; } " and se the type of city as "?int"

👍 1
avatar
You can use Markdown
avatar

Turns out the original issue I described was actually caused by a bug in Livewire which was fixed in v3.4.7

https://github.com/livewire/livewire/pull/7972

avatar
You can use Markdown
avatar
You can use Markdown