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 lines 114 kB
{"version":3,"sources":["../src/dynamic-table-state-helper.ts","../src/http-context-tokens.ts","../src/types.ts","../src/utils.ts","../src/paged-table-state-helper.ts","../src/table-utils.ts","../src/memoized-data-storage.ts","../src/component-state.ts","../src/component-data-storage.ts","../src/ng-select-helper.ts","../src/ng-select-utils.ts"],"sourcesContent":["import { HttpClient, HttpContext } from \"@angular/common/http\";\nimport { signal, Signal } from \"@angular/core\";\nimport { signalState, patchState } from \"@ngrx/signals\";\nimport { FilterMetadata } from \"primeng/api\";\nimport { Table, TableLazyLoadEvent } from \"primeng/table\";\nimport { firstValueFrom } from \"rxjs\";\n\nimport { SkipLoadingSpinner } from \"./http-context-tokens\";\nimport {\n DynamicQueryDto,\n DynamicQueryFilterDto,\n DynamicQuerySortDto,\n PagedDataResponse,\n PrimeNgTableState,\n PrimeNgTableStateHelperQueryParam,\n FilterTypeMapped,\n dynamicQueryResponseZodSchema\n} from \"./types\";\nimport { routeParamConcat } from \"./utils\";\n\n/**\n * Initial state factory function for dynamic table\n */\nfunction initialDynamicState<T>(): PrimeNgTableState<T> {\n return {\n data: [],\n isLoading: false,\n totalRecords: 0,\n size: 15,\n page: 1,\n filter: [],\n sort: []\n };\n}\n\n/**\n * Options for creating PrimeNgDynamicTableStateHelper\n */\ntype PrimeNgDynamicTableStateOpts = {\n url: string;\n httpClient: HttpClient;\n skipLoadingSpinner?: boolean;\n};\n\n/**\n * PrimeNG Dynamic Table State Helper class for managing table state with lazy loading, filtering, and sorting\n *\n * This helper provides advanced table functionality including:\n * - Lazy loading with pagination\n * - Column filtering with multiple filter types\n * - Multi-column sorting\n * - State management with NgRx Signals\n * - Automatic API integration\n * - Route parameter support\n * - Query parameter management\n *\n * @example\n * ```typescript\n * const tableState = PrimeNgDynamicTableStateHelper.create<User>({\n * url: '/api/users',\n * httpClient: this.httpClient\n * });\n *\n * // In template\n * <p-table [value]=\"tableState.data()\"\n * [lazy]=\"true\"\n * [loading]=\"tableState.isLoading()\"\n * [totalRecords]=\"tableState.totalRecords()\"\n * (onLazyLoad)=\"tableState.onLazyLoad($event)\">\n * </p-table>\n * ```\n */\nexport class PrimeNgDynamicTableStateHelper<T> {\n private readonly state = signalState<PrimeNgTableState<T>>(initialDynamicState<T>());\n private urlWithOutRouteParam: string;\n private skipLoadingSpinner: boolean;\n readonly #uniqueKey = signal(\"id\");\n readonly uniqueKey = this.#uniqueKey.asReadonly();\n #queryParams: PrimeNgTableStateHelperQueryParam = {};\n\n // Public readonly signals\n readonly totalRecords: Signal<number> = this.state.totalRecords;\n readonly isLoading: Signal<boolean> = this.state.isLoading;\n readonly data: Signal<Array<T>> = this.state.data;\n\n private constructor(\n private url: string,\n private readonly httpClient: HttpClient,\n skipLoadingSpinner: boolean = true\n ) {\n this.urlWithOutRouteParam = url;\n this.skipLoadingSpinner = skipLoadingSpinner;\n }\n\n /**\n * Creates a new instance of PrimeNgDynamicTableStateHelper\n * @param options - Configuration options\n * @returns New instance of PrimeNgDynamicTableStateHelper\n */\n public static create<T>(\n options: PrimeNgDynamicTableStateOpts\n ): PrimeNgDynamicTableStateHelper<T> {\n return new PrimeNgDynamicTableStateHelper<T>(\n options.url,\n options.httpClient,\n options.skipLoadingSpinner ?? true\n );\n }\n\n /**\n * Sets whether to skip the loading spinner\n * @param skip - Whether to skip the loading spinner\n * @returns This instance for method chaining\n */\n public setSkipLoadingSpinner(skip: boolean): this {\n this.skipLoadingSpinner = skip;\n return this;\n }\n\n /**\n * Sets the unique key field for table rows\n * @param newUniqueKey - The field name to use as unique identifier\n * @returns This instance for method chaining\n */\n public setUniqueKey(newUniqueKey: string): this {\n this.#uniqueKey.set(newUniqueKey);\n return this;\n }\n\n /**\n * Updates the API URL\n * @param newUrl - The new API URL\n * @returns This instance for method chaining\n */\n public setUrl(newUrl: string): this {\n this.url = newUrl;\n this.urlWithOutRouteParam = newUrl;\n return this;\n }\n\n /**\n * Appends a route parameter to the URL\n * @param newRouteParam - The route parameter to append\n * @returns This instance for method chaining\n */\n public setRouteParam(newRouteParam: string): this {\n this.url = routeParamConcat(this.urlWithOutRouteParam, newRouteParam);\n return this;\n }\n\n /**\n * Patches existing query parameters\n * @param value - Query parameters to merge\n * @returns This instance for method chaining\n */\n public patchQueryParams(value: PrimeNgTableStateHelperQueryParam): this {\n this.#queryParams = { ...this.#queryParams, ...value };\n return this;\n }\n\n /**\n * Removes a specific query parameter\n * @param key - The key to remove\n * @returns This instance for method chaining\n */\n public removeQueryParam(key: string): this {\n delete this.#queryParams[key];\n return this;\n }\n\n /**\n * Sets all query parameters (replaces existing)\n * @param newQueryParams - New query parameters\n * @returns This instance for method chaining\n */\n public setQueryParams(newQueryParams: PrimeNgTableStateHelperQueryParam): this {\n this.#queryParams = newQueryParams;\n return this;\n }\n\n /**\n * Handles PrimeNG table lazy load events\n * @param event - The lazy load event from PrimeNG table\n */\n public async onLazyLoad(event: TableLazyLoadEvent): Promise<void> {\n if (this.isLoading()) {\n return;\n }\n patchState(this.state, {\n size: event.rows || 15,\n page: Math.floor((event.first || 0) / (event.rows || 15)) + 1,\n filter: this.filterMapper(event.filters || {}),\n sort:\n Object.keys(event.multiSortMeta || {}).length > 0\n ? (event.multiSortMeta || []).map((sort: any) => ({\n field: sort.field,\n dir: (sort.order === 1 ? \"asc\" : \"desc\") as \"asc\" | \"desc\"\n }))\n : event.sortField\n ? [\n {\n field: event.sortField,\n dir: ((event.sortOrder || 1) === 1 ? \"asc\" : \"desc\") as \"asc\" | \"desc\"\n }\n ]\n : []\n });\n\n await this.fetchData(this.dtoBuilder());\n }\n\n /**\n * Clears table data and resets to first page\n * @param table - Optional PrimeNG Table reference to reset\n */\n public async clearTableData(table?: Table): Promise<void> {\n if (this.isLoading()) {\n return;\n }\n patchState(this.state, {\n data: [],\n totalRecords: 0,\n page: 1,\n filter: [],\n sort: []\n });\n\n if (table) {\n table.reset();\n }\n\n await this.fetchData(this.dtoBuilder());\n }\n\n /**\n * Manually triggers data refresh with current state\n */\n public async refresh(): Promise<void> {\n if (this.isLoading()) {\n return;\n }\n await this.fetchData(this.dtoBuilder());\n }\n\n /**\n * Fetches data from the API\n */\n private async fetchData(dto: DynamicQueryDto): Promise<void> {\n if (this.isLoading()) {\n return;\n }\n try {\n patchState(this.state, { isLoading: true });\n\n const params = new URLSearchParams();\n Object.entries(this.#queryParams).forEach(([key, value]) => {\n params.append(key, String(value));\n });\n\n const urlWithParams = params.toString() ? `${this.url}?${params.toString()}` : this.url;\n\n const response = await firstValueFrom(\n this.httpClient.post(urlWithParams, dto, {\n context: new HttpContext().set(SkipLoadingSpinner, this.skipLoadingSpinner)\n })\n );\n\n const validatedResponse = dynamicQueryResponseZodSchema.parse(response);\n\n patchState(this.state, {\n data: validatedResponse.data,\n totalRecords: validatedResponse.last_row,\n isLoading: false\n });\n } catch (error) {\n patchState(this.state, {\n data: [],\n totalRecords: 0,\n isLoading: false\n });\n throw error;\n }\n }\n\n /**\n * Builds the DTO for API requests\n */\n private dtoBuilder(): DynamicQueryDto {\n return {\n size: this.state.size(),\n page: this.state.page(),\n filter: this.state.filter(),\n sort: this.state.sort()\n };\n }\n\n /**\n * Maps PrimeNG filters to API filter format\n */\n private filterMapper(\n dto: Record<string, FilterMetadata | FilterMetadata[] | undefined>\n ): DynamicQueryFilterDto[] {\n const filters: DynamicQueryFilterDto[] = [];\n\n Object.entries(dto).forEach(([field, filterData]) => {\n if (!filterData) return;\n\n const processFilter = (filter: FilterMetadata) => {\n if (filter.value === null || filter.value === undefined || filter.value === \"\") return;\n\n const mappedType = this.evaluateInput(filter.matchMode || \"contains\");\n if (mappedType) {\n filters.push({\n field,\n value: String(filter.value),\n type: mappedType\n });\n }\n };\n\n if (Array.isArray(filterData)) {\n filterData.forEach(processFilter);\n } else {\n processFilter(filterData);\n }\n });\n\n return filters;\n }\n\n /**\n * Maps PrimeNG filter match modes to API filter types\n */\n private evaluateInput(input: string): FilterTypeMapped | null {\n const filterMap: Record<string, FilterTypeMapped> = {\n startsWith: \"starts\",\n notStartsWith: \"!starts\",\n endsWith: \"ends\",\n notEndsWith: \"!ends\",\n contains: \"like\",\n notContains: \"!like\",\n equals: \"=\",\n notEquals: \"!=\",\n greaterThan: \">\",\n lessThan: \"<\",\n greaterThanOrEqual: \">=\",\n lessThanOrEqual: \"<=\"\n };\n\n return filterMap[input] || null;\n }\n}\n","import { HttpContextToken } from \"@angular/common/http\";\n\n/**\n * HTTP Context Token to skip loading spinner on HTTP requests\n * Usage: Set this token to true in HttpContext to skip showing loading spinner\n * \n * @example\n * ```typescript\n * const context = new HttpContext().set(SkipLoadingSpinner, true);\n * this.httpClient.get('/api/data', { context });\n * ```\n */\nexport const SkipLoadingSpinner = new HttpContextToken(() => false);\n","import { z } from \"zod\";\n\n// ===============================================================================\n// TypeScript Utility Types\n// ===============================================================================\n\n/**\n * Makes all properties of T nullable (T | null) recursively, handling functions, arrays, and objects\n * @template Thing - The type to make nullable\n * @example\n * ```typescript\n * type User = { id: number; name: string; email: string; tags: string[] };\n * type NullableUser = RecursiveNullable<User>;\n * // Result: {\n * // id: number | null;\n * // name: string | null;\n * // email: string | null;\n * // tags: (string | null)[] | null;\n * // }\n * ```\n */\nexport type RecursiveNullable<Thing> = Thing extends Function\n ? Thing\n : Thing extends Array<infer InferredArrayMember>\n ? RecursiveNullableArray<InferredArrayMember>\n : Thing extends Record<string, any>\n ? RecursiveNullableObject<Thing>\n : Exclude<Thing, undefined> | null;\n\ntype RecursiveNullableObject<Thing extends object> = {\n [Key in keyof Thing]: RecursiveNullable<Thing[Key]>;\n};\n\ninterface RecursiveNullableArray<Thing> extends Array<RecursiveNullable<Thing>> {}\n\n/**\n * Makes all properties of T nullish (T | null | undefined)\n * @template T - The base type\n * @example\n * ```typescript\n * type User = { id: number; name: string };\n * type NullishUser = Nullish<User>;\n * // Result: { id: number | null | undefined; name: string | null | undefined }\n * ```\n */\nexport type Nullish<T> = {\n [P in keyof T]: Exclude<T[P], null | undefined> | null | undefined;\n};\n\n/**\n * Makes all properties of T nullish (T | null | undefined) recursively, handling functions, arrays, and objects\n * @template Thing - The type to make nullish\n * @example\n * ```typescript\n * type User = { id: number; profile: { name: string; age: number }; tags: string[] };\n * type NullishUser = RecursiveNullish<User>;\n * // Result: {\n * // id: number | null | undefined;\n * // profile: {\n * // name: string | null | undefined;\n * // age: number | null | undefined\n * // } | null | undefined;\n * // tags: (string | null | undefined)[] | null | undefined;\n * // }\n * ```\n */\nexport type RecursiveNullish<Thing> = Thing extends Function\n ? Thing\n : Thing extends Array<infer InferredArrayMember>\n ? RecursiveNullishArray<InferredArrayMember>\n : Thing extends Record<string, any>\n ? RecursiveNullishObject<Thing>\n : Exclude<Thing, null | undefined> | null | undefined;\n\ntype RecursiveNullishObject<Thing extends object> = {\n [Key in keyof Thing]: RecursiveNullish<Thing[Key]>;\n};\n\ninterface RecursiveNullishArray<Thing> extends Array<RecursiveNullish<Thing>> {}\n\n/**\n * Makes all properties optional recursively, useful for partial updates, handling functions, arrays, and objects\n * @template Thing - The type to make recursively partial\n * @example\n * ```typescript\n * type User = {\n * id: number;\n * profile: { name: string; age: number };\n * settings: { theme: string; notifications: boolean };\n * tags: string[];\n * };\n * type PartialUser = RecursivePartial<User>;\n * // Result: {\n * // id?: number | undefined;\n * // profile?: { name?: string | undefined; age?: number | undefined } | undefined;\n * // settings?: { theme?: string | undefined; notifications?: boolean | undefined } | undefined;\n * // tags?: (string | undefined)[] | undefined;\n * // }\n * ```\n */\nexport type RecursivePartial<Thing> = Thing extends Function\n ? Thing\n : Thing extends Array<infer InferredArrayMember>\n ? RecursivePartialArray<InferredArrayMember>\n : Thing extends object\n ? RecursivePartialObject<Thing>\n : Thing | undefined;\n\ntype RecursivePartialObject<Thing> = {\n [Key in keyof Thing]?: RecursivePartial<Thing[Key]>;\n};\n\ninterface RecursivePartialArray<Thing> extends Array<RecursivePartial<Thing>> {}\n\n// ===============================================================================\n// Core Data Types\n// ===============================================================================\n\n/**\n * Enumeration for manipulation types in component operations\n * Used for tracking the current operation state in component management\n *\n * @example\n * ```typescript\n * // In a component\n * currentOperation: ManipulationType = ManipulationType.Create;\n *\n * // Check operation type\n * if (this.currentOperation === ManipulationType.Update) {\n * // Handle update logic\n * }\n * ```\n */\nexport enum ManipulationType {\n /** Creating a new item */\n Create = \"Create\",\n /** Updating an existing item */\n Update = \"Update\",\n /** Creating a child item */\n CreateChild = \"Create Child\",\n /** Deleting an item */\n Delete = \"Delete\",\n /** Viewing item details */\n View = \"View\",\n /** Saving an item */\n Save = \"Save\"\n}\n\n/**\n * Key-value pair type for common data structures\n * @template K The type of the key\n * @template D The type of the data\n */\nexport interface KeyData<K, D> {\n key: K;\n data: D;\n}\n\n/**\n * Common API response wrapper type\n * @template T The type of the data payload\n */\nexport interface ApiResponse<T> {\n data: T;\n message?: string;\n status: number;\n success: boolean;\n}\n\n/**\n * Pagination metadata interface\n */\nexport interface PaginationMeta {\n currentPage: number;\n totalPages: number;\n totalItems: number;\n itemsPerPage: number;\n hasNextPage: boolean;\n hasPreviousPage: boolean;\n}\n\n/**\n * Paginated response type\n * @template T The type of the data items\n */\nexport interface PaginatedResponse<T> {\n data: T[];\n meta: PaginationMeta;\n}\n\n/**\n * Query parameters for pagination\n */\nexport interface PaginationParams {\n page?: number;\n limit?: number;\n offset?: number;\n}\n\n/**\n * Common sort parameters\n */\nexport interface SortParams {\n sortBy?: string;\n sortOrder?: \"asc\" | \"desc\";\n}\n\n/**\n * Combined query parameters for API requests\n */\nexport type QueryParams = Record<string, string | number | boolean> &\n Partial<PaginationParams> &\n Partial<SortParams>;\n\n// ===============================================================================\n// Table State Types\n// ===============================================================================\n\n/**\n * String filter types for PrimeNG table filtering\n */\nexport type StringFilterType =\n | \"startsWith\"\n | \"notStartsWith\"\n | \"endsWith\"\n | \"notEndsWith\"\n | \"contains\"\n | \"notContains\";\n\n/**\n * Numeric filter types for PrimeNG table filtering\n */\nexport type NumericFilterType =\n | \"equals\"\n | \"notEquals\"\n | \"greaterThan\"\n | \"lessThan\"\n | \"greaterThanOrEqual\"\n | \"lessThanOrEqual\";\n\n/**\n * Boolean filter types for PrimeNG table filtering\n */\nexport type BooleanFilterType = Extract<NumericFilterType, \"equals\" | \"notEquals\">;\n\n/**\n * Combined filter types\n */\nexport type FilterType = StringFilterType | NumericFilterType;\n\n/**\n * Filter type mappings for backend API\n */\nexport type FilterTypeMapped =\n | \"starts\"\n | \"!starts\"\n | \"ends\"\n | \"!ends\"\n | \"like\"\n | \"!like\"\n | \"=\"\n | \"!=\"\n | \">\"\n | \"<\"\n | \">=\"\n | \"<=\";\n\n/**\n * PrimeNG table header configuration interface\n */\nexport interface PrimeNgTableHeader {\n identifier: {\n label?: string;\n field: string;\n hasSort?: boolean;\n isBoolean?: boolean;\n isNested?: boolean;\n isDate?: boolean;\n isDateTime?: boolean;\n styleClass?: string;\n };\n filter?: {\n type: \"text\" | \"numeric\" | \"boolean\" | \"date\" | \"dropdown\" | \"multiselect\";\n placeholder?: string;\n matchModeOptions?: any[];\n defaultMatchMode: FilterType;\n ariaLabel?: string;\n colspan?: number;\n styleClass?: Record<string, string>;\n };\n}\n\n/**\n * Dynamic query DTO interface\n */\nexport interface DynamicQueryDto {\n size: number;\n page: number;\n filter: DynamicQueryFilterDto[];\n sort: DynamicQuerySortDto[];\n}\n\n/**\n * Filter DTO for dynamic queries\n */\nexport interface DynamicQueryFilterDto {\n field: string;\n value: string;\n type: FilterTypeMapped;\n}\n\n/**\n * Sort DTO for dynamic queries\n */\nexport interface DynamicQuerySortDto {\n field: string;\n dir: \"asc\" | \"desc\";\n}\n\n/**\n * Paged data response interface\n */\nexport interface DynamicQueryPagedDataResponse<T> {\n data: T[];\n last_page: number;\n last_row: number;\n}\n\n/**\n * Paged data response interface for simple pagination\n */\nexport interface PagedDataResponse<T> {\n payload: T[];\n totalCount: number;\n}\n\n/**\n * Internal table state interface for dynamic table\n */\nexport interface PrimeNgTableState<T> {\n data: Array<T>;\n isLoading: boolean;\n size: number;\n page: number;\n totalRecords: number;\n filter: DynamicQueryFilterDto[];\n sort: DynamicQuerySortDto[];\n}\n\n/**\n * Internal state interface for paged table\n */\nexport interface PrimeNgPagedTableState<T> {\n data: Array<T>;\n isLoading: boolean;\n totalRecords: number;\n limit: number;\n page: number;\n}\n\n/**\n * Query DTO interface for paged data requests\n */\nexport interface PagedDataQueryDto {\n limit: number;\n page: number;\n}\n\n/**\n * Query parameters type for additional HTTP request parameters\n */\nexport type PrimeNgTableStateHelperQueryParam = Record<string, string | number | boolean>;\n\n// ===============================================================================\n// Zod Schemas\n// ===============================================================================\n\n/**\n * Zod schema for dynamic query response validation\n */\nexport const dynamicQueryResponseZodSchema = z.object({\n data: z.any().array(),\n last_page: z.number(),\n last_row: z.number()\n});\n\n/**\n * Zod schema for paged data response validation\n */\nexport const PagedDataResponseZodSchema = z.object({\n payload: z.any().array(),\n totalCount: z.number()\n});\n\n// ===============================================================================\n// Utility Functions and Type Guards\n// ===============================================================================\n\n/**\n * Error response interface\n */\nexport interface ErrorResponse {\n error: string;\n message: string;\n statusCode: number;\n timestamp: string;\n}\n\n/**\n * Creates a key-value pair object\n * @param key The key value\n * @param data The data value\n * @returns KeyData object\n */\nexport function createKeyData<K, D>(key: K, data: D): KeyData<K, D> {\n return { key, data };\n}\n\n/**\n * Type guard to check if a response is an API response\n * @param response The response to check\n * @returns true if response is ApiResponse, false otherwise\n */\nexport function isApiResponse<T>(response: any): response is ApiResponse<T> {\n return (\n typeof response === \"object\" &&\n response !== null &&\n \"data\" in response &&\n \"status\" in response &&\n \"success\" in response\n );\n}\n\n/**\n * Type guard to check if a response is paginated\n * @param response The response to check\n * @returns true if response is PaginatedResponse, false otherwise\n */\nexport function isPaginatedResponse<T>(response: any): response is PaginatedResponse<T> {\n return (\n typeof response === \"object\" &&\n response !== null &&\n \"data\" in response &&\n Array.isArray(response.data) &&\n \"meta\" in response &&\n typeof response.meta === \"object\"\n );\n}\n\n/**\n * Type guard to check if a response is a simple paged response\n * @param response The response to check\n * @returns true if response is PagedDataResponse, false otherwise\n */\nexport function isSimplePagedResponse<T>(response: any): response is PagedDataResponse<T> {\n return (\n typeof response === \"object\" &&\n response !== null &&\n \"payload\" in response &&\n Array.isArray(response.payload) &&\n \"totalCount\" in response &&\n typeof response.totalCount === \"number\"\n );\n}\n\n/**\n * Type guard to check if a response is a dynamic query response\n * @param response The response to check\n * @returns true if response is PagedDataResponse, false otherwise\n */\nexport function isDynamicQueryResponse<T>(\n response: any\n): response is DynamicQueryPagedDataResponse<T> {\n return (\n typeof response === \"object\" &&\n response !== null &&\n \"data\" in response &&\n Array.isArray(response.data) &&\n \"last_page\" in response &&\n \"last_row\" in response\n );\n}\n\nexport type NestableColumn = {\n isNested?: boolean;\n};\n","import { KeyData } from \"./types\";\n\n/**\n * Utility function to remove null and undefined values from an object\n *\n * This is particularly useful when setting query parameters, as null/undefined\n * values should typically be filtered out before sending to APIs.\n *\n * @param o The object to clean\n * @returns A new object with null and undefined values removed\n *\n * @example\n * ```typescript\n * const queryParams = {\n * name: 'John',\n * age: null,\n * email: 'john@example.com',\n * phone: undefined\n * };\n *\n * const cleaned = cleanNullishFromObject(queryParams);\n * // Result: { name: 'John', email: 'john@example.com' }\n *\n * // Use case with table state\n * this.tableState.setQueryParams(cleanNullishFromObject(queryParams));\n * ```\n */\nexport function cleanNullishFromObject(o: object): Record<string, any> {\n return Object.fromEntries(Object.entries(o).filter(([, v]) => v != null));\n}\n\nexport function hasNullishInObject(obj: object): boolean {\n return Object.values(obj).some((val) => val === null || val === undefined);\n}\n\nexport function routeParamConcat(baseUrl: string, routeParam: number | string) {\n if (routeParam === undefined || routeParam === null) {\n throw new Error(\"routeParam cannot be null or undefined\");\n }\n\n if (baseUrl.endsWith(\"/\")) {\n return baseUrl.concat(routeParam.toString());\n }\n return baseUrl.concat(`/${routeParam.toString()}`);\n}\n\nexport function binarySearch<TItem>(arr: TItem[], val: TItem): number {\n if (arr.length === 0) {\n return -1;\n }\n let left = 0;\n let right = arr.length - 1;\n while (left <= right) {\n const mid = Math.floor((left + right) / 2);\n if (arr[mid] === val) {\n return mid;\n } else if (arr[mid] < val) {\n left = mid + 1;\n } else {\n right = mid - 1;\n }\n }\n return -1;\n}\n\nexport function emptyCallback() {}\n\nexport function nullableKeyData<TKey, TData>(\n key: TKey | null,\n data: TData | null\n): KeyData<TKey, TData> | null {\n if (key && data) {\n return { key, data };\n }\n return null;\n}\n\nexport class ReloadNotification {\n static create() {\n return new ReloadNotification();\n }\n}\n","import { HttpClient, HttpContext } from \"@angular/common/http\";\nimport { signal, Signal } from \"@angular/core\";\nimport { patchState, signalState } from \"@ngrx/signals\";\nimport { Table, TableLazyLoadEvent } from \"primeng/table\";\nimport { firstValueFrom } from \"rxjs\";\nimport { SkipLoadingSpinner } from \"./http-context-tokens\";\nimport {\n PagedDataQueryDto,\n PagedDataResponseZodSchema,\n PrimeNgPagedTableState,\n PrimeNgTableStateHelperQueryParam\n} from \"./types\";\nimport { routeParamConcat } from \"./utils\";\n\n/**\n * Initial state factory function for paged table\n */\nfunction initialPagedState<T>(): PrimeNgPagedTableState<T> {\n return {\n data: [],\n isLoading: false,\n totalRecords: 0,\n limit: 15,\n page: 1\n };\n}\n\n/**\n * Options for creating PrimengPagedDataTableStateHelper\n */\ntype PrimeNgPagedTableStateOpts = {\n url: string;\n httpClient: HttpClient;\n skipLoadingSpinner?: boolean;\n};\n\n/**\n * Simple paged data table state helper for basic pagination without filtering\n *\n * This helper provides basic table functionality including:\n * - Simple pagination (page and limit only)\n * - Basic state management with NgRx Signals\n * - API integration for paged data\n * - Route parameter support\n * - Query parameter management\n *\n * Use this when you need simple pagination without complex filtering and sorting.\n * For advanced features, use PrimeNgDynamicTableStateHelper instead.\n *\n * @example\n * ```typescript\n * const tableState = PrimengPagedDataTableStateHelper.create<Product>({\n * url: '/api/products',\n * httpClient: this.httpClient\n * });\n *\n * // In template\n * <p-table [value]=\"tableState.data()\"\n * [lazy]=\"true\"\n * [loading]=\"tableState.isLoading()\"\n * [totalRecords]=\"tableState.totalRecords()\"\n * (onLazyLoad)=\"tableState.onLazyLoad($event)\">\n * </p-table>\n * ```\n */\nexport class PrimeNgPagedDataTableStateHelper<T> {\n readonly #state = signalState<PrimeNgPagedTableState<T>>(initialPagedState<T>());\n private urlWithOutRouteParam: string;\n private skipLoadingSpinner: boolean;\n readonly #uniqueKey = signal(\"id\");\n readonly uniqueKey = this.#uniqueKey.asReadonly();\n #queryParams: PrimeNgTableStateHelperQueryParam = {};\n\n // Public readonly signals\n readonly totalRecords: Signal<number> = this.#state.totalRecords;\n readonly isLoading: Signal<boolean> = this.#state.isLoading;\n readonly data: Signal<Array<T>> = this.#state.data;\n readonly currentPage = this.#state.page;\n readonly currentPageSize = this.#state.limit;\n\n private constructor(\n private url: string,\n private readonly httpClient: HttpClient,\n skipLoadingSpinner: boolean = true\n ) {\n this.urlWithOutRouteParam = url;\n this.skipLoadingSpinner = skipLoadingSpinner;\n }\n\n /**\n * Creates a new instance of PrimengPagedDataTableStateHelper\n * @param option - Configuration options\n * @returns New instance of PrimengPagedDataTableStateHelper\n */\n public static create<T>(option: PrimeNgPagedTableStateOpts): PrimeNgPagedDataTableStateHelper<T> {\n return new PrimeNgPagedDataTableStateHelper<T>(\n option.url,\n option.httpClient,\n option.skipLoadingSpinner ?? true\n );\n }\n\n /**\n * Creates a new instance without initial URL (can be set later)\n * @param option - Configuration options without URL\n * @returns New instance of PrimengPagedDataTableStateHelper\n */\n public static createWithBlankUrl<T>(\n option: Omit<PrimeNgPagedTableStateOpts, \"url\">\n ): PrimeNgPagedDataTableStateHelper<T> {\n return new PrimeNgPagedDataTableStateHelper<T>(\n \"\",\n option.httpClient,\n option.skipLoadingSpinner ?? true\n );\n }\n\n /**\n * Sets whether to skip the loading spinner\n * @param skip - Whether to skip the loading spinner\n * @returns This instance for method chaining\n */\n public setSkipLoadingSpinner(skip: boolean): this {\n this.skipLoadingSpinner = skip;\n return this;\n }\n\n /**\n * Sets the unique key field for table rows\n * @param newUniqueKey - The field name to use as unique identifier\n * @returns This instance for method chaining\n */\n public setUniqueKey(newUniqueKey: string): this {\n this.#uniqueKey.set(newUniqueKey);\n return this;\n }\n\n /**\n * Updates the API URL\n * @param newUrl - The new API URL\n * @returns This instance for method chaining\n */\n public setUrl(newUrl: string): this {\n this.url = newUrl;\n this.urlWithOutRouteParam = newUrl;\n return this;\n }\n\n /**\n * Appends a route parameter to the URL\n * @param newRouteParam - The route parameter to append\n * @returns This instance for method chaining\n */\n public setRouteParam(newRouteParam: string): this {\n this.url = routeParamConcat(this.urlWithOutRouteParam, newRouteParam);\n return this;\n }\n\n /**\n * Patches existing query parameters\n * @param value - Query parameters to merge\n * @returns This instance for method chaining\n */\n public patchQueryParams(value: PrimeNgTableStateHelperQueryParam): this {\n this.#queryParams = { ...this.#queryParams, ...value };\n return this;\n }\n\n /**\n * Removes a specific query parameter\n * @param key - The key to remove\n * @returns This instance for method chaining\n */\n public removeQueryParam(key: string): this {\n delete this.#queryParams[key];\n return this;\n }\n\n /**\n * Removes all query parameters\n * @returns This instance for method chaining\n */\n public removeAllQueryParams(): this {\n this.#queryParams = {};\n return this;\n }\n\n /**\n * Sets all query parameters (replaces existing)\n * @param newQueryParams - New query parameters\n * @returns This instance for method chaining\n */\n public setQueryParams(newQueryParams: PrimeNgTableStateHelperQueryParam): this {\n this.#queryParams = newQueryParams;\n return this;\n }\n\n /**\n * Handles PrimeNG table lazy load events\n * @param event - The lazy load event from PrimeNG table\n */\n public async onLazyLoad(event: TableLazyLoadEvent): Promise<void> {\n if (this.isLoading()) {\n return;\n }\n const newPage = Math.floor((event.first || 0) / (event.rows || 15)) + 1;\n const newLimit = event.rows || 15;\n\n patchState(this.#state, {\n limit: newLimit,\n page: newPage\n });\n\n await this.fetchData(this.dtoBuilder());\n }\n\n /**\n * Clears table data and resets to first page\n * @param table - Optional PrimeNG Table reference to reset\n */\n public async clearTableData(table?: Table): Promise<void> {\n if (this.isLoading()) {\n return;\n }\n patchState(this.#state, {\n data: [],\n totalRecords: 0,\n page: 1\n });\n\n if (table) {\n table.reset();\n }\n\n await this.fetchData(this.dtoBuilder());\n }\n\n /**\n * Manually triggers data refresh with current state\n */\n public async refresh(): Promise<void> {\n if (this.isLoading()) {\n return;\n }\n await this.fetchData(this.dtoBuilder());\n }\n\n /**\n * Fetches data from the API\n */\n private async fetchData(dto: PagedDataQueryDto): Promise<void> {\n if (this.isLoading()) {\n return;\n }\n try {\n patchState(this.#state, { isLoading: true });\n\n const params = new URLSearchParams();\n Object.entries(this.#queryParams).forEach(([key, value]) => {\n params.append(key, String(value));\n });\n Object.entries(dto).forEach(([key, value]) => {\n params.append(key, String(value));\n });\n\n const urlWithParams = params.toString() ? `${this.url}?${params.toString()}` : this.url;\n\n const response = await firstValueFrom(\n this.httpClient.get(urlWithParams, {\n context: new HttpContext().set(SkipLoadingSpinner, this.skipLoadingSpinner)\n })\n );\n\n const validatedResponse = PagedDataResponseZodSchema.parse(response);\n\n patchState(this.#state, {\n data: validatedResponse.payload,\n totalRecords: validatedResponse.totalCount,\n isLoading: false\n });\n } catch (error) {\n patchState(this.#state, {\n data: [],\n totalRecords: 0,\n isLoading: false\n });\n throw error;\n }\n }\n\n /**\n * Builds the DTO for API requests\n */\n private dtoBuilder(): PagedDataQueryDto {\n return {\n limit: this.#state.limit(),\n page: this.#state.page()\n };\n }\n}\n","import { SelectItem } from \"primeng/api\";\nimport { StringFilterType, NumericFilterType, PrimeNgTableHeader, NestableColumn } from \"./types\";\n\n/**\n * Creates PrimeNG SelectItem array for numeric filter match modes\n * @param styleClass - CSS class for styling the options\n * @param disabled - Whether the options should be disabled\n * @returns Array of SelectItem for numeric filters\n */\nexport function createPrimengNumberMatchModes(\n styleClass: string = \"p-text-capitalize\",\n disabled: boolean = false\n): SelectItem<NumericFilterType>[] {\n return [\n {\n label: \"Equals\",\n value: \"equals\",\n title: \"Equals\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Not Equals\",\n value: \"notEquals\",\n title: \"Not Equals\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Greater Than\",\n value: \"greaterThan\",\n title: \"Greater Than\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Greater Than Or Equals\",\n value: \"greaterThanOrEqual\",\n title: \"Greater Than Or Equals\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Less Than\",\n value: \"lessThan\",\n title: \"Less Than\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Less Than Or Equals\",\n value: \"lessThanOrEqual\",\n title: \"Less Than Or Equals\",\n styleClass: styleClass,\n disabled: disabled\n }\n ];\n}\n\n/**\n * Creates PrimeNG SelectItem array for string filter match modes\n * @param styleClass - CSS class for styling the options\n * @param disabled - Whether the options should be disabled\n * @returns Array of SelectItem for string filters\n */\nexport function createPrimengStringMatchModes(\n styleClass: string = \"p-text-capitalize\",\n disabled: boolean = false\n): SelectItem<StringFilterType>[] {\n return [\n {\n label: \"Contains\",\n value: \"contains\",\n title: \"Contains\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Not Contains\",\n value: \"notContains\",\n title: \"Not Contains\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Starts With\",\n value: \"startsWith\",\n title: \"Starts With\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Not Starts With\",\n value: \"notStartsWith\",\n title: \"Not Starts With\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Ends With\",\n value: \"endsWith\",\n title: \"Ends With\",\n styleClass: styleClass,\n disabled: disabled\n },\n {\n label: \"Not Ends With\",\n value: \"notEndsWith\",\n title: \"Not Ends With\",\n styleClass: styleClass,\n disabled: disabled\n }\n ];\n}\n\n/**\n * Creates a complete table header configuration for text columns\n * @param field - The field name for the column\n * @param label - Display label for the column header\n * @param options - Additional configuration options\n * @returns Complete PrimeNgTableHeader configuration\n */\nexport function createTextColumn(\n field: string,\n label: string,\n options: {\n hasSort?: boolean;\n hasFilter?: boolean;\n placeholder?: string;\n matchModeOptions?: SelectItem<StringFilterType>[];\n defaultMatchMode?: StringFilterType;\n styleClass?: string;\n filterStyleClass?: Record<string, string>;\n } & NestableColumn = {}\n): PrimeNgTableHeader {\n const header: PrimeNgTableHeader = {\n identifier: {\n label,\n field,\n isNested: options.isNested,\n hasSort: options.hasSort ?? false,\n styleClass: options.styleClass\n }\n };\n\n if (options.hasFilter ?? false) {\n header.filter = {\n type: \"text\",\n placeholder: options.placeholder ?? `Search by ${label.toLowerCase()}`,\n matchModeOptions: options.matchModeOptions ?? createPrimengStringMatchModes(),\n defaultMatchMode: options.defaultMatchMode ?? \"contains\",\n ariaLabel: `Filter by ${label}`,\n styleClass: options.filterStyleClass\n };\n }\n\n return header;\n}\n\n/**\n * Creates a complete table header configuration for numeric columns\n * @param field - The field name for the column\n * @param label - Display label for the column header\n * @param options - Additional configuration options\n * @returns Complete PrimeNgTableHeader configuration\n */\nexport function createNumericColumn(\n field: string,\n label: string,\n options: {\n hasSort?: boolean;\n hasFilter?: boolean;\n placeholder?: string;\n matchModeOptions?: SelectItem<NumericFilterType>[];\n defaultMatchMode?: NumericFilterType;\n styleClass?: string;\n filterStyleClass?: Record<string, string>;\n } & NestableColumn = {}\n): PrimeNgTableHeader {\n const header: PrimeNgTableHeader = {\n identifier: {\n label,\n field,\n isNested: options.isNested,\n hasSort: options.hasSort ?? false,\n styleClass: options.styleClass\n }\n };\n\n if (options.hasFilter ?? false) {\n header.filter = {\n type: \"numeric\",\n placeholder: options.placeholder ?? `Filter by ${label.toLowerCase()}`,\n matchModeOptions: options.matchModeOptions ?? createPrimengNumberMatchModes(),\n defaultMatchMode: options.defaultMatchMode ?? \"equals\",\n ariaLabel: `Filter by ${label}`,\n styleClass: options.filterStyleClass\n };\n }\n\n return header;\n}\n\n/**\n * Creates a complete table header configuration for boolean columns\n * @param field - The field name for the column\n * @param label - Display label for the column header\n * @param options - Additional configuration options\n * @returns Complete PrimeNgTableHeader configuration\n */\nexport function createBooleanColumn(\n field: string,\n label: string,\n options: {\n hasSort?: boolean;\n hasFilter?: boolean;\n styleClass?: string;\n filterStyleClass?: Record<string, string>;\n } & NestableColumn = {}\n): PrimeNgTableHeader {\n const header: PrimeNgTableHeader = {\n identifier: {\n label,\n field,\n isNested: options.isNested,\n hasSort: options.hasSort ?? false,\n isBoolean: true,\n styleClass: options.styleClass\n }\n };\n\n if (options.hasFilter ?? false) {\n header.filter = {\n type: \"boolean\",\n defaultMatchMode: \"equals\",\n ariaLabel: `Filter by ${label}`,\n styleClass: options.filterStyleClass\n };\n }\n\n return header;\n}\n\n/**\n * Creates a complete table header configuration for date columns\n * @param field - The field name for the column\n * @param label - Display label for the column header\n * @param options - Additional configuration options\n * @returns Complete PrimeNgTableHeader configuration\n */\nexport function createDateColumn(\n field: string,\n label: string,\n options: {\n hasSort?: boolean;\n hasFilter?: boolean;\n placeholder?: string;\n styleClass?: string;\n filterStyleClass?: Record<string, string>;\n } & NestableColumn = {}\n): PrimeNgTableHeader {\n const header: PrimeNgTableHeader = {\n identifier: {\n label,\n field,\n isNested: options.isNested,\n hasSort: options.hasSort ?? false,\n styleClass: options.styleClass\n }\n };\n\n if (options.hasFilter ?? false) {\n header.filter = {\n type: \"date\",\n placeholder: options.placeholder ?? `Select ${label.toLowerCase()}`,\n defaultMatchMode: \"equals\",\n ariaLabel: `Filter by ${label}`,\n styleClass: options.filterStyleClass\n };\n }\n\n return header;\n}\n\n/**\n * Creates a complete table header configuration for dropdown columns\n * @param field - The field name for the column\n * @param label - Display label for the column header\n * @param dropdownOptions - Options for the dropdown filter\n * @param options - Additional configuration options\n * @returns Complete PrimeNgTableHeader configuration\n */\nexport function createDropdownColumn(\n field: string,\n label: string,\n dropdownOptions: SelectItem[],\n options: {\n hasSort?: boolean;\n hasFilter?: boolean;\n placeholder?: string;\n styleClass?: string;\n filterStyleClass?: Record<string, string>;\n } = {}\n): PrimeNgTableHeader {\n const header: PrimeNgTableHeader = {\n identifier: {\n label,\n field,\n hasSort: options.hasSort ?? false,\n styleClass: options.styleClass\n }\n };\n\n if (options.hasFilter ?? false) {\n header.filter = {\n type: \"dropdown\",\n placeholder: options.placeholder ?? `Select ${label.toLowerCase()}`,\n matchModeOptions: dropdownOptions,\n defaultMatchMode: \"equals\",\n ariaLabel: `Filter by ${label}`,\n styleClass: options.filterStyleClass\n };\n }\n\n return header;\n}\n\n/**\n * Creates a complete table header configuration for multiselect columns\n * @param field - The field name for the column\n * @param label - Display label for the column header\n * @param multiselectOptions - Options for the multiselect filter\n * @param options - Additional configuration options\n * @returns Complete PrimeNgTableHeader configuration\n */\nexport function createMultiselectColumn(\n field: string,\n label: string,\n multiselectOptions: SelectItem[],\n options: {\n hasSort?: boolean;\n hasFilter?: boolean;\n placeholder?: string;\n styleClass?: string;\n filterStyleClass?: Record<string, string>;\n } = {}\n): PrimeNgTableHeader {\n const header: PrimeNgTableHeader = {\n identifier: {\n label,\n field,\n hasSort: options.hasSort ?? false,\n styleClass: options.styleClass\n }\n };\n\n if (options.hasFilter ?? false) {\n header.filter = {\n type: \"multiselect\",\n placeholder: options.placeholder ?? `Select ${label.toLowerCase()}`,\n matchModeOptions: multiselectOptions,\n defaultMatchMode: \"equals\",\n ariaLabel: `Filter by ${label}`,\n styleClass: options.filterStyleClass\n };\n }\n\n return header;\n}\n\n/**\n * Creates a simple table header configuration without filtering\n * @param field - The field name for the column\n * @param label - Display label for the column header\n * @param options - Additional configuration options\n * @returns Simple PrimeNgTableHeader configuration\n */\nexport function createSimpleColumn(\n field: string,\n label: string,\n options: {\n hasSort?: boolean;\n styleClass?: string;\n } & NestableColumn = {}\n): PrimeNgTableHeader {\n return {\n identifier: {\n label,\n field,\n isNested: options.isNested,\n hasSort: options.hasSort ?? false,\n styleClass: options.styleClass\n }\n };\n}\n\n/**\n * Utility function to merge multiple table header configurations\n * @param headers - Array of table header configurations\n * @returns Array of merged headers\n */\nexport function mergeTableHeaders(...headers: PrimeNgTableHeader[]): PrimeNgTableHeader[] {\n return headers;\n}\n\n/**\n * Utility function to create boolean SelectItem options\n * @param trueLabel - Label for true value\n * @param falseLabel - Label for false value\n * @returns Array of SelectItem for boolean values\n */\nexport function createBooleanSelectItems(\n trueLabel: string = \"Yes\",\n falseLabel: string = \"No\"\n): SelectItem[] {\n return [\n { label: trueLabel, value: true },\n { label: falseLabel, value: false }\n ];\n}\n\n/**\n * Utility function to create status SelectItem options\n * @param statusOptions - Object mapping status values to labels\n * @returns Array of SelectItem for status values\n */\nexport function createStatusSelectItems(\n statusOptions: Record<string | number, string>\n): SelectItem[] {\n return Object.entries(statusOptions).map(([value, label]) => ({\n label,\n value: isNaN(Number(value)) ? value : Number(value)\n }));\n}\n","import { signal } from \"@angular/core\";\nimport { HttpClient, HttpContext } from \"@angular/common/http\";\nimport { firstValueFrom } from \"rxjs\";\nimport { SkipLoadingSpinner } from \"./http-context-tokens\";\n\n/**\n * A generic class for memoizing data storage with HTTP caching capabilities\n * Provides methods to load and cache single data objects or arrays with automatic loading states\n * \n * @template T The type of data to be stored and managed\n * \n * @example\n * ```typescript\n * interface User { id: number; name: string; }\n * \n * const userStorage = new MemoizedDataStorage<User>(httpClient);\n * await userStorage.loadSingleData('/api/user/1');\n * console.log(userStorage.singleData()); // User data or null\n * \n * const usersStorage = new MemoizedDataStorage<User>(httpClient);\n * await usersStorage.loadMultipleData('/api/users');\n * console.log(usersStorage.multipleData()); // Array of User data\n * ```\n */\nexport class MemoizedDataStorage<T> {\n private skipLoadingSpinner: boolean = true;\n\n /**\n * Creates a new instance of MemoizedDataStorage\n * @param httpClient Angular HttpClient instance for making HTTP requests\n * @param skipLoadingSpinner Whether to skip the loading spinner for HTTP requests\n */\n constructor(readonly httpClient: HttpClient, skipLoadingSpinner: boolean = true) {\n this.skipLoadingSpinner = skipLoadingSpinner;\n }\n\n /**\n * Sets whether to skip the loading spinner for HTTP requests\n * @param skip Whether to skip the loading spinner\n * @returns This instance for method chaining\n */\n setSkipLoadingSpinner(skip: boolean): this {\n this.skipLoadingSpinner = skip;\n return this;\n }\n readonly #singleData = signal<T | null>(null);\n readonly #multipleData = signal<Array<T>>([]);\n readonly #isLoading = signal<boolean>(false);\n\n // Public readonly signals for external consumption\n /**\n * Read-only signal containing single data object or null\n */\n readonly singleData = this.#singleData.asReadonly();\n\n /**\n * Read-only signal containing array of data objects\n */\n readonly multipleData = this.#multipleData.asReadonly();\n\n /**\n * Read-only signal indicating whether a request is currently loading\n */\n readonly isLoading = this.#isLoading.asReadonly();\n\n // Private flag to control memoization behavior\n #isMemoizationDisabledOnNextRead = false;\n\n /**\n * Disables memoization for the next read operation and clears cached data\n * This forces the next loadSingleData or loadMultipleData call to fetch fresh data\n * \n * @example\n * ```typescript\n * const storage = new MemoizedDataStorage<User>(httpClient);\n * await storage.loadSingleData('/api/user/1'); // Fetches data\n * await storage.loadSingleData('/api/user/1'); // Returns cached data\n * \n * storage.disableMemoizationOnNextRead();\n * await storage.loadSingleData('/api/user/1'); // Fetches fresh data\n * ```\n */\n disableMemoizationOnNextRead(): void {\n this.#isMemoizationDisabledOnNextRead = true;\n this.#singleData.set(null);\n this.#multipleData.set([]);\n }\n\n /**\n * Loads a single data object from the specified URL with optional query parameters\n * Uses memoization to avoid redundant requests unless explicitly disabled\n * \n * @param url The URL to fetch data from\n * @param queryParams Optional query parameters to include in the request\n * @returns Promise that resolves when the data is loaded\n * @throws Error if the HTTP request fails\n * \n * @example\n * ```typescript\n * const storage = new Memoize