diff --git a/package-lock.json b/package-lock.json
index b3898ea828..910f476846 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -182,9 +182,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -202,9 +199,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -222,9 +216,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -242,9 +233,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -545,9 +533,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -562,9 +547,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -579,9 +561,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -596,9 +575,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -1433,9 +1409,6 @@
"arm"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1453,9 +1426,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1473,9 +1443,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1493,9 +1460,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1513,9 +1477,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1533,9 +1494,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1553,9 +1511,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1573,9 +1528,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -1593,9 +1545,6 @@
"arm"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1619,9 +1568,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1645,9 +1591,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1671,9 +1614,6 @@
"riscv64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1697,9 +1637,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1723,9 +1660,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1749,9 +1683,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -1775,9 +1706,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2093,6 +2021,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2113,6 +2042,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2133,6 +2063,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2153,6 +2084,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2173,9 +2105,7 @@
"cpu": [
"arm"
],
- "libc": [
- "glibc"
- ],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2196,9 +2126,7 @@
"cpu": [
"arm"
],
- "libc": [
- "musl"
- ],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2219,9 +2147,7 @@
"cpu": [
"arm64"
],
- "libc": [
- "glibc"
- ],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2242,9 +2168,7 @@
"cpu": [
"arm64"
],
- "libc": [
- "musl"
- ],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2265,9 +2189,7 @@
"cpu": [
"x64"
],
- "libc": [
- "glibc"
- ],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2288,9 +2210,7 @@
"cpu": [
"x64"
],
- "libc": [
- "musl"
- ],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2311,6 +2231,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2331,6 +2252,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2470,9 +2392,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2490,9 +2409,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2510,9 +2426,6 @@
"ppc64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2530,9 +2443,6 @@
"s390x"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2550,9 +2460,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2570,9 +2477,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2873,9 +2777,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2893,9 +2794,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2913,9 +2811,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -2933,9 +2828,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MIT",
"optional": true,
"os": [
@@ -5562,9 +5454,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -5586,9 +5475,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -5610,9 +5496,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -5634,9 +5517,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -6891,7 +6771,6 @@
"cpu": [
"arm"
],
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -6908,7 +6787,6 @@
"cpu": [
"arm64"
],
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -6925,7 +6803,6 @@
"cpu": [
"arm"
],
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -6942,7 +6819,6 @@
"cpu": [
"arm64"
],
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -6959,7 +6835,6 @@
"cpu": [
"riscv64"
],
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -6976,7 +6851,6 @@
"cpu": [
"x64"
],
- "libc": "musl",
"license": "MIT",
"optional": true,
"os": [
@@ -6993,7 +6867,6 @@
"cpu": [
"riscv64"
],
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -7010,7 +6883,6 @@
"cpu": [
"x64"
],
- "libc": "glibc",
"license": "MIT",
"optional": true,
"os": [
@@ -8278,9 +8150,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -8302,9 +8171,6 @@
"arm64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -8326,9 +8192,6 @@
"x64"
],
"dev": true,
- "libc": [
- "glibc"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
@@ -8350,9 +8213,6 @@
"x64"
],
"dev": true,
- "libc": [
- "musl"
- ],
"license": "MPL-2.0",
"optional": true,
"os": [
diff --git a/samples/inputs/combo/remote-binding/README.md b/samples/inputs/combo/remote-binding/README.md
new file mode 100644
index 0000000000..80076743d1
--- /dev/null
+++ b/samples/inputs/combo/remote-binding/README.md
@@ -0,0 +1,15 @@
+This folder contains a React application with the Combo Remote Binding example.
+
+- [Run in the samples browser](https://www.infragistics.com/react-demos/inputs/combo/remote-binding)
+- [Source](./src/index.tsx)
+
+## Run locally
+
+```
+git clone https://github.com/IgniteUI/igniteui-react-examples.git
+cd igniteui-react-examples/samples/inputs/combo/remote-binding
+npm install
+npm start
+```
+
+Then open http://localhost:4200/ in your browser.
diff --git a/samples/inputs/combo/remote-binding/index.html b/samples/inputs/combo/remote-binding/index.html
new file mode 100644
index 0000000000..ed6a157174
--- /dev/null
+++ b/samples/inputs/combo/remote-binding/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Combo Remote Binding
+
+
+
+
+
+
+
+
diff --git a/samples/inputs/combo/remote-binding/package.json b/samples/inputs/combo/remote-binding/package.json
new file mode 100644
index 0000000000..c546dd50b9
--- /dev/null
+++ b/samples/inputs/combo/remote-binding/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "react-combo-remote-binding",
+ "description": "This project provides example of Combo Remote Binding using Infragistics React components",
+ "author": "Infragistics",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "start": "vite --port 4200",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "igniteui-react": "19.9.0",
+ "igniteui-webcomponents": "~7.4.1",
+ "react": "^19.3.0",
+ "react-dom": "^19.3.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.3.0",
+ "@types/react-dom": "^19.3.0",
+ "@vitejs/plugin-react": "^6.1.1",
+ "typescript": "^5.9.3",
+ "vite": "^8.3.1"
+ }
+}
diff --git a/samples/inputs/combo/remote-binding/src/ProductsService.ts b/samples/inputs/combo/remote-binding/src/ProductsService.ts
new file mode 100644
index 0000000000..480d70629e
--- /dev/null
+++ b/samples/inputs/combo/remote-binding/src/ProductsService.ts
@@ -0,0 +1,59 @@
+export interface Product {
+ ProductID: number;
+ ProductName: string;
+}
+
+export interface ProductsPage {
+ items: Product[];
+ /** How many products match the query on the server, across all pages. */
+ total: number;
+}
+
+export interface ProductsQuery {
+ search: string;
+ skip: number;
+ top: number;
+}
+
+const PRODUCTS_URL = 'https://services.odata.org/V4/Northwind/Northwind.svc/Products';
+
+/** Builds an OData request for one page of products whose name contains `search`. */
+export function buildProductsUrl({ search, skip, top }: ProductsQuery): string {
+ const params = [
+ '$select=ProductID,ProductName',
+ '$orderby=ProductName',
+ '$count=true',
+ `$skip=${skip}`,
+ `$top=${top}`,
+ ];
+
+ if (search) {
+ // OData escapes a single quote inside a string literal by doubling it.
+ const term = search.toLowerCase().replace(/'/g, "''");
+ params.push(`$filter=contains(tolower(ProductName),'${encodeURIComponent(term)}')`);
+ }
+
+ return `${PRODUCTS_URL}?${params.join('&')}`;
+}
+
+export async function fetchProducts(url: string, signal: AbortSignal): Promise {
+ const response = await fetch(url, { signal });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status} ${response.statusText}`);
+ }
+
+ const json = await response.json();
+ return { items: json.value, total: json['@odata.count'] };
+}
+
+/** Waits `ms`, or rejects as soon as `signal` aborts. */
+export function delay(ms: number, signal: AbortSignal): Promise {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(resolve, ms);
+ signal.addEventListener('abort', () => {
+ clearTimeout(timer);
+ reject(signal.reason);
+ }, { once: true });
+ });
+}
diff --git a/samples/inputs/combo/remote-binding/src/index.css b/samples/inputs/combo/remote-binding/src/index.css
new file mode 100644
index 0000000000..92370b9ab1
--- /dev/null
+++ b/samples/inputs/combo/remote-binding/src/index.css
@@ -0,0 +1,144 @@
+/* shared styles are loaded from: */
+/* https://dl.infragistics.com/x/css/samples/shared.v8.css */
+
+.sample {
+ gap: 24px;
+ max-width: 760px;
+}
+
+/* Slotted into the combo's dropdown */
+
+/* A thin bar at the top of the dropdown, instead of Bootstrap's 1rem track */
+.combo-progress {
+ --ig-linear-bar-track-height: 4px;
+ --ig-linear-bar-track-border-radius: 0;
+}
+
+.combo-empty {
+ padding: 12px 16px;
+ color: var(--ig-gray-700);
+}
+
+.combo-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ min-height: 40px;
+ padding: 0 4px 0 16px;
+ border-top: 1px solid var(--ig-gray-300);
+}
+
+.combo-footer-text {
+ font-size: 0.8125rem;
+ color: var(--ig-gray-700);
+}
+
+/* Background operations log */
+
+.log {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--ig-gray-300);
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+.log-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px 16px;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--ig-gray-300);
+ background: var(--ig-gray-50);
+}
+
+/* Two classes, to win over the theme's `.ig-typography h2` */
+.log .log-title {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 600;
+ line-height: 1.5;
+ letter-spacing: normal;
+}
+
+.log-options {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 16px;
+}
+
+.log-entries {
+ height: 280px;
+ margin: 0;
+ padding: 4px 0;
+ overflow-y: auto;
+ list-style: none;
+ font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+}
+
+.log-entry {
+ --kind-color: var(--ig-gray-700);
+
+ display: grid;
+ grid-template-columns: auto 5.5rem 1fr;
+ align-items: baseline;
+ gap: 12px;
+ padding: 2px 12px;
+}
+
+.log-entry--event {
+ --kind-color: var(--ig-secondary-700);
+}
+
+.log-entry--request {
+ --kind-color: var(--ig-primary-600);
+}
+
+.log-entry--response,
+.log-entry--data {
+ --kind-color: var(--ig-success-700);
+}
+
+.log-entry--abort {
+ --kind-color: var(--ig-warn-800);
+}
+
+.log-entry--error {
+ --kind-color: var(--ig-error-600);
+}
+
+.log-entry--error .log-message {
+ color: var(--ig-error-600);
+}
+
+.log-time {
+ color: var(--ig-gray-600);
+}
+
+.log-kind {
+ justify-self: start;
+ padding: 0 6px;
+ border-radius: 4px;
+ font-size: 0.6875rem;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--kind-color);
+ background: color-mix(in srgb, var(--kind-color) 12%, transparent);
+}
+
+.log-message {
+ min-width: 0;
+}
+
+.log-detail {
+ display: block;
+ color: var(--ig-gray-600);
+ word-break: break-all;
+}
diff --git a/samples/inputs/combo/remote-binding/src/index.tsx b/samples/inputs/combo/remote-binding/src/index.tsx
new file mode 100644
index 0000000000..f49c7b40ff
--- /dev/null
+++ b/samples/inputs/combo/remote-binding/src/index.tsx
@@ -0,0 +1,290 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import ReactDOM from 'react-dom/client';
+import {
+ IgrButton,
+ IgrCombo,
+ IgrComboChangeEventArgs,
+ IgrLinearProgress,
+ IgrSwitch,
+} from 'igniteui-react';
+import 'igniteui-webcomponents/themes/light/bootstrap.css';
+import './index.css';
+import { buildProductsUrl, delay, fetchProducts, Product, ProductsQuery } from './ProductsService';
+
+const PAGE_SIZE = 20;
+const DEBOUNCE_MS = 300;
+const SLOW_NETWORK_MS = 1500;
+const MAX_LOG_ENTRIES = 200;
+
+type Status = 'idle' | 'loading' | 'error';
+
+type LogKind = 'event' | 'input' | 'info' | 'request' | 'response' | 'data' | 'abort' | 'error';
+
+interface LogEntry {
+ id: number;
+ time: Date;
+ kind: LogKind;
+ message: string;
+ /** Secondary line, e.g. the request URL. */
+ detail?: string;
+}
+
+let nextLogId = 0;
+
+const createEntry = (kind: LogKind, message: string, detail?: string): LogEntry => ({
+ id: nextLogId++,
+ time: new Date(),
+ kind,
+ message,
+ detail,
+});
+
+const formatTime = (time: Date) =>
+ `${time.toLocaleTimeString('en-GB', { hour12: false })}.${String(time.getMilliseconds()).padStart(3, '0')}`;
+
+export default function ComboRemoteBinding() {
+ const comboRef = useRef(null);
+ const logRef = useRef(null);
+
+ // What the combo renders. The remote results, plus any selected products they don't contain.
+ const [data, setData] = useState([]);
+ const [search, setSearch] = useState('');
+ const [loaded, setLoaded] = useState(0);
+ const [total, setTotal] = useState(null);
+ const [status, setStatus] = useState('idle');
+ const [slowNetwork, setSlowNetwork] = useState(false);
+ const [failRequests, setFailRequests] = useState(false);
+ const [entries, setEntries] = useState(() => [
+ createEntry('info', 'Ready. Open the combo to load the first page of products.'),
+ ]);
+
+ // Read inside async callbacks, so they live in refs rather than state.
+ const results = useRef([]);
+ const selected = useRef(new Map());
+ const appliedSearch = useRef('');
+ const lastQuery = useRef(null);
+ const inFlight = useRef(null);
+ const debounceTimer = useRef(undefined);
+ const simulation = useRef({ slowNetwork: false, failRequests: false });
+
+ const log = useCallback((kind: LogKind, message: string, detail?: string) => {
+ setEntries(prev => [...prev, createEntry(kind, message, detail)].slice(-MAX_LOG_ENTRIES));
+ }, []);
+
+ const load = useCallback(async (query: ProductsQuery) => {
+ if (inFlight.current) {
+ inFlight.current.abort();
+ log('abort', 'Cancelled the request in flight: its results would be stale');
+ }
+
+ const controller = new AbortController();
+ inFlight.current = controller;
+ lastQuery.current = query;
+
+ const url = buildProductsUrl(query);
+ const page = query.skip / query.top + 1;
+ log('request', `GET page ${page} of products${query.search ? ` matching "${query.search}"` : ''}`, decodeURIComponent(url));
+ setStatus('loading');
+ const started = performance.now();
+ const elapsed = () => Math.round(performance.now() - started);
+
+ try {
+ if (simulation.current.slowNetwork) {
+ log('info', `Slow network: holding the request for ${SLOW_NETWORK_MS / 1000} s`);
+ await delay(SLOW_NETWORK_MS, controller.signal);
+ }
+ if (simulation.current.failRequests) {
+ throw new Error('Simulated failure (HTTP 500 Internal Server Error)');
+ }
+
+ const result = await fetchProducts(url, controller.signal);
+ const rows = query.skip === 0 ? result.items : [...results.current, ...result.items];
+ results.current = rows;
+ log('response', `200 OK in ${elapsed()} ms: ${result.items.length} products, ${rows.length} of ${result.total} loaded`);
+
+ // A selected product missing from the new results stays in the data:
+ // the combo drops a selected value it can't find in its data.
+ const ids = new Set(rows.map(p => p.ProductID));
+ const kept = [...selected.current.values()].filter(p => !ids.has(p.ProductID));
+ setData([...kept, ...rows]);
+ setLoaded(rows.length);
+ setTotal(result.total);
+ setStatus('idle');
+ log('data', `Bound ${rows.length + kept.length} items to the combo` +
+ (kept.length ? `, ${kept.length} of them selected earlier and kept so the selection survives` : ''));
+ } catch (error) {
+ // The request that aborted this one has already logged it.
+ if (controller.signal.aborted) return;
+ setStatus('error');
+ log('error', `Request failed after ${elapsed()} ms: ${(error as Error).message}`);
+ } finally {
+ if (inFlight.current === controller) {
+ inFlight.current = null;
+ }
+ }
+ }, [log]);
+
+ // The combo has no event of its own for its search input, but the input's
+ // igcInput event bubbles out of the combo's shadow DOM.
+ useEffect(() => {
+ const combo = comboRef.current;
+ if (!combo) return;
+
+ const handleSearchInput = (event: Event) => {
+ const text = (event as CustomEvent).detail;
+ const term = text.trim();
+ log('input', `Search input: "${text}"`);
+
+ window.clearTimeout(debounceTimer.current);
+ debounceTimer.current = window.setTimeout(() => {
+ if (term === appliedSearch.current) {
+ log('info', `Typing paused, and the results for "${term}" are already loaded: no request`);
+ return;
+ }
+ log('info', `Typing paused for ${DEBOUNCE_MS} ms: searching on the server`);
+ appliedSearch.current = term;
+ setSearch(term);
+ void load({ search: term, skip: 0, top: PAGE_SIZE });
+ }, DEBOUNCE_MS);
+ };
+
+ combo.addEventListener('igcInput', handleSearchInput);
+ return () => combo.removeEventListener('igcInput', handleSearchInput);
+ }, [load, log]);
+
+ useEffect(() => () => {
+ window.clearTimeout(debounceTimer.current);
+ inFlight.current?.abort();
+ }, []);
+
+ // Keep the newest entry in view.
+ useEffect(() => {
+ const list = logRef.current;
+ if (list) {
+ list.scrollTop = list.scrollHeight;
+ }
+ }, [entries]);
+
+ const handleOpening = () => {
+ log('event', 'Dropdown opening');
+ // Nothing is loaded until the first open, and a failed first load retries on the next one.
+ if (total === null && !inFlight.current) {
+ log('info', 'Nothing loaded yet: requesting the first page');
+ void load({ search: appliedSearch.current, skip: 0, top: PAGE_SIZE });
+ }
+ };
+
+ const handleChange = (event: IgrComboChangeEventArgs) => {
+ const { newValue, type } = event.detail;
+ const items = event.detail.items as Product[];
+
+ if (newValue.length === 0) {
+ selected.current.clear();
+ log('event', 'Selection cleared');
+ return;
+ }
+
+ for (const item of items) {
+ if (type === 'deselection') {
+ selected.current.delete(item.ProductID);
+ } else {
+ selected.current.set(item.ProductID, item);
+ }
+ }
+ const names = items.map(p => p.ProductName).join(', ');
+ log('event', `${type === 'deselection' ? 'Deselected' : 'Selected'} ${names} (${newValue.length} selected)`);
+ };
+
+ const loadMore = () => {
+ log('event', 'Load more clicked');
+ void load({ search: appliedSearch.current, skip: results.current.length, top: PAGE_SIZE });
+ };
+
+ const retry = () => {
+ log('event', 'Retry clicked');
+ if (lastQuery.current) {
+ void load(lastQuery.current);
+ }
+ };
+
+ const emptyMessage =
+ status === 'loading' ? 'Loading products…' :
+ status === 'error' ? 'Couldn’t load the products.' :
+ total === 0 ? `No products match "${search}".` :
+ 'No matching products.';
+
+ return (
+
+
log('event', 'Dropdown closed')}
+ onChange={handleChange}>
+ {status === 'loading' && (
+
+
+
+ )}
+ {emptyMessage}
+ {(total !== null || status === 'error') && (
+
+
+ {status === 'loading' ? 'Loading…' :
+ status === 'error' ? 'The request failed.' :
+ `${loaded} of ${total} loaded`}
+
+ {status === 'error' && (
+ Retry
+ )}
+ {status === 'idle' && total !== null && loaded < total && (
+ Load more
+ )}
+
+ )}
+
+
+
+
+
+ {entries.map(entry => (
+ -
+
+ {entry.kind}
+
+ {entry.message}
+ {entry.detail &&
{entry.detail}}
+
+
+ ))}
+
+
+
+ );
+}
+
+// rendering above component in the React DOM
+const root = ReactDOM.createRoot(document.getElementById('root')!);
+root.render();
diff --git a/samples/inputs/combo/remote-binding/tsconfig.json b/samples/inputs/combo/remote-binding/tsconfig.json
new file mode 100644
index 0000000000..e27a22be9b
--- /dev/null
+++ b/samples/inputs/combo/remote-binding/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "es2020",
+ "lib": [
+ "es2020",
+ "dom",
+ "dom.iterable"
+ ],
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "types": [
+ "vite/client"
+ ],
+ "strict": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "skipLibCheck": true,
+ "noEmit": true
+ },
+ "include": [
+ "src"
+ ]
+}
diff --git a/samples/inputs/combo/remote-binding/vite.config.js b/samples/inputs/combo/remote-binding/vite.config.js
new file mode 100644
index 0000000000..4fc593892f
--- /dev/null
+++ b/samples/inputs/combo/remote-binding/vite.config.js
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ outDir: 'build'
+ },
+ server: {
+ open: false
+ },
+});