Skip to content

feat(pds-combobox): add async to pds-combobox#651

Merged
QuintonJason merged 4 commits intomainfrom
fix/combobox-add-async
Jan 29, 2026
Merged

feat(pds-combobox): add async to pds-combobox#651
QuintonJason merged 4 commits intomainfrom
fix/combobox-add-async

Conversation

@QuintonJason
Copy link
Contributor

Description

Added comprehensive async data fetching capabilities to the pds-combobox component to match the functionality of pds-multiselect.

This change enables the combobox to fetch options asynchronously from a URL endpoint, with support for debounced search, pagination, loading states, and both component-managed and consumer-managed async patterns.

Fixes DSS-103

Type of change

  • New feature (non-breaking change which adds functionality)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests you've added and run to verify your changes.
Provide instructions so that we can reproduce.
Please also list any relevant details for your test configuration.

  • unit tests
  • tested manually

Test Configuration:

  • Pine versions: 3.14.2
  • OS: macOS
  • Browsers: Chrome, Safari
  • Screen readers: N/A
  • Misc: Built successfully with no errors

Checklist:

If not applicable, leave options unchecked.

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing tests pass locally with my changes
  • Design has QA'ed and approved this PR

@netlify
Copy link

netlify bot commented Jan 28, 2026

Deploy Preview for pine-design-system ready!

Name Link
🔨 Latest commit 13a0d54
🔍 Latest deploy log https://app.netlify.com/projects/pine-design-system/deploys/697bd7ee15c9a20008f45313
😎 Deploy Preview https://deploy-preview-651--pine-design-system.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions bot added bug Something isn't working package: core Changes have been made to the Core package labels Jan 28, 2026
@QuintonJason
Copy link
Contributor Author

@coderabbitai review this pr

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

@QuintonJason: I'll review the changes in this PR right away. This looks like a significant enhancement adding async capabilities to the combobox component.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

The pds-combobox component is enhanced with asynchronous data loading capabilities. New properties (asyncUrl, asyncMethod, debounce, loading, options, formatResult) enable fetching options from remote endpoints with configurable HTTP methods and debounced requests. Two new events (pdsComboboxSearch and pdsComboboxLoadOptions) allow consumers to respond to search and pagination interactions. Internal state management handles pagination and result caching. New SCSS classes and slot support (empty, loading) provide visual feedback for different states. Type definitions establish data shapes for options, event payloads, and async responses. The component now maintains a dependency on pds-loader.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the main functionality added, references the fixed issue (DSS-103), and indicates testing and documentation updates. However, several checklist items remain unchecked despite claims of work completion.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title 'feat(pds-combobox): add async to pds-combobox' clearly summarizes the main change—adding async functionality to the pds-combobox component, which aligns with the changeset's primary objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@libs/core/src/components/pds-combobox/pds-combobox.tsx`:
- Around line 404-465: fetchOptions can hang on slow responses, throw on
malformed asyncUrl, and assume API items are objects; wrap URL construction in a
try/catch to guard against TypeError when creating new URL(this.asyncUrl, ...),
implement a fetch timeout using AbortController (or AbortSignal.timeout) and
wire the timeout signal into the same controller used for aborting previous
requests, and make the default mapping in fetchOptions (the branch that uses
...item) defensive by checking typeof item === 'object' && item !== null before
spreading — if the item is a primitive, convert it to a safe { id: item, text:
String(item) } shape or call formatResult if present; ensure you clear the
timeout on success/failure and keep
currentPage/internalOptions/updateOptionsFromAsync usage unchanged.

In `@libs/core/src/components/pds-loader/readme.md`:
- Around line 36-38: The unordered list in
libs/core/src/components/pds-loader/readme.md has inconsistent indentation for
the items, so update the three list entries referencing pds-button,
pds-combobox, and pds-multiselect to the same indentation level as the
surrounding list (remove or add leading spaces so each line begins with the
hyphen at the same column) and ensure there is a single space after each hyphen
to satisfy markdownlint.
🧹 Nitpick comments (4)
libs/core/src/components/pds-combobox/combobox-interface.ts (1)

20-22: Prefer reusing ComboboxOption in AsyncResponse.

Proposed refactor
 export interface AsyncResponse {
-  results: Array<{ id: string | number; text: string; [key: string]: unknown }>;
+  results: ComboboxOption[];
   totalCount?: number;
 }
libs/core/src/components/pds-combobox/pds-combobox.tsx (3)

1246-1260: Pagination scroll handler looks good, with a minor edge case.

The infinite scroll implementation is well-designed with appropriate guards. However, rapid scrolling could potentially trigger multiple debounced fetches before the loading flag is set (since loading is only set inside fetchOptions, not immediately when debouncedFetchAsyncOptions is called).

Consider setting a flag immediately when pagination is triggered:

💡 Optional: Prevent rapid scroll triggers
  private handleScroll = (e: Event) => {
-   if (!this.asyncUrl || !this.hasMore || this.loading) return;
+   if (!this.asyncUrl || !this.hasMore || this.loading || this.fetchDebounceTimer !== undefined) return;

    const target = e.target as HTMLElement;
    const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;

1283-1301: Consider different loading behavior for initial load vs. pagination.

Currently, when loading is true, the loading indicator is shown at the top of the listbox regardless of whether it's an initial load or a pagination request. During pagination, this could be confusing since existing options are still visible below.

For pagination, the load-more spinner at the bottom (lines 1371-1375) is more appropriate. Consider showing the top loading indicator only when there are no existing options:

💡 Suggested improvement
-       {this.loading && (
+       {this.loading && this.filteredItems.length === 0 && (
          <li class="pds-combobox__loading" role="presentation">
            {hasSlottedLoading ? (
              <slot name="loading" />
            ) : (
              <pds-loader size="small" />
            )}
          </li>
        )}

251-251: Remove unused observer field.

The observer MutationObserver field is declared but never instantiated anywhere in the component. While disconnect() is called on it in disconnectedCallback, the optional chaining masks that observer will always be undefined at runtime, making this dead code.

Comment on lines +404 to +465
private async fetchOptions(query: string, page: number = 1) {
if (!this.asyncUrl) return;

this.abortController?.abort();
this.abortController = new AbortController();

this.loading = true;

try {
const url = new URL(this.asyncUrl, window.location.origin);
if (this.asyncMethod === 'GET') {
url.searchParams.set('search', query);
url.searchParams.set('page', String(page));
}

const response = await fetch(url.toString(), {
method: this.asyncMethod,
signal: this.abortController.signal,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
...(this.asyncMethod === 'POST' && {
body: JSON.stringify({ search: query, page }),
}),
});

if (!response.ok) throw new Error('Failed to fetch options');

const data: AsyncResponse = await response.json();

const formattedResults = data.results.map(item => {
if (this.formatResult) {
return this.formatResult(item);
}
return {
id: item.id,
text: item.text,
...item,
};
});

if (page === 1) {
this.internalOptions = formattedResults;
} else {
this.internalOptions = [...this.internalOptions, ...formattedResults];
}

this.hasMore = data.totalCount ? this.internalOptions.length < data.totalCount : false;
this.currentPage = page;

// Update DOM options from async data
this.updateOptionsFromAsync();

} catch (error) {
if ((error as Error).name !== 'AbortError') {
console.error('PdsCombobox: Failed to fetch options', error);
}
} finally {
this.loading = false;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider adding a fetch timeout and guarding against malformed URLs.

A few observations on the fetch implementation:

  1. No timeout: The fetch could hang indefinitely if the server doesn't respond. Consider using AbortSignal.timeout() or a manual timeout.

  2. URL construction: new URL(this.asyncUrl, window.location.origin) will throw a TypeError for malformed URLs, which isn't caught explicitly.

  3. Default formatResult assumption (lines 439-444): The spread ...item assumes item is an object. If the API returns primitives, this will fail silently or produce unexpected results.

🛡️ Suggested improvements
  private async fetchOptions(query: string, page: number = 1) {
    if (!this.asyncUrl) return;

    this.abortController?.abort();
    this.abortController = new AbortController();

    this.loading = true;

    try {
-     const url = new URL(this.asyncUrl, window.location.origin);
+     let url: URL;
+     try {
+       url = new URL(this.asyncUrl, window.location.origin);
+     } catch {
+       console.error('PdsCombobox: Invalid asyncUrl', this.asyncUrl);
+       return;
+     }
+
      if (this.asyncMethod === 'GET') {
        url.searchParams.set('search', query);
        url.searchParams.set('page', String(page));
      }

+     // Add timeout to prevent hanging requests
+     const timeoutId = setTimeout(() => this.abortController?.abort(), 30000);
+
      const response = await fetch(url.toString(), {
        method: this.asyncMethod,
        signal: this.abortController.signal,
        // ...
      });

+     clearTimeout(timeoutId);
🤖 Prompt for AI Agents
In `@libs/core/src/components/pds-combobox/pds-combobox.tsx` around lines 404 -
465, fetchOptions can hang on slow responses, throw on malformed asyncUrl, and
assume API items are objects; wrap URL construction in a try/catch to guard
against TypeError when creating new URL(this.asyncUrl, ...), implement a fetch
timeout using AbortController (or AbortSignal.timeout) and wire the timeout
signal into the same controller used for aborting previous requests, and make
the default mapping in fetchOptions (the branch that uses ...item) defensive by
checking typeof item === 'object' && item !== null before spreading — if the
item is a primitive, convert it to a safe { id: item, text: String(item) } shape
or call formatResult if present; ensure you clear the timeout on success/failure
and keep currentPage/internalOptions/updateOptionsFromAsync usage unchanged.

@QuintonJason QuintonJason marked this pull request as ready for review January 29, 2026 20:33
@QuintonJason QuintonJason self-assigned this Jan 29, 2026
Copy link
Member

@pixelflips pixelflips left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No docs or stories? Stories might be tricky, but we should document the additions in some way.

@pixelflips
Copy link
Member

also just a note for the changelog, format the pr title feat(pds-comboox): ...

@QuintonJason QuintonJason changed the title feat: add async to pds-combobox feat(pds-combobox): add async to pds-combobox Jan 29, 2026
@QuintonJason QuintonJason force-pushed the fix/combobox-add-async branch from f2f9abd to 13a0d54 Compare January 29, 2026 21:58
@QuintonJason QuintonJason merged commit 796baa7 into main Jan 29, 2026
15 checks passed
@QuintonJason QuintonJason deleted the fix/combobox-add-async branch January 29, 2026 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working package: core Changes have been made to the Core package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants