sn-datatable 0.0.1
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.
- package/README.md +134 -0
- package/ng-package.json +7 -0
- package/package.json +38 -0
- package/src/lib/sn-datatable.html +71 -0
- package/src/lib/sn-datatable.scss +140 -0
- package/src/lib/sn-datatable.spec.ts +139 -0
- package/src/lib/sn-datatable.ts +117 -0
- package/src/public-api.ts +5 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# sn-table
|
|
2
|
+
A powerful and flexible data table component for Angular with sorting, pagination, and multiple styling options.
|
|
3
|
+
## Overview
|
|
4
|
+
The `sn-table` component provides:
|
|
5
|
+
- ✅ Sortable columns (click header to sort)
|
|
6
|
+
- ✅ Pagination with next/previous navigation
|
|
7
|
+
- ✅ Multiple row styling (striped, hoverable, bordered)
|
|
8
|
+
- ✅ Responsive table layout
|
|
9
|
+
- ✅ Empty state handling
|
|
10
|
+
- ✅ Support for multiple data types
|
|
11
|
+
- ✅ Customizable column alignment
|
|
12
|
+
- ✅ Full accessibility support
|
|
13
|
+
## Installation
|
|
14
|
+
```bash
|
|
15
|
+
npm install sn-table
|
|
16
|
+
```
|
|
17
|
+
## Usage
|
|
18
|
+
### Basic Table
|
|
19
|
+
```typescript
|
|
20
|
+
import { Component } from '@angular/core';
|
|
21
|
+
import { SnTableComponent, TableColumn, TableRow } from 'sn-table';
|
|
22
|
+
@Component({
|
|
23
|
+
selector: 'app-demo',
|
|
24
|
+
template: `
|
|
25
|
+
<sn-table
|
|
26
|
+
[columns]="columns"
|
|
27
|
+
[data]="employees"
|
|
28
|
+
></sn-table>
|
|
29
|
+
`,
|
|
30
|
+
imports: [SnTableComponent]
|
|
31
|
+
})
|
|
32
|
+
export class DemoComponent {
|
|
33
|
+
columns: TableColumn[] = [
|
|
34
|
+
{ header: 'ID', key: 'id', sortable: true },
|
|
35
|
+
{ header: 'Name', key: 'name', sortable: true },
|
|
36
|
+
{ header: 'Email', key: 'email', sortable: true },
|
|
37
|
+
{ header: 'Status', key: 'status', sortable: false },
|
|
38
|
+
];
|
|
39
|
+
employees: TableRow[] = [
|
|
40
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'Active' },
|
|
41
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'Inactive' },
|
|
42
|
+
{ id: 3, name: 'Charlie', email: 'charlie@example.com', status: 'Active' },
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
### With Styling Options
|
|
47
|
+
```typescript
|
|
48
|
+
<sn-table
|
|
49
|
+
[columns]="columns"
|
|
50
|
+
[data]="data"
|
|
51
|
+
[striped]="true"
|
|
52
|
+
[hoverable]="true"
|
|
53
|
+
[bordered]="false"
|
|
54
|
+
[pageSize]="25"
|
|
55
|
+
(sorted)="onSort($event)"
|
|
56
|
+
></sn-table>
|
|
57
|
+
```
|
|
58
|
+
### With Custom Column Width and Alignment
|
|
59
|
+
```typescript
|
|
60
|
+
columns: TableColumn[] = [
|
|
61
|
+
{ header: 'ID', key: 'id', width: '100px', align: 'center' },
|
|
62
|
+
{ header: 'Name', key: 'name', width: '200px', align: 'left' },
|
|
63
|
+
{ header: 'Email', key: 'email', width: '250px', align: 'left' },
|
|
64
|
+
{ header: 'Amount', key: 'amount', width: '150px', align: 'right' },
|
|
65
|
+
];
|
|
66
|
+
```
|
|
67
|
+
## API
|
|
68
|
+
### Inputs
|
|
69
|
+
| Input | Type | Default | Description |
|
|
70
|
+
|-------|------|---------|-------------|
|
|
71
|
+
| `columns` | `TableColumn[]` | `[]` | Column definitions |
|
|
72
|
+
| `data` | `TableRow[]` | `[]` | Table data rows |
|
|
73
|
+
| `striped` | `boolean` | `true` | Alternate row colors |
|
|
74
|
+
| `hoverable` | `boolean` | `true` | Highlight row on hover |
|
|
75
|
+
| `bordered` | `boolean` | `false` | Show table border |
|
|
76
|
+
| `pageSize` | `number` | `10` | Rows per page |
|
|
77
|
+
### Outputs
|
|
78
|
+
| Output | Type | Description |
|
|
79
|
+
|--------|------|-------------|
|
|
80
|
+
| `sorted` | `EventEmitter<SortEvent>` | Emitted when column is sorted |
|
|
81
|
+
### Interfaces
|
|
82
|
+
```typescript
|
|
83
|
+
interface TableColumn {
|
|
84
|
+
header: string; // Column header text
|
|
85
|
+
key: string; // Data key (object property)
|
|
86
|
+
sortable?: boolean; // Allow sorting (default: true)
|
|
87
|
+
width?: string; // CSS width (e.g., '200px', '20%')
|
|
88
|
+
align?: 'left' | 'center' | 'right'; // Text alignment
|
|
89
|
+
}
|
|
90
|
+
interface TableRow {
|
|
91
|
+
[key: string]: any; // Any key-value pairs
|
|
92
|
+
}
|
|
93
|
+
interface SortEvent {
|
|
94
|
+
column: string; // Sorted column key
|
|
95
|
+
direction: 'asc' | 'desc'; // Sort direction
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
## Features
|
|
99
|
+
### Sorting
|
|
100
|
+
Click any sortable column header to sort. Click again to reverse direction. Supports:
|
|
101
|
+
- Strings (alphabetical)
|
|
102
|
+
- Numbers (numeric)
|
|
103
|
+
- Dates (chronological)
|
|
104
|
+
### Pagination
|
|
105
|
+
Automatically paginated based on `pageSize`. Navigation buttons appear when data exceeds one page.
|
|
106
|
+
### Styling Options
|
|
107
|
+
- **Striped:** Alternate row background colors
|
|
108
|
+
- **Hoverable:** Highlight rows on hover
|
|
109
|
+
- **Bordered:** Show table border
|
|
110
|
+
## Styling
|
|
111
|
+
The table uses Tailwind CSS utilities and custom SCSS. Customization via:
|
|
112
|
+
- CSS variables (future)
|
|
113
|
+
- SCSS variables override
|
|
114
|
+
- Custom CSS classes
|
|
115
|
+
## Testing
|
|
116
|
+
```bash
|
|
117
|
+
ng test sn-table
|
|
118
|
+
```
|
|
119
|
+
## Building
|
|
120
|
+
```bash
|
|
121
|
+
ng build sn-table
|
|
122
|
+
```
|
|
123
|
+
## Accessibility
|
|
124
|
+
- Semantic HTML table structure
|
|
125
|
+
- Proper heading hierarchy
|
|
126
|
+
- Sortable column indicators
|
|
127
|
+
- Keyboard navigation support
|
|
128
|
+
- Screen reader friendly
|
|
129
|
+
## Best Practices
|
|
130
|
+
1. **Make relevant columns sortable** - Set `sortable: false` for status/action columns
|
|
131
|
+
2. **Set appropriate column widths** - Fixed widths for better layout control
|
|
132
|
+
3. **Use correct alignment** - Right-align numbers, left-align text
|
|
133
|
+
4. **Handle empty states** - Component shows "No data available" message
|
|
134
|
+
5. **Optimize page size** - Balance between scrolling and loading time
|
package/ng-package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sn-datatable",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Angular data table component with sorting and pagination - SnUI Library",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Swapnil Nakate",
|
|
7
|
+
"email": "nakate.swapnil7@gmail.com",
|
|
8
|
+
"url": "https://swapnilnakate.in"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"table",
|
|
12
|
+
"data-table",
|
|
13
|
+
"angular",
|
|
14
|
+
"sorting",
|
|
15
|
+
"pagination",
|
|
16
|
+
"tailwindcss",
|
|
17
|
+
"accessible",
|
|
18
|
+
"snui"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@angular/common": "^21.2.0",
|
|
22
|
+
"@angular/core": "^21.2.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"tslib": "^2.3.0"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/swapnilnakate7/sn-ui/issues",
|
|
30
|
+
"email": "nakate.swapnil7@gmail.com"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"url": "https://github.com/swapnilnakate7/sn-ui",
|
|
34
|
+
"type": "git",
|
|
35
|
+
"directory": "projects/sn-table"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<div class="sn-table-wrapper">
|
|
2
|
+
<table class="sn-table" [class.striped]="striped" [class.hoverable]="hoverable" [class.bordered]="bordered">
|
|
3
|
+
<thead>
|
|
4
|
+
<tr>
|
|
5
|
+
@for (column of columns; track column.key) {
|
|
6
|
+
<th
|
|
7
|
+
class="sn-table-header"
|
|
8
|
+
[style.width]="column.width"
|
|
9
|
+
[style.text-align]="column.align || 'left'"
|
|
10
|
+
[class.sortable]="column.sortable !== false"
|
|
11
|
+
(click)="column.sortable !== false && sort(column.key)"
|
|
12
|
+
>
|
|
13
|
+
<span class="sn-table-header-content">
|
|
14
|
+
{{ column.header }}
|
|
15
|
+
@if (column.sortable !== false) {
|
|
16
|
+
<span class="sn-table-sort-icon">{{ getSortIcon(column.key) }}</span>
|
|
17
|
+
}
|
|
18
|
+
</span>
|
|
19
|
+
</th>
|
|
20
|
+
}
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody>
|
|
24
|
+
@if (paginatedData.length > 0) {
|
|
25
|
+
@for (row of paginatedData; track $index) {
|
|
26
|
+
<tr class="sn-table-row">
|
|
27
|
+
@for (column of columns; track column.key) {
|
|
28
|
+
<td
|
|
29
|
+
class="sn-table-cell"
|
|
30
|
+
[style.text-align]="column.align || 'left'"
|
|
31
|
+
>
|
|
32
|
+
{{ row[column.key] }}
|
|
33
|
+
</td>
|
|
34
|
+
}
|
|
35
|
+
</tr>
|
|
36
|
+
}
|
|
37
|
+
} @else {
|
|
38
|
+
<tr>
|
|
39
|
+
<td [attr.colspan]="columns.length" class="sn-table-empty">
|
|
40
|
+
No data available
|
|
41
|
+
</td>
|
|
42
|
+
</tr>
|
|
43
|
+
}
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
|
|
47
|
+
@if (totalPages > 1) {
|
|
48
|
+
<div class="sn-table-pagination">
|
|
49
|
+
<button
|
|
50
|
+
class="sn-table-btn-prev"
|
|
51
|
+
(click)="prevPage()"
|
|
52
|
+
[disabled]="currentPage === 1"
|
|
53
|
+
>
|
|
54
|
+
← Previous
|
|
55
|
+
</button>
|
|
56
|
+
|
|
57
|
+
<span class="sn-table-page-info">
|
|
58
|
+
Page {{ currentPage }} of {{ totalPages }}
|
|
59
|
+
</span>
|
|
60
|
+
|
|
61
|
+
<button
|
|
62
|
+
class="sn-table-btn-next"
|
|
63
|
+
(click)="nextPage()"
|
|
64
|
+
[disabled]="currentPage === totalPages"
|
|
65
|
+
>
|
|
66
|
+
Next →
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
}
|
|
70
|
+
</div>
|
|
71
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
.sn-table-wrapper {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
width: 100%;
|
|
5
|
+
overflow: auto;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.sn-table {
|
|
9
|
+
width: 100%;
|
|
10
|
+
border-collapse: collapse;
|
|
11
|
+
font-size: 0.875rem;
|
|
12
|
+
background-color: #ffffff;
|
|
13
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
14
|
+
border-radius: 0.375rem;
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
|
|
17
|
+
&.bordered {
|
|
18
|
+
border: 1px solid #e5e7eb;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.sn-table-header {
|
|
23
|
+
padding: 0.75rem;
|
|
24
|
+
background-color: #f9fafb;
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
color: #374151;
|
|
27
|
+
border-bottom: 2px solid #e5e7eb;
|
|
28
|
+
user-select: none;
|
|
29
|
+
transition: all 0.2s ease-in-out;
|
|
30
|
+
|
|
31
|
+
&.sortable {
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
|
|
34
|
+
&:hover {
|
|
35
|
+
background-color: #f3f4f6;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.sn-table-header-content {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 0.5rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.sn-table-sort-icon {
|
|
47
|
+
font-size: 0.75rem;
|
|
48
|
+
color: #9ca3af;
|
|
49
|
+
transition: color 0.2s ease-in-out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.sn-table-header:hover .sn-table-sort-icon {
|
|
53
|
+
color: #3b82f6;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.sn-table-row {
|
|
57
|
+
border-bottom: 1px solid #f3f4f6;
|
|
58
|
+
transition: background-color 0.15s ease-in-out;
|
|
59
|
+
|
|
60
|
+
&:last-child {
|
|
61
|
+
border-bottom: none;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@media (hover: hover) {
|
|
65
|
+
.sn-table.hoverable &:hover {
|
|
66
|
+
background-color: #f9fafb;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.sn-table-cell {
|
|
72
|
+
padding: 0.75rem;
|
|
73
|
+
color: #1f2937;
|
|
74
|
+
|
|
75
|
+
&:first-child {
|
|
76
|
+
font-weight: 500;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.sn-table.striped tbody tr:nth-child(odd) {
|
|
81
|
+
background-color: #f9fafb;
|
|
82
|
+
|
|
83
|
+
&:hover {
|
|
84
|
+
background-color: #f3f4f6;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.sn-table-empty {
|
|
89
|
+
text-align: center;
|
|
90
|
+
color: #9ca3af;
|
|
91
|
+
padding: 2rem 0.75rem !important;
|
|
92
|
+
font-style: italic;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.sn-table-pagination {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: center;
|
|
99
|
+
gap: 1rem;
|
|
100
|
+
padding: 1rem;
|
|
101
|
+
border-top: 1px solid #e5e7eb;
|
|
102
|
+
background-color: #f9fafb;
|
|
103
|
+
flex-wrap: wrap;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.sn-table-page-info {
|
|
107
|
+
font-size: 0.875rem;
|
|
108
|
+
color: #6b7280;
|
|
109
|
+
font-weight: 500;
|
|
110
|
+
min-width: 8rem;
|
|
111
|
+
text-align: center;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.sn-table-btn-prev,
|
|
115
|
+
.sn-table-btn-next {
|
|
116
|
+
padding: 0.5rem 1rem;
|
|
117
|
+
border: 1px solid #d1d5db;
|
|
118
|
+
border-radius: 0.375rem;
|
|
119
|
+
background-color: #ffffff;
|
|
120
|
+
color: #374151;
|
|
121
|
+
font-size: 0.875rem;
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
transition: all 0.2s ease-in-out;
|
|
124
|
+
|
|
125
|
+
&:hover:not(:disabled) {
|
|
126
|
+
border-color: #3b82f6;
|
|
127
|
+
color: #3b82f6;
|
|
128
|
+
background-color: #f0f9ff;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
&:disabled {
|
|
132
|
+
opacity: 0.5;
|
|
133
|
+
cursor: not-allowed;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&:active:not(:disabled) {
|
|
137
|
+
background-color: #dbeafe;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { SnDatatableComponent, TableColumn, TableRow } from './sn-datatable';
|
|
3
|
+
|
|
4
|
+
describe('SnDatatableComponent', () => {
|
|
5
|
+
let component: SnDatatableComponent;
|
|
6
|
+
let fixture: ComponentFixture<SnDatatableComponent>;
|
|
7
|
+
|
|
8
|
+
const mockColumns: TableColumn[] = [
|
|
9
|
+
{ header: 'ID', key: 'id', sortable: true },
|
|
10
|
+
{ header: 'Name', key: 'name', sortable: true },
|
|
11
|
+
{ header: 'Email', key: 'email', sortable: true },
|
|
12
|
+
{ header: 'Status', key: 'status', sortable: false },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const mockData: TableRow[] = [
|
|
16
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com', status: 'Active' },
|
|
17
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com', status: 'Inactive' },
|
|
18
|
+
{ id: 3, name: 'Charlie', email: 'charlie@example.com', status: 'Active' },
|
|
19
|
+
{ id: 4, name: 'David', email: 'david@example.com', status: 'Active' },
|
|
20
|
+
{ id: 5, name: 'Eve', email: 'eve@example.com', status: 'Inactive' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
await TestBed.configureTestingModule({
|
|
25
|
+
imports: [SnDatatableComponent],
|
|
26
|
+
}).compileComponents();
|
|
27
|
+
|
|
28
|
+
fixture = TestBed.createComponent(SnDatatableComponent);
|
|
29
|
+
component = fixture.componentInstance;
|
|
30
|
+
component.columns = mockColumns;
|
|
31
|
+
component.data = mockData;
|
|
32
|
+
fixture.detectChanges();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should create', () => {
|
|
36
|
+
expect(component).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should display all data on first page', () => {
|
|
40
|
+
expect(component.paginatedData.length).toBe(5);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should sort data in ascending order', () => {
|
|
44
|
+
component.sort('name');
|
|
45
|
+
fixture.detectChanges();
|
|
46
|
+
expect(component.sortedData[0]['name']).toBe('Alice');
|
|
47
|
+
expect(component.sortDirection).toBe('asc');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should sort data in descending order on second click', () => {
|
|
51
|
+
component.sort('name');
|
|
52
|
+
component.sort('name');
|
|
53
|
+
fixture.detectChanges();
|
|
54
|
+
expect(component.sortedData[0]['name']).toBe('Eve');
|
|
55
|
+
expect(component.sortDirection).toBe('desc');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should emit sorted event', () => {
|
|
59
|
+
const spy = spyOn(component.sorted, 'emit');
|
|
60
|
+
component.sort('email');
|
|
61
|
+
expect(spy).toHaveBeenCalledWith({ column: 'email', direction: 'asc' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should not sort non-sortable columns', () => {
|
|
65
|
+
component.sort('status');
|
|
66
|
+
expect(component.sortColumn).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle pagination correctly', () => {
|
|
70
|
+
component.pageSize = 2;
|
|
71
|
+
component.ngOnInit();
|
|
72
|
+
fixture.detectChanges();
|
|
73
|
+
|
|
74
|
+
expect(component.totalPages).toBe(3);
|
|
75
|
+
expect(component.paginatedData.length).toBe(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should navigate to next page', () => {
|
|
79
|
+
component.pageSize = 2;
|
|
80
|
+
component.ngOnInit();
|
|
81
|
+
component.nextPage();
|
|
82
|
+
expect(component.currentPage).toBe(2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should navigate to previous page', () => {
|
|
86
|
+
component.pageSize = 2;
|
|
87
|
+
component.ngOnInit();
|
|
88
|
+
component.currentPage = 2;
|
|
89
|
+
component.prevPage();
|
|
90
|
+
expect(component.currentPage).toBe(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should not go beyond last page', () => {
|
|
94
|
+
component.pageSize = 2;
|
|
95
|
+
component.ngOnInit();
|
|
96
|
+
component.goToPage(10);
|
|
97
|
+
expect(component.currentPage).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should display correct sort icon', () => {
|
|
101
|
+
component.sort('name');
|
|
102
|
+
expect(component.getSortIcon('name')).toBe('↑');
|
|
103
|
+
component.sort('name');
|
|
104
|
+
expect(component.getSortIcon('name')).toBe('↓');
|
|
105
|
+
expect(component.getSortIcon('email')).toBe('↕');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should handle empty data', () => {
|
|
109
|
+
component.data = [];
|
|
110
|
+
component.ngOnInit();
|
|
111
|
+
fixture.detectChanges();
|
|
112
|
+
expect(component.paginatedData.length).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should sort numbers correctly', () => {
|
|
116
|
+
component.sort('id');
|
|
117
|
+
fixture.detectChanges();
|
|
118
|
+
expect(component.sortedData[0]['id']).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should apply striped style', () => {
|
|
122
|
+
component.striped = true;
|
|
123
|
+
fixture.detectChanges();
|
|
124
|
+
expect(component.striped).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should apply hoverable style', () => {
|
|
128
|
+
component.hoverable = true;
|
|
129
|
+
fixture.detectChanges();
|
|
130
|
+
expect(component.hoverable).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should apply bordered style', () => {
|
|
134
|
+
component.bordered = true;
|
|
135
|
+
fixture.detectChanges();
|
|
136
|
+
expect(component.bordered).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
|
|
4
|
+
export interface TableColumn {
|
|
5
|
+
header: string;
|
|
6
|
+
key: string;
|
|
7
|
+
sortable?: boolean;
|
|
8
|
+
width?: string;
|
|
9
|
+
align?: 'left' | 'center' | 'right';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TableRow {
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SortEvent {
|
|
17
|
+
column: string;
|
|
18
|
+
direction: 'asc' | 'desc';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Component({
|
|
22
|
+
selector: 'sn-datatable',
|
|
23
|
+
standalone: true,
|
|
24
|
+
imports: [CommonModule],
|
|
25
|
+
templateUrl: './sn-datatable.html',
|
|
26
|
+
styleUrl: './sn-datatable.scss',
|
|
27
|
+
})
|
|
28
|
+
export class SnDatatableComponent implements OnInit {
|
|
29
|
+
@Input() columns: TableColumn[] = [];
|
|
30
|
+
@Input() data: TableRow[] = [];
|
|
31
|
+
@Input() striped: boolean = true;
|
|
32
|
+
@Input() hoverable: boolean = true;
|
|
33
|
+
@Input() bordered: boolean = false;
|
|
34
|
+
@Input() pageSize: number = 10;
|
|
35
|
+
@Output() sorted = new EventEmitter<SortEvent>();
|
|
36
|
+
|
|
37
|
+
sortedData: TableRow[] = [];
|
|
38
|
+
paginatedData: TableRow[] = [];
|
|
39
|
+
currentPage: number = 1;
|
|
40
|
+
totalPages: number = 1;
|
|
41
|
+
sortColumn: string | null = null;
|
|
42
|
+
sortDirection: 'asc' | 'desc' = 'asc';
|
|
43
|
+
|
|
44
|
+
ngOnInit(): void {
|
|
45
|
+
this.updateTable();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ngOnChanges(): void {
|
|
49
|
+
this.updateTable();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
sort(column: string): void {
|
|
53
|
+
const col = this.columns.find(c => c.key === column);
|
|
54
|
+
if (!col || col.sortable === false) return;
|
|
55
|
+
|
|
56
|
+
if (this.sortColumn === column) {
|
|
57
|
+
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
58
|
+
} else {
|
|
59
|
+
this.sortColumn = column;
|
|
60
|
+
this.sortDirection = 'asc';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.sorted.emit({ column, direction: this.sortDirection });
|
|
64
|
+
this.updateTable();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private updateTable(): void {
|
|
68
|
+
// Sort data
|
|
69
|
+
this.sortedData = [...this.data];
|
|
70
|
+
if (this.sortColumn) {
|
|
71
|
+
this.sortedData.sort((a, b) => {
|
|
72
|
+
const aVal = a[this.sortColumn!];
|
|
73
|
+
const bVal = b[this.sortColumn!];
|
|
74
|
+
|
|
75
|
+
if (aVal === null || aVal === undefined) return 1;
|
|
76
|
+
if (bVal === null || bVal === undefined) return -1;
|
|
77
|
+
|
|
78
|
+
let comparison = 0;
|
|
79
|
+
if (typeof aVal === 'string') {
|
|
80
|
+
comparison = aVal.localeCompare(bVal);
|
|
81
|
+
} else if (typeof aVal === 'number') {
|
|
82
|
+
comparison = aVal - bVal;
|
|
83
|
+
} else if (aVal instanceof Date && bVal instanceof Date) {
|
|
84
|
+
comparison = aVal.getTime() - bVal.getTime();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return this.sortDirection === 'asc' ? comparison : -comparison;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Paginate
|
|
92
|
+
this.totalPages = Math.ceil(this.sortedData.length / this.pageSize);
|
|
93
|
+
const startIdx = (this.currentPage - 1) * this.pageSize;
|
|
94
|
+
this.paginatedData = this.sortedData.slice(startIdx, startIdx + this.pageSize);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
goToPage(page: number): void {
|
|
98
|
+
if (page >= 1 && page <= this.totalPages) {
|
|
99
|
+
this.currentPage = page;
|
|
100
|
+
this.updateTable();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
nextPage(): void {
|
|
105
|
+
this.goToPage(this.currentPage + 1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
prevPage(): void {
|
|
109
|
+
this.goToPage(this.currentPage - 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getSortIcon(column: string): string {
|
|
113
|
+
if (this.sortColumn !== column) return '↕';
|
|
114
|
+
return this.sortDirection === 'asc' ? '↑' : '↓';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/lib",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"types": []
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.ts"
|
|
13
|
+
],
|
|
14
|
+
"exclude": [
|
|
15
|
+
"**/*.spec.ts"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "./tsconfig.lib.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"declarationMap": false
|
|
7
|
+
},
|
|
8
|
+
"angularCompilerOptions": {
|
|
9
|
+
"compilationMode": "partial"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
+
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
+
{
|
|
4
|
+
"extends": "../../tsconfig.json",
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"outDir": "../../out-tsc/spec",
|
|
7
|
+
"types": [
|
|
8
|
+
"jasmine"
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"src/**/*.d.ts",
|
|
13
|
+
"src/**/*.spec.ts"
|
|
14
|
+
]
|
|
15
|
+
}
|