Compare commits
9 Commits
4c5c444002
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5833fcbf8 | ||
|
|
95254027c4 | ||
|
|
436217a8ff | ||
|
|
8c561385b8 | ||
|
|
67ecf689a6 | ||
|
|
b0f40dea5f | ||
|
|
27488dd1b3 | ||
|
|
2a851983b2 | ||
|
|
9825d25090 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
export type TableOptions<TRow extends object> = {
|
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:
|
* 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.
|
* ```{ 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.
|
* @template TRow A type that the shape of a row should be. E.g.
|
||||||
* ```
|
* ```
|
||||||
* type User = {
|
* 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,
|
* 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 +86,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 +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.
|
* 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 +118,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 +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}.
|
* @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,19 +144,31 @@ 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 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");
|
const row = document.createElement("tr");
|
||||||
|
|
||||||
for (const column of this.columns) {
|
for (const column of this.columns) {
|
||||||
const cell: HTMLTableCellElement = document.createElement("td");
|
|
||||||
const value = data[column.key]
|
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.
|
// Element values are inserted as children, not stringified.
|
||||||
if (value instanceof HTMLElement) {
|
if (value instanceof HTMLElement) {
|
||||||
cell.appendChild(value);
|
cell.appendChild(value);
|
||||||
row.append(cell);
|
row.append(cell);
|
||||||
continue; // skip the textContent path that would wipe it
|
continue; // skip the textContent path that would wipe it
|
||||||
}
|
}
|
||||||
|
|
||||||
let cellValue = "";
|
let cellValue = "";
|
||||||
@@ -170,6 +185,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 +211,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 +221,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 +234,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 it is not set.
|
||||||
*/
|
*/
|
||||||
getMessageElement(): HTMLElement | null {
|
getMessageElement(): HTMLElement | null {
|
||||||
return this.messageEl;
|
return this.messageEl;
|
||||||
@@ -268,6 +289,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.
|
||||||
}
|
}
|
||||||
29
package-lock.json
generated
Normal file
29
package-lock.json
generated
Normal 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
22
package.json
Normal 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
87
table.css
Normal 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
12
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user