Skip to content

Instantly share code, notes, and snippets.

@TrevorBenson
Last active February 11, 2024 19:51
Show Gist options
  • Save TrevorBenson/20268a35b213a766e753ca62eea60288 to your computer and use it in GitHub Desktop.
Save TrevorBenson/20268a35b213a766e753ca62eea60288 to your computer and use it in GitHub Desktop.
Patternfly web-component for sortable table with attributes of headers, rows, sort-index and sort-direction
import './App.css';
import React from 'react';
import { Table, Thead, Tr, Th, Tbody, Td, ThProps } from '@patternfly/react-table';
import r2wc from "@r2wc/react-to-web-component";
interface TableRow {
[key: string]: string | number;
};
interface TableSortableProps {
headers: string; // JSON string representing an array of header objects
rows: string; // JSON string representing an array of row objects
sortIndex?: string | undefined; // JSON string representing the initial sort index
sortDirection?: string | undefined; // JSON string representing the initial sort direction
}
export const TableSortable: React.FunctionComponent<TableSortableProps> = React.memo(({ headers, rows, sortIndex, sortDirection }) => {
let headersJson: string[];
let rowsJson: (string | number)[][];
try {
headersJson = headers ? JSON.parse(headers) : [];
rowsJson = rows ? JSON.parse(rows) : [];
} catch (error) {
console.error('Error parsing JSON props:', error);
headersJson = [];
rowsJson = [];
}
const initialSortIndex = sortIndex !== undefined && !isNaN(Number(sortIndex)) ? JSON.parse(sortIndex) : 0;
const [activeSortIndex, setActiveSortIndex] = React.useState<number>(initialSortIndex);
const initialSortDirection = sortDirection === 'asc' || sortDirection === 'desc' ? sortDirection : 'asc';
const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc'>(initialSortDirection);
const getSortableRowValues = (row: TableRow | (string | number)[]): (string | number)[] => {
if (Array.isArray(row)) {
return row;
} else {
return headersJson.map(header => row[header.key]);
}
};
const sortRows = (a: TableRow | (string | number)[], b: TableRow | (string | number)[]): number => {
if (activeSortIndex === null) {
return 0; // or however you want to handle this case
}
const aValue = getSortableRowValues(a)[activeSortIndex];
const bValue = getSortableRowValues(b)[activeSortIndex];
if (typeof aValue === 'number') {
return activeSortDirection === 'asc' ? (aValue as number) - (bValue as number) : (bValue as number) - (aValue as number);
} else {
return activeSortDirection === 'asc' ? (aValue as string).localeCompare(bValue as string) : (bValue as string).localeCompare(aValue as string);
}
};
const handleSort = (_event: React.MouseEvent, index: number, direction: 'asc' | 'desc'): void => {
setActiveSortIndex(index);
setActiveSortDirection(direction);
};
const renderTableWithKeysAndLabels = () => (
<Table aria-label="Sortable table" ouiaId="SortableTable">
<Thead>
<Tr>
{headersJson.map((header, index) => (
<Th
key={header.key}
sort={{
sortBy: {
index: activeSortIndex,
direction: activeSortDirection,
defaultDirection: 'asc'
},
onSort: (_event, _index, direction) => handleSort(_event, index, direction),
columnIndex: index
}}
>
{header.label}
</Th>
))}
</Tr>
</Thead>
<Tbody>
{rowsJson.sort(sortRows).map((row, rowIndex) => (
<Tr key={rowIndex}>
{headersJson.map((header, colIndex) => (
<Td key={colIndex}>{row[header.key]}</Td>
))}
</Tr>
))}
</Tbody>
</Table>
);
const renderTableWithLists = () => (
<Table aria-label="Sortable table" ouiaId="SortableTable">
<Thead>
<Tr>
{headersJson.map((header, index) => (
<Th
key={index}
sort={{
sortBy: {
index: activeSortIndex,
direction: activeSortDirection,
defaultDirection: 'asc'
},
onSort: (_event, _index, direction) => handleSort(_event, index, direction),
columnIndex: index
}}
>
{header}
</Th>
))}
</Tr>
</Thead>
<Tbody>
{rowsJson.sort(sortRows).map((row, rowIndex) => (
<Tr key={rowIndex}>
{row.map((value, colIndex) => (
<Td key={colIndex}>{value}</Td>
))}
</Tr>
))}
</Tbody>
</Table>
);
return typeof headersJson[0] === 'string' ? renderTableWithLists() : renderTableWithKeysAndLabels();
});
TableSortable.defaultProps = {
headers: '["Default Header 1", "Default Header 2", "Default Header 3"]',
rows: '[["Default Row 1 Column 1", "Default Row 1 Column 2", "Default Row 1 Column 3"], ["Default Row 2 Column 1", "Default Row 2 Column 2", "Default Row 2 Column 3"]]'
};
export default TableSortable;
const PfTableSortableHeadersRows = r2wc(TableSortable, {
props: {
headers: "string",
rows: "string",
sortIndex: "string",
sortDirection: "string",
},
})
customElements.define("pf-table-sortable-hr", PfTableSortableHeadersRows);
"""FastAPI application with github-oauth."""
import asyncio
import json
from os import access # noqa: F401 # pylint: disable=unused-import # type: ignore
import httpx
from fastapi import Cookie, Depends, FastAPI, HTTPException, Request, Response, Security
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.get("/table", response_class=HTMLResponse)
async def table_headers_rows(request: Request):
headers = ["Name", "Age", "Country"]
rows = [
["John", 28, "USA"],
["Alice", 23, "Canada"],
["Bob", 31, "UK"],
]
return templates.TemplateResponse(
"index.html", {"request": request, "headers": headers, "rows": rows}
)
<!doctype html>
<html>
<head>
<title>Rsbuild App</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<script defer="defer" src="/static/js/lib-lodash.741c9a83.js"></script>
<script defer="defer" src="/static/js/lib-polyfill.9a13c3e5.js"></script>
<script defer="defer" src="/static/js/lib-react.6eeec448.js"></script>
<script defer="defer" src="/static/js/320.c03fd9b3.js"></script>
<script defer="defer" src="/static/js/index.17b941a7.js"></script>
<link href="/static/css/320.9e553c6e.css" rel="stylesheet" />
<link href="/static/css/index.a11cfb11.css" rel="stylesheet" />
</head>
<body>
<!-- <pf-table-sortable-hr
headers='[{"key": "name", "label": "Name"}, {"key": "age", "label": "Age"}, {"key": "birthplace", "label": "Birthplace"}]'
rows='[{"name": "Alice", "age": 23, "birthplace": "Santa Cruz, CA"}, {"name": "Bob", "age": 27, "birthplace": "San Diego, CA"}]'
sort-index='2'>
</pf-table-sortable-hr> -->
<pf-table-sortable-hr
headers="{{ headers|tojson }}"
rows="{{ rows|tojson }}"
sort-index="2"
>
</pf-table-sortable-hr>
</body>
</html>
@TrevorBenson
Copy link
Author

TrevorBenson commented Feb 10, 2024

Overview

Using react-to-web-component / rsbuild to create Patternfly usable web components. This version accepts serialized headers and rows attributes. There is also control over the sort-index and sort-direction.

The headers and rows can be a list of strings, or alist of keys and labels. The Types contained in headers and rows must be in sync.

Building the web component

  1. Scaffold the react application
$ npm create rsbuild@latest
pf-table-sortable-hr
React
TypeScript
cd my-project-delete
  1. Install the dependencies into the node modules using npm
npm install react @r2wc/react-to-web-component @patternfly/react-table --save
  1. Edit the ./src/App.tsx and add the TypeScipt XML
  2. Run the build to create the compiled JavaScript and sample index.html
npm run build

Alternatively, testing can be performed via npm run dev before collecting the JavaScript of the web component.

Making the web component accessible to your application

Example for FastAPI

  1. Copy the ./dist/static/ to the directory for StaticFiles() used with app.mount() for fastapi.
  2. Copy the ./dist/index.html to the directory where you host html files for the application. In the example, this is a directory of Jinja2 templates so the directory used in Jinja2Templates().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment