Skip to content

Fix duplicate-value rendering and stale remote results on reopen; release 0.7.2 - #21

Merged
cmm-cmm merged 2 commits into
mainfrom
claude/readme-documentation-expansion-1j8wa1
Aug 2, 2026
Merged

Fix duplicate-value rendering and stale remote results on reopen; release 0.7.2#21
cmm-cmm merged 2 commits into
mainfrom
claude/readme-documentation-expansion-1j8wa1

Conversation

@cmm-cmm

@cmm-cmm cmm-cmm commented Aug 2, 2026

Copy link
Copy Markdown
Owner

What & why

Two rendering bugs, a recycling improvement, and dead-code removal — found while auditing the row caches and search helpers.

Fixed

Duplicate values rendered the wrong label. Row content was cached by option.value, but duplicateValuePolicy defaults to warning rather than rejecting duplicates, so two options sharing a value both rendered the first one's content — the second option's label was silently replaced. Confirmed pre-existing on main, not introduced here. The cache is now keyed by the option object itself, which also removes the need for a size cap.

Reopening an AJAX select after a search showed the previous query's page under an empty search box. Measured before the fix:

After searching "be", then close → open Value
Search box ""
getSearchQuery() ""
Rows shown ["Beta"] — 1 of 3
Ajax calls ["", "be"] — no refetch

close() clears the search box but left the loaded page in place, and remoteLoaded suppressed the refetch, so the visible state contradicted the visible data. The page is now discarded with the query it belongs to. The remote cache normally serves the refetch with no extra request, and a select whose box was already empty still does not refetch (covered by its own test).

Changed

  • Row recycling keyed by option value instead of value-plus-row-index. Filtering shifts every index below the first change, so the element cache previously missed on every keystroke — exactly when the list re-renders most. Measured on a 2,000-option list: reuse across a narrowing query goes 0% → 18% (55 rather than 67 fresh <li>s). Honest caveat: for type-then-backspace it is only 49% → 52%, since indexes return to their earlier values and the old key already hit. Because keys are no longer unique per render, each element is claimed at most once, so duplicate values still render as separate rows.
  • Removed scoreOption(), an unused duplicate of SearchIndex.score(). Not public API and already tree-shaken from the published bundle, but it left two copies of the scoring rules to hand-synchronize, and its dead branches were what held search.ts branch coverage down. Added tests for the genuinely-uncovered live branches instead — i18n.ts100%, search.ts 85 → 95%, native-select.ts 82 → 91%.
  • Angular and Svelte wrappers dropped from the roadmap (per maintainer decision). docs/examples.md now documents mounting Forge Select in Angular alongside the existing Svelte entry, so the README no longer points at a section that does not exist.

Test plan

  • Updated relevant docs — docs/examples.md (new Angular section), README.md and site/index.html (roadmap/FAQ wording)
  • Added a CHANGELOG.md entry (under the new [0.7.2] section being released here)
  • Added/updated tests — 159 passing (was 149). Each regression test verified to fail without its fix:
    • reopen test → expected [ … ] to have a length of 3 but got 1
    • duplicate-value test → expected [ 'First', 'First', 'Third' ] to deeply equal [ 'First', 'Second', 'Third' ] (fails identically on main, confirming pre-existing)
  • npm run verify passes locally — lint, format check, typecheck, coverage-enforced tests, build
  • Verified in real Chromium, since jsdom cannot model layout:
    • duplicate values render First / Second / Third
    • tree indents hold at 12 / 30 / 12px across a filter change (value-keyed recycling does not leak indent)
    • 2,000-option list still windows to 11 rows with 2 spacers; mid-scroll lands on Option 995 with scrollTop preserved, bottom reaches Option 1999
    • zero console errors

Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Angular integration example, including setup, event handling, and cleanup guidance.
    • Added improved search matching behavior for normalized and accent-insensitive queries.
  • Bug Fixes

    • Fixed duplicate option values rendering incorrectly.
    • Improved row reuse when filtering options.
    • Fixed stale remote search results and unnecessary reloads when reopening dropdowns.
    • Improved handling of empty labels, inherited enabled states, locale fallbacks, and custom translations.
  • Documentation

    • Clarified direct Angular and Svelte integration guidance and updated roadmap information.
  • Release

    • Updated the package to version 0.7.2.

…ease 0.7.2

Two rendering bugs, a recycling improvement, and dead-code removal, found
while auditing the row caches and search helpers.

Duplicate values rendered the wrong label. Row content was cached by
option value, but duplicateValuePolicy defaults to warning rather than
rejecting duplicates, so two options sharing a value both rendered the
first one's content. The cache is now keyed by the option object, which
also removes the need for a size cap.

Reopening an AJAX select after a search showed the previous query's page
under an empty search box: close() clears the search box but left the
loaded page in place, and remoteLoaded suppressed the refetch. The page
is now discarded with the query it belongs to. The remote cache normally
serves the refetch without an extra request, and a select whose box was
already empty still does not refetch.

Row recycling is keyed by option value instead of value-plus-row-index.
Filtering shifts every index below the first change, so the element cache
previously missed on every keystroke; reuse across a narrowing query goes
from 0% to ~18% on a 2,000-option list. Since keys are no longer unique
per render, each element is claimed at most once so duplicate values still
render as separate rows.

Removed scoreOption(), an unused duplicate of SearchIndex.score(). Not
public API and already tree-shaken from the bundle, but it left two copies
of the scoring rules to hand-synchronize, and its dead branches were what
held search.ts branch coverage down. Added tests for the live uncovered
branches in i18n and native-select instead: i18n reaches 100%, search
85->95%, native-select 82->91%.

Angular and Svelte wrappers are dropped from the roadmap; docs/examples.md
now documents mounting Forge Select in Angular alongside the Svelte entry.

Verified in Chromium: duplicate values render First/Second/Third, tree
indents hold at 12/30/12px across a filter change, and a 2,000-option list
still windows to 11 rows reaching Option 1999 at the bottom.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
forge-select 09eb58c Commit Preview URL

Branch Preview URL
Aug 02 2026, 10:12 AM

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cmm-cmm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e2c6028d-d432-4fca-bb27-c536dec2a7e3

📥 Commits

Reviewing files that changed from the base of the PR and between 9e240a9 and 09eb58c.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/ForgeSelect.ts
  • tests/forge-select.test.ts
  • tests/helpers.test.ts
📝 Walkthrough

Walkthrough

Forge Select 0.7.2 updates row caching and recycling, remote search reopening, search helpers, Angular integration guidance, and release documentation. Tests cover duplicate values, AJAX requests, native-select parsing, scoring, and i18n behavior.

Changes

Forge Select 0.7.2

Layer / File(s) Summary
Row caching and recycling
src/ForgeSelect.ts, tests/forge-select.test.ts
Row content now uses Option identity. Recycled elements use content-based keys. Duplicate option values render as separate elements.
Remote query lifecycle
src/ForgeSelect.ts, tests/forge-select.test.ts
Closing a filtered remote dropdown marks its data stale. Reopening loads the unfiltered results without redundant requests.
Search and helper behavior
src/search.ts, tests/helpers.test.ts
SearchIndex replaces scoreOption(). Tests cover scoring, native-select parsing, and i18n fallback behavior.
Framework guidance and release
docs/examples.md, README.md, site/index.html, package.json, CHANGELOG.md
The docs add an Angular mounting example, remove Angular and Svelte roadmap wording, and record version 0.7.2.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two primary fixes and identifies the 0.7.2 release.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/readme-documentation-expansion-1j8wa1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 10-22: Add a concise Changed entry under the [Unreleased] section
of CHANGELOG.md, before the [0.7.2] release, noting that docs/examples.md now
includes Angular integration guidance for mounting Forge Select directly.

In `@src/ForgeSelect.ts`:
- Around line 136-140: Replace the WeakMap in ForgeSelect with an identity-keyed
bounded Map<Option, Node>, preserving object-identity handling for duplicate
values. In src/ForgeSelect.ts lines 1559, update clearRowCaches() to reset this
Map, and in lines 1875-1880 update the row-cache insertion path to evict the
oldest entry after adding a new node when the configured bound is exceeded.
- Around line 360-367: Update the close logic in ForgeSelect around hadQuery so
closing after a query invalidates the active AJAX request: increment
ajaxRequestId, clear any pending request timer, and abort the active controller
before resetting remoteLoaded. Ensure loadRemote checks the request identifier
before applying results and again in finally, and add a regression test covering
close/reopen before the filtered request resolves.

In `@tests/helpers.test.ts`:
- Around line 91-95: Align the test case description with its assertion by
renaming the `it` block around `parseNativeOptions` to state that an option with
no text preserves an empty label; keep the expected label as `""` unless the
implementation contract explicitly requires value fallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f05a2e6-e201-4471-9a3c-a891edd8d6a9

📥 Commits

Reviewing files that changed from the base of the PR and between cc64e0f and 9e240a9.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • CHANGELOG.md
  • README.md
  • docs/examples.md
  • package.json
  • site/index.html
  • src/ForgeSelect.ts
  • src/search.ts
  • tests/forge-select.test.ts
  • tests/helpers.test.ts
💤 Files with no reviewable changes (1)
  • src/search.ts

Comment thread CHANGELOG.md
Comment on lines +10 to +22
## [0.7.2] - 2026-08-02

### Fixed

- Two options sharing a `value` no longer render the same content. Rendered row content was cached by option value, so with `duplicateValuePolicy` left at its default (warn, not reject) the second option displayed the first one's label. Content is now cached against the option object itself.
- Reopening an AJAX-backed select after a search no longer shows the previous query's results under an empty search box. Closing clears the search box, so the loaded page is now discarded with it and refetched on reopen — normally served from the remote cache without an extra request. Selects whose search box was already empty are unaffected and still do not refetch.

### Changed

- Row `<li>` recycling is keyed by option value instead of value-plus-row-index. Filtering shifts every index below the first change, which previously invalidated the whole element cache on each keystroke; reuse across a narrowing query goes from 0% to ~18% in a 2,000-option list. Rows are claimed at most once per render, so duplicate values still render as separate rows.
- Removed `scoreOption()`, an unused duplicate of `SearchIndex.score()`. It was never part of the public API and was already tree-shaken out of the published bundle, but left two copies of the scoring rules to keep in sync by hand.
- Angular and Svelte wrapper packages are no longer planned and have been dropped from the roadmap. Both frameworks mount Forge Select directly; `docs/examples.md` now documents the Angular approach alongside the existing Svelte one.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the Angular documentation change under [Unreleased].

The new Angular integration guidance is documented in docs/examples.md, but the changelog records it only under [0.7.2]. Add a concise entry under [Unreleased] before the versioned release.

As per coding guidelines, docs/**/*.md requires an entry under the Unreleased section of CHANGELOG.md when behavior changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 10 - 22, Add a concise Changed entry under the
[Unreleased] section of CHANGELOG.md, before the [0.7.2] release, noting that
docs/examples.md now includes Angular integration guidance for mounting Forge
Select directly.

Source: Coding guidelines

Comment thread src/ForgeSelect.ts Outdated
Comment thread src/ForgeSelect.ts Outdated
Comment thread tests/helpers.test.ts Outdated
Review follow-up on 0.7.2.

Switching the row-content cache to a WeakMap dropped its size bound on the
reasoning that weak keys collect on their own. They do not while `data` holds
every key, so scrolling a long list would retain a detached content node per
option visited, where the previous cache capped at 2,000. Restored an
explicitly bounded Map, still keyed by option identity so duplicate values
keep rendering their own content.

Closing after a search reset remoteLoaded but left the request for that query
running. Resolving after the close, it applied its filtered page and set
remoteLoaded, so the reopen skipped the empty-query load and showed exactly
the stale rows the change was meant to prevent. close() now retires that
request: bumps the request id, clears the pending timer, aborts the
controller, and settles loading state. Confirmed the new deferred-response
test fails without it (1 row instead of 3).

Also dropped a redundant native-select test whose name claimed a value
fallback it did not assert; an equivalent empty-label case was already
covered a few lines above.

cmm-cmm commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

Thanks — three of the four are applied in the follow-up commit; one is skipped.

Applied

  • Bounded row-content cache. Correct, and a regression I introduced in this PR. WeakMap collects nothing while data holds every key, so a long list would retain a detached content node per option visited, against the previous 2,000 cap. Restored an explicitly bounded Map with FIFO eviction, still keyed by option identity so duplicate values keep rendering their own content.
  • Retiring the request a closed query owns. Also correct, and it defeated the fix in the race case: the in-flight "be" request applied its filtered page and set remoteLoaded, so the reopen skipped the empty-query load. close() now bumps the request id, clears the pending timer, aborts the controller, and settles loading state. Added the deferred-response regression test you asked for — confirmed it fails without the fix (1 row instead of 3).
  • native-select test name vs assertion. Right that they disagreed. The test also duplicated "parses a top-level option (no optgroup) with an empty label" a few lines above, so I removed it rather than renaming it. Worth noting for anyone chasing the remaining uncovered branch on that line: ?? option.value is unreachable, since Element.textContent is only nullable in the type, never at runtime.

Skipped

  • Angular docs entry under [Unreleased]. The guideline assumes a PR that ships ahead of a release. This PR is the 0.7.2 release — same pattern as 0.7.0 and 0.7.1 — so the Angular docs change is already recorded under [0.7.2]. Adding it under [Unreleased] as well would leave a dangling entry describing something already shipped.

Generated by Claude Code

@sonarqubecloud

sonarqubecloud Bot commented Aug 2, 2026

Copy link
Copy Markdown

@cmm-cmm
cmm-cmm merged commit ffc1a0d into main Aug 2, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant