Compare commits

...

5 Commits

Author SHA1 Message Date
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
2 changed files with 108 additions and 8 deletions

View File

@@ -74,7 +74,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, * 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. * @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. exampleObject: TRow, // Needed for runtime, since types don't exist in JS.
displayNamesAndOrder?: Record<keyof TRow, string> displayNamesAndOrder?: Record<keyof TRow, string>
): Column<TRow>[] { ): Column<TRow>[] {
@@ -85,7 +85,7 @@ export abstract class Table<TRow extends object> {
return keys.map(key => ({ return keys.map(key => ({
key, // Raw property name from TRow 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 type: typeof exampleObject[key], // The type of the column
})); }));
} }
@@ -93,13 +93,13 @@ export abstract class Table<TRow extends object> {
/** /**
* Sets the header cell values on the header row from the {@link columns} on this 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 thead = document.createElement("thead");
const headerRow = document.createElement("tr"); const headerRow = document.createElement("tr");
for (const column of this.columns) { for (const column of this.columns) {
const th = document.createElement("th") const th = document.createElement("th")
th.textContent = column.value; th.textContent = column.headerValue; // Column's header text
headerRow.appendChild(th); headerRow.appendChild(th);
} }
@@ -117,6 +117,7 @@ export abstract class Table<TRow extends object> {
/** /**
* Appends one more row to the current rows. * Appends one more row to the current rows.
* @param data Data to append as a row. * @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 { appendRow(data: TRow): void {
const row = this.createPopulatedRow(data) const row = this.createPopulatedRow(data)
@@ -124,8 +125,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}. * @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 { appendRows(data: TRow[]): void {
if (data.length === 0) { if (data.length === 0) {
@@ -141,8 +143,13 @@ export abstract class Table<TRow extends object> {
* Creates a row, populated with data, for displaying in the table. * Creates a row, populated with data, for displaying in the table.
* @param data The data in the shape of `TRow`, with values populated. * @param data The data in the shape of `TRow`, with values populated.
* @return A HTMLTableRowElement that can then be set on a table. * @return A HTMLTableRowElement that can then be set on a table.
* @remarks If a cell value is an HTMLElement it is appended to the cell. 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"); const row = document.createElement("tr");
for (const column of this.columns) { for (const column of this.columns) {
@@ -170,6 +177,10 @@ export abstract class Table<TRow extends object> {
cell.classList.add("table-cell-number"); cell.classList.add("table-cell-number");
break; break;
case "bigint":
cellValue = String(value);
cell.classList.add("table-cell-number");
break;
default: default:
cellValue = value != null ? String(value) : ""; cellValue = value != null ? String(value) : "";
} }
@@ -192,6 +203,7 @@ export abstract class Table<TRow extends object> {
/** /**
* Deletes all current rows and sets a single row to the data in data param. * 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} * @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 { setSingleRow(data: TRow): void {
this.clearRows() this.clearRows()
@@ -201,6 +213,7 @@ export abstract class Table<TRow extends object> {
/** /**
* Deletes all current rows and sets the rows to the data in data param. * 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} * @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 { setRows(data: TRow[]): void {
this.clearRows() this.clearRows()
@@ -213,7 +226,7 @@ export abstract class Table<TRow extends object> {
/** /**
* Get the message element for the table. * 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 i iss not set.
*/ */
getMessageElement(): HTMLElement | null { getMessageElement(): HTMLElement | null {
return this.messageEl; return this.messageEl;
@@ -268,6 +281,6 @@ export abstract class Table<TRow extends object> {
export type Column<TRow> = { export type Column<TRow> = {
key: keyof TRow; // The name of the field. 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. type: string; // The type of the field, can be string since typesof are returned as strings.
} }

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
}