Remove all Alpine.js calls to fix memory leaks

- Replace reactive watchers with explicit method calls
- Add setFilterKey() and setFilterTable() methods for manual updates
- Update sort(), nextPage(), prevPage() to trigger computations
- Update templates to use new setter methods
- Compute derived data explicitly after data loading
This commit is contained in:
mahdahar 2026-02-16 10:52:49 +07:00
parent e947fc74d4
commit 8d762261d4
4 changed files with 64 additions and 68 deletions

View File

@ -12,35 +12,35 @@
<!-- Status Filters --> <!-- Status Filters -->
<div class="join shadow-sm bg-base-100 rounded-lg"> <div class="join shadow-sm bg-base-100 rounded-lg">
<button @click="filterKey = 'Total'" <button @click="setFilterKey('$1')"
:class="filterKey === 'Total' ? 'btn-active btn-neutral text-white' : 'btn-ghost'" :class="filterKey === 'Total' ? 'btn-active btn-neutral text-white' : 'btn-ghost'"
class="btn btn-sm join-item"> class="btn btn-sm join-item">
All <span class="badge badge-sm badge-ghost ml-1" x-text="counters.Total"></span> All <span class="badge badge-sm badge-ghost ml-1" x-text="counters.Total"></span>
</button> </button>
<button @click="filterKey = 'Pend'" <button @click="setFilterKey('$1')"
:class="filterKey === 'Pend' ? 'btn-active btn-status-pend' : 'btn-ghost'" :class="filterKey === 'Pend' ? 'btn-active btn-status-pend' : 'btn-ghost'"
class="btn btn-sm join-item"> class="btn btn-sm join-item">
Pending <span class="badge badge-sm badge-status-pend ml-1" x-text="counters.Pend"></span> Pending <span class="badge badge-sm badge-status-pend ml-1" x-text="counters.Pend"></span>
</button> </button>
<button @click="filterKey = 'Coll'" <button @click="setFilterKey('$1')"
:class="filterKey === 'Coll' ? 'btn-active btn-status-coll' : 'btn-ghost'" :class="filterKey === 'Coll' ? 'btn-active btn-status-coll' : 'btn-ghost'"
class="btn btn-sm join-item"> class="btn btn-sm join-item">
Coll <span class="badge badge-sm badge-status-coll ml-1" x-text="counters.Coll"></span> Coll <span class="badge badge-sm badge-status-coll ml-1" x-text="counters.Coll"></span>
</button> </button>
<button @click="filterKey = 'Recv'" <button @click="setFilterKey('$1')"
:class="filterKey === 'Recv' ? 'btn-active btn-status-recv' : 'btn-ghost'" class="btn btn-sm join-item"> :class="filterKey === 'Recv' ? 'btn-active btn-status-recv' : 'btn-ghost'" class="btn btn-sm join-item">
Recv <span class="badge badge-sm badge-status-recv ml-1" x-text="counters.Recv"></span> Recv <span class="badge badge-sm badge-status-recv ml-1" x-text="counters.Recv"></span>
</button> </button>
<button @click="filterKey = 'Inc'" <button @click="setFilterKey('$1')"
:class="filterKey === 'Inc' ? 'btn-active btn-status-inc' : 'btn-ghost'" class="btn btn-sm join-item"> :class="filterKey === 'Inc' ? 'btn-active btn-status-inc' : 'btn-ghost'" class="btn btn-sm join-item">
Inc <span class="badge badge-sm badge-status-inc ml-1" x-text="counters.Inc"></span> Inc <span class="badge badge-sm badge-status-inc ml-1" x-text="counters.Inc"></span>
</button> </button>
<button @click="filterKey = 'Fin'" <button @click="setFilterKey('$1')"
:class="filterKey === 'Fin' ? 'btn-active btn-status-fin' : 'btn-ghost'" :class="filterKey === 'Fin' ? 'btn-active btn-status-fin' : 'btn-ghost'"
class="btn btn-sm join-item"> class="btn btn-sm join-item">
Fin <span class="badge badge-sm badge-status-fin ml-1" x-text="counters.Fin"></span> Fin <span class="badge badge-sm badge-status-fin ml-1" x-text="counters.Fin"></span>
</button> </button>
<button @click="filterKey = 'Validated'" <button @click="setFilterKey('$1')"
:class="filterKey === 'Validated' ? 'btn-active btn-primary text-white' : 'btn-ghost'" :class="filterKey === 'Validated' ? 'btn-active btn-primary text-white' : 'btn-ghost'"
class="btn btn-sm join-item"> class="btn btn-sm join-item">
Val <span class="badge badge-sm badge-primary ml-1" x-text="validatedCount"></span> Val <span class="badge badge-sm badge-primary ml-1" x-text="validatedCount"></span>
@ -70,7 +70,7 @@
<div class="form-control w-full md:w-auto"> <div class="form-control w-full md:w-auto">
<label class='input input-sm input-bordered'> <label class='input input-sm input-bordered'>
<i class="fa fa-filter"></i> <i class="fa fa-filter"></i>
<input type="text" placeholder="Type to filter..." x-model="filterTable" /> <input type="text" placeholder="Type to filter..." x-model="filterTable" @input.debounce.300ms="setFilterTable(filterTable)" />
</label> </label>
</div> </div>
</div> </div>

View File

@ -42,7 +42,7 @@
<div class="form-control w-full md:w-auto"> <div class="form-control w-full md:w-auto">
<label class="input input-sm input-bordered"> <label class="input input-sm input-bordered">
<i class="fa fa-filter"></i> <i class="fa fa-filter"></i>
<input type="text" placeholder="Type to filter..." x-model="filterTable" /> <input type="text" placeholder="Type to filter..." x-model="filterTable" @input.debounce.300ms="setFilterTable(filterTable)" />
</label> </label>
</div> </div>
</div> </div>

View File

@ -61,14 +61,39 @@ document.addEventListener('alpine:init', () => {
this.sortCol = col; this.sortCol = col;
this.sortAsc = true; this.sortAsc = true;
} }
this.computeSorted();
this.computePaginated();
}, },
nextPage() { nextPage() {
if (this.currentPage < this.totalPages) this.currentPage++; if (this.currentPage < this.totalPages) {
this.currentPage++;
this.computePaginated();
}
}, },
prevPage() { prevPage() {
if (this.currentPage > 1) this.currentPage--; if (this.currentPage > 1) {
this.currentPage--;
this.computePaginated();
}
},
setFilterKey(key) {
this.filterKey = key;
this.computeFiltered();
this.computeSorted();
this.computeTotalPages();
this.computePaginated();
},
setFilterTable(value) {
this.filterTable = value;
this.currentPage = 1;
this.computeFiltered();
this.computeSorted();
this.computeTotalPages();
this.computePaginated();
}, },
// Compute methods - called only when dependencies change // Compute methods - called only when dependencies change
@ -120,32 +145,6 @@ document.addEventListener('alpine:init', () => {
const defaultPrinter = '<?= $config[session()->get("userrole")]["sampleDialog"]["defaultPrinter"] ?? "lab" ?>'; const defaultPrinter = '<?= $config[session()->get("userrole")]["sampleDialog"]["defaultPrinter"] ?? "lab" ?>';
this.selectedPrinter = defaultPrinter || 'lab'; this.selectedPrinter = defaultPrinter || 'lab';
// Watchers for reactive updates
this.$watch('list', () => {
this.computeFiltered();
this.computeValidatedCount();
});
this.$watch('filterKey', () => this.computeFiltered());
this.$watch('filterTable', () => {
this.currentPage = 1;
this.computeFiltered();
});
this.$watch('filtered', () => {
this.computeSorted();
this.computeTotalPages();
});
this.$watch('sortCol', () => this.computeSorted());
this.$watch('sortAsc', () => this.computeSorted());
this.$watch('sorted', () => this.computePaginated());
this.$watch('currentPage', () => this.computePaginated());
this.$watch('auditData', () => {
this.computeAuditEvents();
});
// Initial load only - no auto-refresh // Initial load only - no auto-refresh
this.fetchList(); this.fetchList();
}, },
@ -175,6 +174,9 @@ document.addEventListener('alpine:init', () => {
let codeB = statusOrder[b.STATS] ?? 0; let codeB = statusOrder[b.STATS] ?? 0;
return codeA - codeB; return codeA - codeB;
}); });
// Compute derived data after list is loaded
this.computeFiltered();
this.computeValidatedCount();
}).finally(() => { }).finally(() => {
this.isLoading = false; this.isLoading = false;
}); });

View File

@ -29,14 +29,31 @@ document.addEventListener('alpine:init', () => {
this.sortCol = col; this.sortCol = col;
this.sortAsc = true; this.sortAsc = true;
} }
this.computeUnvalidatedSorted();
this.computeUnvalidatedPaginated();
}, },
nextPage() { nextPage() {
if (this.currentPage < this.unvalidatedTotalPages) this.currentPage++; if (this.currentPage < this.unvalidatedTotalPages) {
this.currentPage++;
this.computeUnvalidatedPaginated();
}
}, },
prevPage() { prevPage() {
if (this.currentPage > 1) this.currentPage--; if (this.currentPage > 1) {
this.currentPage--;
this.computeUnvalidatedPaginated();
}
},
setFilterTable(value) {
this.filterTable = value;
this.currentPage = 1;
this.computeUnvalidatedFiltered();
this.computeUnvalidatedSorted();
this.computeUnvalidatedTotalPages();
this.computeUnvalidatedPaginated();
}, },
// Compute methods - called only when dependencies change // Compute methods - called only when dependencies change
@ -82,35 +99,6 @@ document.addEventListener('alpine:init', () => {
this.filter.date1 = this.today; this.filter.date1 = this.today;
this.filter.date2 = this.today; this.filter.date2 = this.today;
this.$watch('filterTable', () => {
this.currentPage = 1;
});
// Watchers for reactive updates
this.$watch('unvalidatedList', () => {
this.computeUnvalidatedFiltered();
this.computeUnvalidatedCount();
});
this.$watch('filterTable', () => {
this.computeUnvalidatedFiltered();
});
this.$watch('unvalidatedFiltered', () => {
this.computeUnvalidatedSorted();
this.computeUnvalidatedTotalPages();
});
this.$watch('sortCol', () => {
this.computeUnvalidatedSorted();
});
this.$watch('sortAsc', () => {
this.computeUnvalidatedSorted();
});
this.$watch('unvalidatedSorted', () => {
this.computeUnvalidatedPaginated();
});
this.$watch('currentPage', () => {
this.computeUnvalidatedPaginated();
});
// Initial load only - no auto-refresh // Initial load only - no auto-refresh
this.fetchUnvalidated(); this.fetchUnvalidated();
@ -137,6 +125,12 @@ document.addEventListener('alpine:init', () => {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}).then(res => res.json()).then(data => { }).then(res => res.json()).then(data => {
this.unvalidatedList = data.data ?? []; this.unvalidatedList = data.data ?? [];
// Compute derived data after list is loaded
this.computeUnvalidatedFiltered();
this.computeUnvalidatedCount();
this.computeUnvalidatedSorted();
this.computeUnvalidatedTotalPages();
this.computeUnvalidatedPaginated();
}).finally(() => { }).finally(() => {
this.isLoading = false; this.isLoading = false;
}); });