Livewire v3: Modelable, Events, and Data Sharing

A bunch of Livewire-alien like creatures sharing strawberries and floating. Below them is a bunch of strawberry houses!
Image by Annie Ruygt

Need a place for your Laravel app in the cloud? Fly it with Fly.io, it’ll be up and running in minutes!

Oftentimes, we use drop-downs in combination with other html elements to make our website interactive.

One kind of interaction is inter-dependency amongst several drop-downs. This happens when changing a selected option of one drop-down affects the choices given for another drop-down. In our last article, we created dynamic, inter-dependent drop-downs. These drop-downs were dynamically instantiable as they all used one Livewire component as template( named filter component ). Then, to allow inter-dependency among the drop-downs, we created another component called filter-section to manage data and data-dependency for each filter drop-down instance.

Another kind of interaction involves the drop-downs affecting other html elements, like say, filtering data on a table.

Of course, given the nature of dynamically instantiated elements, we need to be able to uniquely identify and track each drop-down instantiated. Furthermore, if our table element is found in a separate Livewire component, we need to find a way to share the data from each filter drop-down to the table component.

Today, we’ll get around this data sharing easily with Livewire Modelable and client-side dispatched events.

—Let’s get to it!

Dynamic Drop-Down Set Up

Let’s say we use a filter-section component to manage a bunch of drop-downs. It would have a public attribute $filters that contains an array of data which we’ll use to initialize a bunch of drop-downs:

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

Notice however that each item initializes another Livewire component called filter. This component contains the template we use to create our drop-downs:

<!-- 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>

Let’s say that the $filters array contains three drop-down data: “Offer Type”, “Property Type”, and “Sub Property Type”. We should get a view like so: A filter section containing a button labeled "Apply", and three drop-downs: "Offer Type", "Property Type", "Sub Property Type"

We’re going to use these drop-downs to show filtered results in a table. The first thing we need to do, then, is to get the selected option of each filter drop-down.

How exactly do we do that?

Uniquely Identifying Drop-Downs

In our setup, we have a parent component filter-section, and it dynamically initializes a bunch of child filter components through an array of data. Each filter component instance contains a drop-down element.

If we want to get the selected option for each drop-down, we’ll have to of course, uniquely identify each one. So, first, make sure that the $filters array includes a unique key value for each drop-down data. Then, we’ll need a ‘value’ attribute that will represent the option selected for each drop-down:

public function mount()
{
  $this->filters = [
+       'offer_type'=>[     // Unique key
            'label'=> 'OFFER TYPE',
            'options'=> OfferType::all()->toArray(),
+           'value' => 0    // Value attr
        ],
+        'property_type'=>[  // Unique key
            'label'=>'PROPERTY TYPE',
            'options'=> PropertyType::all()->toArray(),
+           'value' => 0  // Value attr
        ],
+        'sub_property_type'=>[ // Unique key
            'label'=>'SUB PROPERTY TYPE',
            'options'=> SubPropertyType::all()->toArray(),
+           'value'=>0   // Value attr
        ]
  ];
}

With this setup, we can refer to each drop-down’s selected value through the attribute $filters.[drop_down_key].value.

Now that we have an attribute name to identify each drop-down value, the next step is to link each attribute to its respective drop-down element in our view. In Livewire, we link server attributes with elements in the blade through the use of wire:model directives:

<!-- resources/livewire/filter-section.blade.php -->

@foreach( $filters as $key=> $filter )
  <livewire:filter
    ...
+   :wire:model="'filters.'.$key.'.value'"
  />
@endforeach     

Usually, that’s how we would link attributes to html elements. But notice that filter-section only has direct access to its filter child instances and not the drop-down elements we want to wire to.

This syntax then won’t work alone. Because, after all, how would Livewire know which element in the filter component’s blade should it wire an attribute with?

Enter Modelable: Linking Parent and Child Attributes

Livewire provides us with its Modelable attribute feature. It allows us to match a parent wire:model with any of its child components’ attributes, and ultimately, that child component’s html element.

With it, we can have attributes in our parent component in sync with child components’ attributes!

Applying this directive is just a two-step process. The first step we’ve already done above, which is wiring a specific filter-section‘s attribute to the instance of its filter child component.

Next, we need to let the filter component know where to map the wired model it received. We can do this by attaching “Modelable” to the attribute we want to link with. Since our filter component has $value wired to its select element, we want to specifically attach to this attribute:

/* app/Livewire/Filter.php */

+  use Livewire\Attributes\Modelable;

class Filter extends Component
{
+  #[Modelable] 
   public $value = '';

And that’s it! Now that we’ve attached Modelable to $value and wired it to the parent’s $filters.[drop_down_key].value attribute, any changes to $value would also be reflected in its corresponding $filters.[drop_down_key].value, thereby giving filter-section access to the drop-down values found in each instantiation of filter child components.

Neat, right?

Let’s test that out with a little bit of Alpine and console logging. From our parent component, filter-section, we have an Apply button. When the user clicks on this button, we can log the value of a specific drop-down:

<button
  x-on:click="console.log( $wire.filters.sub_property_type.value )"
>

Selecting different options in "Sub Property Type" drop-down and clicking on the Apply button logs the selected values' id in the browser console. And…tada! Clicking on the Apply button shows us the value of our intended drop-down!

Sharing Data Outside

Now that we have a way to get each selected drop-down data, let’s finish this by sharing them to another component — our table component.

The table component will make use of Livewire’s WithPagination trait to display rows of our data. And to allow filtering, we’ll also add in a public attribute $filterList which we’ll pass to the pagination query:

/* app/Livewire/Table.php */
use Livewire\WithPagination;

class Table extends Component
{
  use WithPagination;

  public $filters = [];

  public function render()
  {
      return view('livewire.property-table',[
          'rows'=>Property::filter( $this->filters )->paginate(10)
      ]);
  }
}

Then, with the data passed through the $rows variable in render(), we create the view which includes the data and pagination links:

<!-- resources/views/livewire/table.blade.php -->
<table>
    <thead>
        <tr>
            <th class="bg-blue-100 border text-left px-8 py-4">Name</th>
            <th class="bg-blue-100 border text-left px-8 py-4">Price</th>
        </tr>
    </thead> 
    <tbody>
        @foreach( $rows as $row )
          <tr>
              <td>{{$row['name']}}</td>
              <td>{{$row['price']}}</td>
          </tr>
        @endforeach
    </tbody>
    {{ $rows->links() }}
</table>

Now if we declare this table component in our main page, we should finally get a complete setup like so: A page that contain a filters section with three dropdowns, and a tabletion below showing 10 out of 30 rows of data.

The final question in this article we’ll be answering is, how do we pass data from one component, to another?

Client-Dispatched Events

To share data from one component to another, we usually dispatch events in Livewire. Livewire components can then listen to these events and react accordingly.

To immediately and thriftily dispatch an event, we’ll dispatch one in our blade template.

When the user clicks on the Apply button in our filter-section component, we want to take the selected drop-downs data it has access to, and share these with the table component.

To do so, we can dispatch an event containing each drop-down’s selected value from a button click:

<!-- resources/views/livewire/filter-section.blade.php -->

<button 
  wire:click="$dispatch('apply-filter', { 
    offer_type_id: $wire.filters.offer_type.value,
    property_type_id: $wire.filters.property_type.value,
    sub_property_type_id: $wire.filters.sub_property_type.value,
  })"
>Apply</button>

Since this apply-filter event is available on our entire page, we can listen to this from our table component’s view( or any other component really! ) and react.

We can take the selected drop-down data from event.detail, and directly update the $filtersList variable in our table component with it:

<script>
window.addEventListener('apply-filter', event => {
    console.log( 'received event from apply filter in table!', event.detail );
+   @this.set( 'filtersList', event.detail );
});
</script>

And that’s it! We’ve shared our selected drop-down values with the table component!

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!  

Once Livewire updates the $filtersList attribute in the server using @this.set(), Livewire would afterward call the render() method of table component due to an update to its public attribute.

This time, the $filtersList will contain data the user selected, and will now be used to filter the rows for our table! Clicking on the Apply button triggers a change in the rows of data contained in the table.