Fix duplicate-value rendering and stale remote results on reopen; release 0.7.2 - #21
Conversation
…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.
Deploying with
|
| 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 |
|
Warning Review limit reached
Next review available in: 53 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughForge 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. ChangesForge Select 0.7.2
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (9)
CHANGELOG.mdREADME.mddocs/examples.mdpackage.jsonsite/index.htmlsrc/ForgeSelect.tssrc/search.tstests/forge-select.test.tstests/helpers.test.ts
💤 Files with no reviewable changes (1)
- src/search.ts
| ## [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. | ||
|
|
There was a problem hiding this comment.
📐 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
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.
|
Thanks — three of the four are applied in the follow-up commit; one is skipped. Applied
Skipped
Generated by Claude Code |
|



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, butduplicateValuePolicydefaults 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 onmain, 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:
"be", then close → open""getSearchQuery()""["Beta"]— 1 of 3["", "be"]— no refetchclose()clears the search box but left the loaded page in place, andremoteLoadedsuppressed 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
<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.scoreOption(), an unused duplicate ofSearchIndex.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 heldsearch.tsbranch coverage down. Added tests for the genuinely-uncovered live branches instead —i18n.ts→ 100%,search.ts85 → 95%,native-select.ts82 → 91%.docs/examples.mdnow 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
docs/examples.md(new Angular section),README.mdandsite/index.html(roadmap/FAQ wording)CHANGELOG.mdentry (under the new[0.7.2]section being released here)expected [ … ] to have a length of 3 but got 1expected [ 'First', 'First', 'Third' ] to deeply equal [ 'First', 'Second', 'Third' ](fails identically onmain, confirming pre-existing)npm run verifypasses locally — lint, format check, typecheck, coverage-enforced tests, buildFirst / Second / ThirdscrollToppreserved, bottom reaches Option 1999Generated by Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Release