Compare commits

...

9 Commits

Author SHA1 Message Date
CentreMetre
f5833fcbf8 Fix doc typos and add Table class summary line
Correct 'A object' -> 'An object' and 'if i iss not set' -> 'if it is
not set', and add a one-line summary to the Table class JSDoc.
2026-06-15 21:15:44 +01:00
CentreMetre
95254027c4 Use a passed HTMLTableCellElement as the cell directly
When a cell value is a <td>/<th>, append it to the row as-is instead of
wrapping it in a generated <td>, which would have produced invalid nested
cells. Any other HTMLElement still gets wrapped as before.
2026-06-15 21:15:34 +01:00
CentreMetre
436217a8ff Add TypeScript build config for .d.ts declaration output
Set up tsconfig.json (declaration: true, DOM lib), package.json with
main/types/files pointing at dist, and .gitignore for node_modules/dist
so the library can be consumed with type declarations.
2026-06-15 17:30:46 +01:00
CentreMetre
8c561385b8 Add licence 2026-06-15 16:49:38 +01:00
CentreMetre
67ecf689a6 Add default css 2026-06-14 21:46:00 +01:00
CentreMetre
b0f40dea5f Reword HTMLElement cloneNode remark for clarity 2026-06-14 21:45:04 +01:00
CentreMetre
27488dd1b3 Mark internal helpers protected and document HTMLElement cell behaviour
Make createColumnsFromType, setHeaderRow and createPopulatedRow protected
to shrink the public surface, and add @remarks warning that an HTMLElement
cell value must be a unique node per row. Also fix doc typos.
2026-06-14 21:41:38 +01:00
CentreMetre
2a851983b2 Rename Column key to be clearer 2026-06-14 21:39:04 +01:00
CentreMetre
9825d25090 Add bigint handling 2026-06-14 21:38:35 +01:00
7 changed files with 205 additions and 11 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
dist

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Martin McLaren (CentreMetre)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -4,7 +4,7 @@
*/
export type TableOptions<TRow extends object> = {
/**
* A object with the same properties, but all string type for the header cell values.
* An object with the same properties, but all string type for the header cell values.
* Used by passing an object with the keys of {@link TRow} and the name preferred. E.g. for a user:
* ```{ id: "ID", name: "Name" }```, this makes the columns appear in the order of ID then Name.
*/
@@ -23,6 +23,7 @@ export type TableOptions<TRow extends object> = {
};
/**
* Abstract base class that renders typed row data as an HTML `<table>` on a web page.
* @template TRow A type that the shape of a row should be. E.g.
* ```
* type User = {
@@ -74,7 +75,7 @@ export abstract class Table<TRow extends object> {
* e.g. User = { fullname: "Full Name", id: "User ID" }, the columns would be "Full Name" and "User ID" in that order,
* @return An array of {@link Column} type shape that holds the column info.
*/
createColumnsFromType(
protected createColumnsFromType(
exampleObject: TRow, // Needed for runtime, since types don't exist in JS.
displayNamesAndOrder?: Record<keyof TRow, string>
): Column<TRow>[] {
@@ -85,7 +86,7 @@ export abstract class Table<TRow extends object> {
return keys.map(key => ({
key, // Raw property name from TRow
value: displayNamesAndOrder?.[key] ?? String(key), // The value of the th html element, with a nullish coalescing operator for if its null
headerValue: displayNamesAndOrder?.[key] ?? String(key), // The value of the th html element, with a nullish coalescing operator for if it is null
type: typeof exampleObject[key], // The type of the column
}));
}
@@ -93,13 +94,13 @@ export abstract class Table<TRow extends object> {
/**
* Sets the header cell values on the header row from the {@link columns} on this object.
*/
setHeaderRow() {
protected setHeaderRow() {
const thead = document.createElement("thead");
const headerRow = document.createElement("tr");
for (const column of this.columns) {
const th = document.createElement("th")
th.textContent = column.value;
th.textContent = column.headerValue; // Column's header text
headerRow.appendChild(th);
}
@@ -117,6 +118,7 @@ export abstract class Table<TRow extends object> {
/**
* Appends one more row to the current rows.
* @param data Data to append as a row.
* @remarks An HTMLElement cell value must be a unique node per row, reusing one HTMLElement instance across rows moves it to the last row only.
*/
appendRow(data: TRow): void {
const row = this.createPopulatedRow(data)
@@ -124,8 +126,9 @@ export abstract class Table<TRow extends object> {
}
/**
* Appends multiple rows to the table, at the end off the table.
* Appends multiple rows to the table, at the end of the table.
* @param data The rows to append to the table, in shape of {@link TRow}.
* @remarks An HTMLElement cell value must be a unique node per row, reusing one HTMLElement instance across rows moves it to the last row only.
*/
appendRows(data: TRow[]): void {
if (data.length === 0) {
@@ -141,19 +144,31 @@ export abstract class Table<TRow extends object> {
* Creates a row, populated with data, for displaying in the table.
* @param data The data in the shape of `TRow`, with values populated.
* @return A HTMLTableRowElement that can then be set on a table.
* @remarks If a cell value is an HTMLTableCellElement (a `<td>`/`<th>`) it is used as the cell
* directly; any other HTMLElement is appended inside a generated `<td>`. A DOM node can only
* exist in one place, so passing the same element instance to multiple rows moves it to the
* last row only. Pass a fresh element (or `el.cloneNode(true)`) per row. But keep in mind that
* el.cloneNode(true) does not keep event listeners, so if event listeners are used it is best
* to generate a fresh element for each row that needs one.
*/
createPopulatedRow(data: TRow): HTMLTableRowElement {
protected createPopulatedRow(data: TRow): HTMLTableRowElement {
const row = document.createElement("tr");
for (const column of this.columns) {
const cell: HTMLTableCellElement = document.createElement("td");
const value = data[column.key]
if (value instanceof HTMLTableCellElement) {
row.append(value); // already a cell, use it as-is
continue;
}
const cell: HTMLTableCellElement = document.createElement("td");
// Element values are inserted as children, not stringified.
if (value instanceof HTMLElement) {
cell.appendChild(value);
row.append(cell);
continue; // skip the textContent path that would wipe it
continue; // skip the textContent path that would wipe it
}
let cellValue = "";
@@ -170,6 +185,10 @@ export abstract class Table<TRow extends object> {
cell.classList.add("table-cell-number");
break;
case "bigint":
cellValue = String(value);
cell.classList.add("table-cell-number");
break;
default:
cellValue = value != null ? String(value) : "";
}
@@ -192,6 +211,7 @@ export abstract class Table<TRow extends object> {
/**
* Deletes all current rows and sets a single row to the data in data param.
* @param data The data to set the table to. Should be of shape {@link TRow}
* @remarks An HTMLElement cell value must be a unique node per row, reusing one HTMLElement instance across rows moves it to the last row only.
*/
setSingleRow(data: TRow): void {
this.clearRows()
@@ -201,6 +221,7 @@ export abstract class Table<TRow extends object> {
/**
* Deletes all current rows and sets the rows to the data in data param.
* @param data The data to set the table to. Should be of shape {@link TRow}
* @remarks An HTMLElement cell value must be a unique node per row, reusing one HTMLElement instance across rows moves it to the last row only.
*/
setRows(data: TRow[]): void {
this.clearRows()
@@ -213,7 +234,7 @@ export abstract class Table<TRow extends object> {
/**
* Get the message element for the table.
* @return The HTMLElement thats used for showing messaged. Or null if its not set.
* @return The HTMLElement that is used for showing messages. Or null if it is not set.
*/
getMessageElement(): HTMLElement | null {
return this.messageEl;
@@ -268,6 +289,6 @@ export abstract class Table<TRow extends object> {
export type Column<TRow> = {
key: keyof TRow; // The name of the field.
value: string; // The name of the header cell
headerValue: string; // The name of the header cell
type: string; // The type of the field, can be string since typesof are returned as strings.
}

29
package-lock.json generated Normal file
View File

@@ -0,0 +1,29 @@
{
"name": "abstract-ts-table",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "abstract-ts-table",
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
"typescript": "^6.0.3"
}
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
}
}
}

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "abstract-ts-table",
"version": "1.0.0",
"description": "A utility class for creating HTML tables.",
"type": "module",
"main": "dist/abstract-table.js",
"types": "dist/abstract-table.d.ts",
"files": ["dist"],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc"
},
"repository": {
"type": "git",
"url": "https://git.centremetre.com/CentreMetre/abstract-ts-table"
},
"author": "CentreMetre",
"license": "MIT",
"devDependencies": {
"typescript": "^6.0.3"
}
}

87
table.css Normal file
View File

@@ -0,0 +1,87 @@
thead {
background: hsl(0, 0%, 90%);
}
th {
border-left: 1px solid hsl(0, 0%, 16%);
border-bottom: 1px solid hsl(0, 0%, 16%);
border-right: 1px solid hsl(0, 0%, 16%);
/*padding: 5px;*/
width: 1%;
}
table {
font-family: "inter", sans-serif;
border-collapse: collapse;
}
.table-div {
/*width: 50%;*/
}
.row-id {
font-weight: bold;
border-bottom: 1px solid hsl(0, 0%, 16%);
border-right: 1px solid hsl(0, 0%, 16%);
}
tr:nth-child(even) {
background: hsl(210, 100%, 96.08%)
}
table th:first-of-type,
table td:first-of-type {
border-left: 0;
}
table th:last-of-type,
table td:last-of-type {
border-right: 0;
}
table th:first-of-type { /*add border radius to top left of table*/
border-top-left-radius: 7px;
}
table th:last-of-type { /*add border radius to top right of table*/
border-top-right-radius: 7px;
}
table td {
padding-left: 5px;
padding-right: 5px;
border-right: 1px solid #737373;
border-left: 1px solid darkgrey;
}
td input[type=checkbox] {
color: #0c2d4a;
}
.table-cell-boolean {
text-align: center;
}
.table-cell-boolean-true {
color: green;
}
.table-cell-boolean-false {
color: red;
}
.table-cell-number {
text-align: right;
}
.table-cell-datetime {
text-align: right;
/*width: 20ch;*/
/*padding-left: 2ch;*/
white-space: normal;
min-width: 0;
/*font-family: monospace;*/
max-width: 14ch
}

12
tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2020", "DOM"],
"declaration": true,
"outDir": "dist",
"strict": true
},
"include": ["abstract-table.ts"]
}