Checkboxes, and Streamed updates with Livewire

Checkboxes fly across the sky as a Livewire alien carries them through threads, as if the checkboxes were kites.
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!

Imagine having a table with ten rows. Imagine even more, having a “Bulk Action” button above this table. When the user selects all ten rows from the table and clicks on the button, all ten rows are sent to the server for bulk processing.

Each row is processed one after the other, until finally, all rows complete and the server responds back to the client. This setup means that even though a portion of the group might’ve already completed its processing, this portion will not be updated as “processed” in the UI just yet—not until all other remaining parts of the group complete as well.

Well, what if, we want to progressively update the UI as each row completes? Do we create a new connection to poll for and reflect updates? Or do we create a web socket connection to listen for this change continuously?

Not quite! We don’t have to do any of the two above. Cause here comes Livewire’s wire:stream directive! Our ones-top directive for progressively listening and reflecting changes from the server.

Let’s get to it!

What We’ll Do

To showcase Livewire’s wire:stream directive, today, we’ll make use of a table displaying rows of “properties for sale”. Each row will have a corresponding checkbox a user can use to select different properties, and a button to send selected rows to the server for bulk processing.

Finally, we’ll make use of Livewire’s wire:stream directive to update our UI as each row completes processing, showing as each row goes through “WIP” and finally “Processed”—all this with only one request to the server.

Set Up

Start with creating a Livewire component with php artisan livewire:make my-table. This component’s view will contain a table, a bunch of rows, and a checkbox per row:

<div>
    <table>
        <thead>
            <tr>
              <th></th>
              <th>Name</th>
              <th>Processed</th>
            </tr>
        </thead> 
        <tbody>
            <!-- List real estate properties -->
            @foreach( $properties as $prop )
            <tr>
                <!-- Yes Checkbox for each row! -->
                <td><input type="checkbox"></td>
                <td>{{$prop['name']}}</td>
                <td> TBD </td>
            </tr>
            @endforeach
        </tbody>
         {{ $properties->links() }}

It’ll also include a button to trigger a method called process() in the component class:

      <button wire:click="process">Process</button>
    </table>   
</div>

Then, in its class, we pass $properties containing row data to the view, and declare process() as a public method:

public class MyTable
{
  // The method to process our rows
  public function process(){}

  public function render()
  {
      // Pass properties for sale data to the view
      return view('livewire.my-table',[
          'properties'=>
            Property::paginate(10)
      ]);
  }
}

And now we have our table, checkboxed rows, and Process button! A table contains ten rows of data showing 10 different properties for sale. The table contains 3 columns, the first column a checkbox for each row, the second a name for each row, and the third a verified status for each row.

Wiring Checkboxes

Our table has a bunch of checkboxes, one for each row of data in the $properties array. Each checkbox represents its row’s ID, retrieved from $prop->id.

When the user clicks on the “Process” button, we want to send every selected checkbox’s ID to the server. So, in the component’s class, we’ll declare a public array public $ids = [];. Then, we’ll wire each checkbox to the array’s index at $prop->id—the checkbox’s ID:

We’ll use the row’s id, “$prop->id” to uniquely identify each checkbox.

@foreach( $properties as $prop )
  <input type="checkbox"
+    wire:model="ids.{{$prop->id}}" >

With the setup above, if we were to select any checkbox, and do a dd( $this->ids ) in the process() method, we’ll get ids as an array of trues, with each index representing an ID of a checkbox checked by the user:

array:2 [ // app/Livewire/MyTable.php
  11 => true // 11 and 12 as selected ids 
  12 => true
]

Streaming Updates with wire:stream

Now that we’re able to get the IDs of each row a user selects, it’s time to process these, and show in the UI as each row goes through the different statuses of processing: “WIP”, and finally, “Processed”.

First, we’ll have to associate a status attribute per item in the $ids array. At the current structure of this array, however, we can’t simply add a status attribute an item. We’ll have to first revise the array’s structure so it can hold an array of key-value pairs.

We can do this re-structuring by revising the wiring of each row:

@foreach( $properties as $prop )
  <input type="checkbox"
+    wire:model="ids.{{$prop->id}}.checked" >

Notice we simply add a checked attribute. This will assign an array of key-value pairs to each row, the first item key being the checked key:

// New structure!
array:2 [ // app/Livewire/MyTable.php:14
  11 => ['checked'=>true],
  12 => ['checked'=>true]
]

This structure will now allow us to add other attributes later on, just like the status attribute we’ve mentioned above.

Next, let’s check out how we can show progress as each row completes processing.

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!  

Streaming with wire:stream

Livewire v3 now offers the incredible wire:stream directive. We can “stream” progress as each row completes processing.

In fact, we can even stream a status even before we start processing an item. We can say a row is WIP by updating $this->ids[]['status'] to “WIP”, then “streaming” this update to the client:

to represents the wire:stream block we want to stream the update to,

content represents the public attribute we want to show the update of,

and replace tells Livewire that we want to replace the previous content

foreach( $this->ids as $id=>$idDetails ){

     // Update status to WIP
    $this->ids[ $id ][ 'status'] = 'WIP';

    // Stream this change to the client
    $this->stream(
        to: 'process-stream'.$id,
        content:  $this->ids[ $id ][ 'status'],
        replace: true 
    );

    // Processing might take 3 seconds...
    sleep( 3 ); 
}

This stream can now be received in the view by setting up a matching receiver:

@foreach( $properties as $prop )
  <tr>
      <td>
+     <td wire:stream="process-stream.{{ $prop->id }}">
+        {{ $content }} 
      </td>
  </tr>
  @endforeach

Then, once an item completes processing, we can update the status attribute to “Processed” and finally stream this change to the UI again:

foreach( $this->ids as $id=>$idDetails ){
    // Before processing stream here...

    // Processing takes up to 3 seconds
    sleep(3);

    // Completed update and stream
    $this->ids[ $id ][ 'status' ] = 'Processed!';

+    $this->stream(
+      to: 'process-stream'.$id,
+      content: $this->ids[$id]['status'],
+      replace: true
+    );
}

The user selects the first, second, and fourth rows' checkboxes and clicks on the "Process" button. The first row immediately gets a status "WIP" which turns to "Process". Afterwards, the second row's status gets updated to "WIP", then "Processed". Until finally, the fourth row gets updated to "WIP", then "Processed".

And that’s it! We now have a bunch of selectable rows, we can send them to the server for processing, and we get to update each row in the client table, real time!