Essentially I need to be able to emitTo() an instance of a component rather than the whole component
Livewire + Alpine optimisation
I have a component that displays the title of a model, then when you click on the button it switches to a form with one field for the title. This component is intended to be displayed for a collection of models. It does the job but I have some queries.
-
For all the functions that are called as a result of events, the only way I could manage to isolate the component that needs it, is to have a check for $model_id which means there is still a request for every component. Is there a better way to do this? I have read the docs.
-
minor issue. I would like the form to be submitted by clicking away from the component (ie. no submit button). The current implementation works with wire:model.lazy="title" and having the updatedTitle() listen for it, however I can't call updatedTitle() in my pest tests. again, I wonder if there is a better way to do this? I'm using filament for the forms trait on the component
// HeadingForm.php
<?php
namespace App\Http\Livewire;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Actions\Action;
use Filament\Notifications\Notification;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\ValidationException;
use Livewire\Component;
class HeadingForm extends Component implements HasForms
{
use InteractsWithForms;
public Model $model;
protected $listeners = [
'deleteAction' => 'deleteModel',
'recoverAction' => 'recover',
'focus',
];
public function mount(): void
{
$this->form->fill([
'title' => $this->model->title,
]);
}
public function recover($model_id): void
{
if ($this->model->id === $model_id) {
$this->mount();
$this->focus($model_id);
}
}
public function focus($model_id): void
{
if ($this->model->id === $model_id) {
$this->emitSelf('focusInput');
}
}
public function deleteModel($model_id): void
{
if ($this->model->id === $model_id) {
// parent component needs a refreshComponent listener
$this->emitUp('refreshComponent');
$this->model->delete();
}
}
// magic method that listens for changes on $this->title
public function updatedTitle(): void
{
$this->model->update($this->form->getState());
}
public function render()
{
return view('livewire.heading-form');
}
protected function getFormSchema(): array
{
return [
TextInput::make('title')
->required()
->maxLength(255)
->label(false)
->validationAttribute(
str(class_basename($this->model))->headline().' Title'
),
];
}
protected function onValidationError(ValidationException $exception): void
{
$error_msg = $exception->getMessage();
// HACK: find a better way to detect type of exception
if (! strstr($error_msg, 'required')) {
Notification::make()
->title($exception->getMessage())
->danger()
->send();
return;
}
// actionable notification if user has left the title field empty
// ie. if exception contains 'required' message
Notification::make()
->title($error_msg)
->body(
"Did you mean to delete <b>{$this->model->title}</b>? \n \
This action will remove all related records"
)
->actions([
Action::make('delete')
->button()
->color('danger')
// NOTE: can't emitSelf as Notification is seperated component
->emitTo(
'heading-form',
'deleteAction',
[$this->model->id],
)
->close(),
Action::make('recover')
->emitTo(
'heading-form',
'recoverAction',
[$this->model->id],
)
->close(),
])
->danger()
->persistent()
->send();
}
}
// heading-form.blade.php
<x-filament::card x-data="{
form: false,
cardWidth: 0,
limitTitle: function(title) {
const limit = this.cardWidth / 10;
return title.length > limit ? title.slice(0, limit).trim() + '...' : title;
}
}">
<form x-bind:hidden="form === false" x-show="form" @click.outside="form = false" wire:model.lazy="title"
x-init="() => { $wire.on('focusInput', () => { form = true }) }" x-trap.inert="form">
{{ $this->form }}
</form>
<div x-show="!form" class="flex items-center justify-between" x-init="cardWidth = $el.parentElement.offsetWidth"
x-on:resize.window="cardWidth = $el.offsetWidth">
<h3 x-text="limitTitle('{{ $model->title }}')"></h3>
<button
class="relative flex items-center justify-center rounded-full outline-none hover:bg-gray-500/5 text-primary-500 focus:bg-primary-500/10 h-8 w-8"
@click="form = true" type="button">
<x-feathericon-edit-2 class="w-4 h-4" />
</button>
</div>
</x-filament::card>
// calling the component example
@forelse($posts as $post)
<livewire:heading-form :model="$post" :wire:key="$post->id"/>
@empty
No Posts
@endforelse
Please or to participate in this conversation.