I have a problem with AlpineJS in my complicated app, and I'm finding it very difficult to replicate under a simplified example. This may well mean that it's a bug in Alpine, but I'll ask here for help on the off-chance anyway. I've tried to reduce the code below to only the bare essentials that are necessary to explain the problem, and doing so may have lead to some typos. Therefore please excuse me in advance for any errors that are not related to the problem itself.
I'm using Livewire to synch data between my PHP classes and my AlpineJS front-end. The two variables that are relevant in the PHP class are:
public $colOrder; // users are able to "re-order" columns on their table-view. This preference is saved into their profile and stored in this variable as a 1D array of the column-IDs
public $datasourceData; // contains a 2D data that is pulled from a database with: Model->get()->toArray(); [0 => ['col1'=>'data1,1', 'col2'=>'data1,2'], 1 => ['col1'=>'data2,1', 'col2'=>'data2,2']];
These arrays are then entangled with Alpine variables, and the template is generated from those arrays of data as follows. Ostensibly, this template works fine:
<div x-data="{
eColOrder: @entangle('colOrder').defer,
eData: @entangle('datasourceData').defer
}">
<table class="table" x-cloak>
<thead>
<tr>
<template x-for="(col, ix) in eColOrder" :key="'th-'+ix">
<th x-text="col"></th>
</template>
</tr>
</thead>
<tbody>
<template x-if="eData.length==0">
<tr>
<td :colspan="eColOrder.length" style="padding: 1em">No data found</td>
</tr>
</template>
<template x-if="eData.length>0">
<template x-for="(rec, ix) in eData" :key="'row-'+ix">
<tr>
<td class="action"></td>
<template x-for="(col, pos) in eColOrder" :key="'td-'+ix+'-'+pos">
<td x-text="rec[col]"></td> <!-- I also tried `eData[ix][col]`, but it produced errors in the browser console, even though the on-screen display was fine -->
</template>
</tr>
</template>
</template>
</tbody>
</table>
In this screen-shot, you can see that the user's search (in the top-row) has produced a tabulated grid of data below. Happy days.
The problem arises when the user re-submits a different search. They do so by updating the search fields, and pressing the "search" button again. This re-submits the search (through a Livewire JSON call), refreshing the $datasourceData
array with new data, entangling itself with the eData
variable in Alpine, and producing the following result:
What appears to be happening, is that the results of the new search are correctly pulled through. But for whatever reason, Alpine hasn't cleared the screen from the last set of search-results. It's interesting that only the data-level of the HTML table is corrupted (that is to say, the <td>
cells). Note that the <th>
cells have (correctly) not been duplicated above the right-hand-half of the new table.
I've debugged and checked that the data returned from the Eloquent models is correct, and that the structure of the data in the entangled JavaScript variable eData
is also correct. This problem is not data-related, it's the rendering that's at fault.
My gut-feel is that this is an Alpine bug, but I haven't been able to prove it yet.
My problem stops there. However, in an attempt to replicate the issue and narrow down the cause of the issue, what I've done is to create a simplified Livewire/Blade/Alpine page. Strictly speaking, I wasn't able to replicate the problem there either directly, but I did (accidentally) manage to replicate a similar output when I entered a deliberate "bug" into my code.
Take the following PHP/Livewire component:
<?php
namespace App\Business\Tbd;
use Livewire\Component;
class StartLw extends Component
{
public array $data = [];
public array $headings = [];
public int $count = 0;
public function mount() {
for ($i=1; $i <= 6; $i++) {
$this->headings[] = "col{$i}";
}
$this->data = [];
}
public function formSubmit() {
$src = 1;
$this->data = [];
for ($i=0; $i < 10; $i++) {
$this->data[$i] = [];
for ($y=1; $y <= 6; $y++) {
$this->data[$i]["col{$y}"] = "source {$src} ({$i},{$y})";
}
}
$this->count++;
}
public function relatedToButSeparateFromForm() {
$src = 2;
$this->data = [];
for ($i=0; $i < 4; $i++) {
$this->data[$i] = [];
for ($y=1; $y <= 6; $y++) {
$this->data[$i]["col{$y}"] = "source {$src} ({$i},{$y})";
}
}
$this->count++;
}
public function render()
{
return view('components.tbd.lw-start-lw')
->layout('layouts.tbd.lw');
}
}
And this cut-down HTML to render the page:
<div class="container" x-data="{
eData: @entangle('data').defer,
eHeadings: @entangle('headings').defer
}">
<div class="row">
<div class="col"><p>{{ $count }}</p></div>
</div>
<div class="row">
<div class="col">
<form method="post" wire:submit.prevent="formSubmit">
<p>
<button type="submit">Load data source 1</button>
<button type="button" wire:click="relatedToButSeparateFromForm">Load data source 2</button>
</p>
</form>
</div>
</div>
<div class="row">
<div class="col">
<table>
<thead>
<tr>
<template x-for="hd in eHeadings">
<th x-text="hd" style="padding: 0.5em; background-color:rgb(220,220,230); border: 1px solid rgb(210,210,230)"></th>
</template>
</tr>
</thead>
<tbody>
<template x-for="(row, ix) in eData" :key="ix">
<tr>
<template x-for="(col, pos) in eHeadings" :key="'td-'+ix+'-'+pos">
<td x-text="row[col]" :class="id" style="padding: 0.5em; background-color:rgb(240,240,255); border: 1px solid rgb(210,210,230)"></td>
</template>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
Note the deliberate error! On the <td>
element, :class="id"
should really say :class="col"
. Now if I take out the error, the page works as I would expect it to. But with the error re-introduced into the code (together with a bunch of error messages in the browser console saying: Uncaught ReferenceError: id is not defined
), after toggling the two buttons back and forth a bit, I get this:
As I think you'll agree, that picture is spookily reminiscent of the situation I get in my real world application (except that in the real-world-app, I don't end up with any errors in the browser's console).
This leads me to the strong belief that there is a silent bug being triggered somewhere in the Alpine engine which triggers the same net result. I will go and log this on their GitHub support pages too, but I have always found the Stack community to be super useful in the past too. I hope that someone out there is able to help validate that I'm not missing anything obvious!
Posted the issue on the Alpine bug-report pages, and got the response I wanted. See >> https://github.com/alpinejs/alpine/discussions/2523#discussioncomment-1860670
Apparently, it's not an Alpine issue at all. The problem is that Livewire is treading on Alpine's toes. Livewire "watches" the DOM for updates, and it seems that it's then failing to release (or clean-up, or whatever the correct term is) certain subsections of the DOM as Alpine refreshes it with the new load of data. This explains why earlier incarnations of the DOM are hanging about for longer than they are required.
Resolution is to force Livewire to not-watch the DOM for differences by using the wire:ignore
directive. This can be put on the <table>
itself, or any parent element thereof. In my example I put it on the immediately encapsulating <div>
:
<div class="whoopsie" wire:ignore>
<table>
<!-- etc -->
<tbody>
<template x-for="(col, pos) in eColOrder" :key="'td-'+ix+'-'+pos">
<td x-data="row[col]"></td>
</template>
</tbody>
<!-- etc -->
</table>
</div>