UNPKG

ngx-primeng-toolkit

Version:

A comprehensive TypeScript utility library for Angular component state management, PrimeNG table state management, ng-select helpers, data storage, and memoized HTTP caching. Compatible with Angular 19+ and PrimeNG 19+ (optimized for Angular 20+ and Prime

1,484 lines (1,204 loc) 42.6 kB
# NGX PrimeNG Toolkit A comprehensive TypeScript utility library for Angular component state management, including PrimeNG table helpers, ng-select integration, data storage, and HTTP caching utilities with NgRx Signals. Compatible with **Angular 19+** and **PrimeNG 19+** (optimized for Angular 20+ and PrimeNG 20+). ## Features ### 🏗️ Table State Management - **Lazy Loading**: Efficient data loading with pagination - **Advanced Filtering**: Support for string, numeric, boolean, date, dropdown, and multiselect filters - **Sorting**: Multi-column sorting capabilities - **State Management**: Built with NgRx Signals for reactive state management - **TypeScript Support**: Full type safety and IntelliSense - **PrimeNG Integration**: Seamless integration with PrimeNG Table component ### 🎛️ Select Component Integration - **ng-select Helper**: Complete ng-select lifecycle management - **Search & Pagination**: Debounced search with infinite scroll - **HTTP Caching**: Intelligent response caching for performance - **Multi-Select Support**: Advanced multi-selection capabilities - **Error Handling**: Built-in error management and recovery ### Memoized Data Storage - 💾 **Smart Caching**: Automatic data caching with memoization - 🔄 **Cache Control**: Fine-grained cache invalidation and refresh - 📡 **HTTP Integration**: Built-in HTTP client integration - 🔧 **Configurable**: Flexible query parameters and options - ⚡ **Performance**: Reduces redundant API calls ### Component State Management - 🎛️ **Reactive State**: Signal-based component state management - 🔄 **Manipulation Types**: Support for Create, Update, Delete, View operations - 📋 **Dialog Management**: Built-in dialog state handling - 🔄 **Ajax State**: Loading and request state management - 🎯 **Computed Values**: Derived state through computed signals ### Component Data Storage - 📦 **Data Management**: Generic data storage for single and multiple objects - 🔄 **Reactive Updates**: Signal-based data updates with patching support - 🎯 **Type Safe**: Full TypeScript support for data operations - 🔍 **Search & Filter**: Built-in methods for finding and manipulating data - ⚡ **Performance**: Efficient data updates with minimal re-renders ### 🛠️ Utility Functions & Types - **Object Utilities**: Helper functions for cleaning and processing objects - **TypeScript Types**: Advanced utility types for nullable and partial types - **Form Processing**: Utilities for handling form data and API payloads - **Type Safety**: Enhanced TypeScript support for complex object operations ### Additional Features - 📦 **Tree-shakeable**: Import only what you need - 🏗️ **Dual Helpers**: Dynamic table helper (with filtering) and simple paged table helper - 🛠️ **Utility Functions**: Column configuration and filter utilities - 🔒 **Type Safety**: Comprehensive TypeScript support A TypeScript utility library for advanced PrimeNG table state management in Angular applications, featuring lazy loading, filtering, sorting, and pagination with NgRx Signals integration. ## Features - � **Lazy Loading**: Efficient data loading with pagination - 🔍 **Advanced Filtering**: Support for string, numeric, boolean, date, dropdown, and multiselect filters - 🔄 **Sorting**: Multi-column sorting capabilities - 📊 **State Management**: Built with NgRx Signals for reactive state management - 🎯 **TypeScript Support**: Full type safety and IntelliSense - 🎨 **PrimeNG Integration**: Seamless integration with PrimeNG Table component - 📦 **Tree-shakeable**: Import only what you need - � **Configurable**: Flexible configuration options for various use cases ## Installation ```bash npm install ngx-primeng-toolkit ``` ## Peer Dependencies Ensure you have compatible versions of the peer dependencies: ```bash # Core dependencies (Angular 19+ supported, 20+ recommended) npm install @angular/common@^19.0.0 @angular/core@^19.0.0 @ngrx/signals@^19.0.0 rxjs@^7.0.0 # For Angular 20+ projects (recommended) npm install @angular/common@^20.0.0 @angular/core@^20.0.0 @ngrx/signals@^20.0.0 rxjs@^7.0.0 # UI library dependencies (choose what you need) npm install primeng@^19.0.0 # For PrimeNG table helpers (19+ supported) npm install primeng@^20.0.0 # For PrimeNG table helpers (20+ recommended) npm install @ng-select/ng-select@^15.0.0 # For ng-select helpers ``` Optional (for response validation): ```bash npm install zod@^3.0.0 ``` ## Basic Usage ### 1. Setting Up the Table State Helper ```typescript import { Component, inject, viewChild } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Table } from 'primeng/table'; import { PrimeNgDynamicTableStateHelper } from 'ngx-primeng-toolkit'; interface User { id: number; name: string; email: string; active: boolean; createdAt: Date; } @Component({ selector: 'app-user-table', template: ` <p-table #dt [value]="tableState.data()" [lazy]="true" [paginator]="true" [rows]="15" [totalRecords]="tableState.totalRecords()" [loading]="tableState.isLoading()" (onLazyLoad)="tableState.onLazyLoad($event)"> <ng-template pTemplate="header"> <tr> <th pSortableColumn="name"> Name <p-sortIcon field="name"></p-sortIcon> </th> <th pSortableColumn="email"> Email <p-sortIcon field="email"></p-sortIcon> </th> <th>Active</th> </tr> <tr> <th> <p-columnFilter type="text" field="name" placeholder="Search by name"> </p-columnFilter> </th> <th> <p-columnFilter type="text" field="email" placeholder="Search by email"> </p-columnFilter> </th> <th> <p-columnFilter type="boolean" field="active"> </p-columnFilter> </th> </tr> </ng-template> <ng-template pTemplate="body" let-user> <tr> <td>{{ user.name }}</td> <td>{{ user.email }}</td> <td> <i class="pi" [ngClass]="user.active ? 'pi-check text-green-500' : 'pi-times text-red-500'"> </i> </td> </tr> </ng-template> </p-table> ` }) export class UserTableComponent { private readonly httpClient = inject(HttpClient); private readonly dataTableRef = viewChild.required<Table>('dt'); readonly tableState = PrimeNgDynamicTableStateHelper.create<User>({ url: '/api/users/query', httpClient: this.httpClient }); ngOnInit() { // Optional: Configure unique key and query parameters this.tableState .setUniqueKey('id') .patchQueryParams({ includeDeleted: false }); } ### 2. Using Table Configuration Utilities ```typescript import { createTextColumn, createNumericColumn, createBooleanColumn, createDateColumn, createDropdownColumn, mergeTableHeaders, createStatusSelectItems } from 'ngx-primeng-toolkit'; @Component({ // ... component configuration }) export class AdvancedTableComponent { readonly tableHeaders = mergeTableHeaders( createTextColumn('name', 'Full Name', { hasSort: true, placeholder: 'Search names...' }), createTextColumn('email', 'Email Address', { defaultMatchMode: 'contains' }), createNumericColumn('age', 'Age', { defaultMatchMode: 'greaterThanOrEqual' }), createBooleanColumn('active', 'Status'), createDateColumn('createdAt', 'Created Date'), createDropdownColumn('role', 'Role', createStatusSelectItems({ 'admin': 'Administrator', 'user': 'Regular User', 'moderator': 'Moderator' })) ); } ``` ### 3. Advanced Configuration ```typescript @Component({ // ... component configuration }) export class AdvancedUserTableComponent { private readonly httpClient = inject(HttpClient); readonly tableState = PrimeNgDynamicTableStateHelper.create<User>({ url: '/api/users/query', httpClient: this.httpClient, // Optional: Pass loading spinner context token skipLoadingSpinnerContext: SkipLoadingSpinner }); async ngOnInit() { // Set up the table with advanced configuration this.tableState .setUniqueKey('userId') .setQueryParams({ includeDeleted: false, tenantId: this.currentTenant.id }); // Load initial data await this.tableState.refreshData(); } // Method to change API endpoint dynamically async switchToInactiveUsers() { this.tableState .setUrl('/api/users/inactive/query') .clearTableData(this.dataTableRef()); await this.tableState.refreshData(); } // Method to add route parameters async loadUsersByDepartment(departmentId: number) { this.tableState .setRouteParam(departmentId.toString()) .clearTableData(this.dataTableRef()); await this.tableState.refreshData(); } // Method to update query parameters async filterByTenant(tenantId: number) { this.tableState .patchQueryParams({ tenantId }) .clearTableData(this.dataTableRef()); await this.tableState.refreshData(); } } ``` ## API Response Format The table state helper expects your API to return data in the following format: ```typescript interface ApiResponse<T> { data: T[]; // Array of table row data last_page: number; // Total number of pages last_row: number; // Total number of records } ``` ## API Request Format The helper sends POST requests with the following structure: ```typescript interface DynamicQueryDto { size: number; // Page size page: number; // Current page (1-based) filter: DynamicQueryFilterDto[]; // Array of filters sort: DynamicQuerySortDto[]; // Array of sort criteria } interface DynamicQueryFilterDto { field: string; // Field name to filter value: string; // Filter value (always string) type: FilterTypeMapped; // Filter operation type } interface DynamicQuerySortDto { field: string; // Field name to sort dir: 'asc' | 'desc'; // Sort direction } ``` ## Filter Types The library supports the following filter mappings: | PrimeNG Filter Type | Backend Filter Type | Description | |-------------------|-------------------|-------------| | `startsWith` | `starts` | Field starts with value | | `notStartsWith` | `!starts` | Field does not start with value | | `endsWith` | `ends` | Field ends with value | | `notEndsWith` | `!ends` | Field does not end with value | | `contains` | `like` | Field contains value | | `notContains` | `!like` | Field does not contain value | | `equals` | `=` | Field equals value | | `notEquals` | `!=` | Field does not equal value | | `greaterThan` | `>` | Field is greater than value | | `lessThan` | `<` | Field is less than value | | `greaterThanOrEqual` | `>=` | Field is greater than or equal to value | | `lessThanOrEqual` | `<=` | Field is less than or equal to value | ## Utility Functions ### Column Creation Functions - `createTextColumn(field, label, options)` - Creates text column with string filtering - `createNumericColumn(field, label, options)` - Creates numeric column with number filtering - `createBooleanColumn(field, label, options)` - Creates boolean column with true/false filtering - `createDateColumn(field, label, options)` - Creates date column with date filtering - `createDropdownColumn(field, label, options, dropdownOptions)` - Creates dropdown filtered column - `createMultiselectColumn(field, label, options, selectOptions)` - Creates multiselect filtered column - `createSimpleColumn(field, label, options)` - Creates basic column without filtering ### Helper Functions - `createPrimengStringMatchModes()` - Returns SelectItem array for string filter modes - `createPrimengNumberMatchModes()` - Returns SelectItem array for numeric filter modes - `createBooleanSelectItems()` - Creates boolean dropdown options - `createStatusSelectItems()` - Creates status dropdown from object mapping - `mergeTableHeaders()` - Combines multiple table header configurations ## Class Methods ### PrimeNgDynamicTableStateHelper Methods - `static create<T>(options)` - Creates new instance - `setUniqueKey(key)` - Sets unique identifier field name - `setUrl(url)` - Sets API endpoint URL and resets state - `setRouteParam(param)` - Appends route parameter to URL - `patchQueryParams(params)` - Merges additional query parameters - `removeQueryParam(key)` - Removes specific query parameter - `setQueryParams(params)` - Replaces all query parameters - `refreshData()` - Reloads data with current state - `clearTableData(table)` - Clears table data and resets state - `onLazyLoad(event)` - Handles PrimeNG lazy load events ### Readonly Signals - `data` - Current table data array - `isLoading` - Loading state boolean - `totalRecords` - Total number of records - `uniqueKey` - Current unique key field name ## Memoized Data Storage The library also includes a powerful memoized data storage system for caching HTTP responses and managing single/multiple data objects. ### Basic Usage ```typescript import { Component, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { MemoizedDataStorage } from 'ng-component-state-utils'; interface User { id: number; name: string; email: string; } @Component({ selector: 'app-data-example', template: ` <div> <!-- Single Data --> <button (click)="loadUser()" [disabled]="userStorage.isLoading()"> {{ userStorage.isLoading() ? 'Loading...' : 'Load User' }} </button> @if (userStorage.singleData(); as user) { <div> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> </div> } <!-- Multiple Data --> <button (click)="loadUsers()">Load Users</button> @for (user of userStorage.multipleData(); track user.id) { <div>{{ user.name }}</div> } </div> ` }) export class DataExampleComponent { private httpClient = inject(HttpClient); userStorage = new MemoizedDataStorage<User>(this.httpClient); async loadUser() { await this.userStorage.loadSingleData('/api/user/1'); } async loadUsers() { await this.userStorage.loadMultipleData('/api/users'); } } ``` ### Advanced Usage with Cache Control ```typescript export class AdvancedDataComponent { private httpClient = inject(HttpClient); // Create storage for different data types configStorage = new MemoizedDataStorage<ApiResponse<any>>(this.httpClient); categoryStorage = new MemoizedDataStorage<KeyData<string, string>>(this.httpClient); async loadConfig() { // First call fetches from API await this.configStorage.loadSingleData('/api/config'); // Second call uses cached data await this.configStorage.loadSingleData('/api/config'); } async refreshConfig() { // Force fresh data on next load this.configStorage.disableMemoizationOnNextRead(); await this.configStorage.loadSingleData('/api/config'); } async loadCategoriesWithParams() { await this.categoryStorage.loadMultipleData('/api/categories', { active: true, sort: 'name' }); } clearAllData() { this.configStorage.clear(); this.categoryStorage.clear(); } } ``` ### MemoizedDataStorage Methods - `loadSingleData(url, queryParams?)` - Loads single object with optional query parameters - `loadMultipleData(url, queryParams?)` - Loads array of objects with optional query parameters - `disableMemoizationOnNextRead()` - Forces fresh data on next load call - `clear()` - Clears all cached data - `hasSingleData()` - Checks if single data is cached - `hasMultipleData()` - Checks if multiple data is cached ### MemoizedDataStorage Signals - `singleData` - Current single data object or null - `multipleData` - Current array of data objects - `isLoading` - Loading state boolean ## Simple Paged Table Helper For scenarios where you only need basic pagination without filtering or sorting, use the `PrimengPagedDataTableStateHelper`: ```typescript import { PrimengPagedDataTableStateHelper } from 'ngx-primeng-toolkit'; interface Product { id: number; name: string; price: number; } @Component({ selector: 'app-simple-table', template: ` <p-table [value]="pagedHelper.data()" [lazy]="true" [loading]="pagedHelper.isLoading()" [totalRecords]="pagedHelper.totalRecords()" [paginator]="true" [rows]="10" (onLazyLoad)="pagedHelper.onLazyLoad($event)"> <ng-template pTemplate="body" let-product> <tr> <td>{{ product.name }}</td> <td>{{ product.price | currency }}</td> </tr> </ng-template> </p-table> ` }) export class SimpleTableComponent { private httpClient = inject(HttpClient); pagedHelper = PrimengPagedDataTableStateHelper.create<Product>({ url: '/api/products', httpClient: this.httpClient }); } ``` ### Paged Table Expected Response Format ```typescript interface PagedResponse<T> { payload: T[]; // Array of data objects totalCount: number; // Total number of records } ``` ## Component State Management The `ComponentState` class provides reactive state management for common component operations using Angular signals. ### Basic Usage ```typescript import { Component } from '@angular/core'; import { ComponentState, ManipulationType } from 'ng-component-state-utils'; @Component({ selector: 'app-user-management', template: ` <h2>{{ componentState.componentTitleWithManipulationType() }}</h2> <div class="status-indicators"> <span>Loading: {{ componentState.isAnyAjaxOperationRunning() }}</span> <span>State: {{ componentState.manipulationType() }}</span> <span>Multi-Select: {{ componentState.hasMultipleSelection() }}</span> </div> <p-button label="Create User" (click)="openCreateDialog()" [disabled]="componentState.isAnyAjaxOperationRunning()"> </p-button> <p-dialog [header]="componentState.componentTitleWithManipulationType()" [(visible)]="componentState.isCreateOrUpdateDialogOpen()"> <!-- Dialog content --> </p-dialog> ` }) export class UserManagementComponent { componentState = new ComponentState() .updateComponentTitle('User') .updateCheckBoxSelectionStatus(true); openCreateDialog() { this.componentState .updateManipulationType(ManipulationType.Create) .setCreateOrUpdateDialogOpen(true); } } ``` ### ComponentState Properties #### Signals - `isAjaxDataIncoming` - Incoming data loading state - `isAjaxRequestOutgoing` - Outgoing request state - `enableCheckBoxSelection` - Checkbox selection enabled state - `isSelectableRowEnabled` - Row selection enabled state - `hasMultipleSelection` - Multiple selection mode state - `isCreateOrUpdateDialogOpen` - Generic dialog open state - `isUpdateDialogOpen` - Update dialog specific state - `isCreateDialogOpen` - Create dialog specific state - `manipulationType` - Current operation type (Create/Update/Delete/View) - `componentTitle` - Component title #### Computed Signals - `componentTitleWithManipulationType()` - Title combined with operation type - `isOnUpdateState()` - True if in update mode - `isOnCreateState()` - True if in create mode - `isOnDeleteState()` - True if in delete mode - `isOnViewState()` - True if in view mode - `isAnyAjaxOperationRunning()` - True if any Ajax operation is active - `isAnyDialogOpen()` - True if any dialog is open ### ComponentState Methods ```typescript // Fluent API for chaining operations componentState .updateComponentTitle('Product Management') .updateManipulationType(ManipulationType.Update) .setCreateOrUpdateDialogOpen(true) .updateMultipleSelectionStatus(false); ``` ## Component Data Storage The `ComponentDataStorage<T>` class provides reactive data management for single objects and arrays. ### Basic Usage ```typescript import { Component, OnInit } from '@angular/core'; import { ComponentDataStorage } from 'ng-component-state-utils'; interface User { id: number; name: string; email: string; } @Component({ selector: 'app-user-data', template: ` <!-- Single User Display --> @if (dataStorage.singleData(); as user) { <div class="user-card"> <h3>{{ user.name }}</h3> <p>{{ user.email }}</p> <p-button label="Update Email" (click)="updateEmail()"></p-button> </div> } <!-- Multiple Users Display --> <div class="users-list"> <h4>Users ({{ dataStorage.getMultipleDataCount() }})</h4> @for (user of dataStorage.multipleData(); track user.id) { <div class="user-item"> {{ user.name }} - {{ user.email }} <p-button icon="pi pi-times" size="small" (click)="removeUser(user.id)"> </p-button> </div> } </div> <p-button label="Add Sample User" (click)="addSampleUser()"></p-button> ` }) export class UserDataComponent implements OnInit { dataStorage = new ComponentDataStorage<User>(); ngOnInit() { // Initialize with sample data this.dataStorage .updateSingleData({ id: 1, name: 'John Doe', email: 'john@example.com' }) .updateMultipleData([ { id: 1, name: 'John Doe', email: 'john@example.com' }, { id: 2, name: 'Jane Smith', email: 'jane@example.com' } ]); } updateEmail() { this.dataStorage.patchSingleData({ email: `updated-${Date.now()}@example.com` }); } addSampleUser() { const newUser: User = { id: Date.now(), name: 'New User', email: 'newuser@example.com' }; this.dataStorage.addToMultipleData(newUser); } removeUser(userId: number) { this.dataStorage.removeFromMultipleData(user => user.id === userId); } } ``` ### ComponentDataStorage Methods #### Data Operations - `updateSingleData(data)` - Replace single data completely - `updateMultipleData(array)` - Replace multiple data array - `patchSingleData(partial)` - Merge partial data with existing single data - `patchMultipleData(array)` - Append new items to existing array - `addToMultipleData(item)` - Add single item to array - `removeFromMultipleData(predicate)` - Remove items matching predicate - `updateItemInMultipleData(predicate, updateFn)` - Update specific items #### Utility Methods - `clearAll()` - Clear both single and multiple data - `clearSingleData()` - Clear only single data - `clearMultipleData()` - Clear only multiple data - `hasSingleData()` - Check if single data exists - `hasMultipleData()` - Check if multiple data has items - `getMultipleDataCount()` - Get count of multiple data items - `findInMultipleData(predicate)` - Find item in multiple data - `existsInMultipleData(predicate)` - Check if item exists ### Advanced Usage Example ```typescript @Component({ selector: 'app-advanced-data-management' }) export class AdvancedDataManagementComponent { userStorage = new ComponentDataStorage<User>(); componentState = new ComponentState() .updateComponentTitle('Advanced User Management'); async loadUsers() { this.componentState.setAjaxDataIncoming(true); try { const users = await this.fetchUsers(); this.userStorage.updateMultipleData(users); // Set first user as selected if (users.length > 0) { this.userStorage.updateSingleData(users[0]); } } catch (error) { console.error('Failed to load users:', error); } finally { this.componentState.setAjaxDataIncoming(false); } } async updateUser(userId: number, updates: Partial<User>) { this.componentState.setAjaxRequestOutgoing(true); try { const updatedUser = await this.updateUserAPI(userId, updates); // Update in both single and multiple data if it's the same user this.userStorage.updateItemInMultipleData( user => user.id === userId, () => updatedUser ); if (this.userStorage.singleData()?.id === userId) { this.userStorage.updateSingleData(updatedUser); } } catch (error) { console.error('Failed to update user:', error); } finally { this.componentState.setAjaxRequestOutgoing(false); } } private async fetchUsers(): Promise<User[]> { // Your API call here return []; } private async updateUserAPI(userId: number, updates: Partial<User>): Promise<User> { // Your API call here return {} as User; } } ``` ## NgSelect Helper with Centralized Initialization The library provides an `initNgSelect` utility function for streamlined initialization of multiple NgSelect helpers with centralized error handling. ### Recommended Usage Pattern ```typescript import { Component, inject, signal } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; import { HttpClient } from '@angular/common/http'; import { DestroyRef } from '@angular/core'; import { NgSelectHelper, initNgSelect } from 'ngx-primeng-toolkit'; interface KeyData<K, V> { key: K; data: V; } @Component({ selector: 'app-select-example' }) export class SelectExampleComponent { private readonly httpClient = inject(HttpClient); private readonly destroyRef = inject(DestroyRef); private readonly toastService = inject(ToastService); // Your toast service // Define NgSelect helpers readonly floorOptionsHelper = new NgSelectHelper<KeyData<number, string>>( '/api/shared/floors', this.httpClient, this.destroyRef, false, // usePostRequest 50, // limit (page size) false // useCache ); readonly lineOptionsHelper = new NgSelectHelper<KeyData<number, string>>( '/api/shared/lines', this.httpClient, this.destroyRef, false, // usePostRequest 50, // limit false // useCache ); // Centralized helper management readonly ngSelectHelpers = signal([ this.floorOptionsHelper, this.lineOptionsHelper ]); constructor() { // Initialize all NgSelect helpers with error handling initNgSelect( toObservable(this.ngSelectHelpers), this.destroyRef, (err) => this.toastService.showAjaxErrorToast(err) ); } } ``` ### Benefits of Centralized Initialization 1. **Single Point of Error Handling**: All NgSelect errors are handled consistently 2. **Automatic Cleanup**: Uses `takeUntilDestroyed` for proper subscription cleanup 3. **Lazy Initialization**: Only initializes helpers that haven't been initialized yet 4. **Type Safety**: Full TypeScript support with proper type inference ### initNgSelect Function API ```typescript function initNgSelect( helpers$: Observable<NgSelectHelper<unknown>[]>, destroyRef: DestroyRef, onAjaxError: (err: Error) => void ): void ``` **Parameters:** - `helpers$`: Observable of NgSelectHelper instances (typically from `toObservable(signal)`) - `destroyRef`: Angular DestroyRef for automatic subscription cleanup - `onAjaxError`: Callback function to handle AJAX errors from any helper ## NgSelectHelper (Individual Usage) The `NgSelectHelper` class provides comprehensive state management for ng-select components with features like pagination, search, caching, and error handling. ### Basic Usage ```typescript import { Component, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { NgSelectHelper } from 'ng-component-state-utils'; interface User { id: number; name: string; email: string; } @Component({ selector: 'app-user-select', template: ` <ng-select [items]="userSelectHelper.loadedData().payload" bindLabel="name" bindValue="id" placeholder="Search users..." [loading]="userSelectHelper.isLoading()" [typeahead]="userSelectHelper.inputSubject" [(ngModel)]="selectedUserId" (open)="userSelectHelper.onOpen()" (close)="userSelectHelper.onClose()" (clear)="userSelectHelper.onClear()" (scrollToEnd)="userSelectHelper.onScrollToEnd()"> <ng-option-highlight [term]="userSelectHelper.inputSubject | async" [text]="option.name"> </ng-option-highlight> </ng-select> ` }) export class UserSelectComponent { private httpClient = inject(HttpClient); private destroyRef = inject(DestroyRef); userSelectHelper = NgSelectHelper.create<User>({ ajaxUrl: '/api/users', httpClient: this.httpClient, destroyRef: this.destroyRef, limit: 20, useCache: true }); selectedUserId: number | null = null; ngOnInit() { this.userSelectHelper.init(); } } ``` ### Advanced Usage with POST Requests ```typescript @Component({ selector: 'app-product-select' }) export class ProductSelectComponent { productSelectHelper = NgSelectHelper.create<Product>({ ajaxUrl: '/api/products/search', httpClient: this.httpClient, destroyRef: this.destroyRef, usePostRequest: true, limit: 15, useCache: false }); ngOnInit() { this.productSelectHelper .setDebounceTimeInSecond(1) .setBody({ filters: [{ field: 'active', value: true }], sort: [{ field: 'name', direction: 'asc' }] }) .init(); // Handle errors this.productSelectHelper.ajaxError$.subscribe(error => { console.error('Product search failed:', error); }); } filterByCategory(categoryId: number) { this.productSelectHelper .setBody({ filters: [ { field: 'active', value: true }, { field: 'categoryId', value: categoryId } ] }) .resetAll({ resetCache: true }); } } ``` ### Multi-Select with Form Integration ```typescript import { FormControl, FormGroup } from '@angular/forms'; @Component({ selector: 'app-department-multi-select', template: ` <form [formGroup]="form"> <ng-select [items]="departmentSelectHelper.loadedData().payload" bindLabel="name" bindValue="id" [multiple]="true" [closeOnSelect]="false" formControlName="departments" [loading]="departmentSelectHelper.isLoading()" [typeahead]="departmentSelectHelper.inputSubject" (open)="departmentSelectHelper.onOpen()" (scrollToEnd)="departmentSelectHelper.onScrollToEnd()"> </ng-select> </form> ` }) export class DepartmentMultiSelectComponent { departmentSelectHelper = NgSelectHelper.create<Department>({ ajaxUrl: '/api/departments', httpClient: this.httpClient, destroyRef: this.destroyRef, limit: 50 }); form = new FormGroup({ departments: new FormControl<number[]>([]) }); ngOnInit() { this.departmentSelectHelper .setDebounceTimeInSecond(0.5) .init(); } } ``` ### NgSelectHelper Configuration Options ```typescript interface NgSelectHelperConfig<TData> { ajaxUrl: string; // API endpoint URL httpClient: HttpClient; // Angular HttpClient destroyRef: DestroyRef; // Angular DestroyRef for cleanup usePostRequest?: boolean; // Use POST instead of GET (default: false) limit?: number; // Items per page (default: 10) useCache?: boolean; // Enable response caching (default: false) queryParams?: Record<string, any>; // Initial query parameters body?: any; // Request body for POST requests httpOptions?: any; // Additional HTTP options } ``` ### NgSelectHelper Methods **Configuration Methods:** - `init()` - Initialize the helper and load first page - `setDebounceTimeInSecond(seconds: number)` - Set search debounce time - `patchQueryParams(params: Record<string, any>)` - Update query parameters - `removeQueryParam(key: string)` - Remove a query parameter - `setBody(body: any)` - Set request body for POST requests - `resetAll(options)` - Reset all data with options **Event Handlers:** - `onOpen()` - Handle ng-select open event - `onClose()` - Handle ng-select close event - `onClear()` - Handle ng-select clear event - `onScrollToEnd()` - Handle infinite scroll **Cache Management:** - `clearCache()` - Clear cached responses - `refreshData()` - Refresh current data ### NgSelectHelper Signals **Data Signals:** - `loadedData()` - Currently loaded data with pagination info - `isLoading()` - Loading state - `totalCount` - Total available records - `page` - Current page number - `limitReached` - Whether pagination limit reached **Input Management:** - `inputSubject` - Subject for search input (use with typeahead) **Error Handling:** - `ajaxError$` - Observable for API errors - `isLastApiCallSuccessful` - Whether last API call succeeded ### Integration with Other Helpers ```typescript @Component({ selector: 'app-integrated-form' }) export class IntegratedFormComponent { // Component state management componentState = new ComponentState() .updateComponentTitle('User Management'); // Data storage userDataStorage = new ComponentDataStorage<User>(); // Select helpers departmentSelectHelper = NgSelectHelper.create<Department>({ ajaxUrl: '/api/departments', httpClient: this.httpClient, destroyRef: this.destroyRef }); async createUser(userData: Partial<User>) { this.componentState.setAjaxRequestOutgoing(true); try { const newUser = await this.userService.create(userData); this.userDataStorage.addToMultipleData(newUser); // Clear department select cache to ensure fresh data this.departmentSelectHelper.clearCache(); } catch (error) { console.error('Failed to create user:', error); } finally { this.componentState.setAjaxRequestOutgoing(false); } } } ``` For a comprehensive ng-select example, see `example-ng-select-usage.ts`. ## Complete Integration Example For a comprehensive example showing all features working together, see the `example-usage-all-features.ts` file which demonstrates: - Table state management with filtering and sorting - Memoized data storage for reference data - Component state management for dialogs and operations - Component data storage for selected items - Integration between all systems ## Error Handling ``` The library includes built-in error handling: ```typescript try { await this.tableState.refreshData(); } catch (error) { // Handle API errors console.error('Failed to load table data:', error); this.toastService.showError('Failed to load data'); } ``` ## TypeScript Support Full TypeScript support with generic typing: ```typescript interface CustomUser { id: string; firstName: string; lastName: string; department: { id: number; name: string; }; } const tableState = PrimeNgDynamicTableStateHelper.create<CustomUser>({ url: '/api/users', httpClient: this.httpClient }); // tableState.data() is typed as Signal<CustomUser[]> ``` ## Utility Functions & TypeScript Types The library includes general utility functions and TypeScript helper types for common development tasks. ### Object Utilities #### cleanNullishFromObject Removes null and undefined values from an object, useful for cleaning query parameters before API calls. ```typescript import { cleanNullishFromObject } from 'ngx-primeng-toolkit'; // Basic usage const queryParams = { name: 'John', email: null, age: undefined, status: 'active', department: '' }; const cleaned = cleanNullishFromObject(queryParams); // Result: { name: 'John', status: 'active', department: '' } // With array values const filters = { categories: ['tech', 'business'], tags: null, priority: undefined, active: true }; const cleanedFilters = cleanNullishFromObject(filters); // Result: { categories: ['tech', 'business'], active: true } // In component for API calls export class UserSearchComponent { searchForm = new FormGroup({ name: new FormControl<string | null>(null), email: new FormControl<string | null>(null), department: new FormControl<string | null>(null) }); async searchUsers() { const searchParams = cleanNullishFromObject(this.searchForm.value); // Only sends non-null/undefined values to API const users = await this.userService.searchUsers(searchParams); } } ``` ### TypeScript Utility Types The library provides several utility types for handling nullable and partial types: #### RecursiveNullable<T> Makes all properties of a type nullable (T | null): ```typescript import { RecursiveNullable } from 'ngx-primeng-toolkit'; type User = { id: number; name: string; email: string; }; type NullableUser = RecursiveNullable<User>; // Result: { // id: number | null; // name: string | null; // email: string | null; // } // Useful for form models export class UserFormComponent { userForm: FormGroup<{ [K in keyof User]: FormControl<RecursiveNullable<User>[K]> }>; } ``` #### Nullish<T> Represents a value that can be null or undefined: ```typescript import { Nullish } from 'ngx-primeng-toolkit'; type MaybeString = Nullish<string>; // string | null | undefined type MaybeUser = Nullish<User>; // User | null | undefined // Useful for optional service responses export class UserService { getCurrentUser(): Observable<Nullish<User>> { // May return user, null, or undefined return this.http.get<User>('/api/user/current') .pipe(catchError(() => of(null))); } } ``` #### RecursiveNullish<T> Makes all properties nullish (T | null | undefined) recursively: ```typescript import { RecursiveNullish } from 'ngx-primeng-toolkit'; type User = { id: number; profile: { name: string; age: number; }; }; type NullishUser = RecursiveNullish<User>; // Result: { // id: number | null | undefined; // profile: { // name: string | null | undefined; // age: number | null | undefined; // } | null | undefined; // } // Useful for deeply nullable form states export interface FormState extends RecursiveNullish<User> {} ``` #### RecursivePartial<T> Makes all properties optional recursively: ```typescript import { RecursivePartial } from 'ngx-primeng-toolkit'; type User = { id: number; profile: { name: string; age: number; }; settings: { theme: string; notifications: boolean; }; }; type PartialUser = RecursivePartial<User>; // Result: { // id?: number; // profile?: { // name?: string; // age?: number; // }; // settings?: { // theme?: string; // notifications?: boolean; // }; // } // Perfect for update operations export class UserService { updateUser(id: number, updates: RecursivePartial<User>) { // Can update any subset of user properties return this.http.patch<User>(`/api/users/${id}`, updates); } } // Usage in components export class UserProfileComponent { updateProfile() { const updates: RecursivePartial<User> = { profile: { name: 'New Name' // Only updating name, age is optional } // settings and id are completely optional }; this.userService.updateUser(this.userId, updates); } } ``` ### Combining Utilities These utilities work great together for complex form and API scenarios: ```typescript import { cleanNullishFromObject, RecursivePartial, RecursiveNullish } from 'ngx-primeng-toolkit'; export class AdvancedFormComponent { // Form model with nullable fields formModel: RecursiveNullish<User> = { id: null, profile: { name: null, age: undefined } }; // Update with partial data updateUser(updates: RecursivePartial<User>) { // Clean nullish values before sending to API const cleanUpdates = cleanNullishFromObject(updates); if (Object.keys(cleanUpdates).length > 0) { this.userService.updateUser(this.userId, cleanUpdates); } } } ``` ## License MIT License - see LICENSE file for details. ## Contributing Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository. ## Support For issues and questions, please use the GitHub issues page. This package uses automated versioning and publishing through GitHub Actions. Contributors don't need to manually update versions or publish to NPM. #### Commit Message Format Use conventional commit messages to trigger automatic version bumps: ```bash # For bug fixes (patch version: 1.0.0 → 1.0.1) git commit -m "fix: resolve checkbox state issue" # For new features (minor version: 1.0.0 → 1.1.0) git commit -m "feat: add new utility function" # For breaking changes (major version: 1.0.0 → 2.0.0) git commit -m "major: change API interface" # OR git commit -m "feat: new feature BREAKING CHANGE: API has changed" # Other commits (no version bump) git commit -m "docs: update README" git commit -m "chore: update dependencies" ``` #### Development Workflow 1. **Make your changes** 2. **Commit with proper message format** 3. **Push to main branch** 4. **GitHub Actions automatically:** - Runs tests and type checking - Bumps version based on commit message - Publishes to NPM - Creates git tags ```bash # Example workflow git add . git commit -m "feat: add new tri-state utility" git push origin main # 🎉 Package automatically published! ``` #### Pull Request Workflow For larger changes, use pull requests: ```bash # Create feature branch git checkout -b feature/new-functionality # Make changes and commit git commit -m "feat: add advanced tri-state features" # Push and create PR git push origin feature/new-functionality # Create PR on GitHub → Merge → Automatic publish! ``` ## Browser Support - Modern browsers supporting ES2020+ - Angular 19+ (Angular 20+ recommended) - PrimeNG 19+ (PrimeNG 20+ recommended) (optional, for table helpers) - @ng-select/ng-select 15+ (optional, for select helpers) ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License MIT License. See LICENSE file for details. ## Links - [PrimeNG Documentation](https://primeng.org/) - [@ng-select/ng-select Documentation](https://github.com/ng-select/ng-select) - [Angular Reactive Forms](https://angular.io/guide/reactive-forms) - [NgRx Signals](https://ngrx.io/guide/signals)