Re-usable, Dependent Dropdowns in Livewire v3

Three dropdowns, three Livewire aliens around these dropdowns. One alien is seen sitting on top of a dropdown, another riding this majestic horse, and the last hanging from the edges of the 2nd dropdown.
Image by Annie Ruygt

In just a few steps, deploy your Laravel application on Fly.io, you’ll be up and running in minutes!

How do we create dependency among dropdowns that use the same, re-usable component? One way we can solve this is by utilizing Livewire’s powerful component nesting capabilities.

Let’s check it out, here’s a repository for reference.

Problem

We have a re-usable filter component that contains a select dropdown:

<!-- resources/views/livewire/filter.blade.php -->
<div>
  <!-- Show the label of the filter -->
  <label>{{ $label }}</label>

  <!-- Filter -->
  <select wire:model="value">
    @foreach( $options as $option )
      <option value="{{ $option['id'] }}">{{ $option['name'] }}</option>
    @endforeach
  </select>
</div>

We want to use this to create 3 different dropdown filters ( Offer Type, Property Type, and Amenities ) for our page.

Property Type filters the Amenities dropdown

One key point is that changes on the Property Type dropdown should filter the options in the Amenities dropdown.

How do we create such dependency that come from the same component?

Solution: A Parent Data Manager

At the end of the day, what we really have here is data dependency: Data from one filter component reacting to data from another filter component.

And in the beginning, where did we even get data to instantiate these two components? —Somewhere outside of it.

So, if we get data from outside of the component, why not handle data stuff like dependency outside of the component as well?

We can create a filter-section component that will manage the dropdowns. It will be responsible for managing data, rendering, and coordinating events across its filter child components.

And, thanks to Livewire v3’s component nesting capabilities, all these data and event coordination, as well as re-rendering of UI changes, is going to be smooth.

Set Up

First, create a filter component, it’s the “template” to our dropdowns. Afterwards, create another component called filter-section, this will act as the parent manager.

Getting Data

The filter-section will have a public attribute, $filters, that will contain data on the three dropdowns we want to render in our page:

/* app/Livewire/FilterSection.php */

public $filters = [];

public function mount()
{
  $anyArray = ['id'=>0,'name'=>'Any'];
  $this->filters = [
       'offer_type'=>[
            'value' => 0,
            'label'=> 'OFFER TYPE',
            'options'=> [ $anyArray ]+OfferType::all()->toArray()
        ],
        'property_type'=>[
            'value' => 0,
            'label'=>'PROPERTY TYPE',
            'options'=>[ $anyArray ]+PropertyType::all()->toArray()
        ],
        'amenity'=>[
            'value' => 0,
            'label'=>'AMENITIES',
            'options'=>[ $anyArray ]+Amenity::all()->toArray()
        ]
  ];
}

Initializing Child Components

In its view, we’ll instantiate a bunch of child filter components using the items in $filters. For each item, we initialize a filter component with the item’s $label, $options, and $value:

<!-- resources/livewire/filter-section.blade.php -->
<div>
    @foreach( $filters as $key=> $filter )
        <livewire:filters
          :key="$key.now()"
          :label="$filter['label']"
          :options="$filter['options']"
          :value="$filter['value']"
        />
    @endforeach     
</div>

Saving the code snippet above should give us an instance of filter component for each item in the $filters array: 3 dropdowns from the $filter array are displayed

Next, let’s focus on the :key attribute declared above.

The Importance in :key

Including a :key identifier in each filter child component is a vital, mandatory requirement. Livewire uses this :key attribute to keep track of children components during re-rendering of changes in the page.

And, speaking of “re-rendering”, Livewire by default only renders a child component when the child component has not yet been rendered before.

In fact, a parent component maintains this list called “children”. It contains the :key of all child components previously rendered. Whenever the parent needs to re-render, it skips any child whose key has already been recorded in the list.

Since our “Amenities” dropdown has already been rendered in initial page load, Livewire’s not going to re-render it afterwards. But see, we need re-rendering for changes to reflect, so what do we do?

This, is where $key.now() comes in. This should generate a unique key for our dropdown every time, allowing it to have a different key from before, forcing the re-rendering of the component—and reflecting new changes to our dropdowns ✨

Dependent Filters

Now, let’s go over to the “dependency” part.

When a user selects a “Property Type” option, we want to filter the “Amenities” options to only include options for that specific property type.

There’re two parts to this: Listening for “Property Type” changes, and Requesting the parent component to filter its “Amenities” dropdown. Add this logic to the filter view:

<!-- resources/views/livewire/filter.blade.php -->
<select 
     x-on:change="
        $wire.label == 'PROPERTY TYPE' && 
        $wire.$parent.filterAmenity( $wire.value )"
>

What’s happening above?

First, notice we use $wire everywhere. It’s this magical JavaScript object that represents the current component that caused the action.

Then, for the bigger picture we make use of some Alpine. We combine a quick x:on-change to listen for option changes, and add in a conditional statement to specifically listen to a component that has $wire.label “PROPERTY TYPE”.

Finally, we proceed with calling the parent’s method filterAmenity() to filter the data for the “Amenities” dropdown using the argument passed.

Fly.io ❤️ Laravel

Fly your servers close to your users—and marvel at the speed of close proximity. Deploy globally on Fly in minutes!

Deploy your Laravel app!  

Filtering with PropertyTypeId

Lets wrap this up by declaring the parent’s filterAmenity() method called from above. We declare it as a public method in our filter-section component’s class:

/* app/Livewire/FilterSection.php */

public function filterAmenity( $propertyTypeId )
{
    $this->filters['amenity'] = [
        'value'=>0,
        'label'=>'AMENITIES',
        'options'=>Amenity::filterPropertyType( $propertyTypeId )
        ->get()->toArray()
    ];
}

The function simply receives a $propertyTypeId, and uses it to update the options of the amenities filter item. Save the changes, and see our dropdowns in action! Property Type filters the Amenities dropdown

The Takeaway

When introducing dependency across dropdowns that use the same, re-usable component, there are three general questions that need to be answered:

  1. How can we listen to change from a specific dropdown?
  2. How can we communicate this change to another specific dropdown?
  3. How can this change be reflected in the UI?

The solution we used today relied on a data manager component to communicate dependency among our dropdowns. And with Livewire, we were able to easily implement this solution.

We combined Livewire’s in built Alpine x-on:change, and $wire object to listen for changes from a specific dropdown ( answering Q#1 ). Finally, we accessed the $parent object from our child component to call its parent’s method to filter the data for the dependent “Amenities” dropdown( answering Q#2! ).

With Livewire, we didn’t even have to worry about the third question! We just had to provide the proper always-unique :key value and Livewire handled everything for our Amenities dropdown to reflect its brand new, filtered options.

All this in a few lines of code. Amazing