jattac.libs.web.responsive-table 0.0.25 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,241 +1,354 @@
1
- import React, { CSSProperties, Component, ReactNode } from 'react';
2
- import styles from '../Styles/ResponsiveTable.module.css';
3
- import IResponsiveTableColumnDefinition from '../Data/IResponsiveTableColumnDefinition';
4
-
5
- type ColumnDefinition<TData> =
6
- | IResponsiveTableColumnDefinition<TData>
7
- | ((data: TData, rowIndex?: number) => IResponsiveTableColumnDefinition<TData>);
8
- interface IProps<TData> {
9
- columnDefinitions: ColumnDefinition<TData>[];
10
- data: TData[];
11
- noDataComponent?: ReactNode;
12
- maxHeight?: string;
13
- onRowClick?: (item: TData) => void;
14
- }
15
-
16
- interface IState {
17
- isMobile: boolean;
18
- }
19
-
20
- // Class component
21
- class ResponsiveTable<TData> extends Component<IProps<TData>, IState> {
22
- constructor(props: IProps<TData>) {
23
- super(props);
24
- this.state = {
25
- isMobile: false,
26
- };
27
- }
28
-
29
- private get data(): TData[] {
30
- if (Array.isArray(this.props.data) && this.props.data.length > 0) {
31
- return this.props.data;
32
- } else {
33
- return [];
34
- }
35
- }
36
-
37
- private get noDataSvg(): ReactNode {
38
- return (
39
- <svg xmlns="http://www.w3.org/2000/svg" fill="#ccc" height="40" width="40" viewBox="0 0 24 24">
40
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-14h2v6h-2zm0 8h2v2h-2z" />
41
- </svg>
42
- );
43
- }
44
-
45
- private get hasData(): boolean {
46
- return this.data.length > 0;
47
- }
48
-
49
- private get noDataComponent(): ReactNode {
50
- return (
51
- this.props.noDataComponent || (
52
- <div className={styles.noDataWrapper}>
53
- {this.noDataSvg}
54
- <div className={styles.noData}>No data</div>
55
- </div>
56
- )
57
- );
58
- }
59
-
60
- componentDidMount(): void {
61
- this.setState(() => {
62
- return { isMobile: window.innerWidth <= 600 };
63
- });
64
-
65
- window.addEventListener('resize', this.handleResize);
66
- }
67
-
68
- componentWillUnmount(): void {
69
- window.removeEventListener('resize', this.handleResize);
70
- }
71
-
72
- handleResize = (): void => {
73
- this.setState(() => {
74
- return { isMobile: window.innerWidth <= 600 };
75
- });
76
- };
77
-
78
- private getColumnDefinition(
79
- columnDefinition: ColumnDefinition<TData>,
80
- rowIndex: number,
81
- ): IResponsiveTableColumnDefinition<TData> {
82
- if (!this.hasData) {
83
- return { displayLabel: '', cellRenderer: () => '' };
84
- }
85
- return columnDefinition instanceof Function ? columnDefinition(this.data[0], rowIndex) : columnDefinition;
86
- }
87
-
88
- private getRawColumnDefinition(columnDefinition: ColumnDefinition<TData>): IResponsiveTableColumnDefinition<TData> {
89
- let rawColumnDefinition: IResponsiveTableColumnDefinition<TData> = {} as IResponsiveTableColumnDefinition<TData>;
90
- if (columnDefinition instanceof Function) {
91
- rawColumnDefinition = columnDefinition(this.data[0], 0);
92
- } else {
93
- rawColumnDefinition = columnDefinition as IResponsiveTableColumnDefinition<TData>;
94
- }
95
- return rawColumnDefinition;
96
- }
97
-
98
- private onHeaderClickCallback(columnDefinition: ColumnDefinition<TData>): ((id: string) => void) | undefined {
99
- const rawColumnDefinition = this.getRawColumnDefinition(columnDefinition);
100
- if (rawColumnDefinition.interactivity && rawColumnDefinition.interactivity.onHeaderClick) {
101
- return rawColumnDefinition.interactivity.onHeaderClick;
102
- } else {
103
- return undefined;
104
- }
105
- }
106
-
107
- private getClickableHeaderClassName(
108
- onHeaderClickCallback: ((id: string) => void) | undefined,
109
- colDef: ColumnDefinition<TData>,
110
- ): string {
111
- const rawColumnDefinition = this.getRawColumnDefinition(colDef);
112
- const clickableHeaderClassName = onHeaderClickCallback
113
- ? rawColumnDefinition.interactivity!.className || styles.clickableHeader
114
- : '';
115
- return clickableHeaderClassName;
116
- }
117
-
118
- private get rowClickFunction(): (item: TData) => void {
119
- if (this.props.onRowClick) {
120
- return this.props.onRowClick;
121
- } else {
122
- return () => {};
123
- }
124
- }
125
-
126
- private get rowClickStyle(): CSSProperties {
127
- if (this.props.onRowClick) {
128
- return { cursor: 'pointer' } as CSSProperties;
129
- } else {
130
- return {};
131
- }
132
- }
133
-
134
- private get mobileView(): ReactNode {
135
- return (
136
- <div>
137
- {this.data.map((row, rowIndex) => (
138
- <div
139
- key={rowIndex}
140
- className={styles['card']}
141
- onClick={(e) => {
142
- this.rowClickFunction(row);
143
- e.stopPropagation();
144
- e.preventDefault();
145
- }}
146
- >
147
- <div className={styles['card-header']}> </div>
148
- <div className={styles['card-body']}>
149
- {this.props.columnDefinitions.map((columnDefinition, colIndex) => {
150
- const colDef = this.getColumnDefinition(columnDefinition, rowIndex);
151
- const onHeaderClickCallback = this.onHeaderClickCallback(colDef);
152
- const clickableHeaderClassName = this.getClickableHeaderClassName(onHeaderClickCallback, colDef);
153
- return (
154
- <div key={colIndex}>
155
- <p>
156
- <span
157
- className={`font-bold ${clickableHeaderClassName}`}
158
- onClick={
159
- onHeaderClickCallback ? () => onHeaderClickCallback(colDef.interactivity!.id) : undefined
160
- }
161
- >
162
- {colDef.displayLabel}:
163
- </span>{' '}
164
- {colDef.cellRenderer(row)}
165
- </p>
166
- </div>
167
- );
168
- })}
169
- </div>
170
- </div>
171
- ))}
172
- </div>
173
- );
174
- }
175
-
176
- private get largeScreenView(): ReactNode {
177
- const useFixedHeaders = this.props.maxHeight ? true : false;
178
-
179
- const fixedHeadersStyle = useFixedHeaders
180
- ? ({ maxHeight: this.props.maxHeight, overflowY: 'auto' } as CSSProperties)
181
- : {};
182
-
183
- return (
184
- <div style={fixedHeadersStyle}>
185
- <table className={styles['responsiveTable']} style={{ zIndex: -1 }}>
186
- <thead>
187
- <tr>
188
- {this.props.columnDefinitions.map((columnDefinition, colIndex) => {
189
- const onHeaderClickCallback = this.onHeaderClickCallback(columnDefinition);
190
- const clickableHeaderClassName = this.getClickableHeaderClassName(
191
- onHeaderClickCallback,
192
- columnDefinition,
193
- );
194
- return (
195
- <th
196
- key={colIndex}
197
- className={`${clickableHeaderClassName}`}
198
- style={{ zIndex: 0 }}
199
- onClick={
200
- onHeaderClickCallback
201
- ? () => onHeaderClickCallback(this.getColumnDefinition(columnDefinition, 0).interactivity!.id)
202
- : undefined
203
- }
204
- >
205
- {this.getColumnDefinition(columnDefinition, 0).displayLabel}
206
- </th>
207
- );
208
- })}
209
- </tr>
210
- </thead>
211
- <tbody>
212
- {this.data.map((row, rowIndex) => (
213
- <tr key={rowIndex}>
214
- {this.props.columnDefinitions.map((columnDefinition, colIndex) => (
215
- <td onClick={() => this.rowClickFunction(row)} key={colIndex}>
216
- <span style={{ ...this.rowClickStyle }}>
217
- {this.getColumnDefinition(columnDefinition, rowIndex).cellRenderer(row)}
218
- </span>
219
- </td>
220
- ))}
221
- </tr>
222
- ))}
223
- </tbody>
224
- </table>
225
- </div>
226
- );
227
- }
228
-
229
- render() {
230
- if (!this.hasData) {
231
- return this.noDataComponent;
232
- }
233
- if (this.state.isMobile) {
234
- return this.mobileView;
235
- }
236
-
237
- return this.largeScreenView;
238
- }
239
- }
240
-
241
- export default ResponsiveTable;
1
+ import React, { CSSProperties, Component, ReactNode } from 'react';
2
+ import styles from '../Styles/ResponsiveTable.module.css';
3
+ import IResponsiveTableColumnDefinition from '../Data/IResponsiveTableColumnDefinition';
4
+ import IFooterRowDefinition from '../Data/IFooterRowDefinition';
5
+
6
+ export type ColumnDefinition<TData> =
7
+ | IResponsiveTableColumnDefinition<TData>
8
+ | ((data: TData, rowIndex?: number) => IResponsiveTableColumnDefinition<TData>);
9
+ interface IProps<TData> {
10
+ columnDefinitions: ColumnDefinition<TData>[];
11
+ data: TData[];
12
+ noDataComponent?: ReactNode;
13
+ maxHeight?: string;
14
+ onRowClick?: (item: TData) => void;
15
+ footerRows?: IFooterRowDefinition[];
16
+ mobileBreakpoint?: number;
17
+ isLoading?: boolean;
18
+ animateOnLoad?: boolean;
19
+ }
20
+
21
+ interface IState {
22
+ isMobile: boolean;
23
+ }
24
+
25
+ // Class component
26
+ class ResponsiveTable<TData> extends Component<IProps<TData>, IState> {
27
+ private debouncedResize: () => void;
28
+
29
+ constructor(props: IProps<TData>) {
30
+ super(props);
31
+ this.state = {
32
+ isMobile: false,
33
+ };
34
+
35
+ this.debouncedResize = this.debounce(this.handleResize, 200);
36
+ }
37
+
38
+ private get mobileBreakpoint(): number {
39
+ return this.props.mobileBreakpoint || 600;
40
+ }
41
+
42
+ private debounce(func: () => void, delay: number): () => void {
43
+ let timeout: NodeJS.Timeout;
44
+ return () => {
45
+ clearTimeout(timeout);
46
+ timeout = setTimeout(() => func(), delay);
47
+ };
48
+ }
49
+
50
+ private get data(): TData[] {
51
+ if (Array.isArray(this.props.data) && this.props.data.length > 0) {
52
+ return this.props.data;
53
+ } else {
54
+ return [];
55
+ }
56
+ }
57
+
58
+ private get noDataSvg(): ReactNode {
59
+ return (
60
+ <svg xmlns="http://www.w3.org/2000/svg" fill="#ccc" height="40" width="40" viewBox="0 0 24 24">
61
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-14h2v6h-2zm0 8h2v2h-2z" />
62
+ </svg>
63
+ );
64
+ }
65
+
66
+ private get hasData(): boolean {
67
+ return this.data.length > 0;
68
+ }
69
+
70
+ private get noDataComponent(): ReactNode {
71
+ return (
72
+ this.props.noDataComponent || (
73
+ <div className={styles.noDataWrapper}>
74
+ {this.noDataSvg}
75
+ <div className={styles.noData}>No data</div>
76
+ </div>
77
+ )
78
+ );
79
+ }
80
+
81
+ componentDidMount(): void {
82
+ this.handleResize(); // Initial check
83
+ window.addEventListener('resize', this.debouncedResize);
84
+ }
85
+
86
+ componentWillUnmount(): void {
87
+ window.removeEventListener('resize', this.debouncedResize);
88
+ }
89
+
90
+ handleResize = (): void => {
91
+ this.setState({
92
+ isMobile: window.innerWidth <= this.mobileBreakpoint,
93
+ });
94
+ };
95
+
96
+ private getColumnDefinition(
97
+ columnDefinition: ColumnDefinition<TData>,
98
+ rowIndex: number,
99
+ ): IResponsiveTableColumnDefinition<TData> {
100
+ if (!this.hasData) {
101
+ return { displayLabel: '', cellRenderer: () => '' };
102
+ }
103
+ return columnDefinition instanceof Function ? columnDefinition(this.data[0], rowIndex) : columnDefinition;
104
+ }
105
+
106
+ private getRawColumnDefinition(columnDefinition: ColumnDefinition<TData>): IResponsiveTableColumnDefinition<TData> {
107
+ let rawColumnDefinition: IResponsiveTableColumnDefinition<TData> = {} as IResponsiveTableColumnDefinition<TData>;
108
+ if (columnDefinition instanceof Function) {
109
+ rawColumnDefinition = columnDefinition(this.data[0], 0);
110
+ } else {
111
+ rawColumnDefinition = columnDefinition as IResponsiveTableColumnDefinition<TData>;
112
+ }
113
+ return rawColumnDefinition;
114
+ }
115
+
116
+ private onHeaderClickCallback(columnDefinition: ColumnDefinition<TData>): ((id: string) => void) | undefined {
117
+ const rawColumnDefinition = this.getRawColumnDefinition(columnDefinition);
118
+ if (rawColumnDefinition.interactivity && rawColumnDefinition.interactivity.onHeaderClick) {
119
+ return rawColumnDefinition.interactivity.onHeaderClick;
120
+ } else {
121
+ return undefined;
122
+ }
123
+ }
124
+
125
+ private getClickableHeaderClassName(
126
+ onHeaderClickCallback: ((id: string) => void) | undefined,
127
+ colDef: ColumnDefinition<TData>,
128
+ ): string {
129
+ const rawColumnDefinition = this.getRawColumnDefinition(colDef);
130
+ const clickableHeaderClassName = onHeaderClickCallback
131
+ ? rawColumnDefinition.interactivity!.className || styles.clickableHeader
132
+ : '';
133
+ return clickableHeaderClassName;
134
+ }
135
+
136
+ private get rowClickFunction(): (item: TData) => void {
137
+ if (this.props.onRowClick) {
138
+ return this.props.onRowClick;
139
+ } else {
140
+ return () => {};
141
+ }
142
+ }
143
+
144
+ private get rowClickStyle(): CSSProperties {
145
+ if (this.props.onRowClick) {
146
+ return { cursor: 'pointer' } as CSSProperties;
147
+ } else {
148
+ return {};
149
+ }
150
+ }
151
+
152
+ private get tableFooter(): ReactNode {
153
+ if (!this.props.footerRows || this.props.footerRows.length === 0) {
154
+ return null;
155
+ }
156
+
157
+ return (
158
+ <tfoot>
159
+ {this.props.footerRows.map((row, rowIndex) => (
160
+ <tr key={rowIndex}>
161
+ {row.columns.map((col, colIndex) => (
162
+ <td key={colIndex} colSpan={col.colSpan} className={styles.footerCell}>
163
+ {col.cellRenderer()}
164
+ </td>
165
+ ))}
166
+ </tr>
167
+ ))}
168
+ </tfoot>
169
+ );
170
+ }
171
+
172
+ private get mobileFooter(): ReactNode {
173
+ if (!this.props.footerRows || this.props.footerRows.length === 0) {
174
+ return null;
175
+ }
176
+
177
+ return (
178
+ <div className={styles['card']}>
179
+ <div className={styles['card-body']}>
180
+ {this.props.footerRows.map((row, rowIndex) => (
181
+ <div key={rowIndex}>
182
+ {row.columns.map((col, colIndex) => (
183
+ <div key={colIndex}>{col.cellRenderer()}</div>
184
+ ))}
185
+ </div>
186
+ ))}
187
+ </div>
188
+ </div>
189
+ );
190
+ }
191
+
192
+ private get skeletonView(): ReactNode {
193
+ const skeletonRowCount = 5; // Or make this configurable
194
+ const columnCount = this.props.columnDefinitions.length;
195
+
196
+ if (this.state.isMobile) {
197
+ return (
198
+ <div>
199
+ {[...Array(skeletonRowCount)].map((_, i) => (
200
+ <div key={i} className={styles.skeletonCard}>
201
+ {[...Array(columnCount)].map((_, j) => (
202
+ <div key={j} className={`${styles.skeleton} ${styles.skeletonText}`} style={{ marginBottom: '0.5rem' }} />
203
+ ))}
204
+ </div>
205
+ ))}
206
+ </div>
207
+ );
208
+ }
209
+
210
+ return (
211
+ <table className={styles.responsiveTable}>
212
+ <thead>
213
+ <tr>
214
+ {[...Array(columnCount)].map((_, i) => (
215
+ <th key={i}>
216
+ <div className={`${styles.skeleton} ${styles.skeletonText}`} />
217
+ </th>
218
+ ))}
219
+ </tr>
220
+ </thead>
221
+ <tbody>
222
+ {[...Array(skeletonRowCount)].map((_, i) => (
223
+ <tr key={i}>
224
+ {[...Array(columnCount)].map((_, j) => (
225
+ <td key={j}>
226
+ <div className={`${styles.skeleton} ${styles.skeletonText}`} />
227
+ </td>
228
+ ))}
229
+ </tr>
230
+ ))}
231
+ </tbody>
232
+ </table>
233
+ );
234
+ }
235
+
236
+ private get mobileView(): ReactNode {
237
+ return (
238
+ <div>
239
+ {this.data.map((row, rowIndex) => (
240
+ <div
241
+ key={rowIndex}
242
+ className={`${styles['card']} ${this.props.animateOnLoad ? styles.animatedRow : ''}`}
243
+ style={{ animationDelay: `${rowIndex * 0.05}s` }}
244
+ onClick={(e) => {
245
+ this.rowClickFunction(row);
246
+ e.stopPropagation();
247
+ e.preventDefault();
248
+ }}
249
+ >
250
+ <div className={styles['card-header']}> </div>
251
+ <div className={styles['card-body']}>
252
+ {this.props.columnDefinitions.map((columnDefinition, colIndex) => {
253
+ const colDef = this.getColumnDefinition(columnDefinition, rowIndex);
254
+ const onHeaderClickCallback = this.onHeaderClickCallback(colDef);
255
+ const clickableHeaderClassName = this.getClickableHeaderClassName(onHeaderClickCallback, colDef);
256
+ return (
257
+ <div key={colIndex} className={styles['card-row']}>
258
+ <p>
259
+ <span
260
+ className={`${styles['card-label']} ${clickableHeaderClassName}`}
261
+ onClick={
262
+ onHeaderClickCallback ? () => onHeaderClickCallback(colDef.interactivity!.id) : undefined
263
+ }
264
+ >
265
+ {colDef.displayLabel}:
266
+ </span>
267
+ <span className={styles['card-value']}>{colDef.cellRenderer(row)}</span>
268
+ </p>
269
+ </div>
270
+ );
271
+ })}
272
+ </div>
273
+ </div>
274
+ ))}
275
+ {this.mobileFooter}
276
+ </div>
277
+ );
278
+ }
279
+
280
+ private get largeScreenView(): ReactNode {
281
+ const useFixedHeaders = this.props.maxHeight ? true : false;
282
+
283
+ const fixedHeadersStyle = useFixedHeaders
284
+ ? ({ maxHeight: this.props.maxHeight, overflowY: 'auto' } as CSSProperties)
285
+ : {};
286
+
287
+ return (
288
+ <div style={fixedHeadersStyle}>
289
+ <table className={styles['responsiveTable']} style={{ zIndex: -1 }}>
290
+ <thead>
291
+ <tr>
292
+ {this.props.columnDefinitions.map((columnDefinition, colIndex) => {
293
+ const onHeaderClickCallback = this.onHeaderClickCallback(columnDefinition);
294
+ const clickableHeaderClassName = this.getClickableHeaderClassName(
295
+ onHeaderClickCallback,
296
+ columnDefinition,
297
+ );
298
+ return (
299
+ <th
300
+ key={colIndex}
301
+ className={`${clickableHeaderClassName}`}
302
+ style={{ zIndex: 0 }}
303
+ onClick={
304
+ onHeaderClickCallback
305
+ ? () => onHeaderClickCallback(this.getColumnDefinition(columnDefinition, 0).interactivity!.id)
306
+ : undefined
307
+ }
308
+ >
309
+ {this.getColumnDefinition(columnDefinition, 0).displayLabel}
310
+ </th>
311
+ );
312
+ })}
313
+ </tr>
314
+ </thead>
315
+ <tbody>
316
+ {this.data.map((row, rowIndex) => (
317
+ <tr
318
+ key={rowIndex}
319
+ className={this.props.animateOnLoad ? styles.animatedRow : ''}
320
+ style={{ animationDelay: `${rowIndex * 0.05}s` }}
321
+ >
322
+ {this.props.columnDefinitions.map((columnDefinition, colIndex) => (
323
+ <td onClick={() => this.rowClickFunction(row)} key={colIndex}>
324
+ <span style={{ ...this.rowClickStyle }}>
325
+ {this.getColumnDefinition(columnDefinition, rowIndex).cellRenderer(row)}
326
+ </span>
327
+ </td>
328
+ ))}
329
+ </tr>
330
+ ))}
331
+ </tbody>
332
+ {this.tableFooter}
333
+ </table>
334
+ </div>
335
+ );
336
+ }
337
+
338
+ render() {
339
+ if (this.props.isLoading) {
340
+ return this.skeletonView;
341
+ }
342
+
343
+ if (!this.hasData) {
344
+ return this.noDataComponent;
345
+ }
346
+ if (this.state.isMobile) {
347
+ return this.mobileView;
348
+ }
349
+
350
+ return this.largeScreenView;
351
+ }
352
+ }
353
+
354
+ export default ResponsiveTable;
package/src/index.tsx CHANGED
@@ -1,5 +1,5 @@
1
- import IResponsiveTableColumnDefinition from "./Data/IResponsiveTableColumnDefinition";
2
- import ResponsiveTable from "./UI/ResponsiveTable";
1
+ import IResponsiveTableColumnDefinition from './Data/IResponsiveTableColumnDefinition';
2
+ import ResponsiveTable, { ColumnDefinition } from './UI/ResponsiveTable';
3
3
 
4
- export {IResponsiveTableColumnDefinition}
5
- export default ResponsiveTable
4
+ export { IResponsiveTableColumnDefinition, ColumnDefinition };
5
+ export default ResponsiveTable;