Enhance table pagination: update URL building for server-side pagination, improve refresh logic, and add pagination metadata handling in response headers.

This commit is contained in:
Yaro Kasear 2025-08-21 16:15:39 -05:00
parent 220bd3e4b6
commit f3f4493698
4 changed files with 147 additions and 55 deletions

View file

@ -3,9 +3,15 @@ function Table(cfg) {
id: cfg.id,
refreshUrl: cfg.refreshUrl,
headers: cfg.headers || [],
fields: cfg.fields || [],
// external API
perPage: cfg.perPage || 10,
offset: cfg.offset || 0,
fields: cfg.fields || [],
// derived + server-fed state
page: Math.floor((cfg.offset || 0) / (cfg.perPage || 10)) + 1,
total: 0,
pages: 0,
init() {
if (this.refreshUrl) this.refresh();
@ -14,18 +20,96 @@ function Table(cfg) {
buildRefreshUrl() {
if (!this.refreshUrl) return null;
const u = new URL(this.refreshUrl, window.location.origin);
u.search = new URLSearchParams({ view: 'table', offset: this.offset, 'limit': this.perPage, 'fields': this.fields }).toString();
// We want server-side pagination with page/per_page
u.searchParams.set('view', 'table');
u.searchParams.set('page', this.page);
u.searchParams.set('per_page', this.perPage);
// Send requested fields in the way your backend expects
// If your route supports &field=... repeaters, do this:
this.fields.forEach(f => u.searchParams.append('field', f));
// If your route only supports "fields=a,b,c", then use:
// if (this.fields.length) u.searchParams.set('fields', this.fields.join(','));
return u.toString();
},
async refresh() {
const url = this.buildRefreshUrl();
if (!url) return;
const res = await fetch(url, { headers: { 'HX-Request': 'true' } });
const text = await res.text();
if (this.$refs.body) {
this.$refs.body.innerHTML = text;
}
const res = await fetch(url, { headers: { 'X-Requested-With': 'fetch' } });
const html = await res.text();
// Dump the server-rendered <tr> rows into the tbody
if (this.$refs.body) this.$refs.body.innerHTML = html;
// Read pagination metadata from headers
const toInt = (v, d=0) => {
const n = parseInt(v ?? '', 10);
return Number.isFinite(n) ? n : d;
};
const total = toInt(res.headers.get('X-Total'));
const pages = toInt(res.headers.get('X-Pages'));
const page = toInt(res.headers.get('X-Page'), this.page);
const per = toInt(res.headers.get('X-Per-Page'), this.perPage);
// Update local state
this.total = total;
this.pages = pages;
this.page = page;
this.perPage = per;
this.offset = (this.page - 1) * this.perPage;
// Update pager UI (if you put <ul x-ref="pagination"> in your caption)
this.buildPager();
// Caption numbers are bound via x-text so they auto-update.
},
buildPager() {
const ul = this.$refs.pagination;
if (!ul) return;
ul.innerHTML = '';
const mk = (label, page, disabled=false, active=false) => {
const li = document.createElement('li');
li.className = `page-item${disabled ? ' disabled' : ''}${active ? ' active' : ''}`;
const a = document.createElement('a');
a.className = 'page-link';
a.href = '#';
a.textContent = label;
a.onclick = (e) => {
e.preventDefault();
if (disabled || active) return;
this.page = page;
this.refresh();
};
li.appendChild(a);
return li;
};
// Prev
ul.appendChild(mk('«', Math.max(1, this.page - 1), this.page <= 1));
// Windowed page buttons
const maxButtons = 7;
let start = Math.max(1, this.page - Math.floor(maxButtons/2));
let end = Math.min(this.pages || 1, start + maxButtons - 1);
start = Math.max(1, Math.min(start, Math.max(1, end - maxButtons + 1)));
if (start > 1) ul.appendChild(mk('1', 1));
if (start > 2) ul.appendChild(mk('…', this.page, true));
for (let p = start; p <= end; p++) {
ul.appendChild(mk(String(p), p, false, p === this.page));
}
if (end < (this.pages || 1) - 1) ul.appendChild(mk('…', this.page, true));
if (end < (this.pages || 1)) ul.appendChild(mk(String(this.pages), this.pages));
// Next
ul.appendChild(mk('»', Math.min(this.pages || 1, this.page + 1), this.page >= (this.pages || 1)));
},
};
}

View file

@ -6,11 +6,3 @@
{% endfor %}
</tr>
{% endfor %}
{% if pages > 1 %}
<tr>
<td colspan="{{ rows[0]|length - 1 }}">
Page {{ page }} of {{ pages }} ({{ total }} total)
</td>
</tr>
{% endif %}

View file

@ -51,7 +51,8 @@
{% endif %}
{% endmacro %}
{% macro dynamic_table(id, headers=none, fields=none, entry_route=None, title=None, per_page=15, offset=0, refresh_url=none) %}
{% macro dynamic_table(id, headers=none, fields=none, entry_route=None, title=None, per_page=15, offset=0,
refresh_url=none) %}
<!-- Table Fragment -->
{% if rows or refresh_url %}
@ -67,7 +68,18 @@
fields: {{ fields|tojson if fields else "[]" }}
})'>
<table id="datatable-{{ id|default('table')|replace(' ', '-')|lower }}"
class="table table-bordered table-sm table-hover table-striped table-light m-0{% if title %} caption-top{% endif %}">
class="table table-bordered table-sm table-hover table-striped table-light m-0 caption-bottom">
<caption class="p-0">
<nav class="d-flex flex-column align-items-center px-2 py-1">
<!-- This is your pagination control -->
<ul class="pagination mb-0" x-ref="pagination"></ul>
<!-- This is just Alpine text binding -->
<div>
Page <span x-text="page"></span> of <span x-text="pages"></span>
(<span x-text="total"></span> total)
</div>
</nav>
</caption>
<thead class="sticky-top">
<tr>
{% for h in headers %}
@ -77,9 +89,6 @@
</thead>
<tbody x-ref="body"></tbody>
</table>
<nav>
<ul class="pagination"></ul>
</nav>
</div>
{% else %}
<div class="container text-center">No data.</div>

View file

@ -1,4 +1,4 @@
from flask import Blueprint, request, render_template, jsonify, abort
from flask import Blueprint, request, render_template, jsonify, abort, make_response
from sqlalchemy.engine import ScalarResult
from sqlalchemy.exc import IntegrityError
from sqlalchemy.sql import Select
@ -183,14 +183,21 @@ def list_items(model_name):
if want_list:
return render_template("fragments/_list_fragment.html", options=items)
if want_table:
return render_template("fragments/_table_data_fragment.html",
rows=items,
model_name=model_name,
total=total,
page=page,
per_page=per_page,
pages=(0 if unlimited else ((total + per_page - 1) // per_page)),
)
# return render_template("fragments/_table_data_fragment.html",
# rows=items,
# model_name=model_name,
# total=total,
# page=page,
# per_page=per_page,
# pages=(0 if unlimited else ((total + per_page - 1) // per_page)),
# )
resp = make_response(render_template("fragments/_table_data_fragment.html",
rows=items, model_name=model_name))
resp.headers['X-Total'] = str(total)
resp.headers['X-Page'] = str(page)
resp.headers['X-PAges'] = str((0 if unlimited else ((total + per_page - 1) // per_page)))
resp.headers['X-Per-Page'] = str(per_page)
return resp
return jsonify({
"items": items,