DEV Community

Victor R.
Victor R.

Posted on

Dynamic Livewire Listeners

The challenge: We have an unusually long list of items, displayed as cards on a single page. We are using a modal to update those items. On save, we only want that specific card to re-render. We don't want nor need the whole list page (parent component to re-render).

There are just a few livewire components in play here:

  • The Index component, which hosts the list of $items using a @foreach, and displays an edit button next to each card
  • The Card component, which is passed the $item and displays the $item information in the card, as well as timestamp of last render
  • The Edit component, a modal which when triggered is passed an $item to update, and then emits a change event when $item is saved.

The App

Image description

The Index component code

class Index extends Component
{
    public $tasks;

    public function render()
    {
        $this->tasks = Task::all();
        return view('livewire.index');
    }
}
Enter fullscreen mode Exit fullscreen mode

The Index component blade

<div class="py-10 w-full flex
    justify-center min-h-screen items-center
    bg-black text-white
    text-sm"
    <div class="w-full">
        @foreach($tasks as $task)
            <div wire:key="task-{{$loop->index}}">
                <livewire:task.card :task="$task"/>
                <button 
       id="complete-{{$loop->index}}"
       wire:click="$emitTo('task.edit', 'show', {{$task->id}})"> 
                   Edit
                 </button>
            </div>
        @endforeach
    </div>
    <livewire:task.edit />
</div>
Enter fullscreen mode Exit fullscreen mode

The Edit component code

class Edit extends Component
{
    protected $listeners = ['show'];
    public $task;
    public $showing = false;

    protected $rules = [
        'task.status' => 'required',
        'task.name' => 'required|string|max:255'
    ];

    public function show(Task $task){
        $this->task = $task;
        $this->showing = true;
    }

    public function render(){
        return view('livewire.task.edit');
    }

    public function save(){
        $this->validate();
        $this->task->save();
        $this->showing = false;

        //tell card to refresh
        $this->emit('update-task-' . $this->task->id);
    }
}
Enter fullscreen mode Exit fullscreen mode

The Edit component blade

<div class="{{$showing? '' : 'hidden'}}"
    wire:click="$set('showing',false)">
    @if($task)
     <form wire:click.stop>
        <div>
            Task
            <input type="text" wire:model="task.name">
        </div>
        <div>Status
            <select wire:model="task.status">
             <option value="Delayed">Delay</option>
             <option value="Completed">Complete</option>
             <option value="Pending">Not Started</option>
             <option value="Canceled">Cancel</option>
            </select>
        </div>
        <div class="w-full text-right">
            <button 
            wire:click.prevent="$set('showing', false)"
            >
            Cancel
        </button>
        <button wire:click.prevent="save">
          Save
        </button>
        </div>
     </form>
    @endif
</div>
Enter fullscreen mode Exit fullscreen mode

Dynamic Livewire Listeners

There's nothing unusual in the above code, the modal hide/show is pretty standard, and we have the Edit button emit an event that populates the Edit component with a specific task ands unhides the modal. For brevity I removed the tailwind classes, so this will look a little weird, but it works.

Instead of using AlpineJS to show hide I just went with standard blade/livewire. The part I want to draw attention to in the above code snippets is the last line of the save function in the Edit Component:

$this->emit('update-task-' . $this->task->id);
Enter fullscreen mode Exit fullscreen mode

Here we are emitting an event that is very specific to just this $task. Now let's look at the code behind of the Card component:

class Card extends Component
{
    public $listeners = []; // must be public!
    public $task;

    public function mount($task){
      // add listener specific to this task only.
      $this->listeners =["update-task-" . $task->id =>"updateTask"];  
    }

    public function updateTask(){
        // calling this triggers render, 
        // no code needed here
    }

    public function render(){
        return view('livewire.task.card');
    }
}
Enter fullscreen mode Exit fullscreen mode

Here I've added a mount() function that adds a $task->id specific listener, and points to an empty function. Now, when a $task is edited and saved, ONLY this Card instance will be listening for that specific event, and only this card will refresh, the rest of the Index page will stay unaltered.

Source code for this project can be found (using Laravel 9, PHP 8.1, Tailwind 2) here

Latest comments (1)

Collapse
 
seongbae profile image
Seong Bae • Edited

Thank you for sharing. I was stuck with creating dynamic listeners in Livewire and this post helped me generate some ideas. In my case, I'm trying to update Livewire page content based on events generated by Laravel Echo.