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
2,738 lines • 80.7 kB
JavaScript
// src/dynamic-table-state-helper.ts
import { HttpContext } from "@angular/common/http";
import { signal } from "@angular/core";
import { signalState, patchState } from "@ngrx/signals";
import { firstValueFrom } from "rxjs";
// src/http-context-tokens.ts
import { HttpContextToken } from "@angular/common/http";
var SkipLoadingSpinner = new HttpContextToken(() => false);
// src/types.ts
import { z } from "zod";
var ManipulationType = /* @__PURE__ */ ((ManipulationType2) => {
ManipulationType2["Create"] = "create";
ManipulationType2["Update"] = "update";
ManipulationType2["CreateChild"] = "create-child";
ManipulationType2["Delete"] = "delete";
ManipulationType2["View"] = "view";
ManipulationType2["Save"] = "save";
return ManipulationType2;
})(ManipulationType || {});
var dynamicQueryResponseZodSchema = z.object({
data: z.any().array(),
last_page: z.number(),
last_row: z.number()
});
var PagedDataResponseZodSchema = z.object({
payload: z.any().array(),
totalCount: z.number()
});
var NumberStringKeyDataSchema = z.object({
key: z.union([z.number(), z.string()]),
data: z.string()
});
var NullableApiResponseSchema = (payloadSchema) => z.object({
payload: payloadSchema.nullable()
});
function createKeyData(key, data) {
return { key, data };
}
function isApiResponse(response) {
return typeof response === "object" && response !== null && "data" in response && "status" in response && "success" in response;
}
function isPaginatedResponse(response) {
return typeof response === "object" && response !== null && "data" in response && Array.isArray(response.data) && "meta" in response && typeof response.meta === "object";
}
function isSimplePagedResponse(response) {
return typeof response === "object" && response !== null && "payload" in response && Array.isArray(response.payload) && "totalCount" in response && typeof response.totalCount === "number";
}
function isDynamicQueryResponse(response) {
return typeof response === "object" && response !== null && "data" in response && Array.isArray(response.data) && "last_page" in response && "last_row" in response;
}
// src/utils.ts
function cleanNullishFromObject(obj) {
if (obj === void 0) {
return {};
}
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null));
}
function hasNullishInObject(obj) {
return Object.values(obj).some((val) => val === null || val === void 0);
}
function routeParamConcat(baseUrl, routeParam) {
if (routeParam === void 0 || routeParam === null) {
throw new Error("routeParam cannot be null or undefined");
}
if (baseUrl.endsWith("/")) {
return baseUrl.concat(routeParam.toString());
}
return baseUrl.concat(`/${routeParam.toString()}`);
}
function binarySearch(arr, val) {
if (arr.length === 0) {
return -1;
}
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === val) {
return mid;
} else if (arr[mid] < val) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
function emptyCallback() {
}
function nullableKeyData(key, data) {
if (key && data) {
return { key, data };
}
return null;
}
var ReloadNotification = class _ReloadNotification {
static create() {
return new _ReloadNotification();
}
};
function createHierarchicalTree(data, idKey, parentIdKey, expanded = false) {
if (!Array.isArray(data)) {
throw new Error("data must be an array");
}
const categoryMap = new Map(
data.map((item) => {
if (!Object.hasOwn(item, idKey) || !Object.hasOwn(item, parentIdKey)) {
throw new Error("idKey or parentIdKey is missing", { cause: item });
}
return [
item[idKey],
{
data: item,
children: [],
expanded
}
];
})
);
const rootNodes = [];
data.forEach((item) => {
const parentId = item[parentIdKey];
if (parentId != null && categoryMap.has(parentId)) {
const parent = categoryMap.get(parentId);
parent.children.push(categoryMap.get(item[idKey]));
} else {
rootNodes.push(categoryMap.get(item[idKey]));
}
});
return rootNodes;
}
// src/dynamic-table-state-helper.ts
function initialDynamicState() {
return {
data: [],
isLoading: false,
totalRecords: 0,
size: 15,
page: 1,
filter: [],
sort: []
};
}
function calculatePrimengTablePagination(event, defaultValue = { page: 1, limit: 15 }) {
if (!event) {
return defaultValue;
}
const page = event.first && event.rows ? Math.floor(event.first / event.rows) : 0;
const limit = event.rows ?? 15;
return { page: page + 1, limit };
}
var PrimeNgDynamicTableStateHelper = class _PrimeNgDynamicTableStateHelper {
constructor(url, httpClient, skipLoadingSpinner = true) {
this.url = url;
this.httpClient = httpClient;
this.urlWithOutRouteParam = url;
this.skipLoadingSpinner = skipLoadingSpinner;
}
state = signalState(
initialDynamicState()
);
urlWithOutRouteParam;
skipLoadingSpinner;
#uniqueKey = signal("id");
uniqueKey = this.#uniqueKey.asReadonly();
#queryParams = {};
// Public readonly signals
totalRecords = this.state.totalRecords;
isLoading = this.state.isLoading;
data = this.state.data;
/**
* Creates a new instance of PrimeNgDynamicTableStateHelper
* @param options - Configuration options
* @returns New instance of PrimeNgDynamicTableStateHelper
*/
static create(options) {
return new _PrimeNgDynamicTableStateHelper(
options.url,
options.httpClient,
options.skipLoadingSpinner ?? true
);
}
/**
* Sets whether to skip the loading spinner
* @param skip - Whether to skip the loading spinner
* @returns This instance for method chaining
*/
setSkipLoadingSpinner(skip) {
this.skipLoadingSpinner = skip;
return this;
}
/**
* Sets the unique key field for table rows
* @param newUniqueKey - The field name to use as unique identifier
* @returns This instance for method chaining
*/
setUniqueKey(newUniqueKey) {
this.#uniqueKey.set(newUniqueKey);
return this;
}
/**
* Updates the API URL
* @param newUrl - The new API URL
* @returns This instance for method chaining
*/
setUrl(newUrl) {
this.url = newUrl;
this.urlWithOutRouteParam = newUrl;
return this;
}
/**
* Appends a route parameter to the URL
* @param newRouteParam - The route parameter to append
* @returns This instance for method chaining
*/
setRouteParam(newRouteParam) {
this.url = routeParamConcat(this.urlWithOutRouteParam, newRouteParam);
return this;
}
/**
* Patches existing query parameters
* @param value - Query parameters to merge
* @returns This instance for method chaining
*/
patchQueryParams(value) {
this.#queryParams = { ...this.#queryParams, ...value };
return this;
}
/**
* Removes a specific query parameter
* @param key - The key to remove
* @returns This instance for method chaining
*/
removeQueryParam(key) {
delete this.#queryParams[key];
return this;
}
/**
* Sets all query parameters (replaces existing)
* @param newQueryParams - New query parameters
* @returns This instance for method chaining
*/
setQueryParams(newQueryParams) {
this.#queryParams = newQueryParams;
return this;
}
/**
* Handles PrimeNG table lazy load events
* @param event - The lazy load event from PrimeNG table
*/
async onLazyLoad(event) {
if (this.isLoading()) {
return;
}
patchState(this.state, {
size: event.rows || 15,
page: Math.floor((event.first || 0) / (event.rows || 15)) + 1,
filter: this.filterMapper(event.filters || {}),
sort: Object.keys(event.multiSortMeta || {}).length > 0 ? (event.multiSortMeta || []).map((sort) => ({
field: sort.field,
dir: sort.order === 1 ? "asc" : "desc"
})) : event.sortField ? [
{
field: event.sortField,
dir: (event.sortOrder || 1) === 1 ? "asc" : "desc"
}
] : []
});
await this.fetchData(this.dtoBuilder());
}
/**
* Clears table data and resets to first page
* @param table - Optional PrimeNG Table reference to reset
*/
async clearTableData(table) {
if (this.isLoading()) {
return;
}
patchState(this.state, {
data: [],
totalRecords: 0,
page: 1,
filter: [],
sort: []
});
if (table) {
table.reset();
}
await this.fetchData(this.dtoBuilder());
}
/**
* Manually triggers data refresh with current state
*/
async refresh() {
if (this.isLoading()) {
return;
}
await this.fetchData(this.dtoBuilder());
}
/**
* Fetches data from the API
*/
async fetchData(dto) {
if (this.isLoading()) {
return;
}
try {
patchState(this.state, { isLoading: true });
const params = new URLSearchParams();
Object.entries(this.#queryParams).forEach(([key, value]) => {
params.append(key, String(value));
});
const urlWithParams = params.toString() ? `${this.url}?${params.toString()}` : this.url;
const response = await firstValueFrom(
this.httpClient.post(urlWithParams, dto, {
context: new HttpContext().set(
SkipLoadingSpinner,
this.skipLoadingSpinner
)
})
);
const validatedResponse = dynamicQueryResponseZodSchema.parse(response);
patchState(this.state, {
data: validatedResponse.data,
totalRecords: validatedResponse.last_row,
isLoading: false
});
} catch (error) {
patchState(this.state, {
data: [],
totalRecords: 0,
isLoading: false
});
throw error;
}
}
/**
* Builds the DTO for API requests
*/
dtoBuilder() {
return {
size: this.state.size(),
page: this.state.page(),
filter: this.state.filter(),
sort: this.state.sort()
};
}
/**
* Maps PrimeNG filters to API filter format
*/
filterMapper(dto) {
const filters = [];
Object.entries(dto).forEach(([field, filterData]) => {
if (!filterData) return;
const processFilter = (filter3) => {
if (filter3.value === null || filter3.value === void 0 || filter3.value === "")
return;
const mappedType = this.evaluateInput(filter3.matchMode || "contains");
if (mappedType) {
filters.push({
field,
value: String(filter3.value),
type: mappedType
});
}
};
if (Array.isArray(filterData)) {
filterData.forEach(processFilter);
} else {
processFilter(filterData);
}
});
return filters;
}
/**
* Maps PrimeNG filter match modes to API filter types
*/
evaluateInput(input) {
const filterMap = {
startsWith: "starts",
notStartsWith: "!starts",
endsWith: "ends",
notEndsWith: "!ends",
contains: "like",
notContains: "!like",
equals: "=",
notEquals: "!=",
greaterThan: ">",
lessThan: "<",
greaterThanOrEqual: ">=",
lessThanOrEqual: "<="
};
return filterMap[input] || null;
}
};
// src/paged-table-state-helper.ts
import { HttpContext as HttpContext2 } from "@angular/common/http";
import { signal as signal2 } from "@angular/core";
import { patchState as patchState2, signalState as signalState2 } from "@ngrx/signals";
import { firstValueFrom as firstValueFrom2 } from "rxjs";
function initialPagedState() {
return {
data: [],
isLoading: false,
totalRecords: 0,
limit: 15,
page: 1
};
}
var PrimeNgPagedDataTableStateHelper = class _PrimeNgPagedDataTableStateHelper {
constructor(url, httpClient, skipLoadingSpinner = true) {
this.url = url;
this.httpClient = httpClient;
this.urlWithOutRouteParam = url;
this.skipLoadingSpinner = skipLoadingSpinner;
}
#state = signalState2(initialPagedState());
urlWithOutRouteParam;
skipLoadingSpinner;
#uniqueKey = signal2("id");
uniqueKey = this.#uniqueKey.asReadonly();
#queryParams = {};
// Public readonly signals
totalRecords = this.#state.totalRecords;
isLoading = this.#state.isLoading;
data = this.#state.data;
currentPage = this.#state.page;
currentPageSize = this.#state.limit;
/**
* Creates a new instance of PrimengPagedDataTableStateHelper
* @param option - Configuration options
* @returns New instance of PrimengPagedDataTableStateHelper
*/
static create(option) {
return new _PrimeNgPagedDataTableStateHelper(
option.url,
option.httpClient,
option.skipLoadingSpinner ?? true
);
}
/**
* Creates a new instance without initial URL (can be set later)
* @param option - Configuration options without URL
* @returns New instance of PrimengPagedDataTableStateHelper
*/
static createWithBlankUrl(option) {
return new _PrimeNgPagedDataTableStateHelper(
"",
option.httpClient,
option.skipLoadingSpinner ?? true
);
}
/**
* Sets whether to skip the loading spinner
* @param skip - Whether to skip the loading spinner
* @returns This instance for method chaining
*/
setSkipLoadingSpinner(skip) {
this.skipLoadingSpinner = skip;
return this;
}
/**
* Sets the unique key field for table rows
* @param newUniqueKey - The field name to use as unique identifier
* @returns This instance for method chaining
*/
setUniqueKey(newUniqueKey) {
this.#uniqueKey.set(newUniqueKey);
return this;
}
/**
* Updates the API URL
* @param newUrl - The new API URL
* @returns This instance for method chaining
*/
setUrl(newUrl) {
this.url = newUrl;
this.urlWithOutRouteParam = newUrl;
return this;
}
/**
* Appends a route parameter to the URL
* @param newRouteParam - The route parameter to append
* @returns This instance for method chaining
*/
setRouteParam(newRouteParam) {
this.url = routeParamConcat(this.urlWithOutRouteParam, newRouteParam);
return this;
}
/**
* Patches existing query parameters
* @param value - Query parameters to merge
* @returns This instance for method chaining
*/
patchQueryParams(value) {
this.#queryParams = { ...this.#queryParams, ...value };
return this;
}
/**
* Removes a specific query parameter
* @param key - The key to remove
* @returns This instance for method chaining
*/
removeQueryParam(key) {
delete this.#queryParams[key];
return this;
}
/**
* Removes all query parameters
* @returns This instance for method chaining
*/
removeAllQueryParams() {
this.#queryParams = {};
return this;
}
/**
* Sets all query parameters (replaces existing)
* @param newQueryParams - New query parameters
* @returns This instance for method chaining
*/
setQueryParams(newQueryParams) {
this.#queryParams = newQueryParams;
return this;
}
/**
* Handles PrimeNG table lazy load events
* @param event - The lazy load event from PrimeNG table
*/
async onLazyLoad(event) {
if (this.isLoading()) {
return;
}
const newPage = Math.floor((event.first || 0) / (event.rows || 15)) + 1;
const newLimit = event.rows || 15;
patchState2(this.#state, {
limit: newLimit,
page: newPage
});
await this.fetchData(this.dtoBuilder());
}
/**
* Clears table data and resets to first page
* @param table - Optional PrimeNG Table reference to reset
*/
async clearTableData(table) {
if (this.isLoading()) {
return;
}
patchState2(this.#state, {
data: [],
totalRecords: 0,
page: 1
});
if (table) {
table.reset();
}
await this.fetchData(this.dtoBuilder());
}
/**
* Manually triggers data refresh with current state
*/
async refresh() {
if (this.isLoading()) {
return;
}
await this.fetchData(this.dtoBuilder());
}
/**
* Fetches data from the API
*/
async fetchData(dto) {
if (this.isLoading()) {
return;
}
try {
patchState2(this.#state, { isLoading: true });
const params = new URLSearchParams();
Object.entries(this.#queryParams).forEach(([key, value]) => {
params.append(key, String(value));
});
Object.entries(dto).forEach(([key, value]) => {
params.append(key, String(value));
});
const urlWithParams = params.toString() ? `${this.url}?${params.toString()}` : this.url;
const response = await firstValueFrom2(
this.httpClient.get(urlWithParams, {
context: new HttpContext2().set(SkipLoadingSpinner, this.skipLoadingSpinner)
})
);
const validatedResponse = PagedDataResponseZodSchema.parse(response);
patchState2(this.#state, {
data: validatedResponse.payload,
totalRecords: validatedResponse.totalCount,
isLoading: false
});
} catch (error) {
patchState2(this.#state, {
data: [],
totalRecords: 0,
isLoading: false
});
throw error;
}
}
/**
* Builds the DTO for API requests
*/
dtoBuilder() {
return {
limit: this.#state.limit(),
page: this.#state.page()
};
}
};
// src/table-utils.ts
function createPrimengNumberMatchModes(styleClass = "p-text-capitalize", disabled = false) {
return [
{
label: "Equals",
value: "equals",
title: "Equals",
styleClass,
disabled
},
{
label: "Not Equals",
value: "notEquals",
title: "Not Equals",
styleClass,
disabled
},
{
label: "Greater Than",
value: "greaterThan",
title: "Greater Than",
styleClass,
disabled
},
{
label: "Greater Than Or Equals",
value: "greaterThanOrEqual",
title: "Greater Than Or Equals",
styleClass,
disabled
},
{
label: "Less Than",
value: "lessThan",
title: "Less Than",
styleClass,
disabled
},
{
label: "Less Than Or Equals",
value: "lessThanOrEqual",
title: "Less Than Or Equals",
styleClass,
disabled
}
];
}
function createPrimengStringMatchModes(styleClass = "p-text-capitalize", disabled = false) {
return [
{
label: "Contains",
value: "contains",
title: "Contains",
styleClass,
disabled
},
{
label: "Not Contains",
value: "notContains",
title: "Not Contains",
styleClass,
disabled
},
{
label: "Starts With",
value: "startsWith",
title: "Starts With",
styleClass,
disabled
},
{
label: "Not Starts With",
value: "notStartsWith",
title: "Not Starts With",
styleClass,
disabled
},
{
label: "Ends With",
value: "endsWith",
title: "Ends With",
styleClass,
disabled
},
{
label: "Not Ends With",
value: "notEndsWith",
title: "Not Ends With",
styleClass,
disabled
}
];
}
function createTextColumn(field, label, options = {}) {
const header = {
identifier: {
label,
field,
isNested: options.isNested,
hasSort: options.hasSort ?? false,
styleClass: options.styleClass
}
};
if (options.hasFilter ?? false) {
header.filter = {
type: "text",
placeholder: options.placeholder ?? `Search by ${label.toLowerCase()}`,
matchModeOptions: options.matchModeOptions ?? createPrimengStringMatchModes(),
defaultMatchMode: options.defaultMatchMode ?? "contains",
ariaLabel: `Filter by ${label}`,
styleClass: options.filterStyleClass
};
}
return header;
}
function createNumericColumn(field, label, options = {}) {
const header = {
identifier: {
label,
field,
isNested: options.isNested,
hasSort: options.hasSort ?? false,
styleClass: options.styleClass
}
};
if (options.hasFilter ?? false) {
header.filter = {
type: "numeric",
placeholder: options.placeholder ?? `Filter by ${label.toLowerCase()}`,
matchModeOptions: options.matchModeOptions ?? createPrimengNumberMatchModes(),
defaultMatchMode: options.defaultMatchMode ?? "equals",
ariaLabel: `Filter by ${label}`,
styleClass: options.filterStyleClass
};
}
return header;
}
function createBooleanColumn(field, label, options = {}) {
const header = {
identifier: {
label,
field,
isNested: options.isNested,
hasSort: options.hasSort ?? false,
isBoolean: true,
styleClass: options.styleClass
}
};
if (options.hasFilter ?? false) {
header.filter = {
type: "boolean",
defaultMatchMode: "equals",
ariaLabel: `Filter by ${label}`,
styleClass: options.filterStyleClass
};
}
return header;
}
function createDateColumn(field, label, options = {}) {
const header = {
identifier: {
label,
field,
isNested: options.isNested,
hasSort: options.hasSort ?? false,
isDate: true,
styleClass: options.styleClass
}
};
if (options.hasFilter ?? false) {
header.filter = {
type: "date",
placeholder: options.placeholder ?? `Select ${label.toLowerCase()}`,
defaultMatchMode: "equals",
ariaLabel: `Filter by ${label}`,
styleClass: options.filterStyleClass
};
}
return header;
}
function createDateTimeColumn(field, label, options = {}) {
const header = {
identifier: {
label,
field,
isNested: options.isNested,
hasSort: options.hasSort ?? false,
isDateTime: true,
styleClass: options.styleClass
}
};
if (options.hasFilter ?? false) {
header.filter = {
type: "date",
placeholder: options.placeholder ?? `Select ${label.toLowerCase()}`,
defaultMatchMode: "equals",
ariaLabel: `Filter by ${label}`,
styleClass: options.filterStyleClass
};
}
return header;
}
function createTimeColumn(field, label, options = {}) {
const header = {
identifier: {
label,
field,
isNested: options.isNested,
hasSort: options.hasSort ?? false,
isTimeOnly: true,
styleClass: options.styleClass
}
};
if (options.hasFilter ?? false) {
header.filter = {
type: "text",
placeholder: options.placeholder ?? `Filter by ${label.toLowerCase()}`,
defaultMatchMode: "contains",
ariaLabel: `Filter by ${label}`,
styleClass: options.filterStyleClass
};
}
return header;
}
function createDropdownColumn(field, label, dropdownOptions, options = {}) {
const header = {
identifier: {
label,
field,
hasSort: options.hasSort ?? false,
styleClass: options.styleClass
}
};
if (options.hasFilter ?? false) {
header.filter = {
type: "dropdown",
placeholder: options.placeholder ?? `Select ${label.toLowerCase()}`,
matchModeOptions: dropdownOptions,
defaultMatchMode: "equals",
ariaLabel: `Filter by ${label}`,
styleClass: options.filterStyleClass
};
}
return header;
}
function createMultiselectColumn(field, label, multiselectOptions, options = {}) {
const header = {
identifier: {
label,
field,
hasSort: options.hasSort ?? false,
styleClass: options.styleClass
}
};
if (options.hasFilter ?? false) {
header.filter = {
type: "multiselect",
placeholder: options.placeholder ?? `Select ${label.toLowerCase()}`,
matchModeOptions: multiselectOptions,
defaultMatchMode: "equals",
ariaLabel: `Filter by ${label}`,
styleClass: options.filterStyleClass
};
}
return header;
}
function createSimpleColumn(field, label, options = {}) {
return {
identifier: {
label,
field,
isNested: options.isNested,
hasSort: options.hasSort ?? false,
styleClass: options.styleClass
}
};
}
function mergeTableHeaders(...headers) {
return headers;
}
function createBooleanSelectItems(trueLabel = "Yes", falseLabel = "No") {
return [
{ label: trueLabel, value: true },
{ label: falseLabel, value: false }
];
}
function createStatusSelectItems(statusOptions) {
return Object.entries(statusOptions).map(([value, label]) => ({
label,
value: isNaN(Number(value)) ? value : Number(value)
}));
}
// src/memoized-data-storage.ts
import { signal as signal3 } from "@angular/core";
import { HttpContext as HttpContext3 } from "@angular/common/http";
import { firstValueFrom as firstValueFrom3 } from "rxjs";
var MemoizedDataStorage = class {
/**
* Creates a new instance of MemoizedDataStorage
* @param httpClient Angular HttpClient instance for making HTTP requests
* @param skipLoadingSpinner Whether to skip the loading spinner for HTTP requests
*/
constructor(httpClient, skipLoadingSpinner = true) {
this.httpClient = httpClient;
this.skipLoadingSpinner = skipLoadingSpinner;
}
skipLoadingSpinner = true;
/**
* Sets whether to skip the loading spinner for HTTP requests
* @param skip Whether to skip the loading spinner
* @returns This instance for method chaining
*/
setSkipLoadingSpinner(skip) {
this.skipLoadingSpinner = skip;
return this;
}
#singleData = signal3(null);
#multipleData = signal3([]);
#isLoading = signal3(false);
// Public readonly signals for external consumption
/**
* Read-only signal containing single data object or null
*/
singleData = this.#singleData.asReadonly();
/**
* Read-only signal containing array of data objects
*/
multipleData = this.#multipleData.asReadonly();
/**
* Read-only signal indicating whether a request is currently loading
*/
isLoading = this.#isLoading.asReadonly();
// Private flag to control memoization behavior
#isMemoizationDisabledOnNextRead = false;
/**
* Disables memoization for the next read operation and clears cached data
* This forces the next loadSingleData or loadMultipleData call to fetch fresh data
*
* @example
* ```typescript
* const storage = new MemoizedDataStorage<User>(httpClient);
* await storage.loadSingleData('/api/user/1'); // Fetches data
* await storage.loadSingleData('/api/user/1'); // Returns cached data
*
* storage.disableMemoizationOnNextRead();
* await storage.loadSingleData('/api/user/1'); // Fetches fresh data
* ```
*/
disableMemoizationOnNextRead() {
this.#isMemoizationDisabledOnNextRead = true;
this.#singleData.set(null);
this.#multipleData.set([]);
}
/**
* Loads a single data object from the specified URL with optional query parameters
* Uses memoization to avoid redundant requests unless explicitly disabled
*
* @param url The URL to fetch data from
* @param queryParams Optional query parameters to include in the request
* @returns Promise that resolves when the data is loaded
* @throws Error if the HTTP request fails
*
* @example
* ```typescript
* const storage = new MemoizedDataStorage<User>(httpClient);
* await storage.loadSingleData('/api/user/1', { include: 'profile' });
* const user = storage.singleData(); // User data or null
* ```
*/
async loadSingleData(url, queryParams = {}) {
if (!this.#isMemoizationDisabledOnNextRead && this.#singleData() !== null) {
return;
}
try {
this.#isLoading.set(true);
const data = await firstValueFrom3(
this.httpClient.get(url, {
params: queryParams,
context: new HttpContext3().set(SkipLoadingSpinner, this.skipLoadingSpinner)
})
);
this.#singleData.set(data);
} catch (error) {
this.#singleData.set(null);
throw error;
} finally {
this.#isLoading.set(false);
this.#isMemoizationDisabledOnNextRead = false;
}
}
/**
* Loads multiple data objects from the specified URL with optional query parameters
* Uses memoization to avoid redundant requests unless explicitly disabled
*
* @param url The URL to fetch data from
* @param queryParams Optional query parameters to include in the request
* @returns Promise that resolves when the data is loaded
* @throws Error if the HTTP request fails
*
* @example
* ```typescript
* const storage = new MemoizedDataStorage<User>(httpClient);
* await storage.loadMultipleData('/api/users', { page: 1, limit: 10 });
* const users = storage.multipleData(); // Array of User data
* ```
*/
async loadMultipleData(url, queryParams = {}) {
if (!this.#isMemoizationDisabledOnNextRead && this.#multipleData().length !== 0) {
return;
}
try {
this.#isLoading.set(true);
const context = new HttpContext3();
if (this.skipLoadingSpinner) {
context.set(SkipLoadingSpinner, true);
}
const data = await firstValueFrom3(
this.httpClient.get(url, {
params: queryParams,
context
})
);
this.#multipleData.set(Array.isArray(data) ? data : []);
} catch (error) {
this.#multipleData.set([]);
throw error;
} finally {
this.#isLoading.set(false);
this.#isMemoizationDisabledOnNextRead = false;
}
}
/**
* Clears all cached data and resets the storage to initial state
*
* @example
* ```typescript
* const storage = new MemoizedDataStorage<User>(httpClient);
* await storage.loadSingleData('/api/user/1');
* storage.clear(); // Clears cached data
* console.log(storage.singleData()); // null
* console.log(storage.multipleData()); // []
* ```
*/
clear() {
this.#singleData.set(null);
this.#multipleData.set([]);
this.#isMemoizationDisabledOnNextRead = false;
}
/**
* Checks if single data is currently cached
* @returns true if single data is cached, false otherwise
*/
hasSingleData() {
return this.#singleData() !== null;
}
/**
* Checks if multiple data is currently cached
* @returns true if multiple data is cached (non-empty array), false otherwise
*/
hasMultipleData() {
return this.#multipleData().length > 0;
}
};
// src/component-state.ts
import { computed, signal as signal4 } from "@angular/core";
var ComponentState = class {
isAjaxDataIncoming = signal4(false);
enableCheckBoxSelection = signal4(false);
isSelectableRowEnabled = signal4(false);
isAjaxRequestOutgoing = signal4(false);
hasMultipleSelection = signal4(false);
isCreateOrUpdateDialogOpen = signal4(false);
isUpdateDialogOpen = signal4(false);
isCreateDialogOpen = signal4(false);
manipulationType = signal4("create" /* Create */);
componentTitle = signal4("");
isDataManipulationPageOpen = signal4(false);
isCreateOrUpdatePageOpen = signal4(false);
isUpdatePageOpen = signal4(false);
isCreatePageOpen = signal4(false);
manipulationTypeLabel = computed(() => {
switch (this.manipulationType()) {
case "create" /* Create */:
return "Create";
case "update" /* Update */:
return "Update";
case "create-child" /* CreateChild */:
return "Create Child";
case "delete" /* Delete */:
return "Delete";
case "view" /* View */:
return "View";
case "save" /* Save */:
return "Save";
default:
return "";
}
});
/**
* Updates the component title
* @param componentTitle - The new title for the component
* @returns This instance for method chaining
*/
updateComponentTitle = (componentTitle) => {
this.componentTitle.set(componentTitle);
return this;
};
/**
* Updates the multiple selection status
* @param newStatus - Whether multiple selection is enabled
* @returns This instance for method chaining
*/
updateMultipleSelectionStatus = (newStatus) => {
this.hasMultipleSelection.set(newStatus);
return this;
};
/**
* Updates the checkbox selection status
* @param newStatus - Whether checkbox selection is enabled
* @returns This instance for method chaining
*/
updateCheckBoxSelectionStatus = (newStatus) => {
this.enableCheckBoxSelection.set(newStatus);
return this;
};
/**
* Updates the selectable row status
* @param newStatus - Whether row selection is enabled
* @returns This instance for method chaining
*/
updateSelectableRowStatus = (newStatus) => {
this.isSelectableRowEnabled.set(newStatus);
return this;
};
/**
* Updates the manipulation type (Create, Update, Delete, View)
* @param type - The manipulation type
* @returns This instance for method chaining
*/
updateManipulationType = (type) => {
this.manipulationType.set(type);
return this;
};
/**
* Sets the incoming Ajax data status
* @param status - Whether Ajax data is incoming
* @returns This instance for method chaining
*/
setAjaxDataIncoming = (status) => {
this.isAjaxDataIncoming.set(status);
return this;
};
/**
* Sets the outgoing Ajax request status
* @param status - Whether Ajax request is outgoing
* @returns This instance for method chaining
*/
setAjaxRequestOutgoing = (status) => {
this.isAjaxRequestOutgoing.set(status);
return this;
};
/**
* Sets the create or update dialog open status
* @param status - Whether the dialog is open
* @returns This instance for method chaining
*/
setCreateOrUpdateDialogOpen = (status) => {
this.isCreateOrUpdateDialogOpen.set(status);
return this;
};
/**
* Sets the update dialog open status
* @param status - Whether the update dialog is open
* @returns This instance for method chaining
*/
setUpdateDialogOpen = (status) => {
this.isUpdateDialogOpen.set(status);
return this;
};
/**
* Sets the create dialog open status
* @param status - Whether the create dialog is open
* @returns This instance for method chaining
*/
setCreateDialogOpen = (status) => {
this.isCreateDialogOpen.set(status);
return this;
};
/**
* Computed signal that combines component title with manipulation type
*/
componentTitleWithManipulationType = computed(() => {
return this.manipulationTypeLabel() + " " + this.componentTitle();
});
/**
* Computed signal that indicates if component is in update state
*/
isOnUpdateState = computed(() => {
return this.manipulationType() === "update" /* Update */;
});
/**
* Computed signal that indicates if component is in create state
*/
isOnCreateState = computed(() => {
return this.manipulationType() === "create" /* Create */;
});
/**
* Computed signal that indicates if component is in delete state
*/
isOnDeleteState = computed(() => {
return this.manipulationType() === "delete" /* Delete */;
});
/**
* Computed signal that indicates if component is in view state
*/
isOnViewState = computed(() => {
return this.manipulationType() === "view" /* View */;
});
/**
* Computed signal that indicates if any Ajax operation is currently running
*/
isAnyAjaxOperationRunning = computed(() => {
return this.isAjaxDataIncoming() || this.isAjaxRequestOutgoing();
});
/**
* Computed signal that indicates if any dialog is open
*/
isAnyDialogOpen = computed(() => {
return this.isCreateOrUpdateDialogOpen() || this.isUpdateDialogOpen() || this.isCreateDialogOpen();
});
/**
* Resets all state to default values
* @returns This instance for method chaining
*/
reset = () => {
this.isAjaxDataIncoming.set(false);
this.enableCheckBoxSelection.set(false);
this.isSelectableRowEnabled.set(false);
this.isAjaxRequestOutgoing.set(false);
this.hasMultipleSelection.set(false);
this.isCreateOrUpdateDialogOpen.set(false);
this.isUpdateDialogOpen.set(false);
this.isCreateDialogOpen.set(false);
this.manipulationType.set("create" /* Create */);
this.componentTitle.set("");
return this;
};
};
// src/component-data-storage.ts
import { computed as computed2, signal as signal5 } from "@angular/core";
var ComponentDataStorage = class {
singleData = signal5(null);
multipleData = signal5([]);
/**
* Patches multiple data by appending new data to the existing array
* @param newData - Array of new data to append
* @returns This instance for method chaining
*
* @example
* ```typescript
* const storage = new ComponentDataStorage<User>();
* storage.updateMultipleData([{ id: 1, name: 'John' }]);
* storage.patchMultipleData([{ id: 2, name: 'Jane' }]);
* // Result: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }]
* ```
*/
patchMultipleData(newData) {
this.multipleData.update((prevData) => {
return [...prevData, ...newData];
});
return this;
}
/**
* Patches single data by merging new properties with existing data
* If no existing data, creates new object with provided data
* @param newData - Partial data to merge with existing single data
* @returns This instance for method chaining
*
* @example
* ```typescript
* const storage = new ComponentDataStorage<User>();
* storage.updateSingleData({ id: 1, name: 'John', email: 'john@example.com' });
* storage.patchSingleData({ email: 'john.doe@example.com' });
* // Result: { id: 1, name: 'John', email: 'john.doe@example.com' }
* ```
*/
patchSingleData(newData) {
this.singleData.update((prev) => prev ? { ...prev, ...newData } : { ...newData });
return this;
}
/**
* Replaces the entire multiple data array
* @param newData - New array of data to replace existing data
* @returns This instance for method chaining
*/
updateMultipleData(newData) {
this.multipleData.set(newData);
return this;
}
/**
* Replaces the single data object
* @param newData - New data object or null to replace existing data
* @returns This instance for method chaining
*/
updateSingleData(newData) {
this.singleData.set(newData);
return this;
}
/**
* Adds a single item to the multiple data array
* @param item - Single item to add to the array
* @returns This instance for method chaining
*/
addToMultipleData(item) {
this.multipleData.update((prevData) => [...prevData, item]);
return this;
}
/**
* Removes an item from the multiple data array based on a predicate function
* @param predicate - Function that returns true for items to remove
* @returns This instance for method chaining
*
* @example
* ```typescript
* storage.removeFromMultipleData(user => user.id === 1);
* ```
*/
removeFromMultipleData(predicate) {
this.multipleData.update((prevData) => prevData.filter((item) => !predicate(item)));
return this;
}
/**
* Updates an item in the multiple data array based on a predicate function
* @param predicate - Function that returns true for items to update
* @param updateFn - Function that returns the updated item
* @returns This instance for method chaining
*
* @example
* ```typescript
* storage.updateItemInMultipleData(
* user => user.id === 1,
* user => ({ ...user, name: 'Updated Name' })
* );
* ```
*/
updateItemInMultipleData(predicate, updateFn) {
this.multipleData.update(
(prevData) => prevData.map((item) => predicate(item) ? updateFn(item) : item)
);
return this;
}
/**
* Clears all data (both single and multiple)
* @returns This instance for method chaining
*/
clearAll() {
this.singleData.set(null);
this.multipleData.set([]);
return this;
}
/**
* Clears only the single data
* @returns This instance for method chaining
*/
clearSingleData() {
this.singleData.set(null);
return this;
}
/**
* Clears only the multiple data
* @returns This instance for method chaining
*/
clearMultipleData() {
this.multipleData.set([]);
return this;
}
/**
* Checks if single data exists (is not null)
* @returns true if single data exists, false otherwise
*/
hasSingleData = computed2(() => {
return this.singleData() !== null;
});
/**
* Checks if multiple data has items
* @returns true if multiple data array has items, false if empty
*/
hasMultipleData = computed2(() => {
return !Array.isArray(this.multipleData()) ? false : this.multipleData().length > 0;
});
/**
* Gets the count of items in multiple data
* @returns Number of items in the multiple data array
*/
getMultipleDataCount = computed2(() => {
return !this.hasMultipleData() ? 0 : this.multipleData().length;
});
/**
* Finds an item in the multiple data array
* @param predicate - Function that returns true for the item to find
* @returns The found item or undefined
*/
findInMultipleData(predicate) {
return this.multipleData().find(predicate);
}
/**
* Checks if an item exists in the multiple data array
* @param predicate - Function that returns true for the item to check
* @returns true if item exists, false otherwise
*/
existsInMultipleData(predicate) {
return this.multipleData().some(predicate);
}
};
// src/ng-select-helper.ts
import { HttpClient as HttpClient4, HttpContext as HttpContext4 } from "@angular/common/http";
import {
assertInInjectionContext,
DestroyRef,
inject,
Injector,
runInInjectionContext,
signal as signal6
} from "@angular/core";
import {
catchError,
debounceTime,
finalize,
first,
mergeMap,
of,
Subject,
switchMap,
tap
} from "rxjs";
var NgSelectPagedDataResponse = class {
constructor(payload, totalCount) {
this.payload = payload;
this.totalCount = totalCount;
}
};
var defaultResetOpts = {
resetQueryParams: false,
resetBody: false,
resetCache: false
};
var NgSelectHelper = class _NgSelectHelper {
constructor(ajaxUrl, httpClient, destroyRef, usePostRequest = false, limit = 50, useCache = true, skipLoadingSpinner = true, initialSearchText = "") {
this.ajaxUrl = ajaxUrl;
this.httpClient = httpClient;
this.destroyRef = destroyRef;
this.usePostRequest = usePostRequest;
this.useCache = useCache;
this.skipLoadingSpinner = skipLoadingSpinner;
this.#searchText = initialSearchText;
this.#originalAjaxUrl = ajaxUrl;
this.#limit = limit > 0 ? limit : 50;
this.destroyRef.onDestroy(() => {
this.#ajaxErrorSubject.complete();
this.inputSubject.complete();
this.#loadMoreDataSubject.complete();
this.#cache.clear();
if (this.runningApiReq && !this.runningApiReq.closed) {
this.runningApiReq.unsubscribe();
}
});
}
#cache = /* @__PURE__ */ new Map();
#originalAjaxUrl;
#queryParams = {};
#body = {};
#initDone = false;
#searchText = "";
#limit;
#page = 1;
#debounceTimeInSec = 1;
#totalCount = -1;
#isLastApiCallSuccessful = true;
#limitReached = false;
#loadMoreDataSubject = new Subject();
inputSubject = new Subject();
#ajaxErrorSubject = new Subject();
ajaxError$ = this.#ajaxErrorSubject.asObservable();
#loadedData = signal6(
new NgSelectPagedDataResponse([], 0)
);
loadedData = this.#loadedData.asReadonly();
#isLoading = signal6(false);
isLoading = this.#isLoading.asReadonly();
runningApiReq = null;
/**
* Creates a new instance of NgSelectHelper
* @param options Configuration options
* @returns New NgSelectHelper instance
*/
static create({
ajaxUrl,
httpClient,
destroyRef,
usePostRequest = false,
limit = 50,
useCache = true,
skipLoadingSpinner = true,
initialSearchText = ""
}) {
return new _NgSelectHelper(
ajaxUrl,
httpClient,
destroyRef,
usePostRequest,
limit,
useCache,
skipLoadingSpinner,
initialSearchText
);
}
/**
* Sets whether to skip the loading spinner for HTTP requests
* @param skip Whether to skip the loading spinner
* @returns This instance for method chaining
*/
setSkipLoadingSpinner(skip) {
this.skipLoadingSpinner = skip;
return this;
}
/**
* Sets the debounce time for search input in seconds
* @param debounceTimeInSecond Debounce time in seconds
* @returns This instance for method chaining
*/
setDebounceTimeInSecond(debounceTimeInSecond) {
this.#debounceTimeInSec = debounceTimeInSecond > 0 ? debounceTimeInSecond : 1;
return this;
}
/**
* Patches the request body (only works with POST requests)
* @param value Body data to merge
* @returns This instance for method chaining
*/
patchBody(value) {
if (this.usePostRequest) {
this.resetAll();
this.#body = Object.assign(this.#body, value);
}
return this;
}
/**
* Sets the request body (only works with POST requests)
* @param newBody New body data
* @returns This instance for method chaining
*/
setBody(newBody) {
if (this.usePostRequest) {
this.resetAll();
this.#body = newBody;
}
return this;
}
/**
* Clears the internal cache
* @returns This instance for method chaining
*/
clearCache() {
this.#cache.clear();
return this;
}
/**
* Sets route parameters for the URL
* @param newRouteParam Route parameter to append
* @returns This instance for method chaining
*/
setRouteParam(newRouteParam) {
const baseUrl = this.#originalAjaxUrl.endsWith("/") ? this.#originalAjaxUrl.slice(0, -1) : this.#originalAjaxUrl;
let routeParam = newRouteParam.startsWith("/") ? newRouteParam.slice(1) : newRouteParam;
this.ajaxUrl = `${baseUrl}/${routeParam}`;
return this;
}
/**
* Patches query parameters
* @param value Query parameters to merge
* @returns This instance for method chaining
*/
patchQueryParams(value) {
this.resetAll();
this.#queryParams = Object.assign(this.#queryParams, value);
return this;
}
/**
* Removes a query parameter
* @param key Query parameter key to remove
* @returns This instance for method chaining
*/
removeQueryParam(key) {
this.resetAll();
delete this.#queryParams[key];
return this;
}
/**
* Sets query parameters
* @param newQueryParams New query parameters
* @returns This instance for method chaining
*/
setQueryParams(newQueryParams) {
this.resetAll();
this.#queryParams = newQueryParams;
return this;
}
/**
* Handler for ng-select blur event
*/
onBlur() {
this.resetAll();
}
/**
* Handler for ng-select close event
*/
onClose() {
this.resetSearchText();
if (this.runningApiReq && !this.runningApiReq.closed) {
this.runningApiReq.unsubscribe();
this.runningApiReq = null;
}
}
/**
* Handler for ng-select clear event
*/
async onClear() {
this.resetAll();
}
/**
* Handler for ng-select open event
*/
onOpen() {
this.#loadMoreDataSubject.next();
}
/**
* Handler for ng-select scroll to end event
*/
onScrollToEnd() {
if (this.#isLoading()) {
return;
}
if (this.isLastApiCallSuccessful && !this.limitReached) {
this.#page += 1;
}
this.#loadMoreDataSubject.next();
}
/**
* Gets whether the last API call was successful
*/
get isLastApiCallSuccessful() {
return this.#isLastApiCallSuccessful;
}
/**
* Gets whether the limit has been reached
*/
get limitReached() {
return this.#limitReached;
}
/**
* Gets current query parameters
*/
get queryParams() {
return this.#queryParams;
}
/**
* Gets current request body
*/
get body() {
return this.#body;
}
/**
* Gets current page number
*/
get page() {
return this.#page;
}
/**
* Gets total count of available items
*/
get totalCount() {
return this.#totalCount;
}
/**
* Gets whether initialization is complete
*/
get isInitDone() {
return this.#initDone;
}
/**
* Initializes the NgSelectHelper with event handlers
* Should be called in ngOnInit or similar lifecycle method
*/
init() {
if (this.#initDone) {
return;
}
this.#initDone = true;
this.inputSubject.pipe(
debounceTime(this.#debounceTimeInSec * 500),
switchMap((term) => {
this.resetAll();
this.#searchText = term;
return this.loadDataFromApi(this.page, this.#limit, term).pipe(
catchError(() => of(null))
);
})
).subscribe({
next: (res) => {
if (res) {
this.updateStateOnSuccessfulInitialApiCall(res);
} else {
this.updateStateOnFailedApiCall();
}
}
});
if (this.#searchText !== "") {
this.inputSubject.next(this.#searchText);
}
this.#loadMoreDataSubject.pipe(
switchMap(() => {
if (this.#isLoading()) {
return of(null);
}
this.runLimitReachedCheck();
if (this.limitReached) {
return of(null);
}
return this.loadDataFromApi(this.page, this.#limit, this.#searchText).pipe(
catchError(() => of(null))
);
})
).subscribe({
next: (res) => {
if (res) {
this.updateStateOnSuccessfulSubsequentApiCall(res);
} else {
this.updateStateOnFailedApiCall();
}
}
});
}
/**
* Loads data from the API
* @param page Page number
* @param limit Items per page
* @param searchText Search term
* @returns Observable of paged data response
*/
loadDataFromApi(page, limit, searchText) {
const queryParams = { page, limit };
if (searchText) {
queryParams["searchText"] = searchText;
}
const key = {
ajaxUrl: this.ajaxUrl,
page,
limit,
searchText: searchText ?? "",
queryParams: { ...queryParams, ...this.#queryParams },
body: this.#body
};
let strKey = null;
if (this.useCache) {
strKey = JSON.stringify(key);
const cacheData = this.#cache.get(strKey);
if (cacheData !== void 0) {
return of(cacheData);
}
}
this.#isLoading.set(true);
let req;
if (this.usePostRequest) {
req = this.httpClient.post(key.ajaxUrl, key.body, {
params: key.queryParams,
context: new HttpContext4().set(SkipLoadingSpinner, this.skipLoadingSpinner)
});
} else {
req = this.httpClient.get(key.ajaxUrl, {
params: key.queryParams,
context: new HttpContext4().set(SkipLoadingSpinner, this.skipLoadingSpinner)
});
}
return req.pipe(
first(),
mergeMap((val) => {
if (!val) {
throw new Error("Invalid response body for ng-select");
}
if (!Object.hasOwn(val, "payload") || !Object.hasOwn(val, "totalCount")) {
throw new Error("Invalid response body for ng-select");
}
if (!Array.isArray(Reflect.get(val, "payload"))) {
throw new Error("Invalid response body for ng-select");
}
return of(val);
}),
tap((val) => {
if (this.useCache && strKey && val) {
this.#cache.set(strKey, val);
}
}),
catchError((error) => {
this.#ajaxErrorSubject.next(error);
return of(null);
}),
finalize(() => this.#isLoading.set(false))
);
}
/**
/**
* Checks if the limit has been reached
*/
runLimitReachedCheck() {
if (this.totalCount !== -1) {
this.#limitReached = this.#loadedData().payload.length >= this.totalCount;
}
}
/**
* Updates state after successful initial API call
* @param data Response data
*/
updateStateOnSuccessfulInitialApiCall(data) {
this.#loadedData.set(new NgSelectPagedDataResponse(data.payload, data.totalCount));
this.#totalCount = data.totalCount;
this.#isLastApiCallSuccessful = true;
}
/**
* Updates state after successful subsequent API call
* @param data Response data
*/
updateStateOnSuccessfulSubsequentApiCall(data) {
this.#loadedData.update((val) => {
return new NgSelectPagedDataResponse(
[...val.payload, ...data.payload],
data.totalCount
);
});
this.#totalCount = data.totalCount;
this.#isLastApiCallSuccessful = true;
}
/**
* Updates state after failed API call
*/
updateStateOnFailedApiCall() {
this.#isLastApiCallSuccessful = false;
}
/**
* Resets all state to initial values
* @param opts Reset options
*/
resetAll(opts = defaultResetOpts) {
if (this.runningApiReq && !this.runningApiReq.closed) {
this.runningApiReq.unsubscribe();
this.runningApiReq = null;
}
this.#isLastApiCallSuccessful = true;
this.#page = 1;
this.#totalCount = -1;
this.#limitReached = false;
this.#searchText = "";
if (opts.resetCache) {
this.#cache.clear();
}
this.#loadedData.set(new NgSelectPagedDataResponse([], 0));
this.#isLoading.set(false);
if (opts.resetQueryParams) {
this.#queryParams = {};
}
if (opts.resetBody) {
this.#body = {};
}
}
/**
* Resets search text
*/
resetSearchText() {
this.#searchText = "";
}
};
function createNgSelectHelper(options) {
!options.injector && assertInInjectionContext(createNgSelectHelper);
const assertedInjector = options.injector ?? inject(Injector);
return runInInjectionContext(assertedInjector, () => {
const httpClient = inject(HttpClient4);
const destroyRef = inject(DestroyRef);
return NgSelectHelper.create({
...options,
httpClient,
destroyRef
});
});
}
// src/offset-paginated-ng-select-state.ts
import { HttpClient as HttpClient5, HttpContext as HttpContext5, HttpContextToken as HttpContextToken2, HttpParams } from "@angular/common/http";
import {
assertInInjectionContext as assertInInjectionContext2,
computed as computed3,
DestroyRef as DestroyRef2,
inject as inject2,
InjectionToken,
Injector as Injector2,
isDevMode,
isSignal,
runInInjectionContext as runInInjectionContext2,
signal as signal7
} from "@angular/core";
import { takeUntilDestroyed, toObservable } from "@angular/core/rxjs-interop";
import {
catchError as catchError2,
combineLatestWith,
debounceTime as debounceTime2,
defer,
filter,
finalize as finalize2,
interval,
of as of2,
Subject as Subject2,
switchMap as switchMap2,
takeUntil,
tap as tap2
} from "rxjs";
var optionsWithDefaultValue = {
searchQueryParamKey: "searchText",
pageQueryParamKey: "page",
limitQueryParamKey: "limit",
dataArrayKey: "payload",
totalDataCountKey: "totalCount",
debounceTimeMs: 500,
searchOnlyMode: false,
requestMethod: "GET",
queryParams: {},
onCloseReset: { reset: false },
postRequestBody: null,
dataLimitPerRequest: 20,
useCache: false,
cacheTtlSec: 60,
disableCacheExpiration: false
};
var OFFSET_PAGINATED_NG_SELECT_STATE_CONFIG = new InjectionToken(
"OFFSET_PAGINATED_NG_SELECT_STATE_CONFIG",
{
providedIn: "root",
factory: () => optionsWithDefaultValue
}
);
function provideOffsetPaginatedNgSelectStateConfig(configFactory) {
return {
provide: OFFSET_PAGINATED_NG_SELECT_STATE_CONFIG,
useFactory: () => {
const config = configFactory();
return { ...optionsWithDefaultValue, ...config };
}
};
}
var OffsetPaginatedNgSelectNetworkRequest = new HttpContextToken2(() => false);
function isValidData(dataArrayKey, dataCountKey, rawData) {
if (typeof rawData !== "object" || rawData === null || Array.isArray(rawData)) {
return false;
}
const hasData = dataArrayKey in rawData || "payload" in rawData || "data" in rawData;
if (!hasData || !(dataCountKey in rawData)) {
return false;
}
const dataArray = rawData[dataArrayKey] ?? rawData["payload"] ?? rawData["data"];
if (!Array.isArray(dataArray)) {
return false;
}
if (typeof rawData[dataCountKey] !== "number") {
return false;
}
return true;
}
function defaultData() {
return {
payload: [],
totalCount: 0
};
}
var defaultResetOpts2 = {
resetQueryParams: false,
resetBody: false,
resetCache: false
};
function httpParamsToObject(params) {
const result = {};
params.keys().forEach((key) => {
const values = params.getAll(key);
if (values === null) {
return;
}
result[key] = values.length > 1 ? values : values[0];
});
return result;
}
function removeBlacklistedParams(params, blacklistedQueryKeys) {
let current = params;
params.keys().forEach((key) => {
if (blacklistedQueryKeys.includes(key.toLowerCase())) {
current = current.delete(key);
}
});
return current;
}
function offsetPaginatedNgSelectState(url, options) {
if (isDevMode() && !options?.injector) {
assertInInjectionContext2(offsetPaginatedNgSelectState);
}
const assertedInjector = options?.injector ?? inject2(Injector2);
return runInInjectionContext2(assertedInjector, () => {
const destroyRef = inject2(DestroyRef2);
const http = inject2(HttpClient5);
const configFromDi = inject2(OFFSET_PAGINATED_NG_SELECT_STATE_CONFIG);
const typeaheadSubject = new Subject2();
const typeAhead$ = typeaheadSubject.asObservable();
const mergedOptions = {
...configFromDi,
...cleanNullishFromObject(options),
httpContext: options?.httpContext ?? new HttpContext5(),
onError: options?.onError ?? configFromDi.onError
};
const blackListedQueryKeys = [
mergedOptions.searchQueryParamKey,
mergedOptions.pageQueryParamKey,
mergedOptions.limitQueryParamKey
].map((elem) => elem.toLowerCase());
const sanitizedMergedQueryParams = removeBlacklistedParams(
new HttpParams({ fromObject: mergedOptions.queryParams }),
blackListedQueryKeys
);
const internalState = Object.seal({
apiCallNotification: new Subject2(),
apiCallAbortNotifier: new Subject2(),
currentPage: signal7(1),
isCurrentPageSuccessful: signal7(false),
isAllDataLoaded: signal7(false),
isLoading: signal7(false),
searchTermFromSearchEvent: signal7(null),
isSelectPanelOpen: signal7(false),
loadedData: signal7(defaultData()),
searchText: signal7(null),
cache: /* @__PURE__ */ new Map(),
postRequestBody: signal7(mergedOptions.postRequestBody),
queryParamsFromConsumer: signal7(sanitizedMergedQueryParams)
});
const finalQueryParams = computed3(() => {
let val = internalState.queryParamsFromConsumer().set(mergedOptions.pageQueryParamKey, String(internalState.currentPage())).set(mergedOptions.limitQueryParamKey, String(mergedOptions.dataLimitPerRequest));
const searchText = internalState.searchText();
if (searchText !== null) {
val = val.set(mergedOptions.searchQueryParamKey, searchText);
} else {
val = val.delete(mergedOptions.searchQueryParamKey);
}
return val;
});
destroyRef.onDestroy(() => {
if (!typeaheadSubject.closed) {
typeaheadSubject.complete();
}
if (!internalState.apiCallAbortNotifier.closed) {
internalState.apiCallAbortNotifier.next();
internalState.apiCallAbortNotifier.complete();
}
if (!internalState.apiCallNotification.closed) {
internalState.apiCallNotification.complete();
}
internalState.cache.clear();
});
if (mergedOptions.useCache && !mergedOptions.disableCacheExpiration) {
interval(mergedOptions.cacheTtlSec * 1e3).pipe(takeUntilDestroyed(destroyRef)).subscribe(() => {
internalState.cache.clear();
});
}
function reset(options2 = defaultResetOpts2) {
internalState.apiCallAbortNotifier.next();
internalState.currentPage.set(1);
internalState.isCurrentPageSuccessful.set(false);
internalState.searchText.set(null);
internalState.loadedData.set(defaultData());
internalState.isAllDataLoaded.set(false);
internalState.isLoading.set(false);
if (options2.resetBody) {
internalState.postRequestBody.set(mergedOptions.postRequestBody);
}
if (options2.resetQueryParams) {
internalState.queryParamsFromConsumer.set(sanitizedMergedQueryParams);
}
if (options2.resetCache) {
internalState.cache.clear();
}
}
const notifyApiCall = () => {
internalState.apiCallNotification.next();
};
const handleApiResponse = (newData) => {
internalState.isCurrentPageSuccessful.set(true);
if (newData.payload.length > 0) {
internalState.loadedData.update((val) => {
return {
payload: [...val.payload, ...newData.payload],
totalCount: newData.totalCount
};
});
}
internalState.isAllDataLoaded.set(
internalState.loadedData().payload.length === newData.totalCount
);
};
const loadDataFromApi = () => {
const finalUrl = isSignal(url) ? url() : url;
if (finalUrl === null) {
return of2(null);
}
const key = {
url: finalUrl,
body: internalState.postRequestBody(),
queryParams: finalQueryParams().toString()
};
if (mergedOptions.requestMethod !== "POST") {
delete key["body"];
}
let strKey = null;
if (mergedOptions.useCache) {
strKey = JSON.stringify(key);
const cacheData = internalState.cache.get(strKey);
if (cacheData !== void 0) {
return of2(cacheData);
}
}
let req;
switch (mergedOptions.requestMethod) {
case "GET":
req = defer(() => {
internalState.isLoading.set(true);
return http.get(finalUrl, {
params: finalQueryParams(),
context: mergedOptions.httpContext.set(OffsetPaginatedNgSelectNetworkRequest, true)
});
});
break;
case "POST":
req = defer(() => {
internalState.isLoading.set(true);
return http.post(finalUrl, internalState.postRequestBody(), {
params: finalQueryParams(),
context: mergedOptions.httpContext.set(OffsetPaginatedNgSelectNetworkRequest, true)
});
});
break;
default:
throw new Error("Invalid/unsupported request method");
}
return req.pipe(
switchMap2((rawData) => {
if (!isValidData(mergedOptions.dataArrayKey, mergedOptions.totalDataCountKey, rawData)) {
throw new Error(
`The response body must be an object. Valid example: { ${mergedOptions.dataArrayKey}: [], ${mergedOptions.totalDataCountKey}: 0 } or { payload: [], ${mergedOptions.totalDataCountKey}: 0 } or { data: [], ${mergedOptions.totalDataCountKey}: 0 }`
);
}
const dataArray = rawData[mergedOptions.dataArrayKey] ?? rawData["payload"] ?? rawData["data"] ?? [];
const dataInContainer = {
payload: dataArray,
totalCount: rawData[mergedOptions.totalDataCountKey] ?? 0
};
return of2(dataInContainer);
}),
tap2((data) => {
if (mergedOptions.useCache && strKey && data) {
internalState.cache.set(strKey, data);
}
}),
catchError2((error) => {
if (mergedOptions.onError) {
try {
mergedOptions.onError(error);
} catch (errorFromHandler) {
console.warn(
"Exception in onError is handled internally to keep observable running.",
errorFromHandler
);
}
}
console.error(error);
return of2(null);
}),
finalize2(() => internalState.isLoading.set(false)),
takeUntil(internalState.apiCallAbortNotifier)
);
};
internalState.apiCallNotification.pipe(
switchMap2(
() => internalState.isLoading() || internalState.isAllDataLoaded() || !internalState.isSelectPanelOpen() ? of2(null) : loadDataFromApi()
),
filter((val) => val !== null),
takeUntilDestroyed(destroyRef)
).subscribe((data) => {
handleApiResponse(data);
});
typeAhead$.pipe(
combineLatestWith(toObservable(internalState.searchTermFromSearchEvent)),
debounceTime2(mergedOptions.debounceTimeMs),
switchMap2(([typeaheadValue, searchEventValue]) => {
if (typeaheadValue === searchEventValue) {
return of2(typeaheadValue.trim());
}
if (typeaheadValue !== searchEventValue && searchEventValue === "") {
return of2("");
}
return of2(null);
}),
filter((val) => val !== null),
takeUntilDestroyed(destroyRef)
).subscribe((finalValue) => {
reset();
if (mergedOptions.searchOnlyMode && finalValue === "") {
return;
}
internalState.searchText.set(finalValue);
notifyApiCall();
});
return Object.seal({
// data state
reset,
queryParams: computed3(() => {
return httpParamsToObject(finalQueryParams());
}),
postRequestBody: internalState.postRequestBody.asReadonly(),
typeaheadSubject,
data: computed3(() => {
return internalState.loadedData().payload;
}),
isLoading: internalState.isLoading.asReadonly(),
// event handlers
clearCache() {
internalState.cache.clear();
return this;
},
onOpen() {
internalState.isSelectPanelOpen.set(true);
if (!mergedOptions.searchOnlyMode) {
notifyApiCall();
}
},
onClose() {
internalState.isSelectPanelOpen.set(false);
if (mergedOptions.onCloseReset.reset) {
reset(mergedOptions.onCloseReset.resetOpts);
}
},
onClear() {
reset();
},
onSearch(event) {
if (Object.hasOwn(event, "term")) {
internalState.searchTermFromSearchEvent.set(event.term);
}
},
onScrollToEnd() {
if (internalState.isAllDataLoaded() || internalState.isLoading() || !internalState.isSelectPanelOpen()) {
return;
}
if (internalState.isCurrentPageSuccessful()) {
internalState.currentPage.update((currentValue) => {
return currentValue + 1;
});
}
notifyApiCall();
},
// chainable methods
setBody(value) {
if (mergedOptions.requestMethod === "POST") {
reset();
internalState.postRequestBody.set(value);
}
return this;
},
clearBody() {
if (mergedOptions.requestMethod === "POST") {
reset();
internalState.postRequestBody.set(null);
}
return this;
},
patchQueryParams(value) {
const newValue = { ...value };
Object.keys(newValue).forEach((key) => {
if (blackListedQueryKeys.includes(key.toLowerCase())) {
delete newValue[key];
}
});
const keys = Object.keys(newValue);
if (keys.length === 0) {
return this;
}
reset();
internalState.queryParamsFromConsumer.update((current) => {
let next = current;
keys.forEach((key) => {
next = next.delete(key);
});
return next.appendAll(newValue);
});
return this;
},
removeQueryParam(key) {
reset();
internalState.queryParamsFromConsumer.update((currentValue) => {
return currentValue.delete(key);
});
return this;
},
removeAllQueryParams() {
reset();
internalState.queryParamsFromConsumer.set(new HttpParams());
return this;
}
});
});
}
// src/ng-select-utils.ts
import {
assertInInjectionContext as assertInInjectionContext3,
DestroyRef as DestroyRef3,
inject as inject3,
Injector as Injector3,
runInInjectionContext as runInInjectionContext3
} from "@angular/core";
import { takeUntilDestroyed as takeUntilDestroyed2, toObservable as toObservable2 } from "@angular/core/rxjs-interop";
function initNgSelect(helpers$, destroyRef, onAjaxError) {
helpers$.pipe(takeUntilDestroyed2(destroyRef)).subscribe({
next: (helpers) => {
helpers.filter((elem) => elem instanceof NgSelectHelper && !elem.isInitDone).forEach((elem) => {
elem.init();
elem.ajaxError$.pipe(takeUntilDestroyed2(destroyRef)).subscribe(onAjaxError);
});
}
});
}
function initNgSelectHelper(items, options = {}) {
!options.injector && assertInInjectionContext3(initNgSelectHelper);
const assertedInjector = options.injector ?? inject3(Injector3);
runInInjectionContext3(assertedInjector, () => {
const destroyRef = inject3(DestroyRef3);
toObservable2(items).pipe(takeUntilDestroyed2(destroyRef)).subscribe((elem) => {
elem.filter((elem2) => elem2 instanceof NgSelectHelper && !elem2.isInitDone).forEach((elem2) => {
elem2.init();
if (!options.onAjaxError) {
return;
}
elem2.ajaxError$.pipe(takeUntilDestroyed2(destroyRef)).subscribe((payload) => {
if (options.onAjaxError) {
options.onAjaxError(payload);
}
});
});
});
});
}
// src/offset-paginated-prime-ng-table-state.ts
import { HttpClient as HttpClient6, HttpContext as HttpContext6, HttpContextToken as HttpContextToken3, HttpParams as HttpParams2 } from "@angular/common/http";
import {
assertInInjectionContext as assertInInjectionContext4,
computed as computed4,
DestroyRef as DestroyRef4,
effect,
inject as inject4,
InjectionToken as InjectionToken2,
Injector as Injector4,
isDevMode as isDevMode2,
isSignal as isSignal2,
runInInjectionContext as runInInjectionContext4,
signal as signal8,
untracked
} from "@angular/core";
import { takeUntilDestroyed as takeUntilDestroyed3 } from "@angular/core/rxjs-interop";
import {
catchError as catchError3,
defer as defer2,
filter as filter2,
finalize as finalize3,
of as of3,
Subject as Subject3,
switchMap as switchMap3,
takeUntil as takeUntil2
} from "rxjs";
var OffsetPaginatedPrimeNgTableStateNetworkRequest = new HttpContextToken3(() => false);
var PostRequestBody = class {
constructor(payload) {
this.payload = payload;
}
};
var optionsWithDefaultValue2 = {
pageQueryParamKey: "page",
limitQueryParamKey: "limit",
dataArrayKey: "payload",
totalDataCountKey: "totalCount",
requestMethod: "GET",
queryParams: {}
};
var OFFSET_PAGINATED_PRIME_NG_TABLE_STATE_CONFIG = new InjectionToken2(
"OFFSET_PAGINATED_PRIME_NG_TABLE_STATE_CONFIG",
{
providedIn: "root",
factory: () => optionsWithDefaultValue2
}
);
function provideOffsetPaginatedPrimeNgTableStateConfig(configFactory) {
return {
provide: OFFSET_PAGINATED_PRIME_NG_TABLE_STATE_CONFIG,
useFactory: () => {
const config = configFactory();
return { ...optionsWithDefaultValue2, ...config };
}
};
}
function isValidData2(dataArrayKey, dataCountKey, rawData) {
if (typeof rawData !== "object" || rawData === null || Array.isArray(rawData)) {
return false;
}
if (!(dataArrayKey in rawData) || !(dataCountKey in rawData)) {
return false;
}
if (!Array.isArray(rawData[dataArrayKey])) {
return false;
}
if (typeof rawData[dataCountKey] !== "number") {
return false;
}
return true;
}
function defaultData2() {
return {
payload: [],
totalCount: 0
};
}
function offsetPaginatedPrimeNgTableState(url, options) {
if (isDevMode2() && !options?.injector) {
assertInInjectionContext4(offsetPaginatedPrimeNgTableState);
}
const assertedInjector = options?.injector ?? inject4(Injector4);
return runInInjectionContext4(assertedInjector, () => {
const http = inject4(HttpClient6);
const destroyRef = inject4(DestroyRef4);
const configFromDi = inject4(OFFSET_PAGINATED_PRIME_NG_TABLE_STATE_CONFIG);
const mergedOptions = {
...configFromDi,
...cleanNullishFromObject(options),
httpContext: options?.httpContext ?? new HttpContext6(),
onError: options?.onError ?? configFromDi.onError
};
const blackListedQueryKeys = [
mergedOptions.pageQueryParamKey,
mergedOptions.limitQueryParamKey
].map((elem) => elem.toLowerCase());
Object.keys(mergedOptions.queryParams).forEach((key) => {
if (blackListedQueryKeys.includes(key.toLowerCase())) {
delete mergedOptions.queryParams[key];
}
});
const internalState = Object.seal({
apiCallNotification: new Subject3(),
apiCallAbortNotifier: new Subject3(),
isLoading: signal8(false),
currentPage: signal8(1),
currentLimit: signal8(15),
totalRecords: signal8(0),
loadedData: signal8(defaultData2()),
predicate: mergedOptions.predicate ?? signal8(true),
queryParamsFromUser: signal8(
mergedOptions.queryParams
),
postRequestBodyFromLazyLoad: signal8(null),
queryParamsFromLazyLoad: signal8(null)
});
const finalQueryParams = computed4(() => {
const val = {
[mergedOptions.pageQueryParamKey]: internalState.currentPage(),
[mergedOptions.limitQueryParamKey]: internalState.currentLimit(),
...cleanNullishFromObject(internalState.queryParamsFromUser()),
...cleanNullishFromObject(internalState.queryParamsFromLazyLoad() ?? {})
};
return val;
});
effect(() => {
const predicate = internalState.predicate();
untracked(() => {
if (!predicate) {
internalState.apiCallAbortNotifier.next();
}
});
});
destroyRef.onDestroy(() => {
if (!internalState.apiCallNotification.closed) {
internalState.apiCallNotification.complete();
}
if (!internalState.apiCallAbortNotifier.closed) {
internalState.apiCallAbortNotifier.next();
internalState.apiCallAbortNotifier.complete();
}
});
const loadDataFromApi = () => {
const apiUrl = isSignal2(url) ? url() : url;
let req;
switch (mergedOptions.requestMethod) {
case "GET":
req = defer2(() => {
internalState.isLoading.set(true);
return http.get(apiUrl, {
params: { ...finalQueryParams() },
context: mergedOptions.httpContext.set(
OffsetPaginatedPrimeNgTableStateNetworkRequest,
true
)
});
});
break;
case "POST":
req = defer2(() => {
internalState.isLoading.set(true);
return http.post(apiUrl, internalState.postRequestBodyFromLazyLoad(), {
params: { ...finalQueryParams() },
context: mergedOptions.httpContext.set(
OffsetPaginatedPrimeNgTableStateNetworkRequest,
true
)
});
});
break;
default:
throw new Error("Invalid/unsupported request method");
}
return req.pipe(
switchMap3((rawData) => {
if (!isValidData2(mergedOptions.dataArrayKey, mergedOptions.totalDataCountKey, rawData)) {
throw new Error(
`The response body must be an object. Valid example: { ${mergedOptions.dataArrayKey}: [], ${mergedOptions.totalDataCountKey}: 0 }`
);
}
const dataInContainer = {
payload: rawData[mergedOptions.dataArrayKey] ?? [],
totalCount: rawData[mergedOptions.totalDataCountKey] ?? 0
};
return of3(dataInContainer);
}),
catchError3((error) => {
if (mergedOptions.onError) {
try {
mergedOptions.onError(error);
} catch (errorFromHandler) {
console.warn(
"Exception in onError is handled internally to keep observable running.",
errorFromHandler
);
}
}
console.error(error);
return of3(null);
}),
finalize3(() => internalState.isLoading.set(false)),
takeUntil2(internalState.apiCallAbortNotifier)
);
};
const handleApiResponse = (newData) => {
internalState.loadedData.set({
payload: newData.payload,
totalCount: newData.totalCount
});
internalState.totalRecords.set(newData.totalCount);
};
internalState.apiCallNotification.pipe(
switchMap3(() => {
if (internalState.isLoading()) {
return of3(null);
} else if (!internalState.predicate()) {
return of3(defaultData2());
} else {
return loadDataFromApi();
}
}),
filter2((val) => val !== null),
takeUntilDestroyed3(destroyRef)
).subscribe((data) => {
handleApiResponse(data);
});
return Object.seal({
queryParams: finalQueryParams,
predicate: internalState.predicate,
totalRecords: internalState.totalRecords.asReadonly(),
data: computed4(() => {
return internalState.loadedData().payload;
}),
isLoading: internalState.isLoading.asReadonly(),
onLazyLoad(event) {
const offset = event.first || 0;
const limit = event.rows || 15;
const currentPage = Math.floor(offset / limit) + 1;
internalState.currentPage.set(currentPage);
internalState.currentLimit.set(limit);
if (mergedOptions.filterAndSortHandler) {
const handledResult = mergedOptions.filterAndSortHandler(event);
if (handledResult instanceof HttpParams2) {
internalState.queryParamsFromLazyLoad.set(handledResult);
} else if (handledResult instanceof PostRequestBody) {
internalState.postRequestBodyFromLazyLoad.set(handledResult.payload);
}
}
internalState.apiCallNotification.next();
},
patchQueryParams(value) {
const newValue = { ...value };
Object.keys(newValue).forEach((key) => {
if (blackListedQueryKeys.includes(key.toLowerCase())) {
delete newValue[key];
}
});
if (Object.keys(newValue).length > 0) {
internalState.queryParamsFromUser.update((currentValue) => {
return {
...currentValue,
...newValue
};
});
}
return this;
},
removeQueryParam(key) {
internalState.queryParamsFromUser.update((currentValue) => {
const { [key]: _, ...rest } = currentValue;
return rest;
});
return this;
},
removeAllQueryParams() {
internalState.queryParamsFromUser.set({});
return this;
}
});
});
}
var UrlBasedDynamicQuery = {
Contains: "con",
NotContains: "ntcon",
StartsWith: "sw",
NotStartsWith: "ntsw",
EndsWith: "ew",
NotEndsWith: "ntew",
Equal: "eq",
NotEquals: "nteq",
GreaterThan: "gt",
GreaterThanEqual: "gte",
LessThan: "lt",
LessThanEqual: "lte"
};
export {
ComponentDataStorage,
ComponentState,
ManipulationType,
MemoizedDataStorage,
NgSelectHelper,
NgSelectPagedDataResponse,
NullableApiResponseSchema,
NumberStringKeyDataSchema,
OffsetPaginatedNgSelectNetworkRequest,
OffsetPaginatedPrimeNgTableStateNetworkRequest,
PagedDataResponseZodSchema,
PostRequestBody,
PrimeNgDynamicTableStateHelper,
PrimeNgPagedDataTableStateHelper,
ReloadNotification,
SkipLoadingSpinner,
UrlBasedDynamicQuery,
binarySearch,
calculatePrimengTablePagination,
cleanNullishFromObject,
createBooleanColumn,
createBooleanSelectItems,
createDateColumn,
createDateTimeColumn,
createDropdownColumn,
createHierarchicalTree,
createKeyData,
createMultiselectColumn,
createNgSelectHelper,
createNumericColumn,
createPrimengNumberMatchModes,
createPrimengStringMatchModes,
createSimpleColumn,
createStatusSelectItems,
createTextColumn,
createTimeColumn,
dynamicQueryResponseZodSchema,
emptyCallback,
hasNullishInObject,
initNgSelect,
initNgSelectHelper,
isApiResponse,
isDynamicQueryResponse,
isPaginatedResponse,
isSimplePagedResponse,
mergeTableHeaders,
nullableKeyData,
offsetPaginatedNgSelectState,
offsetPaginatedPrimeNgTableState,
provideOffsetPaginatedNgSelectStateConfig,
provideOffsetPaginatedPrimeNgTableStateConfig,
routeParamConcat
};
//# sourceMappingURL=index.mjs.map