I’m creating an admin panel using the TALL stack. I’ve tried abstracting Index pages to one abstract component and view. It worked for the most part but I’ve run into a weird issue.
In the console log I keep getting Alpine Expression Error: <name> is not defined
when trying to wire:model the filters, even though the attributes are definitely there.
Abstract component:
<?php
namespace AppLivewireAdmin;
use AppModelsModel;
use IlluminatePaginationLengthAwarePaginator;
use IlluminateViewView;
use LivewireAttributesComputed;
use LivewireComponent;
use LivewireAttributesLayout;
use LivewireAttributesUrl;
use LivewireFeaturesSupportAttributesAttributeCollection;
use LivewireWithPagination;
#[Layout('components.layouts.admin')]
abstract class AbstractIndex extends Component
{
use WithPagination;
protected LengthAwarePaginator $data;
protected string $view;
protected string $model;
public function clear(): void
{
foreach ($this->urlAttributes() as $_attribute) {
$this->{$_attribute->getName()} = null;
}
$this->search();
}
public function createRouteAttributes(): array
{
return [];
}
public function delete(int $id): void
{
$model = $this->model::findOrFail($id);
$model->delete();
}
public function getValue(Model $model, array $column): mixed
{
return $model->{$column['name']};
}
public function render(): View
{
$this->search();
return view('livewire.admin.abstract-index', [
'data' => $this->data
])
->title($this->title);
}
public function search(): void
{
$data = $this->model::query();
foreach ($this->urlAttributes() as $_attribute) {
if (!empty($_attribute->getValue())) {
if (preg_match('/(.*?)([A-Z].*?)/', $_attribute->getName(), $matches)) {
dd($matches);
}
$data->where($_attribute->getName(), 'LIKE', '%' . $_attribute->getValue() . '%');
}
}
$this->data = $data->paginate();
}
/**
* @return AttributeCollection<int, Url>
*/
#[Computed(persist: true)]
public function urlAttributes(): AttributeCollection
{
return $this->getAttributes()
->filter(fn($_value, $_key) => is_a($_value, Url::class, true));
}
#[Computed(persist: true)]
public function urlAttributesNames(): array
{
return $this->urlAttributes()
->map(fn($_value) => $_value->getName())
->toArray();
}
}
Specific component:
<?php
namespace AppLivewireAdminReleases;
use AppLivewireAdminAbstractIndex;
use AppModelsModel;
use AppModelsRelease;
use LivewireAttributesUrl;
class Index extends AbstractIndex
{
#[Url]
public ?string $name = null;
#[Url]
public ?string $type = null;
#[Url]
public ?string $date = null;
protected string $model = Release::class;
public string $createRoute;
public string $title = 'Releases Index';
public array $columns = [
[
'name' => 'id',
'label' => 'ID',
],
[
'name' => 'name',
'label' => 'Name',
],
[
'name' => 'type',
'label' => 'Type',
],
[
'name' => 'date',
'label' => 'Date',
'filter_type' => 'date',
],
[
'name' => 'downloads',
'label' => 'Downloads',
],
];
public string $editRoute;
public bool $allowDelete = true;
public function editRouteAttributes(Release $release): array
{
return [
'release' => $release,
];
}
public function getValue(Model $model, array $column): mixed
{
if ($column['name'] === 'date') {
return $model->date?->format('jS F Y');
}
return parent::getValue($model, $column);
}
public function mount()
{
$this->createRoute = 'admin.releases.create';
$this->editRoute = 'admin.releases.edit';
}
}
Abstract view:
<div>
<div class="flex justify-between">
<h1 class="text-2xl mb-4">{{ $this->title }}</h1>
@isset($createRoute)
<div>
<x-admin.button href="{{ route($createRoute, $this->createRouteAttributes()) }}" wire:navigate
style="primary">Create</x-admin.button>
</div>
@endisset
</div>
<table class="border w-full rounded-lg border-separate border-spacing-0">
<thead>
<tr class="*:text-left">
@foreach ($columns as $_column)
<th class="border-r">
@if (in_array($_column['name'], $this->urlAttributesNames))
<input type="{{ $_column['filter_type'] ?? 'text' }}" name="{{ $_column['name'] }}"
:wire:model="{{ $_column['name'] }}" wire:keydown.enter="search"
class="w-full leading-9 text-neutral-800">
@endif
</th>
@endforeach
<th class="flex w-full gap-x-1 p-2">
<x-admin.button wire:click="search" class="bi-search grow" style="secondary" />
<x-admin.button wire:click="clear" class="bi-x-lg" style="danger" />
</th>
</tr>
<tr class="*:border-t *:text-left *:p-2">
@foreach ($columns as $_column)
<th class="border-r">
{{ $_column['label'] }}
</th>
@endforeach
<th class="w-[85px]">Actions</th>
</tr>
</thead>
<tbody class="rounded-b-lg">
@foreach ($data as $_row)
@php $parentLoopLast = $loop->last; @endphp
<tr @class([
'*:border-t *:p-2',
'bg-neutral-700' => $loop->odd,
'rounded-b-lg' => $loop->last,
])>
@foreach ($columns as $_column)
<td @class([
'border-r',
'rounded-bl-lg' => $loop->first && $parentLoopLast,
'rounded-br-lg' =>
$loop->last && $parentLoopLast && !isset($editRoute) && !$allowDelete,
])>
{{ $this->getValue($_row, $_column) }}
</td>
@endforeach
@if (isset($editRoute) || $allowDelete)
<td @class(['rounded-br-lg' => $parentLoopLast])>
@isset($editRoute)
<x-admin.button href="{{ route($editRoute, $this->editRouteAttributes($_row)) }}"
style="primary" wire:navigate class="bi-pencil-square" />
@endisset
@if ($allowDelete)
<x-admin.button wire:click="delete({{ $_row->id }})" style="danger"
wire:confirm.prompt="Are you sure?nnType DELETE to confirm|DELETE"
class="bi-trash" />
@endif
</td>
@endif
</tr>
@endforeach
</tbody>
</table>
{{ $data->links() }}
</div>
The problem is caused specifically by :wire:model="{{ $_column['name'] }}"
I’ve tried dumping $type
and $this->type
in the view, they’re both there. Something interesting I’ve noticed is that out of the three filterable columns (name, type, date) the error only happens for the latter two, name is apparently fine?
I guess an alternative would be to just use JS for this but I would like to use the nice wire:model syntax if possible!
user24994274 is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.