Skip to content

Fix Fetch dropping repeated form fields and leaking src into history - #643

Merged
titouanmathis merged 1 commit into
mainfrom
fix/fetch-multi-value-and-history
Sep 1, 2026
Merged

Fix Fetch dropping repeated form fields and leaking src into history#643
titouanmathis merged 1 commit into
mainfrom
fix/fetch-multi-value-and-history

Conversation

@titouanmathis

Copy link
Copy Markdown
Contributor

Two faults in Fetch's URL resolution, both found while building a facet filter form. Targeting main (v1) deliberately: Fetch.ts is byte-identical on main and 2.x, so this patch applies to both, and v1 is what consumers are on today.

1. A repeated GET field only sent one value

get url() folded the form onto the base with url.searchParams.set(key, value) per entry. set replaces, so for a checkbox group — repeated names by design — only one value survived.

Measured in a browser before the fix, on a form with two boxes ticked:

FormData.getAll('project_category[]')      → ['portrait', 'still-life']
new URLSearchParams(new FormData(form))    → project_category[]=portrait&project_category[]=still-life
what Fetch requested                       → project_category[]=still-life

Ticking a second box changed nothing on screen, which is how it was noticed.

set was there for a reason the docblock states — a fixed query in src must be overridable by a live field — so append alone is not the fix: it would let the base's stale value through. The fold now deletes the base's values the first time it meets a name, then appends, which keeps the override and stops discarding the rest:

base ?genre=stale&section=keep, form genre=rock, genre=jazz result
set (before) genre=jazz&section=keep
append only genre=stale&genre=rock&genre=jazz&section=keep
delete-once, then append genre=rock&genre=jazz&section=keep

This also covers <select multiple>, which repeats a name without the [] suffix — so the fix cannot key on the name's shape.

2. History pushed the fetched URL, not the navigation

update() pushed the URL that was requested. With src set to a lighter endpoint, its parameters landed in the address bar and in every link a visitor copied.

src answers what to request. The new historyUrl getter answers what this navigation is: the element's own destination, folded with the same form data.

<a
  href="/projects/page/2?orderby=title"
  data-component="Fetch"
  data-option-history
  data-option-src="/projects/page/2?orderby=title&sections=listing">
  2
</a>

Requests the sections=listing URL, pushes /projects/page/2?orderby=title.

A URL passed explicitly to fetch(url) is still pushed as given — a caller that named a URL meant that URL — so fetch() now captures whether the URL came from the element or from the caller.

Nothing changes for an element without src, where the request URL and the destination are the same. update()'s signature is untouched, and a direct update() call still pushes the URL it was handed.

One consequence, documented rather than engineered around

A back or forward navigation re-fetches window.location.href, which is now the pushed URL rather than the src one. selector therefore has to match elements present in both responses. There is a tip about it in the docs.

Tests

packages/tests/Fetch/Fetch.spec.ts gains eight cases: repeated fields with and without brackets, repeated fields overriding a conflicting src query, historyUrl for links and for forms, historyUrl falling back to url, history pushing the destination rather than the src, and history pushing an explicitly passed URL.

<select multiple> is not among them, with a comment saying why: happy-dom puts only the first selected option into FormData, so the case cannot be expressed in this environment. The bracket-free coverage uses two same-named inputs instead, which makes the same point.

893 tests pass, npm run lint clean.

Two faults in the same resolver, both reported from a facet filter form.

Folding a GET form onto the base URL used `set` per field, which keeps one value
per name. A checkbox group is repeated names by design, so ticking a second box
changed nothing: FormData held both values, `new URLSearchParams(FormData)`
serialised both, and the request carried one. The fold now deletes the base's
values the first time it meets a name and appends after that, which keeps the
override `set` was there for and stops discarding the rest.

History pushed the URL that was fetched, so a `src` pointing at a lighter
endpoint put its own parameters in the address bar and in anything a visitor
copied out of it. History now follows the element's own destination — a link's
href, a form's action folded with its data — through a new `historyUrl` getter. A
URL passed explicitly to `fetch()` is still pushed as given.

Nothing changes for an element without `src`, where the two URLs are the same.
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Code Review

Risk: Medium — issues that should be addressed before merge.

Updates Fetch URL resolution to preserve repeated GET form values while replacing conflicting base parameters. Adds destination-based history resolution for element-triggered fetches and tests the new request and history behaviours.

1 issue found:

  • issuepackages/ui/src/Fetch/Fetch.ts:348 — Clear request-specific history state after update

Review usage: 20,680 in (3,677 cached) / 705 out tokens — $0.0129 (openrouter/openai/gpt-5.6-luna, thinking: low)

Reviewed by @weareikko/code-review v0.9.5 for commit 7edbbdd.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Export size

Bundled per export with peer dependencies left external, dynamic imports excluded and the output minified; sizes are gzipped.

@studiometa/ui

Export Size (gzip) Diff
FetchShopifyPartial 3.11 kB +136 B (+4.5%)
FetchShopifySection 2.82 kB +116 B (+4.2%)
(barrel) 30.88 kB +114 B (+0.4%)
Fetch 2.56 kB +114 B (+4.6%)
manifest 2.99 kB -2 B (-0.1%)
Unchanged (128)

@studiometa/ui

Export Size (gzip) Diff
AbstractCarouselChild 542 B
AbstractCarouselComponent 219 B
AbstractFrameTrigger 1.11 kB
AbstractPrefetch 438 B
AbstractScrollAnimation 651 B
AbstractSliderChild 489 B
Accordion 1.11 kB
AccordionItem 995 B
Action 1.22 kB
AnchorNav 1018 B
AnchorNavLink 867 B
AnchorNavTarget 166 B
AnchorScrollTo 222 B
Carousel 2.89 kB
CarouselBtn 720 B
CarouselDrag 735 B
CarouselItem 786 B
CarouselWrapper 670 B
CircularMarquee 313 B
ClickOutside 219 B
Cursor 676 B
DataBind 3.97 kB
DataComputed 4.06 kB
DataEffect 4.04 kB
DataModel 4.07 kB
DataScope 1.4 kB
Dialog 1.79 kB
Disclosure 2.39 kB
DisclosureGroup 1.16 kB
Draggable 1.14 kB
Figure 1.07 kB
FigureShopify 1.35 kB
FigureTwicpics 1.6 kB
FigureVideo 1.27 kB
FigureVideoTwicpics 1.85 kB
Frame 2.51 kB
FrameAnchor 1.2 kB
FrameForm 1.32 kB
FrameLoader 788 B
FrameTarget 1.1 kB
FrameTriggerLoader 801 B
Hoverable 624 B
InView 204 B
InViewOnce 245 B
Indexable 1022 B
LargeText 499 B
LazyInclude 421 B
Menu 1.72 kB
MenuBtn 186 B
MenuList 1.28 kB
Modal 1.13 kB
ModalWithTransition 1.26 kB
Panel 1.52 kB
PrefetchWhenOver 485 B
PrefetchWhenVisible 510 B
ScrollAnimation 806 B
ScrollAnimationChild 979 B
ScrollAnimationChildWithEase 1.25 kB
ScrollAnimationParent 1.05 kB
ScrollAnimationTarget 898 B
ScrollAnimationTimeline 983 B
ScrollAnimationWithEase 1.08 kB
ScrollReveal 1005 B
Sentinel 175 B
Slider 2.28 kB
SliderBtn 745 B
SliderCount 549 B
SliderDots 1.15 kB
SliderDrag 348 B
SliderItem 574 B
SliderProgress 612 B
Sticky 929 B
Tabs 663 B
Target 125 B
Timer 589 B
TimerProgress 731 B
Toast 968 B
Toaster 1.3 kB
Track 1.61 kB
TrackContext 416 B
TrackShopify 1.71 kB
Transition 746 B
ViewTransition 658 B
animationScrollWithEase 438 B
autoload 3.04 kB
scheduler 324 B
types 20 B
viewTransition 325 B
withDeprecation 250 B
withIndex 986 B
withScrollAnimationDebug 1.63 kB
withTransition 717 B

@studiometa/ui-mapbox

Export Size (gzip) Diff
(barrel) 6.41 kB
AbstractMapboxControl 1.02 kB
AbstractMapboxMapChild 848 B
MAPBOX_CLUSTER_CONNECTED 143 B
MAPBOX_MAP_CONNECTED 128 B
MapboxCluster 2.13 kB
MapboxClusterItem 644 B
MapboxFullscreenControl 1.17 kB
MapboxGeocoder 1.21 kB
MapboxGeolocateControl 1.23 kB
MapboxImage 1.34 kB
MapboxImages 1.38 kB
MapboxLayer 1.36 kB
MapboxMap 723 B
MapboxMarker 1.11 kB
MapboxNavigationControl 1.21 kB
MapboxPopup 1.17 kB
MapboxSource 1.32 kB
StoreLocator 1.96 kB
autoload 681 B
dependencies 342 B
manifest 639 B
provideMapboxGeocoder 107 B
provideMapboxGl 104 B
resolveMapboxGeocoder 148 B
resolveMapboxGl 139 B

@studiometa/ui-motion

Export Size (gzip) Diff
(barrel) 2.7 kB
Motion 1.58 kB
MotionScrollTimeline 1.83 kB
MotionSequence 1.76 kB
MotionView 1010 B
autoload 363 B
dependencies 257 B
manifest 323 B
provideMotion 102 B
resolveMotion 115 B

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.85%. Comparing base (c939977) to head (7edbbdd).

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #643      +/-   ##
============================================
+ Coverage     87.80%   87.85%   +0.05%     
  Complexity      145      145              
============================================
  Files           153      153              
  Lines          5443     5460      +17     
  Branches       1037     1040       +3     
============================================
+ Hits           4779     4797      +18     
+ Misses          580      579       -1     
  Partials         84       84              
Flag Coverage Δ
unittests 87.85% <100.00%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/ui/src/Fetch/Fetch.ts 100.00% <100.00%> (+0.70%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@titouanmathis
titouanmathis merged commit a6f0780 into main Sep 1, 2026
20 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