# 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)
