diff --git a/samples/grids/grid/filtering-remote/.eslintrc.js b/samples/grids/grid/filtering-remote/.eslintrc.js
new file mode 100644
index 0000000000..7168b71441
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/.eslintrc.js
@@ -0,0 +1,78 @@
+// https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
+module.exports = {
+ parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+ parserOptions: {
+ ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
+ sourceType: "module", // Allows for the use of imports
+ ecmaFeatures: {
+ jsx: true // Allows for the parsing of JSX
+ }
+ },
+ settings: {
+ react: {
+ version: "999.999.999" // Tells eslint-plugin-react to automatically detect the version of React to use
+ }
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
+ "plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
+ ],
+ rules: {
+ // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-prototype-builtins": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-var": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "no-prototype-builtins": "off",
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ }
+ }
+ ]
+ };
\ No newline at end of file
diff --git a/samples/grids/grid/filtering-remote/ReadMe.md b/samples/grids/grid/filtering-remote/ReadMe.md
new file mode 100644
index 0000000000..8f078cc35f
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/ReadMe.md
@@ -0,0 +1,56 @@
+
+
+
+This folder contains implementation of React application with example of Remote Filtering feature using [Grid](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html) component.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Branches
+
+> **_NOTE:_** You should use [master](https://github.com/IgniteUI/igniteui-react-examples/tree/master) branch of this repository if you want to run samples on your computer. Use the [vnext](https://github.com/IgniteUI/igniteui-react-examples/tree/vnext) branch only when you want to contribute new samples to this repository.
+
+## Instructions
+
+Follow these instructions to run this example:
+
+
+```
+git clone https://github.com/IgniteUI/igniteui-react-examples.git
+git checkout master
+cd ./igniteui-react-examples
+cd ./samples/grids/grid/filtering-options
+```
+
+open above folder in VS Code or type:
+```
+code .
+```
+
+In terminal window, run:
+```
+npm install --legacy-peer-deps
+npm run-script start
+```
+
+Then open http://localhost:4200/ in your browser
+
+
+## Learn More
+
+To learn more about **Ignite UI for React** components, check out the [React documentation](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html).
diff --git a/samples/grids/grid/filtering-remote/package.json b/samples/grids/grid/filtering-remote/package.json
new file mode 100644
index 0000000000..02dbc47aea
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "example-ignite-ui-react",
+ "description": "This project provides example of using Ignite UI for React components",
+ "author": "Infragistics",
+ "version": "1.4.0",
+ "license": "",
+ "homepage": ".",
+ "private": true,
+ "scripts": {
+ "start": "set PORT=4200 && react-scripts --max_old_space_size=10240 start",
+ "build": "react-scripts --max_old_space_size=10240 build ",
+ "test": "react-scripts test --env=jsdom",
+ "eject": "react-scripts eject",
+ "lint": "eslint ./src/**/*.{ts,tsx}"
+ },
+ "dependencies": {
+ "igniteui-dockmanager": "1.16.1",
+ "igniteui-react": "19.0.2",
+ "igniteui-react-core": "19.0.0",
+ "igniteui-react-grids": "19.0.2",
+ "igniteui-react-inputs": "19.0.0",
+ "igniteui-react-layouts": "19.0.0",
+ "igniteui-webcomponents": "6.0.0",
+ "lit-html": "^3.2.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "^5.0.1",
+ "tslib": "^2.4.0"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.2.0",
+ "@types/node": "^18.11.7",
+ "@types/react": "^18.0.24",
+ "@types/react-dom": "^18.0.8",
+ "eslint": "^8.33.0",
+ "eslint-config-react": "^1.1.7",
+ "eslint-plugin-react": "^7.20.0",
+ "react-app-rewired": "^2.2.1",
+ "typescript": "^4.8.4",
+ "worker-loader": "^3.0.8"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ]
+}
diff --git a/samples/grids/grid/filtering-remote/public/index.html b/samples/grids/grid/filtering-remote/public/index.html
new file mode 100644
index 0000000000..e2d3265576
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/public/index.html
@@ -0,0 +1,11 @@
+
+
+
+ Sample | Ignite UI | React | infragistics
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/grids/grid/filtering-remote/sandbox.config.json b/samples/grids/grid/filtering-remote/sandbox.config.json
new file mode 100644
index 0000000000..07f53508eb
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/sandbox.config.json
@@ -0,0 +1,5 @@
+{
+ "infiniteLoopProtection": false,
+ "hardReloadOnChange": false,
+ "view": "browser"
+}
\ No newline at end of file
diff --git a/samples/grids/grid/filtering-remote/src/RemoteService.ts b/samples/grids/grid/filtering-remote/src/RemoteService.ts
new file mode 100644
index 0000000000..ae748466a8
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/src/RemoteService.ts
@@ -0,0 +1,234 @@
+export enum FILTER_OPERATION {
+ CONTAINS = 'contains',
+ STARTS_WITH = 'startswith',
+ ENDS_WITH = 'endswith',
+ EQUALS = 'eq',
+ DOES_NOT_EQUAL = 'ne',
+ GREATER_THAN = 'gt',
+ LESS_THAN = 'lt',
+ LESS_THAN_EQUAL = 'le',
+ GREATER_THAN_EQUAL = 'ge'
+}
+
+export enum LOGICAL_OPERATOR {
+ AND = 0,
+ OR = 1
+}
+
+export enum SORT_DIRECTION {
+ ASC = 1,
+ DESC = 2
+}
+
+interface FilterCondition {
+ name: string;
+}
+
+interface FilterOperand {
+ fieldName: string;
+ searchVal: string | number;
+ condition: FilterCondition;
+ filteringOperands?: FilterOperand[];
+ operator?: LOGICAL_OPERATOR;
+}
+
+interface FilteringArgs {
+ filteringOperands: FilterOperand[];
+ operator: LOGICAL_OPERATOR;
+}
+
+interface SortingArgs {
+ fieldName: string;
+ dir: SORT_DIRECTION;
+}
+
+interface ODataResponse {
+ '@odata.count'?: number;
+ value: any[];
+}
+
+export interface RemoteServiceConfig {
+ baseUrl: string;
+ pageSize?: number;
+}
+
+export class RemoteService {
+ private config: RemoteServiceConfig;
+ private abortController?: AbortController;
+
+ constructor(config: RemoteServiceConfig) {
+ this.config = {
+ pageSize: 1000,
+ ...config
+ };
+ }
+
+ public async getData(
+ filteringArgs?: FilteringArgs,
+ sortingArgs?: SortingArgs[]
+ ): Promise {
+ // Cancel any in-flight request
+ this.abortController?.abort();
+ this.abortController = new AbortController();
+
+ try {
+ const url = this.buildDataUrl(filteringArgs, sortingArgs);
+ const response = await fetch(url, { signal: this.abortController.signal });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data: ODataResponse = await response.json();
+
+ if (!Array.isArray(data.value)) {
+ throw new Error('Invalid response: missing or invalid data array');
+ }
+
+ return data.value;
+ } catch (error) {
+ // Don't log abort errors as they're intentional
+ if (error instanceof Error && error.name === 'AbortError') {
+ return [];
+ }
+ console.error('Error fetching data:', error);
+ return [];
+ }
+ }
+
+ private buildDataUrl(filteringArgs?: FilteringArgs, sortingArgs?: SortingArgs[]): string {
+ const baseQuery = `${this.config.baseUrl}?$count=true&$top=${this.config.pageSize}`;
+ const parts: string[] = [];
+
+ if (sortingArgs && sortingArgs.length > 0) {
+ const sortExpr = this.buildSortExpression(sortingArgs);
+ if (sortExpr) parts.push(sortExpr);
+ }
+
+ if (filteringArgs?.filteringOperands?.length) {
+ const filterExpr = this.buildFilterExpression(filteringArgs);
+ if (filterExpr) parts.push(filterExpr);
+ }
+
+ return parts.length > 0 ? `${baseQuery}&${parts.join('&')}` : baseQuery;
+ }
+
+ private buildFilterExpression(filteringArgs: FilteringArgs): string {
+ if (!filteringArgs?.filteringOperands?.length) return '';
+
+ const expression = this.buildAdvancedFilterExpression(
+ filteringArgs.filteringOperands,
+ filteringArgs.operator
+ );
+
+ return expression ? `$filter=${expression}` : '';
+ }
+
+ private buildAdvancedFilterExpression(operands: FilterOperand[], operator: LOGICAL_OPERATOR): string {
+ const filterParts: string[] = [];
+
+ operands.forEach((operand) => {
+ if (operand.filteringOperands && operand.filteringOperands.length > 0) {
+ const subExpr = this.buildAdvancedFilterExpression(
+ operand.filteringOperands,
+ operand.operator || LOGICAL_OPERATOR.AND
+ );
+ if (subExpr) {
+ filterParts.push(`(${subExpr})`);
+ }
+ return;
+ }
+
+ const { fieldName, searchVal, condition } = operand;
+ if (searchVal === undefined || condition === undefined || !fieldName) return;
+
+ const filterPart = this.buildSingleFilterExpression(fieldName, searchVal, condition.name);
+ if (filterPart) {
+ filterParts.push(filterPart);
+ }
+ });
+
+ const logicalOp = this.getFilteringLogic(operator);
+ return filterParts.join(` ${logicalOp} `);
+ }
+
+ private buildSingleFilterExpression(fieldName: string, searchVal: string | number, conditionName: string): string {
+ // Input validation
+ if (!fieldName || searchVal === null || searchVal === undefined) {
+ return '';
+ }
+
+ const isNumber = typeof searchVal === 'number';
+ // URL encode string values to handle special characters
+ const filterValue = isNumber
+ ? searchVal
+ : `'${encodeURIComponent(String(searchVal).replace(/'/g, "''"))}'`;
+
+ switch (conditionName) {
+ case 'contains':
+ return `${FILTER_OPERATION.CONTAINS}(${fieldName}, ${filterValue})`;
+ case 'startsWith':
+ return `${FILTER_OPERATION.STARTS_WITH}(${fieldName}, ${filterValue})`;
+ case 'endsWith':
+ return `${FILTER_OPERATION.ENDS_WITH}(${fieldName}, ${filterValue})`;
+ case 'equals':
+ return `${fieldName} ${FILTER_OPERATION.EQUALS} ${filterValue}`;
+ case 'doesNotEqual':
+ return `${fieldName} ${FILTER_OPERATION.DOES_NOT_EQUAL} ${filterValue}`;
+ case 'greaterThan':
+ return `${fieldName} ${FILTER_OPERATION.GREATER_THAN} ${filterValue}`;
+ case 'greaterThanOrEqualTo':
+ return `${fieldName} ${FILTER_OPERATION.GREATER_THAN_EQUAL} ${filterValue}`;
+ case 'lessThan':
+ return `${fieldName} ${FILTER_OPERATION.LESS_THAN} ${filterValue}`;
+ case 'lessThanOrEqualTo':
+ return `${fieldName} ${FILTER_OPERATION.LESS_THAN_EQUAL} ${filterValue}`;
+ case 'null':
+ return `${fieldName} ${FILTER_OPERATION.EQUALS} null`;
+ case 'notNull':
+ return `${fieldName} ${FILTER_OPERATION.DOES_NOT_EQUAL} null`;
+ case 'empty':
+ return `length(${fieldName}) ${FILTER_OPERATION.EQUALS} 0`;
+ case 'notEmpty':
+ return `length(${fieldName}) ${FILTER_OPERATION.GREATER_THAN} 0`;
+ case 'true':
+ return `${fieldName} ${FILTER_OPERATION.EQUALS} true`;
+ case 'false':
+ return `${fieldName} ${FILTER_OPERATION.EQUALS} false`;
+ default:
+ console.warn(`Unknown filter condition: ${conditionName}`);
+ return '';
+ }
+ }
+
+ private buildSortExpression(sortingArgs: SortingArgs[]): string {
+ if (!sortingArgs || sortingArgs.length === 0) return '';
+
+ const sortStrings = sortingArgs
+ .filter(sort => sort.fieldName) // filter out invalid entries
+ .map(sort => {
+ const dir = sort.dir === SORT_DIRECTION.DESC ? 'desc' : 'asc';
+ return `${sort.fieldName} ${dir}`;
+ });
+
+ return sortStrings.length > 0 ? `$orderby=${sortStrings.join(',')}` : '';
+ }
+
+ private getFilteringLogic(operator: LOGICAL_OPERATOR): string {
+ switch (operator) {
+ case LOGICAL_OPERATOR.AND:
+ return 'and';
+ case LOGICAL_OPERATOR.OR:
+ return 'or';
+ default:
+ return 'and';
+ }
+ }
+
+ /**
+ * Cancel any pending requests
+ */
+ public cancelPendingRequests(): void {
+ this.abortController?.abort();
+ }
+}
diff --git a/samples/grids/grid/filtering-remote/src/index.css b/samples/grids/grid/filtering-remote/src/index.css
new file mode 100644
index 0000000000..98682b8543
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/src/index.css
@@ -0,0 +1,2 @@
+/* shared styles are loaded from: */
+/* https://static.infragistics.com/xplatform/css/samples */
diff --git a/samples/grids/grid/filtering-remote/src/index.tsx b/samples/grids/grid/filtering-remote/src/index.tsx
new file mode 100644
index 0000000000..95a29b4dd7
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/src/index.tsx
@@ -0,0 +1,146 @@
+import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
+import ReactDOM from 'react-dom/client';
+import { RemoteService } from './RemoteService'
+import { IgrGrid, IgrColumn, GridColumnDataType } from 'igniteui-react-grids';
+import { IgrSortingExpressionEventArgs, IgrFilteringExpressionsTreeEventArgs } from 'igniteui-react-grids';
+import 'igniteui-react-grids/grids/themes/light/bootstrap.css';
+
+const DATA_URL = 'https://services.odata.org/V4/Northwind/Northwind.svc/Products';
+
+interface ProductData {
+ ProductID: number;
+ ProductName: string;
+ SupplierID: number;
+ CategoryID: number;
+ QuantityPerUnit: string;
+ UnitPrice: number;
+ UnitsInStock: number;
+ UnitsOnOrder: number;
+ ReorderLevel: number;
+ Discontinued: boolean;
+}
+
+interface ColumnConfig {
+ field: string;
+ header: string;
+ dataType: GridColumnDataType;
+}
+
+interface GridState {
+ filterExpressions: any;
+ sortExpressions: any[];
+}
+
+// Column configuration outside of component to avoid recreation on every render
+const columnConfig: ColumnConfig[] = [
+ { field: 'ProductID', header: 'Product ID', dataType: 'number' as GridColumnDataType },
+ { field: 'ProductName', header: 'Product Name', dataType: 'string' as GridColumnDataType },
+ { field: 'SupplierID', header: 'Supplier ID', dataType: 'number' as GridColumnDataType },
+ { field: 'CategoryID', header: 'Category ID', dataType: 'number' as GridColumnDataType },
+ { field: 'QuantityPerUnit', header: 'Quantity Per Unit', dataType: 'string' as GridColumnDataType },
+ { field: 'UnitPrice', header: 'Unit Price', dataType: 'number' as GridColumnDataType },
+ { field: 'UnitsInStock', header: 'Units In Stock', dataType: 'number' as GridColumnDataType },
+ { field: 'UnitsOnOrder', header: 'Units On Order', dataType: 'number' as GridColumnDataType },
+ { field: 'ReorderLevel', header: 'Reorder Level', dataType: 'number' as GridColumnDataType },
+ { field: 'Discontinued', header: 'Discontinued', dataType: 'boolean' as GridColumnDataType }
+];
+
+const RemoteFilteringGrid = () => {
+ const [data, setData] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [gridState, setGridState] = useState({
+ filterExpressions: null,
+ sortExpressions: []
+ });
+ const debounceRef = useRef(null);
+
+ const remoteService = useMemo(() => new RemoteService({
+ baseUrl: DATA_URL,
+ pageSize: 1000
+ }), []);
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ remoteService.cancelPendingRequests();
+ if (debounceRef.current) {
+ clearTimeout(debounceRef.current);
+ }
+ };
+ }, [remoteService]);
+
+ const fetchData = useCallback(async (newGridState?: Partial) => {
+ const currentState = newGridState ? { ...gridState, ...newGridState } : gridState;
+
+ setIsLoading(true);
+
+ try {
+ const result = await remoteService.getData(
+ currentState.filterExpressions,
+ currentState.sortExpressions
+ );
+ setData(result);
+
+ if (newGridState) {
+ setGridState(currentState);
+ }
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ setData([]);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [gridState, remoteService]);
+
+ const debouncedFetchData = useCallback((newGridState: Partial) => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = window.setTimeout(() => {
+ fetchData(newGridState);
+ }, 400);
+ }, [fetchData]);
+
+ const handleSortingExpressionsChange = useCallback((event: IgrSortingExpressionEventArgs) => {
+ const sortExpressions = event.detail;
+ debouncedFetchData({ sortExpressions });
+ }, [debouncedFetchData]);
+
+ const handleFilteringExpressionsTreeChange = useCallback((event: IgrFilteringExpressionsTreeEventArgs) => {
+ const filterExpressions = event.detail;
+ debouncedFetchData({ filterExpressions });
+ }, [debouncedFetchData]);
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ return (
+
+
Remote Filtering & Sorting Grid
+
+ {columnConfig.map((col) => (
+
+ ))}
+
+
+ );
+};
+
+// rendering above function to the React DOM
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
+
+export default RemoteFilteringGrid;
diff --git a/samples/grids/grid/filtering-remote/src/react-app-env.d.ts b/samples/grids/grid/filtering-remote/src/react-app-env.d.ts
new file mode 100644
index 0000000000..6431bc5fc6
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/samples/grids/grid/filtering-remote/tsconfig.json b/samples/grids/grid/filtering-remote/tsconfig.json
new file mode 100644
index 0000000000..42c6ace1da
--- /dev/null
+++ b/samples/grids/grid/filtering-remote/tsconfig.json
@@ -0,0 +1,45 @@
+{
+ "compilerOptions": {
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "baseUrl": ".",
+ "outDir": "build/dist",
+ "module": "esnext",
+ "target": "es5",
+ "lib": [
+ "es6",
+ "dom"
+ ],
+ "sourceMap": true,
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "rootDir": "src",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true,
+ "noUnusedLocals": false,
+ "importHelpers": true,
+ "suppressImplicitAnyIndexErrors": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts",
+ "**/odatajs-4.0.0.js",
+ "config-overrides.js"
+ ],
+ "include": [
+ "src"
+ ]
+}
diff --git a/samples/grids/grid/remote-virtualization/.eslintrc.js b/samples/grids/grid/remote-virtualization/.eslintrc.js
new file mode 100644
index 0000000000..7168b71441
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/.eslintrc.js
@@ -0,0 +1,78 @@
+// https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
+module.exports = {
+ parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+ parserOptions: {
+ ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
+ sourceType: "module", // Allows for the use of imports
+ ecmaFeatures: {
+ jsx: true // Allows for the parsing of JSX
+ }
+ },
+ settings: {
+ react: {
+ version: "999.999.999" // Tells eslint-plugin-react to automatically detect the version of React to use
+ }
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
+ "plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
+ ],
+ rules: {
+ // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-prototype-builtins": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-var": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "no-prototype-builtins": "off",
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ }
+ }
+ ]
+ };
\ No newline at end of file
diff --git a/samples/grids/grid/remote-virtualization/ReadMe.md b/samples/grids/grid/remote-virtualization/ReadMe.md
new file mode 100644
index 0000000000..c5ecc99712
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/ReadMe.md
@@ -0,0 +1,56 @@
+
+
+
+This folder contains implementation of React application with example of Remote Virtualization feature using [Grid](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html) component.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Branches
+
+> **_NOTE:_** You should use [master](https://github.com/IgniteUI/igniteui-react-examples/tree/master) branch of this repository if you want to run samples on your computer. Use the [vnext](https://github.com/IgniteUI/igniteui-react-examples/tree/vnext) branch only when you want to contribute new samples to this repository.
+
+## Instructions
+
+Follow these instructions to run this example:
+
+
+```
+git clone https://github.com/IgniteUI/igniteui-react-examples.git
+git checkout master
+cd ./igniteui-react-examples
+cd ./samples/grids/grid/filtering-options
+```
+
+open above folder in VS Code or type:
+```
+code .
+```
+
+In terminal window, run:
+```
+npm install --legacy-peer-deps
+npm run-script start
+```
+
+Then open http://localhost:4200/ in your browser
+
+
+## Learn More
+
+To learn more about **Ignite UI for React** components, check out the [React documentation](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html).
diff --git a/samples/grids/grid/remote-virtualization/package.json b/samples/grids/grid/remote-virtualization/package.json
new file mode 100644
index 0000000000..02dbc47aea
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "example-ignite-ui-react",
+ "description": "This project provides example of using Ignite UI for React components",
+ "author": "Infragistics",
+ "version": "1.4.0",
+ "license": "",
+ "homepage": ".",
+ "private": true,
+ "scripts": {
+ "start": "set PORT=4200 && react-scripts --max_old_space_size=10240 start",
+ "build": "react-scripts --max_old_space_size=10240 build ",
+ "test": "react-scripts test --env=jsdom",
+ "eject": "react-scripts eject",
+ "lint": "eslint ./src/**/*.{ts,tsx}"
+ },
+ "dependencies": {
+ "igniteui-dockmanager": "1.16.1",
+ "igniteui-react": "19.0.2",
+ "igniteui-react-core": "19.0.0",
+ "igniteui-react-grids": "19.0.2",
+ "igniteui-react-inputs": "19.0.0",
+ "igniteui-react-layouts": "19.0.0",
+ "igniteui-webcomponents": "6.0.0",
+ "lit-html": "^3.2.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "^5.0.1",
+ "tslib": "^2.4.0"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.2.0",
+ "@types/node": "^18.11.7",
+ "@types/react": "^18.0.24",
+ "@types/react-dom": "^18.0.8",
+ "eslint": "^8.33.0",
+ "eslint-config-react": "^1.1.7",
+ "eslint-plugin-react": "^7.20.0",
+ "react-app-rewired": "^2.2.1",
+ "typescript": "^4.8.4",
+ "worker-loader": "^3.0.8"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ]
+}
diff --git a/samples/grids/grid/remote-virtualization/public/index.html b/samples/grids/grid/remote-virtualization/public/index.html
new file mode 100644
index 0000000000..e2d3265576
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/public/index.html
@@ -0,0 +1,11 @@
+
+
+
+ Sample | Ignite UI | React | infragistics
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/grids/grid/remote-virtualization/sandbox.config.json b/samples/grids/grid/remote-virtualization/sandbox.config.json
new file mode 100644
index 0000000000..07f53508eb
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/sandbox.config.json
@@ -0,0 +1,5 @@
+{
+ "infiniteLoopProtection": false,
+ "hardReloadOnChange": false,
+ "view": "browser"
+}
\ No newline at end of file
diff --git a/samples/grids/grid/remote-virtualization/src/RemoteService.ts b/samples/grids/grid/remote-virtualization/src/RemoteService.ts
new file mode 100644
index 0000000000..4d23c07239
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/src/RemoteService.ts
@@ -0,0 +1,96 @@
+const DATA_URL = 'https://services.odata.org/V4/Northwind/Northwind.svc/Products';
+
+interface CachedDataItem {
+ emptyRec?: boolean;
+ [key: string]: any;
+}
+
+interface ODataResponse {
+ '@odata.count': number;
+ value: any[];
+}
+
+export class RemoteService {
+ private static cachedData: CachedDataItem[] = [];
+ private static totalCount: number = 0;
+ private static isInitialized: boolean = false;
+
+ public static getData(skip: number = 0, take: number = 50): Promise {
+ try {
+ if (skip < 0 || take <= 0) {
+ console.warn('Invalid parameters: skip must be >= 0 and take must be > 0');
+ return Promise.resolve([]);
+ }
+
+ if (!this.isInitialized) {
+ return this.fetchAndCacheData(skip, take);
+ }
+
+ return this.fetchDataFromCache(skip, take);
+
+ } catch (error) {
+ console.error('Error in getData:', error);
+ return Promise.resolve([]);
+ }
+ }
+
+ public static getTotalCount(): number {
+ return this.totalCount;
+ }
+
+ public static clearCache(): void {
+ this.cachedData = [];
+ this.totalCount = 0;
+ this.isInitialized = false;
+ }
+
+ private static fetchDataFromCache(skip: number, take: number): Promise {
+ const slice = this.cachedData.slice(skip, skip + take);
+ const allLoaded = slice.every(row => row.emptyRec !== true);
+
+ if (allLoaded) {
+ return Promise.resolve(slice);
+ }
+ return this.fetchAndCacheData(skip, take);
+ }
+
+ private static fetchAndCacheData(skip: number, take: number): Promise {
+ const url = `${DATA_URL}?$count=true&$skip=${skip}&$top=${take}`;
+
+ return fetch(url)
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.json();
+ })
+ .then((json: ODataResponse) => {
+ if (!Array.isArray(json.value)) {
+ throw new Error('Invalid response: missing or invalid data array');
+ }
+
+ if (!this.isInitialized) {
+ if (typeof json['@odata.count'] !== 'number') {
+ throw new Error('Invalid response: missing or invalid count');
+ }
+ this.totalCount = json['@odata.count'];
+ this.cachedData = new Array(this.totalCount).fill({ emptyRec: true });
+ this.isInitialized = true;
+ }
+
+ // Cache the fetched data
+ for (let i = 0; i < json.value.length; i++) {
+ const cacheIndex = skip + i;
+ if (cacheIndex < this.cachedData.length) {
+ this.cachedData[cacheIndex] = json.value[i];
+ }
+ }
+
+ return this.cachedData.slice(skip, skip + take);
+ })
+ .catch(error => {
+ console.error(`Error fetching data (skip: ${skip}, take: ${take}):`, error);
+ return this.cachedData.slice(skip, skip + take);
+ });
+ }
+}
diff --git a/samples/grids/grid/remote-virtualization/src/index.css b/samples/grids/grid/remote-virtualization/src/index.css
new file mode 100644
index 0000000000..98682b8543
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/src/index.css
@@ -0,0 +1,2 @@
+/* shared styles are loaded from: */
+/* https://static.infragistics.com/xplatform/css/samples */
diff --git a/samples/grids/grid/remote-virtualization/src/index.tsx b/samples/grids/grid/remote-virtualization/src/index.tsx
new file mode 100644
index 0000000000..2730a8653d
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/src/index.tsx
@@ -0,0 +1,61 @@
+import React, { useEffect, useState } from 'react';
+import ReactDOM from 'react-dom/client';
+import { RemoteService } from './RemoteService';
+import { IgrGrid, IgrColumn } from 'igniteui-react-grids';
+import 'igniteui-react-grids/grids/themes/light/bootstrap.css';
+
+const RemoteVirtualizationGrid = () => {
+ const [data, setData] = useState([]);
+ const [totalCount, setTotalCount] = useState(0);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const fetchData = (skip: number = 0, take: number = 50) => {
+ setIsLoading(true);
+ RemoteService.getData(skip, take)
+ .then(result => {
+ setData(result);
+ const count = RemoteService.getTotalCount();
+ setTotalCount(count);
+ setIsLoading(false);
+ })
+ .catch(error => {
+ console.error('Error fetching data:', error);
+ setIsLoading(false);
+ });
+ };
+
+ const handleDataPreLoad = (event: any) => {
+ const { startIndex, chunkSize } = event.detail;
+ fetchData(startIndex, chunkSize);
+ };
+
+ useEffect(() => {
+ fetchData(); // initial load
+ }, []);
+
+ return (
+
+
Remote Virtualization Grid ({totalCount.toLocaleString()} records)
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
diff --git a/samples/grids/grid/remote-virtualization/src/react-app-env.d.ts b/samples/grids/grid/remote-virtualization/src/react-app-env.d.ts
new file mode 100644
index 0000000000..6431bc5fc6
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/samples/grids/grid/remote-virtualization/tsconfig.json b/samples/grids/grid/remote-virtualization/tsconfig.json
new file mode 100644
index 0000000000..8c0d146f95
--- /dev/null
+++ b/samples/grids/grid/remote-virtualization/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "baseUrl": ".",
+ "outDir": "build/dist",
+ "module": "esnext",
+ "target": "es5",
+ "lib": [
+ "es6",
+ "dom"
+ ],
+ "sourceMap": true,
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "rootDir": "src",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true,
+ "noUnusedLocals": false,
+ "importHelpers": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts",
+ "**/odatajs-4.0.0.js",
+ "config-overrides.js"
+ ],
+ "include": [
+ "src"
+ ]
+}
diff --git a/samples/grids/grid/unique-columns-value/.eslintrc.js b/samples/grids/grid/unique-columns-value/.eslintrc.js
new file mode 100644
index 0000000000..7168b71441
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/.eslintrc.js
@@ -0,0 +1,78 @@
+// https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project
+module.exports = {
+ parser: "@typescript-eslint/parser", // Specifies the ESLint parser
+ parserOptions: {
+ ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
+ sourceType: "module", // Allows for the use of imports
+ ecmaFeatures: {
+ jsx: true // Allows for the parsing of JSX
+ }
+ },
+ settings: {
+ react: {
+ version: "999.999.999" // Tells eslint-plugin-react to automatically detect the version of React to use
+ }
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
+ "plugin:@typescript-eslint/recommended" // Uses the recommended rules from @typescript-eslint/eslint-plugin
+ ],
+ rules: {
+ // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-prototype-builtins": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ },
+ "overrides": [
+ {
+ "files": ["*.ts", "*.tsx"],
+ "rules": {
+ "default-case": "off",
+ "jsx-a11y/alt-text": "off",
+ "jsx-a11y/iframe-has-title": "off",
+ "no-var": "off",
+ "no-undef": "off",
+ "no-unused-vars": "off",
+ "no-extend-native": "off",
+ "no-throw-literal": "off",
+ "no-useless-concat": "off",
+ "no-mixed-operators": "off",
+ "no-mixed-spaces-and-tabs": 0,
+ "no-prototype-builtins": "off",
+ "prefer-const": "off",
+ "prefer-rest-params": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-inferrable-types": "off",
+ "@typescript-eslint/no-useless-constructor": "off",
+ "@typescript-eslint/no-use-before-define": "off",
+ "@typescript-eslint/no-non-null-assertion": "off",
+ "@typescript-eslint/interface-name-prefix": "off",
+ "@typescript-eslint/prefer-namespace-keyword": "off",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off"
+ }
+ }
+ ]
+ };
\ No newline at end of file
diff --git a/samples/grids/grid/unique-columns-value/ReadMe.md b/samples/grids/grid/unique-columns-value/ReadMe.md
new file mode 100644
index 0000000000..f046157de6
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/ReadMe.md
@@ -0,0 +1,56 @@
+
+
+
+This folder contains implementation of React application with example of Unique Column values feature using [Grid](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html) component.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Branches
+
+> **_NOTE:_** You should use [master](https://github.com/IgniteUI/igniteui-react-examples/tree/master) branch of this repository if you want to run samples on your computer. Use the [vnext](https://github.com/IgniteUI/igniteui-react-examples/tree/vnext) branch only when you want to contribute new samples to this repository.
+
+## Instructions
+
+Follow these instructions to run this example:
+
+
+```
+git clone https://github.com/IgniteUI/igniteui-react-examples.git
+git checkout master
+cd ./igniteui-react-examples
+cd ./samples/grids/grid/filtering-options
+```
+
+open above folder in VS Code or type:
+```
+code .
+```
+
+In terminal window, run:
+```
+npm install --legacy-peer-deps
+npm run-script start
+```
+
+Then open http://localhost:4200/ in your browser
+
+
+## Learn More
+
+To learn more about **Ignite UI for React** components, check out the [React documentation](https://www.infragistics.com/products/ignite-ui-react/react/components/general-getting-started.html).
diff --git a/samples/grids/grid/unique-columns-value/package.json b/samples/grids/grid/unique-columns-value/package.json
new file mode 100644
index 0000000000..02dbc47aea
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "example-ignite-ui-react",
+ "description": "This project provides example of using Ignite UI for React components",
+ "author": "Infragistics",
+ "version": "1.4.0",
+ "license": "",
+ "homepage": ".",
+ "private": true,
+ "scripts": {
+ "start": "set PORT=4200 && react-scripts --max_old_space_size=10240 start",
+ "build": "react-scripts --max_old_space_size=10240 build ",
+ "test": "react-scripts test --env=jsdom",
+ "eject": "react-scripts eject",
+ "lint": "eslint ./src/**/*.{ts,tsx}"
+ },
+ "dependencies": {
+ "igniteui-dockmanager": "1.16.1",
+ "igniteui-react": "19.0.2",
+ "igniteui-react-core": "19.0.0",
+ "igniteui-react-grids": "19.0.2",
+ "igniteui-react-inputs": "19.0.0",
+ "igniteui-react-layouts": "19.0.0",
+ "igniteui-webcomponents": "6.0.0",
+ "lit-html": "^3.2.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-scripts": "^5.0.1",
+ "tslib": "^2.4.0"
+ },
+ "devDependencies": {
+ "@types/jest": "^29.2.0",
+ "@types/node": "^18.11.7",
+ "@types/react": "^18.0.24",
+ "@types/react-dom": "^18.0.8",
+ "eslint": "^8.33.0",
+ "eslint-config-react": "^1.1.7",
+ "eslint-plugin-react": "^7.20.0",
+ "react-app-rewired": "^2.2.1",
+ "typescript": "^4.8.4",
+ "worker-loader": "^3.0.8"
+ },
+ "browserslist": [
+ ">0.2%",
+ "not dead",
+ "not ie <= 11",
+ "not op_mini all"
+ ]
+}
diff --git a/samples/grids/grid/unique-columns-value/public/index.html b/samples/grids/grid/unique-columns-value/public/index.html
new file mode 100644
index 0000000000..e2d3265576
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/public/index.html
@@ -0,0 +1,11 @@
+
+
+
+ Sample | Ignite UI | React | infragistics
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/grids/grid/unique-columns-value/sandbox.config.json b/samples/grids/grid/unique-columns-value/sandbox.config.json
new file mode 100644
index 0000000000..07f53508eb
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/sandbox.config.json
@@ -0,0 +1,5 @@
+{
+ "infiniteLoopProtection": false,
+ "hardReloadOnChange": false,
+ "view": "browser"
+}
\ No newline at end of file
diff --git a/samples/grids/grid/unique-columns-value/src/RemoteService.ts b/samples/grids/grid/unique-columns-value/src/RemoteService.ts
new file mode 100644
index 0000000000..89bec71973
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/src/RemoteService.ts
@@ -0,0 +1,364 @@
+import { IgrColumn, IgrFilteringExpressionsTree, IgrFilteringStrategy } from "igniteui-react-grids";
+
+export enum FILTER_OPERATION {
+ CONTAINS = 'contains',
+ STARTS_WITH = 'startswith',
+ ENDS_WITH = 'endswith',
+ EQUALS = 'eq',
+ DOES_NOT_EQUAL = 'ne',
+ GREATER_THAN = 'gt',
+ LESS_THAN = 'lt',
+ LESS_THAN_EQUAL = 'le',
+ GREATER_THAN_EQUAL = 'ge'
+}
+
+export enum LOGICAL_OPERATOR {
+ AND = 0,
+ OR = 1
+}
+
+export enum SORT_DIRECTION {
+ ASC = 1,
+ DESC = 2
+}
+
+interface FilterCondition {
+ name: string;
+}
+
+interface FilterOperand {
+ fieldName: string;
+ searchVal: string | number;
+ condition: FilterCondition;
+ filteringOperands?: FilterOperand[];
+ operator?: LOGICAL_OPERATOR;
+}
+
+interface FilteringArgs {
+ filteringOperands: FilterOperand[];
+ operator: LOGICAL_OPERATOR;
+}
+
+interface SortingArgs {
+ fieldName: string;
+ dir: SORT_DIRECTION;
+}
+
+interface ODataResponse {
+ '@odata.count'?: number;
+ value: any[];
+}
+
+export interface RemoteServiceConfig {
+ baseUrl: string;
+ pageSize?: number;
+ filteringStrategy?: IgrFilteringStrategy;
+ /** Cache TTL for unique column values in milliseconds (default: 30000 = 30 seconds) */
+ uniqueValuesCacheTtl?: number;
+}
+
+interface CacheEntry {
+ data: T;
+ timestamp: number;
+}
+
+export class RemoteService {
+ private config: RemoteServiceConfig;
+ private dataAbortController?: AbortController;
+ private columnDataAbortController?: AbortController;
+ private uniqueValuesCache: Map> = new Map();
+
+ constructor(config: RemoteServiceConfig) {
+ this.config = {
+ pageSize: 1000,
+ uniqueValuesCacheTtl: 30000,
+ ...config
+ };
+ }
+
+ public setFilteringStrategy(strategy: IgrFilteringStrategy): void {
+ this.config.filteringStrategy = strategy;
+ }
+
+ public async getData(
+ filteringArgs?: FilteringArgs,
+ sortingArgs?: SortingArgs[]
+ ): Promise {
+ // Cancel any in-flight data request
+ this.dataAbortController?.abort();
+ this.dataAbortController = new AbortController();
+
+ try {
+ const url = this.buildDataUrl(filteringArgs, sortingArgs);
+ const response = await fetch(url, { signal: this.dataAbortController.signal });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data: ODataResponse = await response.json();
+
+ if (!Array.isArray(data.value)) {
+ throw new Error('Invalid response: missing or invalid data array');
+ }
+
+ return data.value;
+ } catch (error) {
+ // Don't log abort errors as they're intentional
+ if (error instanceof Error && error.name === 'AbortError') {
+ return [];
+ }
+ console.error('Error fetching data:', error);
+ return [];
+ }
+ }
+
+ private buildDataUrl(filteringArgs?: FilteringArgs, sortingArgs?: SortingArgs[]): string {
+ const baseQuery = `${this.config.baseUrl}?$count=true&$top=${this.config.pageSize}`;
+ const parts: string[] = [];
+
+ if (sortingArgs && sortingArgs.length > 0) {
+ const sortExpr = this.buildSortExpression(sortingArgs);
+ if (sortExpr) parts.push(sortExpr);
+ }
+
+ if (filteringArgs?.filteringOperands?.length) {
+ const filterExpr = this.buildFilterExpression(filteringArgs);
+ if (filterExpr) parts.push(filterExpr);
+ }
+
+ return parts.length > 0 ? `${baseQuery}&${parts.join('&')}` : baseQuery;
+ }
+
+ public async getColumnData(
+ column: IgrColumn,
+ columnExpTree: IgrFilteringExpressionsTree,
+ done: (values: any[]) => void
+ ): Promise {
+ try {
+ // Try to use OData $apply for true remote distinct values
+ const uniqueValues = await this.fetchUniqueColumnValues(column.field);
+
+ if (uniqueValues.length > 0) {
+ done(uniqueValues);
+ return;
+ }
+
+ // Fallback: fetch all data and filter client-side
+ const data = await this.getData();
+
+ if (!this.config.filteringStrategy) {
+ console.warn('Filtering strategy not initialized');
+ done([]);
+ return;
+ }
+
+ const filteredData = this.config.filteringStrategy.filter(data, columnExpTree, null, null);
+ const columnValues = filteredData.map((record: any) => record[column.field]);
+ const uniqueColumnValues = Array.from(new Set(columnValues));
+
+ done(uniqueColumnValues);
+ } catch (error) {
+ console.error('Error fetching column data:', error);
+ done([]);
+ }
+ }
+
+ private async fetchUniqueColumnValues(fieldName: string): Promise {
+ // Check cache first
+ const cachedValues = this.getCachedValues(fieldName);
+ if (cachedValues) {
+ return cachedValues;
+ }
+
+ // Cancel any in-flight column data request
+ this.columnDataAbortController?.abort();
+ this.columnDataAbortController = new AbortController();
+
+ try {
+ // Use OData $apply with groupby for distinct values
+ const url = `${this.config.baseUrl}?$apply=groupby((${fieldName}))`;
+ const response = await fetch(url, { signal: this.columnDataAbortController.signal });
+
+ if (!response.ok) {
+ // If $apply is not supported, return empty to trigger fallback
+ return [];
+ }
+
+ const data: ODataResponse = await response.json();
+
+ if (!Array.isArray(data.value)) {
+ return [];
+ }
+
+ const values = data.value.map(item => item[fieldName]);
+
+ // Cache the results
+ this.setCachedValues(fieldName, values);
+
+ return values;
+ } catch (error) {
+ // Don't log abort errors as they're intentional
+ if (error instanceof Error && error.name === 'AbortError') {
+ return [];
+ }
+ // $apply might not be supported, return empty to trigger fallback
+ return [];
+ }
+ }
+
+ private buildFilterExpression(filteringArgs: FilteringArgs): string {
+ if (!filteringArgs?.filteringOperands?.length) return '';
+
+ const expression = this.buildAdvancedFilterExpression(
+ filteringArgs.filteringOperands,
+ filteringArgs.operator
+ );
+
+ return expression ? `$filter=${expression}` : '';
+ }
+
+ private buildAdvancedFilterExpression(operands: FilterOperand[], operator: LOGICAL_OPERATOR): string {
+ const filterParts: string[] = [];
+
+ operands.forEach((operand) => {
+ if (operand.filteringOperands && operand.filteringOperands.length > 0) {
+ const subExpr = this.buildAdvancedFilterExpression(
+ operand.filteringOperands,
+ operand.operator || LOGICAL_OPERATOR.AND
+ );
+ if (subExpr) {
+ filterParts.push(`(${subExpr})`);
+ }
+ return;
+ }
+
+ const { fieldName, searchVal, condition } = operand;
+ if (searchVal === undefined || condition === undefined || !fieldName) return;
+
+ const filterPart = this.buildSingleFilterExpression(fieldName, searchVal, condition.name);
+ if (filterPart) {
+ filterParts.push(filterPart);
+ }
+ });
+
+ const logicalOp = this.getFilteringLogic(operator);
+ return filterParts.join(` ${logicalOp} `);
+ }
+
+ private buildSingleFilterExpression(fieldName: string, searchVal: string | number, conditionName: string): string {
+ // Input validation
+ if (!fieldName || searchVal === null || searchVal === undefined) {
+ return '';
+ }
+
+ const isNumber = typeof searchVal === 'number';
+ // URL encode string values to handle special characters
+ const filterValue = isNumber
+ ? searchVal
+ : `'${encodeURIComponent(String(searchVal).replace(/'/g, "''"))}'`;
+
+ switch (conditionName) {
+ case 'contains':
+ return `${FILTER_OPERATION.CONTAINS}(${fieldName}, ${filterValue})`;
+ case 'startsWith':
+ return `${FILTER_OPERATION.STARTS_WITH}(${fieldName}, ${filterValue})`;
+ case 'endsWith':
+ return `${FILTER_OPERATION.ENDS_WITH}(${fieldName}, ${filterValue})`;
+ case 'equals':
+ return `${fieldName} ${FILTER_OPERATION.EQUALS} ${filterValue}`;
+ case 'doesNotEqual':
+ return `${fieldName} ${FILTER_OPERATION.DOES_NOT_EQUAL} ${filterValue}`;
+ case 'greaterThan':
+ return `${fieldName} ${FILTER_OPERATION.GREATER_THAN} ${filterValue}`;
+ case 'greaterThanOrEqualTo':
+ return `${fieldName} ${FILTER_OPERATION.GREATER_THAN_EQUAL} ${filterValue}`;
+ case 'lessThan':
+ return `${fieldName} ${FILTER_OPERATION.LESS_THAN} ${filterValue}`;
+ case 'lessThanOrEqualTo':
+ return `${fieldName} ${FILTER_OPERATION.LESS_THAN_EQUAL} ${filterValue}`;
+ case 'null':
+ return `${fieldName} ${FILTER_OPERATION.EQUALS} null`;
+ case 'notNull':
+ return `${fieldName} ${FILTER_OPERATION.DOES_NOT_EQUAL} null`;
+ case 'empty':
+ return `length(${fieldName}) ${FILTER_OPERATION.EQUALS} 0`;
+ case 'notEmpty':
+ return `length(${fieldName}) ${FILTER_OPERATION.GREATER_THAN} 0`;
+ case 'true':
+ return `${fieldName} ${FILTER_OPERATION.EQUALS} true`;
+ case 'false':
+ return `${fieldName} ${FILTER_OPERATION.EQUALS} false`;
+ default:
+ console.warn(`Unknown filter condition: ${conditionName}`);
+ return '';
+ }
+ }
+
+ private buildSortExpression(sortingArgs: SortingArgs[]): string {
+ if (!sortingArgs || sortingArgs.length === 0) return '';
+
+ const sortStrings = sortingArgs
+ .filter(sort => sort.fieldName) // filter out invalid entries
+ .map(sort => {
+ const dir = sort.dir === SORT_DIRECTION.DESC ? 'desc' : 'asc';
+ return `${sort.fieldName} ${dir}`;
+ });
+
+ return sortStrings.length > 0 ? `$orderby=${sortStrings.join(',')}` : '';
+ }
+
+ private getFilteringLogic(operator: LOGICAL_OPERATOR): string {
+ switch (operator) {
+ case LOGICAL_OPERATOR.AND:
+ return 'and';
+ case LOGICAL_OPERATOR.OR:
+ return 'or';
+ default:
+ return 'and';
+ }
+ }
+
+ /**
+ * Cancel any pending data requests
+ */
+ public cancelPendingRequests(): void {
+ this.dataAbortController?.abort();
+ this.columnDataAbortController?.abort();
+ }
+
+ /**
+ * Clear the unique values cache (useful when data changes)
+ */
+ public clearCache(): void {
+ this.uniqueValuesCache.clear();
+ }
+
+ private isCacheValid(fieldName: string): boolean {
+ const entry = this.uniqueValuesCache.get(fieldName);
+ if (!entry) return false;
+
+ const now = Date.now();
+ const ttl = this.config.uniqueValuesCacheTtl!;
+ return (now - entry.timestamp) < ttl;
+ }
+
+ private getCachedValues(fieldName: string): any[] | null {
+ if (this.isCacheValid(fieldName)) {
+ return this.uniqueValuesCache.get(fieldName)!.data;
+ }
+ return null;
+ }
+
+ private setCachedValues(fieldName: string, values: any[]): void {
+ this.uniqueValuesCache.set(fieldName, {
+ data: values,
+ timestamp: Date.now()
+ });
+ }
+}
+
+// Export a default instance for convenience
+export const remoteService = new RemoteService({
+ baseUrl: 'https://services.odata.org/V4/Northwind/Northwind.svc/Products'
+});
diff --git a/samples/grids/grid/unique-columns-value/src/index.css b/samples/grids/grid/unique-columns-value/src/index.css
new file mode 100644
index 0000000000..98682b8543
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/src/index.css
@@ -0,0 +1,2 @@
+/* shared styles are loaded from: */
+/* https://static.infragistics.com/xplatform/css/samples */
diff --git a/samples/grids/grid/unique-columns-value/src/index.tsx b/samples/grids/grid/unique-columns-value/src/index.tsx
new file mode 100644
index 0000000000..fad390fe4c
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/src/index.tsx
@@ -0,0 +1,102 @@
+import React, { useEffect, useState, useRef, useMemo } from 'react';
+import ReactDOM from 'react-dom/client';
+import { RemoteService } from './RemoteService'
+import { IgrGrid, IgrColumn, IgrFilteringExpressionsTree } from 'igniteui-react-grids';
+import { IgrSortingExpressionEventArgs, IgrFilteringExpressionsTreeEventArgs } from 'igniteui-react-grids';
+import 'igniteui-react-grids/grids/themes/light/bootstrap.css';
+
+const DATA_URL = 'https://services.odata.org/V4/Northwind/Northwind.svc/Products';
+
+const RemoteFilteringGrid = () => {
+ const [data, setData] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [currentFilterExpressions, setCurrentFilterExpressions] = useState(null);
+ const [currentSortExpressions, setCurrentSortExpressions] = useState([]);
+ const debounceRef = useRef(null);
+
+ const remoteService = useMemo(() => new RemoteService({
+ baseUrl: DATA_URL,
+ pageSize: 1000
+ }), []);
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ remoteService.cancelPendingRequests();
+ };
+ }, [remoteService]);
+
+ const fetchData = async (filterExpressions: any = null, sortExpressions: any[] = []) => {
+ setIsLoading(true);
+
+ try {
+ const result = await remoteService.getData(filterExpressions, sortExpressions);
+ setData(result);
+ } catch (error) {
+ console.error('Error fetching data:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleSortingExpressionsChange = (event: IgrSortingExpressionEventArgs) => {
+ const sortExpressions = event.detail;
+ setCurrentSortExpressions(sortExpressions);
+
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(() => {
+ fetchData(currentFilterExpressions, sortExpressions);
+ }, 300);
+ };
+
+ const handleFilteringExpressionsTreeChange = (event: IgrFilteringExpressionsTreeEventArgs) => {
+ const filterExpressions = event.detail;
+ setCurrentFilterExpressions(filterExpressions);
+
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(() => {
+ fetchData(filterExpressions, currentSortExpressions);
+ }, 500);
+ };
+
+ const columnValuesStrategy = (column: IgrColumn, columnExpTree: IgrFilteringExpressionsTree, done: (values: any[]) => void) => {
+ remoteService.getColumnData(column, columnExpTree, done);
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ return (
+
+ Remote Filtering & Sorting Grid with Unique Column Values
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+// rendering above function to the React DOM
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render();
+
+export default RemoteFilteringGrid;
diff --git a/samples/grids/grid/unique-columns-value/src/react-app-env.d.ts b/samples/grids/grid/unique-columns-value/src/react-app-env.d.ts
new file mode 100644
index 0000000000..6431bc5fc6
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/src/react-app-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/samples/grids/grid/unique-columns-value/tsconfig.json b/samples/grids/grid/unique-columns-value/tsconfig.json
new file mode 100644
index 0000000000..8c0d146f95
--- /dev/null
+++ b/samples/grids/grid/unique-columns-value/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ "compilerOptions": {
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "baseUrl": ".",
+ "outDir": "build/dist",
+ "module": "esnext",
+ "target": "es5",
+ "lib": [
+ "es6",
+ "dom"
+ ],
+ "sourceMap": true,
+ "allowJs": true,
+ "jsx": "react-jsx",
+ "moduleResolution": "node",
+ "rootDir": "src",
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noImplicitAny": true,
+ "noUnusedLocals": false,
+ "importHelpers": true,
+ "allowSyntheticDefaultImports": true,
+ "skipLibCheck": true,
+ "strict": false,
+ "isolatedModules": true,
+ "noEmit": true
+ },
+ "exclude": [
+ "node_modules",
+ "build",
+ "scripts",
+ "acceptance-tests",
+ "webpack",
+ "jest",
+ "src/setupTests.ts",
+ "**/odatajs-4.0.0.js",
+ "config-overrides.js"
+ ],
+ "include": [
+ "src"
+ ]
+}