Mercurial > borealpoint
view resources/js/ServerTable.js @ 0:9d7dcd54c677
Initial Commit and package setup
| author | luka |
|---|---|
| date | Sat, 23 Aug 2025 22:20:51 -0400 |
| parents | |
| children |
line wrap: on
line source
class ServerTable { /** * @param {HTMLElement} rootEl - The container element for the table * @param {Object} options - Configuration options * @param {string} options.endpoint - API endpoint for data * @param {Array} options.columns - Array of column configs: [{name, label, ...}] * @param {number} [options.pageSize=10] - Default rows per page * @param {Array} [options.initialSort=[]] - Default sort: [{col, dir}] * @param {Object} [options.headers={}] - Additional headers for requests * @param {string} [options.groupBy={}] - Which column to group by * @param {Function} [options.groupRender={}] - Function to render the grouping */ constructor(rootEl, options) { this.rootEl = rootEl; this.endpoint = options.endpoint; this.columns = options.columns || []; this.pageSize = options.pageSize || 10; this.sort = options.initialSort || []; this.filters = options.filters || {}; this.currentPage = 1; this.headers = options.headers || {}; this.groupBy = options.groupBy; this.groupRender = options.groupRender || ((g, rows) => `<tr class="st-group-row"><td colspan="${this.columns.length}">Group: ${g}</td></tr>`); this.skeleton = options.skeleton || ` <div class="st-table-container"> <table> <thead> </thead> <tbody> <!-- Data rows will go here --> </tbody> </table> <div class="st-controls"> <span class="st-pagination"></span> <span class="st-status"></span> </div> </div> `; this.state = { loading: false, error: null, totalRecords: 0, records: [], }; // Render initial skeleton and fetch initial data this.renderSkeleton(); this.fetchData(); } renderSkeleton() { // Build base structure: table shell, controls area this.rootEl.innerHTML = this.skeleton; // Store references this.head = this.rootEl.querySelector("thead"); this.tbody = this.rootEl.querySelector("tbody"); this.statusEl = this.rootEl.querySelector(".st-status"); this.paginationEl = this.rootEl.querySelector(".st-pagination"); // Draw the header this.head.innerHTML = ` <tr> ${this.columns.map((col) => `<th scope="col">${col.label || col.name}</th>`).join("")} </tr> `; } async fetchData() { this.setLoading(true); const payload = { page: this.currentPage, page_size: this.pageSize, sort: this.sort, filters: this.filters, }; try { const res = await fetch(this.endpoint, { method: "POST", headers: { ...this.headers, "Content-Type": "application/json", }, body: JSON.stringify(payload), }); if (!res.ok) { throw new Error(`Server responded with ${res.status}`); } const data = await res.json(); // Minimal shape validation if ( !Array.isArray(data.records) || typeof data.total_records !== "number" ) { throw new Error("Malformed server response"); } // Save data in state this.state.records = data.records; this.state.totalRecords = data.total_records; this.state.error = null; this.renderRows(); this.updateControls(); } catch (err) { this.state.error = err.message; this.renderError(); } finally { this.setLoading(false); } } setLoading(loading) { this.state.loading = loading; this.statusEl.textContent = loading ? "Loading..." : ""; } renderRows() { const { records } = this.state; const cols = this.columns; const groupBy = this.groupBy; const groupRender = typeof this.groupRender === "function" ? this.groupRender : (g, rows) => `<tr class="st-group-row"><td colspan="${cols.length}">Project: ${g}</td></tr>`; if (!records || records.length === 0) { this.tbody.innerHTML = `<tr><td colspan="${cols.length}">No data</td></tr>`; return; } // Group if needed if (groupBy) { // Find grouping field or function const getGroupValue = typeof groupBy === "function" ? groupBy : (row) => row[groupBy]; let lastGroup = undefined; let out = ""; let groupRows = []; for (let i = 0; i < records.length; i++) { const row = records[i]; const groupVal = getGroupValue(row); // On group transition, flush previous group if (i === 0 || groupVal !== lastGroup) { if (i > 0) { // Optionally do something with groupRows if groupRender wants it } // Insert group header row out += groupRender(groupVal, []); lastGroup = groupVal; groupRows = []; } groupRows.push(row); out += `<tr>${cols .map((col, ci) => { // If grouping by this column, suppress repeated values (leave blank except for first row in group) if ( typeof groupBy === "string" && col.name === groupBy && groupVal === lastGroup && groupRows.length > 1 ) { return `<td></td>`; } return `<td class='${ typeof col.class === "string" ? col.class : '' }' >${ typeof col.render === "function" ? col.render(row, col, i) : row[col.name] }</td>`; }) .join("")}</tr>`; } this.tbody.innerHTML = out; } else { // No grouping this.tbody.innerHTML = records .map( (row, i) => `<tr>${cols .map( (col) => `<td class='${ typeof col.class === "string" ? col.class : '' }' >${ typeof col.render === "function" ? col.render(row, col, i) : row[col.name] }</td>`, ) .join("")}</tr>`, ) .join(""); } } renderError() { this.tbody.innerHTML = `<tr><td colspan="${this.columns.length}" style="color:red">${this.state.error}</td></tr>`; } updateControls() { // Basic pagination info (full controls to come later) const from = 1 + (this.currentPage - 1) * this.pageSize; const to = Math.min( this.currentPage * this.pageSize, this.state.totalRecords, ); this.paginationEl.textContent = `Showing ${from}-${to} of ${this.state.totalRecords}`; } // PUBLIC: force reload reload() { this.fetchData(); } // PUBLIC: update filters, resets to page 1 setFilters(newFilters) { this.filters = newFilters; this.currentPage = 1; this.fetchData(); } } // Example usage (not part of module export): /* const table = new ServerTable(document.getElementById('my-table'), { endpoint: '/tickets/get_data', columns: [ {name: 'id', label: 'ID'}, {name: 'subject', label: 'Subject'}, {name: 'project', label: 'Project'}, {name: 'created_at', label: 'Created At'}, ], pageSize: 10, initialSort: [{col: 'created_at', dir: 'desc'}] }); */ window.ServerTable = ServerTable;
