Mercurial > borealpoint
comparison resources/js/ServerTable.js @ 0:9d7dcd54c677
Initial Commit and package setup
| author | luka |
|---|---|
| date | Sat, 23 Aug 2025 22:20:51 -0400 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:9d7dcd54c677 |
|---|---|
| 1 class ServerTable { | |
| 2 /** | |
| 3 * @param {HTMLElement} rootEl - The container element for the table | |
| 4 * @param {Object} options - Configuration options | |
| 5 * @param {string} options.endpoint - API endpoint for data | |
| 6 * @param {Array} options.columns - Array of column configs: [{name, label, ...}] | |
| 7 * @param {number} [options.pageSize=10] - Default rows per page | |
| 8 * @param {Array} [options.initialSort=[]] - Default sort: [{col, dir}] | |
| 9 * @param {Object} [options.headers={}] - Additional headers for requests | |
| 10 * @param {string} [options.groupBy={}] - Which column to group by | |
| 11 * @param {Function} [options.groupRender={}] - Function to render the grouping | |
| 12 */ | |
| 13 constructor(rootEl, options) { | |
| 14 this.rootEl = rootEl; | |
| 15 this.endpoint = options.endpoint; | |
| 16 this.columns = options.columns || []; | |
| 17 this.pageSize = options.pageSize || 10; | |
| 18 this.sort = options.initialSort || []; | |
| 19 this.filters = options.filters || {}; | |
| 20 this.currentPage = 1; | |
| 21 this.headers = options.headers || {}; | |
| 22 this.groupBy = options.groupBy; | |
| 23 this.groupRender = | |
| 24 options.groupRender || | |
| 25 ((g, rows) => | |
| 26 `<tr class="st-group-row"><td colspan="${this.columns.length}">Group: ${g}</td></tr>`); | |
| 27 | |
| 28 this.skeleton = options.skeleton || ` | |
| 29 <div class="st-table-container"> | |
| 30 <table> | |
| 31 <thead> | |
| 32 </thead> | |
| 33 <tbody> | |
| 34 <!-- Data rows will go here --> | |
| 35 </tbody> | |
| 36 </table> | |
| 37 <div class="st-controls"> | |
| 38 <span class="st-pagination"></span> | |
| 39 <span class="st-status"></span> | |
| 40 </div> | |
| 41 </div> | |
| 42 `; | |
| 43 | |
| 44 this.state = { | |
| 45 loading: false, | |
| 46 error: null, | |
| 47 totalRecords: 0, | |
| 48 records: [], | |
| 49 }; | |
| 50 | |
| 51 // Render initial skeleton and fetch initial data | |
| 52 this.renderSkeleton(); | |
| 53 this.fetchData(); | |
| 54 } | |
| 55 | |
| 56 renderSkeleton() { | |
| 57 // Build base structure: table shell, controls area | |
| 58 this.rootEl.innerHTML = this.skeleton; | |
| 59 // Store references | |
| 60 this.head = this.rootEl.querySelector("thead"); | |
| 61 this.tbody = this.rootEl.querySelector("tbody"); | |
| 62 this.statusEl = this.rootEl.querySelector(".st-status"); | |
| 63 this.paginationEl = this.rootEl.querySelector(".st-pagination"); | |
| 64 | |
| 65 // Draw the header | |
| 66 this.head.innerHTML = ` | |
| 67 <tr> | |
| 68 ${this.columns.map((col) => `<th scope="col">${col.label || col.name}</th>`).join("")} | |
| 69 </tr> | |
| 70 `; | |
| 71 } | |
| 72 | |
| 73 async fetchData() { | |
| 74 this.setLoading(true); | |
| 75 const payload = { | |
| 76 page: this.currentPage, | |
| 77 page_size: this.pageSize, | |
| 78 sort: this.sort, | |
| 79 filters: this.filters, | |
| 80 }; | |
| 81 | |
| 82 try { | |
| 83 const res = await fetch(this.endpoint, { | |
| 84 method: "POST", | |
| 85 headers: { | |
| 86 ...this.headers, | |
| 87 "Content-Type": "application/json", | |
| 88 }, | |
| 89 body: JSON.stringify(payload), | |
| 90 }); | |
| 91 | |
| 92 if (!res.ok) { | |
| 93 throw new Error(`Server responded with ${res.status}`); | |
| 94 } | |
| 95 | |
| 96 const data = await res.json(); | |
| 97 // Minimal shape validation | |
| 98 if ( | |
| 99 !Array.isArray(data.records) || | |
| 100 typeof data.total_records !== "number" | |
| 101 ) { | |
| 102 throw new Error("Malformed server response"); | |
| 103 } | |
| 104 | |
| 105 // Save data in state | |
| 106 this.state.records = data.records; | |
| 107 this.state.totalRecords = data.total_records; | |
| 108 this.state.error = null; | |
| 109 | |
| 110 this.renderRows(); | |
| 111 this.updateControls(); | |
| 112 } catch (err) { | |
| 113 this.state.error = err.message; | |
| 114 this.renderError(); | |
| 115 } finally { | |
| 116 this.setLoading(false); | |
| 117 } | |
| 118 } | |
| 119 | |
| 120 setLoading(loading) { | |
| 121 this.state.loading = loading; | |
| 122 this.statusEl.textContent = loading ? "Loading..." : ""; | |
| 123 } | |
| 124 | |
| 125 renderRows() { | |
| 126 const { records } = this.state; | |
| 127 const cols = this.columns; | |
| 128 const groupBy = this.groupBy; | |
| 129 const groupRender = | |
| 130 typeof this.groupRender === "function" | |
| 131 ? this.groupRender | |
| 132 : (g, rows) => | |
| 133 `<tr class="st-group-row"><td colspan="${cols.length}">Project: ${g}</td></tr>`; | |
| 134 | |
| 135 if (!records || records.length === 0) { | |
| 136 this.tbody.innerHTML = `<tr><td colspan="${cols.length}">No data</td></tr>`; | |
| 137 return; | |
| 138 } | |
| 139 | |
| 140 // Group if needed | |
| 141 if (groupBy) { | |
| 142 // Find grouping field or function | |
| 143 const getGroupValue = | |
| 144 typeof groupBy === "function" ? groupBy : (row) => row[groupBy]; | |
| 145 let lastGroup = undefined; | |
| 146 let out = ""; | |
| 147 let groupRows = []; | |
| 148 | |
| 149 for (let i = 0; i < records.length; i++) { | |
| 150 const row = records[i]; | |
| 151 const groupVal = getGroupValue(row); | |
| 152 | |
| 153 // On group transition, flush previous group | |
| 154 if (i === 0 || groupVal !== lastGroup) { | |
| 155 if (i > 0) { | |
| 156 // Optionally do something with groupRows if groupRender wants it | |
| 157 } | |
| 158 // Insert group header row | |
| 159 out += groupRender(groupVal, []); | |
| 160 lastGroup = groupVal; | |
| 161 groupRows = []; | |
| 162 } | |
| 163 | |
| 164 groupRows.push(row); | |
| 165 | |
| 166 out += `<tr>${cols | |
| 167 .map((col, ci) => { | |
| 168 // If grouping by this column, suppress repeated values (leave blank except for first row in group) | |
| 169 if ( | |
| 170 typeof groupBy === "string" && | |
| 171 col.name === groupBy && | |
| 172 groupVal === lastGroup && | |
| 173 groupRows.length > 1 | |
| 174 ) { | |
| 175 return `<td></td>`; | |
| 176 } | |
| 177 return `<td class='${ typeof col.class === "string" ? col.class : '' }' >${ | |
| 178 typeof col.render === "function" | |
| 179 ? col.render(row, col, i) | |
| 180 : row[col.name] | |
| 181 }</td>`; | |
| 182 }) | |
| 183 .join("")}</tr>`; | |
| 184 } | |
| 185 this.tbody.innerHTML = out; | |
| 186 } else { | |
| 187 // No grouping | |
| 188 this.tbody.innerHTML = records | |
| 189 .map( | |
| 190 (row, i) => | |
| 191 `<tr>${cols | |
| 192 .map( | |
| 193 (col) => | |
| 194 `<td class='${ typeof col.class === "string" ? col.class : '' }' >${ | |
| 195 typeof col.render === "function" | |
| 196 ? col.render(row, col, i) | |
| 197 : row[col.name] | |
| 198 }</td>`, | |
| 199 ) | |
| 200 .join("")}</tr>`, | |
| 201 ) | |
| 202 .join(""); | |
| 203 } | |
| 204 } | |
| 205 renderError() { | |
| 206 this.tbody.innerHTML = `<tr><td colspan="${this.columns.length}" style="color:red">${this.state.error}</td></tr>`; | |
| 207 } | |
| 208 | |
| 209 updateControls() { | |
| 210 // Basic pagination info (full controls to come later) | |
| 211 const from = 1 + (this.currentPage - 1) * this.pageSize; | |
| 212 const to = Math.min( | |
| 213 this.currentPage * this.pageSize, | |
| 214 this.state.totalRecords, | |
| 215 ); | |
| 216 this.paginationEl.textContent = `Showing ${from}-${to} of ${this.state.totalRecords}`; | |
| 217 } | |
| 218 | |
| 219 // PUBLIC: force reload | |
| 220 reload() { | |
| 221 this.fetchData(); | |
| 222 } | |
| 223 | |
| 224 // PUBLIC: update filters, resets to page 1 | |
| 225 setFilters(newFilters) { | |
| 226 this.filters = newFilters; | |
| 227 this.currentPage = 1; | |
| 228 this.fetchData(); | |
| 229 } | |
| 230 } | |
| 231 | |
| 232 // Example usage (not part of module export): | |
| 233 /* | |
| 234 const table = new ServerTable(document.getElementById('my-table'), { | |
| 235 endpoint: '/tickets/get_data', | |
| 236 columns: [ | |
| 237 {name: 'id', label: 'ID'}, | |
| 238 {name: 'subject', label: 'Subject'}, | |
| 239 {name: 'project', label: 'Project'}, | |
| 240 {name: 'created_at', label: 'Created At'}, | |
| 241 ], | |
| 242 pageSize: 10, | |
| 243 initialSort: [{col: 'created_at', dir: 'desc'}] | |
| 244 }); | |
| 245 */ | |
| 246 | |
| 247 window.ServerTable = ServerTable; |
