Filament Custom Field - Livewire still updating despite wire:ignore
Hey all,
I've been working on a fairly complex custom Filament form field and am so close to the finish line, but need a little bit of help. My app is to manage products, and a vendor can log in and submit a batch of new products in the form of a CSV upload. That is handled through a NewProductSubmission model that has a hasMany relationship products. A superadmin then needs to review, make edits/comments, and approve that submission. So I have built a ProductTable form field that is basically an editable spreadsheet where each row is a product and each column is one of the product parameters. Because some of these submissions have thousands of products, I've reached for the Tanstack table and virtual packages and virtualized the rows. Doing so SIGNIFICANTLY improved performance up to about 2000 rows. However, when going over that, I'm still having issues with major scroll lag that sometimes crashes the page. When I run the performance profiler, I see that two livewire functions, one just called "on" and the other an anonymous function, are causing major lag. In an attempt to address this, I added wire:ignore to my scrollable table component and set my inputs to imperatively update the component state on blur and did away with entangling my state. However, even after doing this, those Livewire functions still run. My intent is to essentially stop Livewire from watching that table altogether, and to only update its state when the updateProductState function is called on input blur. What am I doing wrong here?
class ProductTable extends Field
{
protected string $view = 'forms.components.product-table';
protected function setUp(): void
{
parent::setUp();
$this->afterStateHydrated(function ($component, $state, $record) {
$component->state($record->products()->with('prices')->get()->toArray());
});
$this->registerListeners([
'updateProduct' => [
function ($component, $data) {
$state = $this->getState();
$state[$data['index']][$data['col']] = $data['value'];
$component->state($state);
}
],
'createComment' => [
function ($component, $comment, $column, $product_id) {
$comment = $this->getRecord()->comments()->create([
'body' => $comment,
'column_name' => $column,
'product_id' => $product_id,
'author_id' => Auth::user()->id
]);
$user = User::find($this->getRecord()->author_id);
$user->notify(new CommentAdded($this->getRecord()->id));
}
]
]);
}
}
<!-- product-table.blade.php -->
<div
wire:ignore
x-ignore
ax-load
ax-load-src="{{ FilamentAsset::getAlpineComponentSrc('product-table') }}"
x-data="productTable({
state: data,
categories: categories,
manufacturers: manufacturers,
comments: comments,
user: user,
wire: $wire
})"
>
<div x-ref="container" style="height: 600px; overflow-y: auto; overflow-x: scroll;">
<table :style="{height: `${virtualizer?.getTotalSize()}px`}">
<thead>
<tr>
<template x-for="(header, index) in headerGroups[0]?.headers" :key="header.id">
<th x-text="header.column.columnDef.header"></th>
</template>
</tr>
</thead>
<tbody>
<template x-for="item in virtualRows" :key="item.index">
<tr
:data-row-index="item.index"
:style="{position: 'absolute', width: '100%', transform: `translateY(${item.start}px)`}"
>
<template x-for="(cell, index) in item.row.getVisibleCells()" :key="cell.id">
<td
:data-col-id="cell.column.id"
@click="currentCell = { row: item.index, col: cell.column.id }"
>
<input
:data-type="cell.column.columnDef.meta.inputType"
x-model="state[item.index][cell.column.id]"
:data-row="item.index"
:data-col="cell.column.id"
x-on:blur="updateProductState"
/>
</td>
</template>
</tr>
</template>
</tbody>
</table>
</div>
</div>
// product-table.js
import {createTable, getCoreRowModel} from '@tanstack/table-core';
import {Virtualizer} from '@tanstack/virtual-core'
export default function productTable({ state, comments, categories, user, manufacturers, wire }) {
return {
state,
comments,
user,
categories,
manufacturers,
currentCell: { row: 0, col: "upc" },
table: null,
headerGroups: [],
rows: [],
virtualRows: [],
virtualizer: null,
init() {
const productsWithCategories = this.state.map((product) => {
// ... product data transformation logic
});
this.state = productsWithCategories;
this.table = createTable({
data: state,
columns: [
...productTableColumns.slice(1).map(c => ({
accessorKey: c.id,
header: c.label,
id: c.id,
meta: { inputType: c.inputType }
}))
],
getCoreRowModel: getCoreRowModel(),
});
this.headerGroups = this.table.getHeaderGroups();
this.rows = this.table.getRowModel().rows;
const scrollContainer = this.$refs.container;
this.virtualizer = new Virtualizer({
count: this.rows.length,
getScrollElement: () => scrollContainer,
estimateSize: () => 35,
overscan: 10,
onChange: () => {
this.updateRows();
}
});
},
updateRows() {
this.virtualRows = this.virtualizer?.getVirtualItems()?.map(r => ({
...r,
row: this.table?.getRow(r.index)
}));
},
async updateProductState(e) {
wire.dispatchFormEvent('updateProduct', {
value: e.target.value,
index: e.target.dataset.row,
col: e.target.dataset.col
});
},
async dispatchComment() {
wire.dispatchFormEvent(
"createComment",
this.commentField,
this.currentCell.col,
this.state[this.currentCell.row].id,
);
// ... comment handling logic
},
// Other methods...
};
}
Any insight would be very much appreciated. Thanks!
Please or to participate in this conversation.