Mercurial > borealpoint
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/resources/js/ServerTable.js Sat Aug 23 22:20:51 2025 -0400 @@ -0,0 +1,247 @@ +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;
