├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .npmrc ├── .parcelrc ├── .prettierignore ├── LICENSE ├── README.md ├── babel.config.js ├── babel.style.config.json ├── benchmark ├── index.html └── index.tsx ├── cspell.config.yaml ├── demo ├── complex │ ├── index.html │ └── index.tsx ├── simple │ ├── index.html │ └── index.tsx └── variable-rows │ ├── index.html │ └── index.tsx ├── jest.config.js ├── other ├── atom-linter-panel.png ├── inline.js ├── simple-table-demo.gif └── simple-table-demo.png ├── package.json ├── pnpm-lock.yaml ├── src ├── SimpleTable.less ├── SimpleTable.tsx └── SimpleTable.types.ts ├── test ├── SimpleTable.test.tsx └── util.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-atomic", 3 | "ignorePatterns": ["dist/", "node_modules/", "coverage/"], 4 | "rules": { 5 | "@typescript-eslint/no-inferrable-types": "off", 6 | "@typescript-eslint/no-non-null-assertion": "off", 7 | "react/react-in-jsx-scope": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | # don't diff machine generated files 4 | dist/ -diff 5 | package-lock.json -diff 6 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - "master" 7 | 8 | jobs: 9 | Test: 10 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - windows-latest 18 | - macos-latest 19 | node: 20 | - 14 21 | - 18 22 | pnpm: 23 | - 7 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node }} 29 | 30 | - name: Setup Pnpm 31 | uses: pnpm/action-setup@v2 32 | with: 33 | version: ${{ matrix.pnpm }} 34 | 35 | - name: Install dependencies 36 | run: pnpm install 37 | 38 | - name: Run tests 👩🏾‍💻 39 | run: pnpm run test 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS metadata 2 | .DS_Store 3 | Thumbs.db 4 | .cache 5 | 6 | # Node 7 | node_modules 8 | package-lock.json 9 | 10 | # TypeScript 11 | *.tsbuildinfo 12 | 13 | # Build directories 14 | dist 15 | .parcel-cache 16 | 17 | # Coverage 18 | coverage 19 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | public-hoist-pattern[]=* 3 | package-lock=false 4 | lockfile=true 5 | prefer-frozen-lockfile=true 6 | side-effects-cache=true 7 | strict-peer-dependencies=false 8 | -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "optimizers": { 4 | "*.{js,mjs}": [ 5 | "@parcel/optimizer-esbuild" 6 | ] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | pnpm-lock.yaml 4 | changelog.md 5 | coverage 6 | build 7 | dist 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Amin Yahyaabadi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solid SimpleTable 2 | 3 | ![CI](https://github.com/aminya/solid-simple-table/workflows/CI/badge.svg) 4 | 5 | Solid SimpleTable is a blazing fast reactive table component that gives you freedom. 6 | 7 | ### Features 8 | 9 | - Very fast as it is compiled down to VanilaJS using Solid 10 | - Very small (~2.7KB) 11 | - Automatic sorting 12 | - Support for custom header and row renderers (so the cells can be components themselves) 13 | - Support for custom sort functions 14 | - Support for onClick on all rows 15 | - Support for DOM accessors 16 | - The library is fully tested with 90% of code coverage. 17 | 18 | ![Simple table demo](other/simple-table-demo.gif) 19 | 20 | This library is production ready. It is currently used as the linter panel of Atom editor! 21 | 22 | ![Atom Linter Panel](other/atom-linter-panel.png) 23 | 24 | ## Installation 25 | 26 | npm install --save solid-simple-table 27 | 28 | ## Usage 29 | 30 | [**Run demo here!**](https://aminya.github.io/solid-simple-table/) 31 | 32 | ```js 33 | import { render } from "solid-js/web" 34 | 35 | import { SimpleTable } from "solid-simple-table" 36 | import type { SortDirection } from "solid-simple-table" 37 | 38 | const rows = [ 39 | { file: "C:/a", message: "Folder a", severity: "error" }, 40 | { file: "C:/b", message: "Folder b", severity: "warning" }, 41 | { file: "C:/c", message: "Folder c", severity: "info" }, 42 | { file: "C:/d", message: "Folder d", severity: "error" }, 43 | ] 44 | 45 | function MyTable() { 46 | return 47 | } 48 | 49 | render(() => , document.getElementById("app")) 50 | ``` 51 | 52 | The css is available under `dist/SimpleTable.css` which you can import into HTML: 53 | 54 | ```html 55 | 56 | ``` 57 | 58 | or in JavaScript: 59 | 60 | ```js 61 | import "solid-simple-table/dist/SimpleTable.css"; 62 | ``` 63 | 64 | For other examples, see [the demo folder](https://github.com/aminya/solid-simple-table/tree/master/demo). 65 | 66 | ## API 67 | 68 | ```ts 69 | 70 | 73 | 74 | // Optional props: 75 | 76 | // columns 77 | 78 | // manually provided columns 79 | columns?: Array> 80 | 81 | /** 82 | if columns is not provided and Row is an object, construct columns based on this row 83 | Takes this Row's keys as Column IDs 84 | @default 0 (first row) 85 | */ 86 | representativeRowNumber?: number 87 | 88 | // renderers 89 | headerRenderer?(column: Column): string | Renderable 90 | bodyRenderer?(row: Row, columnID: K): string | Renderable 91 | 92 | // dynamic CSS classes for table cells 93 | headerCellClass?(column: Column): string 94 | bodyCellClass?(row: Row, columnID: K): string 95 | 96 | // the class name to be used instead of the provided default. The default value is `solid-simple-table light typography` 97 | className?: string 98 | // extra styles 99 | style?: JSX.CSSProperties | string 100 | 101 | // sort options 102 | defaultSortDirection?: NonNullSortDirection 103 | rowSorter?(rows: Array, sortDirection: NonNullSortDirection): Array 104 | 105 | 106 | // accessors 107 | 108 | /** 109 | set to true if you want column, row, and cell accessors 110 | @default false 111 | */ 112 | accessors?: boolean 113 | 114 | /** a function that takes row and returns string unique key for that row 115 | @default {defaultGetRowID} 116 | */ 117 | getRowID?(row: Row): string 118 | 119 | />; 120 | ``` 121 | 122 | In which: 123 | 124 | ```ts 125 | // util types 126 | export type Renderable = any 127 | export type IndexType = string | number 128 | 129 | // row and column types 130 | export type Row = number | string | Record 131 | export type Column = { 132 | id: K 133 | label?: string 134 | sortable?: boolean 135 | onClick?(e: MouseEvent, row: Row): void 136 | } 137 | 138 | /** 139 | * Sort direction. It is a tuple: 140 | * 141 | * @type is The direction of the sort 142 | * @columnID is the key used for sorting 143 | */ 144 | export type NonNullSortDirection = [columnID: K, type: "asc" | "desc"] 145 | export type SortDirection = NonNullSortDirection | [columnID: null, type: null] 146 | ``` 147 | 148 | ## Projects using Solid-Table 149 | 150 | - [Atom's Linter](https://github.com/steelbrain/linter-ui-default) 151 | 152 | ## License 153 | 154 | This package is licensed under the terms of MIT License. Originally, it was inspired by [sb-react-table](https://github.com/steelbrain/react-table/tree/2f8472960a77ca6cf2444c392697772716195bf4). 155 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // base config used in all situations 2 | const config = { 3 | presets: ["babel-preset-solid"], 4 | plugins: [], 5 | } 6 | 7 | module.exports = (api) => { 8 | if (api.env("test")) { 9 | // modify the config for Jest 10 | config.presets.push("@babel/preset-typescript", [ 11 | "@babel/preset-env", 12 | { 13 | targets: { 14 | node: "current", 15 | esmodules: false, 16 | }, 17 | }, 18 | ]) 19 | } 20 | return config 21 | } 22 | -------------------------------------------------------------------------------- /babel.style.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@iwatakeshi/babel-plugin-remove-import-styles", 5 | { 6 | "extensions": [".less", ".sass", ".css"] 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /benchmark/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Solid Simple Table Demo 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /benchmark/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "solid-js/web" 2 | import { SimpleTable } from "../src/SimpleTable" 3 | 4 | import { Chance } from "chance" 5 | const chance = new Chance() 6 | 7 | const rows = new Array(1000) 8 | 9 | for (let iRow = 0; iRow < rows.length; iRow++) { 10 | rows[iRow] = { 11 | file: chance.sentence({ words: 1 }), 12 | message: chance.sentence({ words: 20 }), 13 | severity: chance.sentence({ words: 1 }), 14 | } 15 | } 16 | 17 | export function MySimpleTable() { 18 | return 19 | } 20 | 21 | // render demo 22 | 23 | const ti = window.performance.now() 24 | 25 | render(() => , document.getElementById("app")!) 26 | 27 | const tf = window.performance.now() 28 | console.log(`Render time: ${(tf - ti).toFixed()}`) 29 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | version: "0.2" 2 | files: 3 | - "**/*" 4 | useGitignore: true 5 | enableGlobDot: true 6 | ignorePaths: 7 | - pnpm-lock.yaml 8 | - .git/ 9 | - dist/ 10 | - "**/node_modules/" 11 | words: 12 | - aminya 13 | - esbuild 14 | - esmodule 15 | - esmodules 16 | - iwatakeshi 17 | - npmrc 18 | - pnpm 19 | - terserrc 20 | - tsbuildinfo 21 | - Yahyaabadi 22 | ignoreWords: [] 23 | import: [] 24 | dictionaryDefinitions: [] 25 | dictionaries: [] 26 | language: en, en-GB 27 | allowCompoundWords: true 28 | -------------------------------------------------------------------------------- /demo/complex/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Solid Simple Table Demo 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/complex/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "solid-js/web" 2 | import { SimpleTable } from "../../src/SimpleTable" 3 | import type { NonNullSortDirection } from "../../src/SimpleTable" 4 | import type { IndexType, Props } from "../../src/SimpleTable" 5 | 6 | export const rows = [ 7 | { file: "C:/a", message: "Folder a", severity: "error" }, 8 | { file: "C:/b", message: "Folder b", severity: "warning" }, 9 | { file: "C:/c", message: "Folder c", severity: "info" }, 10 | { file: "C:/d", message: "Folder d", severity: "error" }, 11 | ] 12 | 13 | export const columns = [ 14 | { 15 | id: "file", 16 | label: "File", 17 | }, 18 | { 19 | id: "message", 20 | label: "Message", 21 | sortable: false, 22 | }, 23 | { 24 | id: "severity", 25 | label: "Severity", 26 | }, 27 | ] 28 | 29 | type MyComplexTableRow = typeof rows[0] 30 | type MyComplexTableColumn = typeof columns[0] 31 | type MyColumnKeys = keyof MyComplexTableRow 32 | 33 | function MyComplexTableSorter( 34 | rows_in: Array, // eslint-disable-line no-shadow 35 | sortDirection: NonNullSortDirection 36 | ): Array { 37 | const columnID = sortDirection[0] 38 | const currentSortDirection = sortDirection[1] 39 | return rows_in.sort(function (a, b) { 40 | if (columnID in a && columnID in b) { 41 | const multiplyWith = currentSortDirection === "asc" ? 1 : -1 42 | const sortValue = a.severity.localeCompare(b.severity) 43 | if (sortValue !== 0) { 44 | return multiplyWith * sortValue 45 | } 46 | } 47 | return 0 48 | }) 49 | } 50 | 51 | export function MyComplexTable(props: Props) { 52 | return ( 53 | {column.label}} 57 | bodyRenderer={(row: MyComplexTableRow, columnID: MyColumnKeys) => {row[columnID]}} 58 | defaultSortDirection={["file", "asc"]} 59 | rowSorter={MyComplexTableSorter} 60 | getRowID={(row) => JSON.stringify(row)} 61 | id={props.id} 62 | /> 63 | ) 64 | } 65 | 66 | // skip rendering if in test mode 67 | if (process.env.NODE_ENV !== "test") { 68 | // render demo 69 | render(() => , document.getElementById("app")!) 70 | } 71 | -------------------------------------------------------------------------------- /demo/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Solid Simple Table Demo 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/simple/index.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "solid-js/web" 2 | import { SimpleTable } from "../../src/SimpleTable" 3 | import type { IndexType, Props } from "../../src/SimpleTable" 4 | 5 | export const rows = [ 6 | { file: "C:/a", message: "Folder a", severity: "error" }, 7 | { file: "C:/b", message: "Folder b", severity: "warning" }, 8 | { file: "C:/c", message: "Folder c", severity: "info" }, 9 | { file: "C:/d", message: "Folder d", severity: "error" }, 10 | ] 11 | 12 | export function MySimpleTable(props: Props) { 13 | return 14 | } 15 | 16 | // skip rendering if in test mode 17 | if (process.env.NODE_ENV !== "test") { 18 | // render demo 19 | render(() => , document.getElementById("app")!) 20 | } 21 | -------------------------------------------------------------------------------- /demo/variable-rows/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Solid Simple Table Demo 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /demo/variable-rows/index.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, onMount } from "solid-js" 2 | import { render } from "solid-js/web" 3 | import { SimpleTable } from "../../src/SimpleTable" 4 | 5 | export const initialRows = [ 6 | { file: "C:/a", message: "Folder a", severity: "error" }, 7 | { file: "C:/b", message: "Folder b", severity: "warning" }, 8 | { file: "C:/c", message: "Folder c", severity: "info" }, 9 | { file: "C:/d", message: "Folder d", severity: "error" }, 10 | ] 11 | 12 | type Props = { 13 | initialRows: typeof initialRows 14 | } 15 | 16 | export function MyVariableRowsTable(props: Props) { 17 | // This example pushes and sets to props.rows, so createSignal's second argument needs to be false as this is not an immutable replacement. 18 | const [getRows, setRows] = createSignal(props.initialRows, { equals: false }) 19 | 20 | onMount(() => { 21 | setInterval(() => { 22 | const rows = getRows() 23 | rows.push({ file: "New file", message: "New message", severity: "info" }) 24 | setRows(rows) 25 | }, 1000) 26 | }) 27 | 28 | return 29 | } 30 | 31 | // skip rendering if in test mode 32 | if (process.env.NODE_ENV !== "test") { 33 | // render demo 34 | render(() => , document.getElementById("app")!) 35 | } 36 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // includes resolving, transformation, etc 3 | preset: "solid-jest/preset/browser/", 4 | testEnvironment: "jsdom", 5 | 6 | // coverage 7 | collectCoverageFrom: ["src/**/*.{ts,tsx}"], 8 | coveragePathIgnorePatterns: ["assets", ".css.d.ts"], 9 | 10 | // Handle css, images, etc 11 | moduleNameMapper: { 12 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": 13 | "/src/__mocks__/fileMock.js", 14 | "\\.(scss|sass|css|less)$": "identity-obj-proxy", 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /other/atom-linter-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aminya/solid-simple-table/f35115ce1bec4585138566931fd115aad64e1bd9/other/atom-linter-panel.png -------------------------------------------------------------------------------- /other/inline.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const inline = require("web-resource-inliner") 3 | 4 | const folder = process.argv[2] 5 | 6 | inline.html( 7 | { 8 | fileContent: readFileSync(`${folder}/index.html`), 9 | relativeTo: folder, 10 | }, 11 | (err, result) => { 12 | if (err) { 13 | throw err 14 | } 15 | fs.writeFileSync(`${folder}/index.html`, result) 16 | } 17 | ) 18 | 19 | function readFileSync(file) { 20 | const contents = fs.readFileSync(file, "utf8") 21 | return process.platform === "win32" ? contents.replace(/\r\n/g, "\n") : contents 22 | } 23 | -------------------------------------------------------------------------------- /other/simple-table-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aminya/solid-simple-table/f35115ce1bec4585138566931fd115aad64e1bd9/other/simple-table-demo.gif -------------------------------------------------------------------------------- /other/simple-table-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aminya/solid-simple-table/f35115ce1bec4585138566931fd115aad64e1bd9/other/simple-table-demo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solid-simple-table", 3 | "version": "2.0.0", 4 | "description": "blazing fast reactive table component that gives you freedom", 5 | "homepage": "https://github.com/aminya/solid-simple-table#readme", 6 | "bugs": { 7 | "url": "https://github.com/aminya/solid-simple-table/issues" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/aminya/solid-simple-table.git" 12 | }, 13 | "license": "MIT", 14 | "author": "Amin Yahyaabadi ", 15 | "main": "dist/SimpleTable.mjs", 16 | "source": "src/SimpleTable.tsx", 17 | "types": "dist/SimpleTable.d.ts", 18 | "files": [ 19 | "dist", 20 | "src" 21 | ], 22 | "scripts": { 23 | "benchmark": "cross-env NODE_ENV=production parcel serve --target benchmark ./benchmark/index.html --open --no-hmr", 24 | "build": "run-s clean && run-p build.parcel build.types", 25 | "build.parcel": "cross-env NODE_ENV=production parcel build --target main && run-s build.style", 26 | "build.style": "babel ./dist/SimpleTable.mjs --keep-file-extension --out-dir dist --config-file ./babel.style.config.json --retain-lines", 27 | "build.types": "tsc -p ./tsconfig.json --emitDeclarationOnly", 28 | "clean": "shx rm -rf ./dist/", 29 | "demo.complex": "cross-env NODE_ENV=production parcel serve ./demo/complex/index.html --dist-dir ./demo/complex/dist --open", 30 | "demo.simple": "cross-env NODE_ENV=production parcel serve ./demo/simple/index.html --dist-dir ./demo/simple/dist --open", 31 | "demo.simple.build": "cross-env NODE_ENV=production parcel build --target demo ./demo/simple/index.html && node ./other/inline.js ./demo/simple/dist && shx cp ./demo/simple/dist/index.html .", 32 | "demo.variable-rows": "cross-env NODE_ENV=production parcel serve ./demo/variable-rows/index.html --dist-dir ./demo/variable-rows/dist --open", 33 | "demo.variable-rows.build": "cross-env NODE_ENV=production parcel build --target demo-variable-rows ./demo/variable-rows/index.html --no-optimize && node ./other/inline.js ./demo/variable-rows/dist && shx cp ./demo/variable-rows/dist/index.html .", 34 | "dev": "cross-env NODE_ENV=development parcel watch --target main src/SimpleTable.tsx", 35 | "format": "run-s lint.prettier", 36 | "lint": "run-p --aggregate-output --continue-on-error lint.cspell lint.eslint lint.prettier lint.tsc", 37 | "lint.cspell": "cspell lint --no-progress --show-suggestions --cache --cache-location ./.cache/cspell/.cspellcache", 38 | "lint.eslint": "eslint **/*.{ts,tsx,js,jsx,cjs,mjs,json,yaml} --no-error-on-unmatched-pattern --cache --cache-location ./.cache/eslint/ --fix", 39 | "lint.prettier": "prettier --list-different --write .", 40 | "lint.tsc": "tsc --noEmit", 41 | "prepare": "pnpm run build", 42 | "test": "cross-env jest --coverage" 43 | }, 44 | "prettier": "prettier-config-atomic", 45 | "dependencies": { 46 | "babel-preset-solid": "~1.6.1", 47 | "solid-js": "~1.6.1" 48 | }, 49 | "devDependencies": { 50 | "@babel/cli": "^7.19.3", 51 | "@babel/core": "^7.20.2", 52 | "@babel/plugin-transform-modules-commonjs": "^7.19.6", 53 | "@babel/preset-env": "^7.20.2", 54 | "@babel/preset-typescript": "^7.18.6", 55 | "@iwatakeshi/babel-plugin-remove-import-styles": "^1.0.5", 56 | "@parcel/core": "^2.8.0", 57 | "@parcel/optimizer-esbuild": "^2.8.0", 58 | "@parcel/packager-ts": "2.8.0", 59 | "@parcel/transformer-less": "2.8.0", 60 | "@parcel/transformer-typescript-types": "2.8.0", 61 | "@types/chance": "^1.1.3", 62 | "@types/jest": "^29.2.2", 63 | "@types/node": "18.11.9", 64 | "babel-jest": "^29.3.1", 65 | "chance": "^1.1.9", 66 | "cross-env": "^7.0.3", 67 | "cspell": "^6.14.1", 68 | "eslint": "^8.10.0", 69 | "eslint-config-atomic": "^1.18.1", 70 | "identity-obj-proxy": "^3.0.0", 71 | "jest": "^29.3.1", 72 | "jest-environment-jsdom": "^29.3.1", 73 | "less": "^4.1.3", 74 | "npm-run-all2": "^6.0.4", 75 | "parcel": "^2.8.0", 76 | "prettier": "^2.7.1", 77 | "prettier-config-atomic": "^3.1.0", 78 | "shx": "^0.3.4", 79 | "solid-jest": "^0.2.0", 80 | "typescript": "4.8.4", 81 | "web-resource-inliner": "^6.0.1" 82 | }, 83 | "engines": { 84 | "browsers": "Chrome 76" 85 | }, 86 | "alias": { 87 | "solid-js": "solid-js/dist/solid.js", 88 | "solid-js/web": "solid-js/web/dist/web.js" 89 | }, 90 | "benchmark": "./benchmark/dist/index.html", 91 | "demo": "./demo/simple/dist/index.html", 92 | "demo-variable-rows": "./demo/variable-rows/dist/index.html", 93 | "pnpm": { 94 | "overrides": { 95 | "eslint": "^8.10.0", 96 | "core-js": "3.26.0", 97 | "core-js-pure": "3.26.0", 98 | "babel-eslint": "npm:@babel/eslint-parser" 99 | } 100 | }, 101 | "targets": { 102 | "main": { 103 | "context": "browser", 104 | "includeNodeModules": false, 105 | "engines": { 106 | "browsers": "last 2 versions" 107 | }, 108 | "isLibrary": true, 109 | "optimize": true, 110 | "outputFormat": "esmodule" 111 | }, 112 | "demo": { 113 | "context": "browser", 114 | "includeNodeModules": true 115 | }, 116 | "demo-variable-rows": { 117 | "context": "browser", 118 | "includeNodeModules": true 119 | }, 120 | "benchmark": { 121 | "context": "browser", 122 | "includeNodeModules": true 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/SimpleTable.less: -------------------------------------------------------------------------------- 1 | // Parameters 2 | 3 | // text 4 | @text-size: 14px; 5 | @text-font: sans-serif; 6 | @text-color: black; 7 | @text-color-dark: #939caa; 8 | 9 | // background 10 | @background-color: #fff - @text-color; 11 | @background-color-dark: #31363f; 12 | 13 | // border 14 | @border: (@text-size / 14) solid #fff; 15 | @border-color: #191a1f; 16 | 17 | /* Base Styles */ 18 | 19 | .solid-simple-table { 20 | border: @border; 21 | text-align: left; 22 | border-collapse: collapse; 23 | 24 | th, 25 | td { 26 | padding: @text-size * 0.7; 27 | } 28 | 29 | th { 30 | border-right: @border; 31 | border-bottom: @border; 32 | 33 | &:last-child { 34 | border-right: none; 35 | } 36 | 37 | &.sortable { 38 | cursor: pointer; 39 | // do not select text on clicks 40 | user-select: none; 41 | } 42 | 43 | .header { 44 | display: inline-flex; 45 | align-self: flex-start; 46 | float: left; 47 | } 48 | 49 | .sort-icon { 50 | width: @text-size; 51 | 52 | float: right; 53 | margin-left: @text-size; 54 | display: inline-flex; 55 | align-self: flex-end; 56 | visibility: hidden; 57 | } 58 | 59 | &:hover .sort-icon { 60 | visibility: visible; 61 | } 62 | } 63 | 64 | /* Fonts */ 65 | &.typography { 66 | font-family: @text-font; 67 | font-size: @text-size; 68 | } 69 | 70 | /* Light Theme */ 71 | &.light { 72 | color: @text-color; 73 | border-color: @border-color; 74 | 75 | thead { 76 | background-color: darken(@background-color, 10%); 77 | } 78 | 79 | th { 80 | border-right-color: @border-color; 81 | border-bottom-color: @border-color; 82 | } 83 | 84 | tbody { 85 | background-color: @background-color; 86 | } 87 | } 88 | 89 | /* Dark Theme */ 90 | &.dark { 91 | color: @text-color-dark; 92 | border-color: @border-color; 93 | 94 | thead { 95 | background-color: darken(@background-color-dark, 2%); 96 | } 97 | 98 | th { 99 | border-right-color: @border-color; 100 | border-bottom-color: @border-color; 101 | } 102 | 103 | tbody { 104 | background-color: @background-color-dark; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/SimpleTable.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal, createComputed, For, Show } from "solid-js" 2 | import "./SimpleTable.less" // eslint-disable-line import/no-unassigned-import 3 | import { Props, IndexType, RowsSignal, SortDirection, NonNullSortDirection, Row, Column } from "./SimpleTable.types" 4 | 5 | export * from "./SimpleTable.types" 6 | 7 | export function SimpleTable(props: Props) { 8 | const [getSortDirectionSignal, setSortDirection] = createSignal | undefined>(undefined) 9 | const [getRows, setRows] = createSignal>(props.rows, { equals: false }) 10 | 11 | // update the local copy whenever the parent updates 12 | createComputed(() => { 13 | setRows(props.rows) 14 | }) 15 | 16 | function getSortDirection(): SortDirection { 17 | const sortDirection = getSortDirectionSignal() 18 | if (sortDirection !== undefined) { 19 | return sortDirection 20 | } 21 | // use default sort direction: 22 | else if (props.defaultSortDirection !== undefined) { 23 | return props.defaultSortDirection 24 | } else { 25 | return [null, null] 26 | } 27 | } 28 | 29 | function generateSortCallback(columnID: Ind) { 30 | return (e: MouseEvent) => { 31 | setSortDirection(sortClickHandler(getSortDirection(), columnID, /* append */ e.shiftKey)) 32 | sortRows() 33 | } 34 | } 35 | 36 | const rowSorter: NonNullable["rowSorter"]> = props.rowSorter ?? defaultSorter 37 | 38 | // Row sorting logic: 39 | function sortRows() { 40 | const currentSortDirection = getSortDirection() 41 | // if should reset sort 42 | if ( 43 | currentSortDirection[0] === null && 44 | /* if defaultSortDirection is provided */ props.defaultSortDirection !== undefined 45 | ) { 46 | // reset sort 47 | setRows(rowSorter(getRows(), props.defaultSortDirection)) 48 | } 49 | // if should sort normally 50 | else if (currentSortDirection[0] !== null) { 51 | setRows(rowSorter(getRows(), currentSortDirection as NonNullSortDirection)) 52 | } // else ignore sort 53 | } 54 | 55 | // static props: 56 | // destructure the props that are not tracked and are used inside the loop (cache the property access) 57 | const { 58 | headerRenderer = defaultHeaderRenderer, 59 | bodyRenderer = defaultBodyRenderer, 60 | getRowID = defaultGetRowID, 61 | headerCellClass, 62 | bodyCellClass, 63 | accessors, 64 | id 65 | } = props 66 | 67 | function maybeRowID(row: Row) { 68 | // if accessors are needed 69 | if (accessors === true) { 70 | return getRowID(row) 71 | } else { 72 | return undefined 73 | } 74 | } 75 | 76 | if (props.columns === undefined) { 77 | // if columns are not provided manually provide it 78 | // TODO `Ind` here is a `string`. Remove the cast 79 | props.columns = defaultColumnMaker(props.rows, props.representativeRowNumber) as Column[] 80 | } 81 | 82 | // initial sort 83 | sortRows() 84 | 85 | return ( 86 | 91 | 92 | 93 | 94 | {(column) => { 95 | const isSortable = column.sortable !== false 96 | 97 | let className: string = isSortable ? "sortable" : "" 98 | if (headerCellClass !== undefined) { 99 | className += ` ${headerCellClass(column)}` 100 | } 101 | 102 | return ( 103 | 111 | ) 112 | }} 113 | 114 | 115 | 116 | 117 | 118 | {(row) => { 119 | const rowID = maybeRowID(row) 120 | return ( 121 | 122 | 123 | {(column) => { 124 | return ( 125 | 132 | ) 133 | }} 134 | 135 | 136 | ) 137 | }} 138 | 139 | 140 |
108 | {headerRenderer(column)} 109 | {renderHeaderIcon(getSortDirection(), column.id)} 110 |
column.onClick!(e, row) : undefined} 128 | id={rowID !== undefined ? `${rowID}.${column.id}` : undefined} 129 | > 130 | {bodyRenderer(row, column.id)} 131 |
141 | ) 142 | } 143 | 144 | const ARROW = { 145 | UP: "↑", 146 | DOWN: "↓", 147 | BOTH: "⇅", 148 | } 149 | 150 | function defaultColumnMaker(rows: Array>, representativeRowNumber: number = 0) { 151 | // construct the column information based on the representative row 152 | const representativeRow = rows[representativeRowNumber] 153 | const columnIDs = Object.keys(representativeRow) 154 | 155 | // make Array<{key: columnID}> 156 | const columnNumber = columnIDs.length 157 | const columns: Array> = new Array(columnNumber) 158 | for (let iCol = 0; iCol < columnNumber; iCol++) { 159 | columns[iCol] = { id: columnIDs[iCol] } 160 | } 161 | return columns 162 | } 163 | 164 | // Returns a string from any value 165 | function stringer(value: any) { 166 | if (typeof value === "string") { 167 | return value 168 | } else { 169 | return JSON.stringify(value) 170 | } 171 | } 172 | 173 | function defaultHeaderRenderer(column: Column) { 174 | return
{column.label ?? column.id}
175 | } 176 | 177 | function defaultBodyRenderer(row: Row, columnID: Ind) { 178 | if (typeof row === "object") { 179 | return stringer(row[columnID]) 180 | } else { 181 | return stringer(row) 182 | } 183 | } 184 | 185 | function defaultGetRowID(row: Row) { 186 | return stringer(row) 187 | } 188 | 189 | function renderHeaderIcon(sortDirection: SortDirection, columnID: Ind) { 190 | let icon 191 | if (sortDirection[0] === null || sortDirection[0] !== columnID) { 192 | icon = ARROW.BOTH 193 | } else { 194 | icon = sortDirection[1] === "asc" ? ARROW.DOWN : ARROW.UP 195 | } 196 | return {icon} 197 | } 198 | 199 | function sortClickHandler(sortDirection: SortDirection, columnID: Ind, append: boolean) { 200 | let sortDirectionNew: SortDirection 201 | 202 | // if holding shiftKey while clicking: reset sorting 203 | if (append) { 204 | sortDirectionNew = [null, null] 205 | } 206 | // if clicking on an already sorted column: invert direction on click 207 | else if (sortDirection[0] === columnID) { 208 | sortDirectionNew = [ 209 | /* previousSortedColumn */ sortDirection[0], 210 | /* previousSortedDirection */ sortDirection[1] === "asc" ? "desc" : "asc", // invert direction 211 | ] 212 | } 213 | // if clicking on a new column 214 | else { 215 | sortDirectionNew = [columnID, "asc"] 216 | } 217 | return sortDirectionNew 218 | } 219 | 220 | /** 221 | * Default alphabetical sort function 222 | * 223 | * @param rows: The rows of the table 224 | * @param columnID: The last clicked columnID 225 | */ 226 | function defaultSorter( 227 | rows: Array>, 228 | sortDirection: NonNullSortDirection 229 | ): Array> { 230 | if (!rows.length) { 231 | return rows 232 | } 233 | 234 | let rowsNew: typeof rows 235 | const columnID = sortDirection[0] 236 | if (typeof rows[0] === "object") { 237 | rowsNew = rows.sort((r1, r2) => { 238 | const r1_val = (r1 as Record)[columnID] 239 | const r2_val = (r2 as Record)[columnID] 240 | if (r1_val === r2_val) { 241 | // equal values 242 | return 0 243 | } else if (r1_val < r2_val) { 244 | return -1 //r1_val comes first 245 | } else { 246 | return 1 // r2_val comes first 247 | } 248 | }) 249 | } else { 250 | rowsNew = rows.sort() 251 | } 252 | 253 | return sortDirection[1] === "desc" ? rowsNew.reverse() : rowsNew 254 | } 255 | -------------------------------------------------------------------------------- /src/SimpleTable.types.ts: -------------------------------------------------------------------------------- 1 | import type { JSX } from "solid-js" 2 | 3 | // util types 4 | export type Renderable = any 5 | 6 | export type IndexType = string | number 7 | 8 | // row and column types 9 | export type Row = number | string | Record 10 | 11 | export type Column = { 12 | id: K 13 | label?: string 14 | sortable?: boolean 15 | onClick?(e: MouseEvent, row: Row): void 16 | } 17 | 18 | /** 19 | * Sort direction. It is a tuple: 20 | * 21 | * @type is The direction of the sort 22 | * @columnID is the key used for sorting 23 | */ 24 | export type NonNullSortDirection = [columnID: K, type: "asc" | "desc"] 25 | export type SortDirection = NonNullSortDirection | [columnID: null, type: null] 26 | 27 | // Props 28 | export type Props = { 29 | // rows 30 | rows: Array> 31 | 32 | // Optional props: 33 | 34 | // columns 35 | 36 | // manually provided columns 37 | columns?: Array> 38 | 39 | /** 40 | * If columns is not provided and Row is an object, construct columns based on this row Takes this Row's keys as 41 | * Column IDs 42 | * 43 | * @default 0 (first row) 44 | */ 45 | representativeRowNumber?: number 46 | 47 | // renderers 48 | headerRenderer?(column: Column): string | Renderable 49 | bodyRenderer?(row: Row, columnID: K): string | Renderable 50 | 51 | // dynamic CSS classes 52 | /** 53 | * Optional function to get dynamic CSS class names for each column header cell. 54 | * 55 | * @param `Column` Object (same as for `headerRenderer`) 56 | * @returns `string` of CSS class names to be set for the `th` element of that column 57 | */ 58 | headerCellClass?(column: Column): string 59 | /** 60 | * Optional function to get dynamic CSS class names for each body cell. 61 | * 62 | * @param Row Object, column ID (same as for bodyRenderer) 63 | * @returns String of CSS class names to be set for that cell's td element 64 | */ 65 | bodyCellClass?(row: Row, columnID: K): string 66 | 67 | // styles 68 | style?: JSX.CSSProperties | string 69 | id?: string 70 | className?: string 71 | 72 | // sort options 73 | defaultSortDirection?: NonNullSortDirection 74 | rowSorter?(rows: Array>, sortDirection: NonNullSortDirection): Array> 75 | 76 | // accessors 77 | 78 | /** 79 | * Set to true if you want column, row, and cell accessors 80 | * 81 | * @default false 82 | */ 83 | accessors?: boolean 84 | 85 | /** 86 | * A function that takes row and returns string unique key for that row 87 | * 88 | * @default { defaultGetRowID } 89 | */ 90 | getRowID?(row: Row): string 91 | } 92 | 93 | // Component signals (states) 94 | export type RowsSignal = Array> 95 | -------------------------------------------------------------------------------- /test/SimpleTable.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "solid-js/web" 2 | 3 | import { Column, SortDirection } from "../src/SimpleTable.types" 4 | import { MySimpleTable, rows as mySimpleTableRows } from "../demo/simple/index" 5 | import { MyVariableRowsTable, initialRows as myVariableRowsTableInitialRows } from "../demo/variable-rows/index" 6 | import { MyComplexTable, rows as myComplexTableRows, columns as MyComplexTableColumns } from "../demo/complex/index" 7 | 8 | import { sleep, click, getTagName } from "./util" 9 | import assert from "assert" 10 | 11 | function testTable( 12 | rowsData: Record[], 13 | rootElm: HTMLDivElement, 14 | shouldTestSort: boolean = true, 15 | columnsData?: Column[], 16 | defaultSortDirection?: SortDirection 17 | ) { 18 | const { table, thead, tbody, tr } = testTableSections(rootElm) 19 | 20 | // test headers 21 | const headers = tr!.children as HTMLCollectionOf 22 | const headersData = testTrHeaders(headers, rowsData, columnsData, defaultSortDirection) 23 | 24 | expect(tbody.children.length).toBe(rowsData.length) 25 | const rows = tbody.children as HTMLCollectionOf 26 | 27 | // test rows 28 | testTBodyRows(rows, rowsData) 29 | 30 | // test sorting 31 | if (shouldTestSort) { 32 | testSorting(rowsData, headers, tbody, headersData) 33 | } 34 | 35 | return { table, thead, tbody, tr, rows } 36 | } 37 | 38 | function testTableSections(rootElm: HTMLDivElement) { 39 | const children = rootElm.children 40 | expect(children.length).toBe(1) 41 | 42 | const table = children[0] as HTMLTableElement 43 | expect(getTagName(table)).toBe("table") 44 | expect(table.classList[0]).toBe("solid-simple-table") 45 | 46 | const tableChildren = table.children 47 | expect(tableChildren.length).toBe(2) 48 | 49 | const thead = tableChildren[0] as HTMLTableElement["tHead"] 50 | const tbody = tableChildren[1] as ReturnType 51 | 52 | assert(thead !== null) 53 | expect(getTagName(thead)).toBe("thead") 54 | expect(getTagName(tbody)).toBe("tbody") 55 | 56 | expect(thead.children.length).toBe(1) 57 | 58 | const tr = thead.firstElementChild as HTMLTableRowElement 59 | expect(getTagName(tr)).toBe("tr") 60 | 61 | return { table, thead, tbody, tr } 62 | } 63 | 64 | function testTrHeaders( 65 | headers: HTMLCollectionOf, 66 | rowsData: Record[], 67 | columnsData?: Column[], 68 | defaultSortDirection?: SortDirection 69 | ) { 70 | expect(headers.length).toBe(Object.keys(rowsData[0]).length) 71 | 72 | const headersData = Object.keys(rowsData[0]) 73 | for (let iColumn = 0, columnNum = headers.length; iColumn < columnNum; iColumn++) { 74 | const header = headers[iColumn] 75 | 76 | expect(getTagName(header)).toBe("th") 77 | 78 | const sortable = columnsData?.[iColumn].sortable ?? true 79 | const label = columnsData?.[iColumn].label ?? headersData[iColumn] 80 | 81 | if (sortable) { 82 | expect(header.className).toBe("sortable") 83 | if (defaultSortDirection !== undefined && label.toLowerCase() === defaultSortDirection[0]) { 84 | const direction = defaultSortDirection[1] === "asc" ? "↓" : "↑" 85 | expect(header.textContent).toBe(`${label}${direction}`) 86 | } else { 87 | expect(header.textContent).toBe(`${label}⇅`) 88 | } 89 | } else { 90 | expect(header.className).toBe("") 91 | expect(header.textContent).toBe(`${label}`) 92 | } 93 | } 94 | return headersData 95 | } 96 | 97 | function testTBodyRows(rows: HTMLCollectionOf, rowsData: Record[]) { 98 | // test rows 99 | for (let iRow = 0, rowNum = rows.length; iRow < rowNum; iRow++) { 100 | const row = rows[iRow] 101 | 102 | expect(getTagName(row)).toBe("tr") 103 | 104 | // test cells 105 | const cells = row.children 106 | expect(cells.length).toBe(Object.keys(rowsData[iRow]).length) 107 | 108 | const rowsValues = Object.values(rowsData[iRow]) 109 | for (let iCell = 0, cellNum = cells.length; iCell < cellNum; iCell++) { 110 | const cell = cells[iCell] 111 | expect(getTagName(cell)).toBe("td") 112 | expect(cell.textContent).toBe(rowsValues[iCell]) 113 | } 114 | } 115 | } 116 | 117 | function testSorting( 118 | rowsData: Record[], 119 | headers: HTMLCollectionOf, 120 | tbody: ReturnType, 121 | headersData: string[] 122 | ) { 123 | // test sorting 124 | for (let iColumn = 0, columnNum = headers.length; iColumn < columnNum; iColumn++) { 125 | const header = headers[iColumn] as HTMLTableHeaderCellElement 126 | 127 | // initial sort 128 | const rows = tbody.children as HTMLCollectionOf 129 | const relatedRowsData = rowsData.map((row) => row[headersData[iColumn]]) 130 | 131 | const relatedRows: HTMLTableCellElement[] = new Array(columnNum) 132 | for (let iRow = 0, rowNum = rows.length; iRow < rowNum; iRow++) { 133 | const row = rows[iRow] 134 | relatedRows[iRow] = row.children[iColumn] as HTMLTableCellElement 135 | 136 | expect(relatedRows[iRow].textContent).toBe(relatedRowsData[iRow]) 137 | } 138 | 139 | // ascending sort 140 | 141 | click(header) 142 | expect(header.textContent).toBe(`${headersData[iColumn]}↓`) 143 | 144 | const rowsAsc = tbody.children 145 | const relatedRowsDataAsc = relatedRowsData.sort() 146 | 147 | for (let iRow = 0, rowNum = rowsAsc.length; iRow < rowNum; iRow++) { 148 | const row = rowsAsc[iRow] 149 | relatedRows[iRow] = row.children[iColumn] as HTMLTableCellElement 150 | 151 | expect(relatedRows[iRow].textContent).toBe(relatedRowsDataAsc[iRow]) 152 | } 153 | 154 | // descending sort 155 | 156 | click(header) 157 | expect(header.textContent).toBe(`${headersData[iColumn]}↑`) 158 | 159 | const rowsDesc = tbody.children 160 | const relatedRowsDataDesc = relatedRowsData.sort().reverse() 161 | 162 | for (let iRow = 0, rowNum = rowsDesc.length; iRow < rowNum; iRow++) { 163 | const row = rowsDesc[iRow] 164 | relatedRows[iRow] = row.children[iColumn] as HTMLTableCellElement 165 | 166 | expect(relatedRows[iRow].textContent).toBe(relatedRowsDataDesc[iRow]) 167 | } 168 | 169 | click(header) 170 | expect(header.textContent).toBe(`${headersData[iColumn]}↓`) 171 | 172 | click(header) 173 | expect(header.textContent).toBe(`${headersData[iColumn]}↑`) 174 | } 175 | } 176 | 177 | describe("SolidSimpleTable", () => { 178 | let rootElm: HTMLDivElement 179 | let dispose: () => void 180 | 181 | beforeEach(() => { 182 | rootElm = document.createElement("div") 183 | rootElm.id = "app" 184 | }) 185 | 186 | test("renders simple table", () => { 187 | dispose = render(() => , rootElm) 188 | testTable(mySimpleTableRows, rootElm) 189 | }) 190 | 191 | test("renders variable rows table", async () => { 192 | dispose = render(() => , rootElm) 193 | const { rows } = testTable(myVariableRowsTableInitialRows, rootElm) 194 | 195 | // test added rows 196 | for (let i = 0; i < 4; i++) { 197 | const addedRowIndex = 4 + i 198 | expect(rows.length).toBe(addedRowIndex) 199 | // eslint-disable-next-line no-await-in-loop 200 | await sleep(1000) 201 | 202 | const row = rows[addedRowIndex] 203 | expect(getTagName(row)).toBe("tr") 204 | 205 | // test cells 206 | const cells = row.children 207 | expect(cells.length).toBe(Object.keys({ file: "New file", message: "New message", severity: "info" }).length) 208 | 209 | const mySimpleTableRowsValues = Object.values({ file: "New file", message: "New message", severity: "info" }) 210 | for (let iCell = 0, cellNum = cells.length; iCell < cellNum; iCell++) { 211 | const cell = cells[iCell] 212 | expect(getTagName(cell)).toBe("td") 213 | expect(cell.textContent).toBe(mySimpleTableRowsValues[iCell]) 214 | } 215 | } 216 | }) 217 | 218 | test("renders complex table", () => { 219 | dispose = render(() => , rootElm) 220 | testTable(myComplexTableRows, rootElm, false, MyComplexTableColumns, ["file", "asc"]) 221 | }) 222 | 223 | const testIdPropPassing = (expectedId: string) => { 224 | const tableElement = rootElm.querySelector("table"); 225 | expect(tableElement).not.toBeNull(); 226 | expect(tableElement?.id).toBe(expectedId); 227 | }; 228 | 229 | test("passes id prop to simple table element", () => { 230 | const testId = 'simpletablewithid'; 231 | dispose = render(() => , rootElm); 232 | testIdPropPassing(testId); 233 | testTable(mySimpleTableRows, rootElm); 234 | }); 235 | 236 | test("passes id prop to complex table element", () => { 237 | const testId = 'complextablewithid'; 238 | dispose = render(() => , rootElm); 239 | testTable(myComplexTableRows, rootElm, false, MyComplexTableColumns, ["file", "asc"]) 240 | testIdPropPassing(testId); 241 | }) 242 | 243 | afterEach(() => { 244 | rootElm.textContent = "" 245 | dispose() 246 | }) 247 | }) 248 | -------------------------------------------------------------------------------- /test/util.ts: -------------------------------------------------------------------------------- 1 | export async function sleep(time: number) { 2 | await new Promise((res) => { 3 | setTimeout(res, time) 4 | }) 5 | } 6 | 7 | export function getTagName(elm: Element | undefined): string | undefined { 8 | return elm?.tagName.toLowerCase() 9 | } 10 | 11 | export function click(elm: HTMLElement) { 12 | try { 13 | // @ts-ignore internal solid API 14 | elm.$$click(new MouseEvent("click")) 15 | } catch { 16 | elm.click() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "EsNext", 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "jsxImportSource": "solid-js", 16 | "outDir": "./dist", 17 | "declaration": true 18 | }, 19 | "include": ["src"] 20 | } 21 | --------------------------------------------------------------------------------