ng-adflowtail 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.
- package/README.md +657 -0
- package/fesm2022/ng-adflowtail-ad-data-table.mjs +389 -0
- package/fesm2022/ng-adflowtail-ad-data-table.mjs.map +1 -0
- package/fesm2022/ng-adflowtail-ad-multi-select.mjs +97 -0
- package/fesm2022/ng-adflowtail-ad-multi-select.mjs.map +1 -0
- package/fesm2022/ng-adflowtail.mjs +40 -0
- package/fesm2022/ng-adflowtail.mjs.map +1 -0
- package/package.json +34 -0
- package/types/ng-adflowtail-ad-data-table.d.ts +139 -0
- package/types/ng-adflowtail-ad-multi-select.d.ts +38 -0
- package/types/ng-adflowtail.d.ts +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
# ng-adflowtail 🚀
|
|
2
|
+
|
|
3
|
+
A highly professional, developer-friendly, and lightweight Angular 22 component library built on top of **Tailwind CSS**, **Flowbite**, and modern **Angular Signals**.
|
|
4
|
+
|
|
5
|
+
`ng-adflowtail` is architected with a **secondary entry points** design (similar to `@angular/material`). This enables absolute tree-shaking, meaning consumers only bundle the precise widgets and logic they import, leading to ultra-slim production bundles.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
|
|
11
|
+
- **Angular 22 Core**: Leverages the absolute latest APIs including Standalone Components, Signal-based Inputs/Outputs, Computeds, and Host Directives.
|
|
12
|
+
- **Advanced State Management**: Built natively around Signals to ensure highly performant and predictable change detection.
|
|
13
|
+
- **Signals-Based Forms**: Clean integration with modern Angular Signal Forms (`@angular/forms/signals`).
|
|
14
|
+
- **Dynamic CSS Custom Properties (Theming)**: Easily brand components in real-time by passing a single hex/rgb color string. The built-in directive dynamically computes complementary brand shades using modern CSS `color-mix()`.
|
|
15
|
+
- **Responsive & Accessible**: Fully styled with Tailwind CSS and Flowbite, implementing modern accessibility requirements and responsive layouts.
|
|
16
|
+
- **Full Tree-Shaking**: Secondary entry points keep your production bundles optimal.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
1. [Prerequisites & Installation](#prerequisites--installation)
|
|
23
|
+
2. [Tailwind CSS Configuration](#tailwind-css-configuration)
|
|
24
|
+
3. [Architecture & Imports](#architecture--scaffolding)
|
|
25
|
+
4. [Branding & Custom Theming](#branding--custom-theming)
|
|
26
|
+
5. [Component: AdMultiSelect](#component-admultiselect)
|
|
27
|
+
6. [Component: AdDataTable](#component-addatatable)
|
|
28
|
+
7. [Adding New Components (Library Developers)](#adding-new-components)
|
|
29
|
+
8. [Build & Publish Commands](#build--publish)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Prerequisites & Installation
|
|
34
|
+
|
|
35
|
+
### 1. Install the NPM Package
|
|
36
|
+
Install `ng-adflowtail` and its required peers `flowbite` and `tslib`:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install ng-adflowtail flowbite tslib
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Install Tailwind CSS
|
|
43
|
+
Ensure your Angular project has Tailwind CSS set up. If not, follow the official [Tailwind CSS installation guide for Angular](https://tailwindcss.com/docs/guides/angular).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Tailwind CSS Configuration
|
|
48
|
+
|
|
49
|
+
Because `ng-adflowtail` relies on Tailwind classes, you **must** configure Tailwind to scan the library's compiled templates inside `node_modules` so that styles are not purged in production builds.
|
|
50
|
+
|
|
51
|
+
### For Tailwind CSS v4 (Modern CSS Configuration)
|
|
52
|
+
Add the `@source` directive in your main CSS file (e.g., `styles.css`):
|
|
53
|
+
|
|
54
|
+
```css
|
|
55
|
+
@import "tailwindcss";
|
|
56
|
+
|
|
57
|
+
/* Add the source path to scan ng-adflowtail templates */
|
|
58
|
+
@source "../../node_modules/ng-adflowtail";
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### For Tailwind CSS v3 (`tailwind.config.js`)
|
|
62
|
+
Add the `ng-adflowtail` path to your `content` array:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
/** @type {import('tailwindcss').Config} */
|
|
66
|
+
module.exports = {
|
|
67
|
+
content: [
|
|
68
|
+
"./src/**/*.{html,ts}",
|
|
69
|
+
"./node_modules/ng-adflowtail/**/*.{js,mjs,html}" // Add this line
|
|
70
|
+
],
|
|
71
|
+
theme: {
|
|
72
|
+
extend: {},
|
|
73
|
+
},
|
|
74
|
+
plugins: [
|
|
75
|
+
require('flowbite/plugin') // Enable Flowbite utility classes
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Architecture & Scaffolding
|
|
83
|
+
|
|
84
|
+
To import components, use their specialized entry points. Do **not** import everything from `ng-adflowtail` directly.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Core Directives & Shared Interfaces (Primary Entry Point)
|
|
88
|
+
import { AdPrimaryColorDirective, AdColumn } from 'ng-adflowtail';
|
|
89
|
+
|
|
90
|
+
// Individual Secondary Entry Points
|
|
91
|
+
import { AdMultiSelect } from 'ng-adflowtail/ad-multi-select';
|
|
92
|
+
import { AdDataTable } from 'ng-adflowtail/ad-data-table';
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Branding & Custom Theming
|
|
98
|
+
|
|
99
|
+
### `AdPrimaryColorDirective` (`[adPrimaryColor]`)
|
|
100
|
+
|
|
101
|
+
Both `AdMultiSelect` and `AdDataTable` support dynamic theming via the `primaryColor` input. This input is automatically mapped on both components using Angular's robust **Host Directives** feature, delegating to the standalone `AdPrimaryColorDirective`.
|
|
102
|
+
|
|
103
|
+
This directive dynamically binds modern CSS Custom Properties (`--color-brand`, `--color-fg-brand`, etc.) to the host elements. Under the hood, it uses native CSS `color-mix()` to calculate brand states on-the-fly based on any valid CSS color input (hex, rgb, hsl, or web colors):
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Mapped custom properties on the host element style:
|
|
107
|
+
--color-brand: [primaryColor];
|
|
108
|
+
--color-brand-soft: color-mix(in srgb, [primaryColor] 10%, transparent);
|
|
109
|
+
--color-brand-softer: color-mix(in srgb, [primaryColor] 5%, transparent);
|
|
110
|
+
--color-brand-medium: color-mix(in srgb, [primaryColor] 20%, transparent);
|
|
111
|
+
--color-brand-strong: color-mix(in srgb, [primaryColor] 90%, black);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Example: Dynamic Color Picker Implementation
|
|
115
|
+
You can bind a color input or a signal directly to the widgets:
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<!-- Fully rebranded multi-select -->
|
|
119
|
+
<ad-multi-select
|
|
120
|
+
[formField]="userForm.permissions"
|
|
121
|
+
[options]="permissionOptions"
|
|
122
|
+
primaryColor="#0d9488"
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
<!-- Rebranded data table -->
|
|
126
|
+
<ad-data-table
|
|
127
|
+
[data]="users"
|
|
128
|
+
[config]="tableConfig"
|
|
129
|
+
primaryColor="#7c3aed"
|
|
130
|
+
/>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Component: AdMultiSelect
|
|
136
|
+
|
|
137
|
+
The `AdMultiSelect` component (`ng-adflowtail/ad-multi-select`) is a highly interactive, accessible dropdown multi-select. It is integrated natively with the new **Angular Signal-based Forms** (`@angular/forms/signals`), displaying selection tokens as elegant, dismissible chips.
|
|
138
|
+
|
|
139
|
+
### Key Features
|
|
140
|
+
- **Signal Forms**: Directly binds to form fields via the `formField` input parameter.
|
|
141
|
+
- **Interactive Tokens**: Selected values render as clean chips with a close button.
|
|
142
|
+
- **Global Actions**: Includes fast-access "Select all" and "Clear" utilities.
|
|
143
|
+
- **Robust UX**: Includes click-outside detection and `Escape` keyboard key handling to dismiss the dropdown panel safely.
|
|
144
|
+
- **Validation-Driven**: Automatically applies red borders and error displays when the bound signal form is `touched` and `invalid`.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### Multi-Select API Spec
|
|
149
|
+
|
|
150
|
+
#### `@Input()` Bindings
|
|
151
|
+
| Input | Type | Default | Description |
|
|
152
|
+
| :--- | :--- | :--- | :--- |
|
|
153
|
+
| `formField` (Required) | `FieldTree<string[]>` | — | The signal forms field node to bind (e.g. `myForm.roles`). |
|
|
154
|
+
| `options` | `AdMultiSelectOption[]` | `[]` | List of items available in the dropdown selector. |
|
|
155
|
+
| `placeholder` | `string` | `'Select options'` | Label shown when no selection has been made yet. |
|
|
156
|
+
| `disabled` | `boolean` | `false` | Disables user interaction and prevents opening the menu. |
|
|
157
|
+
| `errorMessage` | `string` | `''` | Custom message text shown underneath the input on validation failure. |
|
|
158
|
+
| `primaryColor` | `string` | — | Overrides default branding colors with computed dynamic CSS colors. |
|
|
159
|
+
|
|
160
|
+
#### Model Interfaces
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
export interface AdMultiSelectOption {
|
|
164
|
+
label: string;
|
|
165
|
+
value: string;
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### Complete Multi-Select Example
|
|
172
|
+
|
|
173
|
+
#### Component TS (`app.component.ts`)
|
|
174
|
+
```typescript
|
|
175
|
+
import { Component, signal } from '@angular/core';
|
|
176
|
+
import { form, required } from '@angular/forms/signals';
|
|
177
|
+
import { AdMultiSelect, AdMultiSelectOption } from 'ng-adflowtail/ad-multi-select';
|
|
178
|
+
|
|
179
|
+
interface PermissionForm {
|
|
180
|
+
permissions: string[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@Component({
|
|
184
|
+
selector: 'app-permission-manager',
|
|
185
|
+
standalone: true,
|
|
186
|
+
imports: [AdMultiSelect],
|
|
187
|
+
templateUrl: './app.component.html'
|
|
188
|
+
})
|
|
189
|
+
export class AppPermissionManager {
|
|
190
|
+
// Brand color customization
|
|
191
|
+
protected readonly themeColor = signal<string>('#0d9488'); // Custom Teal
|
|
192
|
+
|
|
193
|
+
// Local state container
|
|
194
|
+
protected readonly formModel = signal<PermissionForm>({
|
|
195
|
+
permissions: ['read']
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Signal forms validation & controller
|
|
199
|
+
protected readonly userForm = form(this.formModel, (path) => {
|
|
200
|
+
required(path.permissions, { message: 'Please select at least one permission.' });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Available options
|
|
204
|
+
protected readonly permissionOptions: AdMultiSelectOption[] = [
|
|
205
|
+
{ label: 'Read Access', value: 'read' },
|
|
206
|
+
{ label: 'Write Access', value: 'write' },
|
|
207
|
+
{ label: 'Delete Actions', value: 'delete' },
|
|
208
|
+
{ label: 'Super Admin', value: 'admin' }
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
protected onSubmit(event: Event) {
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
if (this.userForm.valid()) {
|
|
214
|
+
console.log('Submitting verified model state:', this.formModel());
|
|
215
|
+
} else {
|
|
216
|
+
this.userForm.markAsTouched();
|
|
217
|
+
console.warn('Form validation failed.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Component HTML Template (`app.component.html`)
|
|
224
|
+
```html
|
|
225
|
+
<div class="max-w-md p-6 bg-white dark:bg-gray-800 rounded-xl shadow border border-gray-200 dark:border-gray-700">
|
|
226
|
+
<form (submit)="onSubmit($event)" class="space-y-4">
|
|
227
|
+
<div>
|
|
228
|
+
<label class="block mb-2 text-sm font-semibold text-gray-900 dark:text-white">
|
|
229
|
+
Role Permissions
|
|
230
|
+
</label>
|
|
231
|
+
|
|
232
|
+
<!-- Multi Select Binding -->
|
|
233
|
+
<ad-multi-select
|
|
234
|
+
[formField]="userForm.permissions"
|
|
235
|
+
[options]="permissionOptions"
|
|
236
|
+
[primaryColor]="themeColor()"
|
|
237
|
+
placeholder="Select key permissions"
|
|
238
|
+
errorMessage="At least one permission is mandatory"
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- Active State Inspector -->
|
|
243
|
+
<div class="p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg text-xs">
|
|
244
|
+
<p class="font-semibold text-gray-500">Live Form Value:</p>
|
|
245
|
+
<code class="text-indigo-600 dark:text-indigo-400">{{ formModel() | json }}</code>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<button type="submit" class="w-full text-white bg-blue-600 hover:bg-blue-700 font-medium rounded-lg text-sm px-5 py-2.5">
|
|
249
|
+
Save Rules
|
|
250
|
+
</button>
|
|
251
|
+
</form>
|
|
252
|
+
</div>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Component: AdDataTable
|
|
258
|
+
|
|
259
|
+
The `AdDataTable` component (`ng-adflowtail/ad-data-table`) is a high-performance, feature-rich responsive table widget that simplifies handling large datasets.
|
|
260
|
+
|
|
261
|
+
### Key Features
|
|
262
|
+
- **Dynamic Columns**: Configurable fields with automated rendering based on data types (`text`, `number`, `date`, `currency` pre-formatted for SAR, `status`, `action`, `template`).
|
|
263
|
+
- **Flexible Row Actions**: Built-in row buttons (EDIT, DELETE, VIEW) with custom text, colors, visibility conditions, and disable state callbacks per row.
|
|
264
|
+
- **Built-in Global Search**: Instant master text filter across row records.
|
|
265
|
+
- **Advanced Column Filters**: Supports field-by-field searching with specific widgets (`text`, `select`, `multiSelect`, `date`, `dateRange`, `number`, `autocomplete`).
|
|
266
|
+
- **Paging Controls**: Custom pagination bar showing start/end ranges, previous/next controls, and customizable page size selections.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### Data Table API Spec
|
|
271
|
+
|
|
272
|
+
#### `@Input()` Bindings
|
|
273
|
+
| Input | Type | Default | Description |
|
|
274
|
+
| :--- | :--- | :--- | :--- |
|
|
275
|
+
| `title` | `string` | `''` | Headline shown above the table card. |
|
|
276
|
+
| `data` | `any[]` | `[]` | Array of data records to display. |
|
|
277
|
+
| `tableStyle` | `string` | — | Custom inline styling or CSS classes applied directly to the outer wrapper. |
|
|
278
|
+
| `config` | `TableConfig` | See defaults below | Master schema and controls governing column capabilities and pagination. |
|
|
279
|
+
| `primaryColor` | `string` | — | Overrides default branding colors with computed dynamic CSS colors. |
|
|
280
|
+
|
|
281
|
+
**Default TableConfig Object:**
|
|
282
|
+
```typescript
|
|
283
|
+
{
|
|
284
|
+
columns: [],
|
|
285
|
+
itemsPerPage: 10,
|
|
286
|
+
showPagination: true,
|
|
287
|
+
showPageSize: true,
|
|
288
|
+
pageSizeOptions: [5, 10, 25, 50],
|
|
289
|
+
showSearch: true,
|
|
290
|
+
searchPlaceholder: 'Search...',
|
|
291
|
+
showActions: false,
|
|
292
|
+
showColumnSearch: false
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
#### `@Output()` Events
|
|
297
|
+
| Event | Payload Type | Description |
|
|
298
|
+
| :--- | :--- | :--- |
|
|
299
|
+
| `pageChange` | `number` | Emits the new page number selected by the user. |
|
|
300
|
+
| `pageSizeChange` | `number` | Emits the new page size count chosen in the dropdown. |
|
|
301
|
+
| `rowClick` | `any` | Emits the raw record object when a row is clicked. |
|
|
302
|
+
| `actionClick` | `{ action: DataTableButtonAction; item: any }` | Triggered when a predefined action button in a row is clicked. |
|
|
303
|
+
| `searchChange` | `string` | Emits the global search string entered by the user. |
|
|
304
|
+
| `columnSearchChange` | `ColumnSearchEvent` | Triggered when any column-specific search input values are modified. |
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### Data Table Types & Interfaces
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
export type DataTableButtonAction = 'EDIT' | 'DELETE' | 'VIEW';
|
|
312
|
+
|
|
313
|
+
export interface TableColumn {
|
|
314
|
+
field: string;
|
|
315
|
+
header: string;
|
|
316
|
+
type?: 'text' | 'number' | 'date' | 'currency' | 'status' | 'action' | 'template';
|
|
317
|
+
format?: string;
|
|
318
|
+
actionTemplate?: (item: any) => string;
|
|
319
|
+
statusColors?: { [key: string]: string };
|
|
320
|
+
searchable?: boolean;
|
|
321
|
+
searchPlaceholder?: string;
|
|
322
|
+
searchType?: 'text' | 'select' | 'multiSelect' | 'date' | 'dateRange' | 'number' | 'autocomplete';
|
|
323
|
+
searchOptions?: Array<{ value: any; label: string }>;
|
|
324
|
+
multiSelectConfig?: {
|
|
325
|
+
maxItems?: number;
|
|
326
|
+
showSelectAll?: boolean;
|
|
327
|
+
};
|
|
328
|
+
autocompleteConfig?: {
|
|
329
|
+
apiUrl?: string;
|
|
330
|
+
debounceTime?: number;
|
|
331
|
+
minChars?: number;
|
|
332
|
+
valueField?: string;
|
|
333
|
+
labelField?: string;
|
|
334
|
+
customSearchFn?: (searchTerm: string) => Promise<Array<{ value: any; label: string }>>;
|
|
335
|
+
};
|
|
336
|
+
actions?: TableAction[];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export interface TableAction {
|
|
340
|
+
id: DataTableButtonAction;
|
|
341
|
+
label: string;
|
|
342
|
+
icon?: string;
|
|
343
|
+
color?: 'primary' | 'success' | 'danger' | 'warning' | 'info' | 'secondary';
|
|
344
|
+
className?: string;
|
|
345
|
+
visible?: (item: any) => boolean;
|
|
346
|
+
disabled?: (item: any) => boolean;
|
|
347
|
+
tooltip?: string;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface TableConfig {
|
|
351
|
+
columns: TableColumn[];
|
|
352
|
+
totalCount?: number;
|
|
353
|
+
currentPage?: number;
|
|
354
|
+
pageSize?: number;
|
|
355
|
+
itemsPerPage?: number;
|
|
356
|
+
showPagination?: boolean;
|
|
357
|
+
showPageSize?: boolean;
|
|
358
|
+
pageSizeOptions?: number[];
|
|
359
|
+
showSearch?: boolean;
|
|
360
|
+
searchPlaceholder?: string;
|
|
361
|
+
showActions?: boolean;
|
|
362
|
+
showColumnSearch?: boolean;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export interface ColumnSearchEvent {
|
|
366
|
+
column: string;
|
|
367
|
+
value: any;
|
|
368
|
+
field: string;
|
|
369
|
+
searchType: string;
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
### Complete Data Table Example
|
|
376
|
+
|
|
377
|
+
#### Component TS (`app.component.ts`)
|
|
378
|
+
```typescript
|
|
379
|
+
import { Component, computed, OnInit, signal } from '@angular/core';
|
|
380
|
+
import { CommonModule } from '@angular/common';
|
|
381
|
+
import { AdDataTable, TableConfig, ColumnSearchEvent } from 'ng-adflowtail/ad-data-table';
|
|
382
|
+
|
|
383
|
+
interface Employee {
|
|
384
|
+
id: number;
|
|
385
|
+
name: string;
|
|
386
|
+
email: string;
|
|
387
|
+
department: string;
|
|
388
|
+
salary: number;
|
|
389
|
+
hireDate: string;
|
|
390
|
+
status: 'Active' | 'On Leave' | 'Terminated';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
@Component({
|
|
394
|
+
selector: 'app-dashboard-table',
|
|
395
|
+
standalone: true,
|
|
396
|
+
imports: [CommonModule, AdDataTable],
|
|
397
|
+
templateUrl: './app.component.html'
|
|
398
|
+
})
|
|
399
|
+
export class AppDashboardTable implements OnInit {
|
|
400
|
+
protected readonly brandColor = signal<string>('#7c3aed'); // Violet theme
|
|
401
|
+
|
|
402
|
+
// Datastore
|
|
403
|
+
protected readonly employees = signal<Employee[]>([]);
|
|
404
|
+
|
|
405
|
+
// Filtering, Search and Pagination states
|
|
406
|
+
protected readonly currentPage = signal<number>(1);
|
|
407
|
+
protected readonly pageSize = signal<number>(5);
|
|
408
|
+
protected readonly globalSearch = signal<string>('');
|
|
409
|
+
protected readonly columnFilters = signal<Record<string, string>>({});
|
|
410
|
+
|
|
411
|
+
ngOnInit() {
|
|
412
|
+
// Bootstrap dummy data
|
|
413
|
+
this.employees.set([
|
|
414
|
+
{ id: 101, name: 'Ahmad Al-Harbi', email: 'ahmad@company.com', department: 'Engineering', salary: 18500, hireDate: '2023-04-12', status: 'Active' },
|
|
415
|
+
{ id: 102, name: 'Sarah Connor', email: 'sarah@company.com', department: 'Operations', salary: 14200, hireDate: '2021-08-19', status: 'Active' },
|
|
416
|
+
{ id: 103, name: 'John Doe', email: 'john.d@company.com', department: 'Engineering', salary: 12000, hireDate: '2025-01-05', status: 'On Leave' },
|
|
417
|
+
{ id: 104, name: 'Lina Gamil', email: 'lina@company.com', department: 'Marketing', salary: 9800, hireDate: '2022-11-01', status: 'Active' },
|
|
418
|
+
{ id: 105, name: 'Michael Scott', email: 'michael@company.com', department: 'Sales', salary: 11500, hireDate: '2019-03-15', status: 'Terminated' }
|
|
419
|
+
]);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Reactive computed: filters records
|
|
423
|
+
protected readonly filteredEmployees = computed(() => {
|
|
424
|
+
let list = this.employees();
|
|
425
|
+
const search = this.globalSearch().toLowerCase();
|
|
426
|
+
const colFilters = this.columnFilters();
|
|
427
|
+
|
|
428
|
+
// 1. Process Global Master Search
|
|
429
|
+
if (search) {
|
|
430
|
+
list = list.filter(item =>
|
|
431
|
+
item.name.toLowerCase().includes(search) ||
|
|
432
|
+
item.email.toLowerCase().includes(search) ||
|
|
433
|
+
item.department.toLowerCase().includes(search)
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 2. Process Specific Column Searches
|
|
438
|
+
for (const [key, value] of Object.entries(colFilters)) {
|
|
439
|
+
if (value) {
|
|
440
|
+
list = list.filter(item => {
|
|
441
|
+
const itemVal = (item as any)[key]?.toString().toLowerCase();
|
|
442
|
+
return itemVal?.includes(value.toLowerCase());
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return list;
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Reactive computed: extracts sliced pagination view
|
|
451
|
+
protected readonly paginatedEmployees = computed(() => {
|
|
452
|
+
const list = this.filteredEmployees();
|
|
453
|
+
const start = (this.currentPage() - 1) * this.pageSize();
|
|
454
|
+
const end = start + this.pageSize();
|
|
455
|
+
return list.slice(start, end);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
protected readonly totalCount = computed(() => this.filteredEmployees().length);
|
|
459
|
+
|
|
460
|
+
// Dynamic Reactive Table Config
|
|
461
|
+
protected readonly tableConfig = computed<TableConfig>(() => ({
|
|
462
|
+
columns: [
|
|
463
|
+
{ field: 'id', header: 'ID', type: 'number' },
|
|
464
|
+
{ field: 'name', header: 'Full Name', type: 'text', searchable: true },
|
|
465
|
+
{ field: 'email', header: 'Email Address', type: 'text', searchable: true },
|
|
466
|
+
{
|
|
467
|
+
field: 'department',
|
|
468
|
+
header: 'Department',
|
|
469
|
+
type: 'text',
|
|
470
|
+
searchable: true,
|
|
471
|
+
searchType: 'select',
|
|
472
|
+
searchOptions: [
|
|
473
|
+
{ value: 'Engineering', label: 'Engineering' },
|
|
474
|
+
{ value: 'Operations', label: 'Operations' },
|
|
475
|
+
{ value: 'Marketing', label: 'Marketing' },
|
|
476
|
+
{ value: 'Sales', label: 'Sales' }
|
|
477
|
+
]
|
|
478
|
+
},
|
|
479
|
+
{ field: 'salary', header: 'Base Salary', type: 'currency' },
|
|
480
|
+
{ field: 'hireDate', header: 'Hire Date', type: 'date' },
|
|
481
|
+
{
|
|
482
|
+
field: 'status',
|
|
483
|
+
header: 'Status',
|
|
484
|
+
type: 'status',
|
|
485
|
+
searchable: true,
|
|
486
|
+
searchType: 'select',
|
|
487
|
+
searchOptions: [
|
|
488
|
+
{ value: 'Active', label: 'Active' },
|
|
489
|
+
{ value: 'On Leave', label: 'On Leave' },
|
|
490
|
+
{ value: 'Terminated', label: 'Terminated' }
|
|
491
|
+
],
|
|
492
|
+
statusColors: {
|
|
493
|
+
'Active': 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
|
|
494
|
+
'On Leave': 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400',
|
|
495
|
+
'Terminated': 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
field: 'actions',
|
|
500
|
+
header: 'Actions',
|
|
501
|
+
type: 'action',
|
|
502
|
+
actions: [
|
|
503
|
+
{ id: 'VIEW', label: 'View Profile', color: 'primary' },
|
|
504
|
+
{
|
|
505
|
+
id: 'DELETE',
|
|
506
|
+
label: 'Remove',
|
|
507
|
+
color: 'danger',
|
|
508
|
+
disabled: (row) => row.status === 'Terminated' // Disable delete button for terminated staff
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
}
|
|
512
|
+
],
|
|
513
|
+
showPagination: true,
|
|
514
|
+
showPageSize: true,
|
|
515
|
+
pageSizeOptions: [5, 10, 20],
|
|
516
|
+
showSearch: true,
|
|
517
|
+
searchPlaceholder: 'Search directory...',
|
|
518
|
+
showActions: true,
|
|
519
|
+
showColumnSearch: true,
|
|
520
|
+
totalCount: this.totalCount(),
|
|
521
|
+
currentPage: this.currentPage(),
|
|
522
|
+
pageSize: this.pageSize()
|
|
523
|
+
}));
|
|
524
|
+
|
|
525
|
+
// Handle Reactive Handlers
|
|
526
|
+
protected onPageChange(page: number) {
|
|
527
|
+
this.currentPage.set(page);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
protected onPageSizeChange(size: number) {
|
|
531
|
+
this.pageSize.set(size);
|
|
532
|
+
this.currentPage.set(1); // Reset to first page
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
protected onGlobalSearch(searchTerm: string) {
|
|
536
|
+
this.globalSearch.set(searchTerm);
|
|
537
|
+
this.currentPage.set(1);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
protected onColumnSearch(event: ColumnSearchEvent) {
|
|
541
|
+
this.columnFilters.update(filters => ({
|
|
542
|
+
...filters,
|
|
543
|
+
[event.field]: event.value
|
|
544
|
+
}));
|
|
545
|
+
this.currentPage.set(1);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
protected onActionTriggered(event: { action: string; item: any }) {
|
|
549
|
+
console.log(`Action triggered: [${event.action}] on record ID:`, event.item.id);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
protected onRowSelected(row: any) {
|
|
553
|
+
console.log('Row record clicked:', row);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### Component HTML Template (`app.component.html`)
|
|
559
|
+
```html
|
|
560
|
+
<div class="container mx-auto p-4">
|
|
561
|
+
<ad-data-table
|
|
562
|
+
title="Staff Directory"
|
|
563
|
+
[data]="paginatedEmployees()"
|
|
564
|
+
[config]="tableConfig()"
|
|
565
|
+
[primaryColor]="brandColor()"
|
|
566
|
+
(pageChange)="onPageChange($event)"
|
|
567
|
+
(pageSizeChange)="onPageSizeChange($event)"
|
|
568
|
+
(searchChange)="onGlobalSearch($event)"
|
|
569
|
+
(columnSearchChange)="onColumnSearch($event)"
|
|
570
|
+
(actionClick)="onActionTriggered($event)"
|
|
571
|
+
(rowClick)="onRowSelected($event)"
|
|
572
|
+
/>
|
|
573
|
+
</div>
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## Adding New Components
|
|
579
|
+
|
|
580
|
+
If you are a library developer working on the `ng-adflowtail` monorepo workspace, you can easily add new components using ng-packagr's standard secondary entry points system.
|
|
581
|
+
|
|
582
|
+
### Steps to scaffold a new widget `ad-badge`
|
|
583
|
+
|
|
584
|
+
#### 1. Setup Folder Structure
|
|
585
|
+
Create a new folder alongside the main `src` directory in `projects/ng-adflowtail/`:
|
|
586
|
+
|
|
587
|
+
```
|
|
588
|
+
projects/ng-adflowtail/
|
|
589
|
+
├── src/ # primary entry point
|
|
590
|
+
├── ad-multi-select/ # secondary entry point 1
|
|
591
|
+
├── ad-data-table/ # secondary entry point 2
|
|
592
|
+
└── ad-badge/ # <--- Your new secondary entry point
|
|
593
|
+
├── ng-package.json
|
|
594
|
+
└── src/
|
|
595
|
+
├── public-api.ts
|
|
596
|
+
└── lib/
|
|
597
|
+
├── ad-badge.ts
|
|
598
|
+
├── ad-badge.html
|
|
599
|
+
└── ad-badge.css
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
#### 2. Create `projects/ng-adflowtail/ad-badge/ng-package.json`
|
|
603
|
+
Define the secondary packaging schema pointing back to the library:
|
|
604
|
+
|
|
605
|
+
```json
|
|
606
|
+
{
|
|
607
|
+
"lib": {
|
|
608
|
+
"entryFile": "src/public-api.ts"
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
#### 3. Create `projects/ng-adflowtail/ad-badge/src/public-api.ts`
|
|
614
|
+
Export your exports correctly:
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
export * from './lib/ad-badge';
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
#### 4. Configure compiler paths mapping in Root `tsconfig.json`
|
|
621
|
+
Allow main apps in the monorepo to resolve imports locally during development:
|
|
622
|
+
|
|
623
|
+
```json
|
|
624
|
+
"paths": {
|
|
625
|
+
"ng-adflowtail": ["./projects/ng-adflowtail/src/public-api.ts"],
|
|
626
|
+
"ng-adflowtail/ad-data-table": ["./projects/ng-adflowtail/ad-data-table/src/public-api.ts"],
|
|
627
|
+
"ng-adflowtail/ad-multi-select": ["./projects/ng-adflowtail/ad-multi-select/src/public-api.ts"],
|
|
628
|
+
"ng-adflowtail/ad-badge": ["./projects/ng-adflowtail/ad-badge/src/public-api.ts"] // Add this
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
---
|
|
633
|
+
|
|
634
|
+
## Build & Publish
|
|
635
|
+
|
|
636
|
+
### Compile the entire package
|
|
637
|
+
Use the Angular CLI to run a production build. Under the hood, `ng-packagr` compiles all the secondary entry points in sequence:
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
ng build ng-adflowtail
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
The compiled distributable files are stored under `/dist/ng-adflowtail`.
|
|
644
|
+
|
|
645
|
+
### Publish to NPM
|
|
646
|
+
Navigate to the build directory and run the publish command:
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
cd dist/ng-adflowtail
|
|
650
|
+
npm publish --access public
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## License
|
|
656
|
+
|
|
657
|
+
MIT © Ahmad Workspace. Fully custom-crafted components built for Angular 22 and styled with Tailwind CSS & Flowbite.
|