Skip to content

Commit 08c43b2

Browse files
bartlomiejuclaude
andcommitted
feat: add _freshIndicator support for partial form submits
Previously only anchor clicks toggled the _freshIndicator signal during partial navigation. This extends the same behavior to form submissions, checking the submitter element first and falling back to the form element. Closes #3504 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 895bcac commit 08c43b2

2 files changed

Lines changed: 234 additions & 3 deletions

File tree

packages/fresh/src/runtime/client/partials.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,14 @@ document.addEventListener("submit", async (e) => {
278278
// this check, every form inside f-client-nav would be intercepted
279279
// because el.action is always non-empty (defaults to the current URL).
280280
if (hasExplicitPartial && rawPartialUrl !== "") {
281+
// deno-lint-ignore no-explicit-any
282+
const indicator = ((e.submitter as any)?._freshIndicator ??
283+
// deno-lint-ignore no-explicit-any
284+
(el as any)._freshIndicator) as { value: boolean } | undefined;
285+
if (indicator !== undefined) {
286+
indicator.value = true;
287+
}
288+
281289
e.preventDefault();
282290

283291
const partialUrl = new URL(rawPartialUrl, location.href);
@@ -295,9 +303,15 @@ document.addEventListener("submit", async (e) => {
295303
init = { body: new FormData(el, e.submitter), method: lowerMethod };
296304
}
297305

298-
await withViewTransition(async () => {
299-
await fetchPartials(actionUrl, partialUrl, true, init);
300-
});
306+
try {
307+
await withViewTransition(async () => {
308+
await fetchPartials(actionUrl, partialUrl, true, init);
309+
});
310+
} finally {
311+
if (indicator !== undefined) {
312+
indicator.value = false;
313+
}
314+
}
301315
}
302316
}
303317
});

packages/fresh/tests/partials_test.tsx

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3281,3 +3281,220 @@ Deno.test({
32813281
});
32823282
},
32833283
});
3284+
3285+
Deno.test({
3286+
name: "partials - submit form indicator on button",
3287+
fn: async () => {
3288+
const app = testApp()
3289+
.get("/partial", (ctx) => {
3290+
return ctx.render(
3291+
<Doc>
3292+
<Partial name="foo">
3293+
<p class="done">done</p>
3294+
</Partial>
3295+
</Doc>,
3296+
);
3297+
})
3298+
.get("/", (ctx) => {
3299+
return ctx.render(
3300+
<Doc>
3301+
<div f-client-nav>
3302+
<form action="/partial">
3303+
<Partial name="foo">
3304+
<p class="init">init</p>
3305+
</Partial>
3306+
<button type="submit" class="update">
3307+
update
3308+
</button>
3309+
</form>
3310+
</div>
3311+
</Doc>,
3312+
);
3313+
});
3314+
3315+
await withBrowserApp(app, async (page, address) => {
3316+
await page.goto(address, { waitUntil: "load" });
3317+
await page.locator(".init").wait();
3318+
3319+
// Attach a tracking indicator to the submit button
3320+
await page.evaluate(() => {
3321+
const btn = document.querySelector(".update")!;
3322+
const indicator = { _wasTrue: false, _value: false };
3323+
Object.defineProperty(indicator, "value", {
3324+
get() {
3325+
return this._value;
3326+
},
3327+
set(v: boolean) {
3328+
this._value = v;
3329+
if (v) this._wasTrue = true;
3330+
},
3331+
});
3332+
// deno-lint-ignore no-explicit-any
3333+
(btn as any)._freshIndicator = indicator;
3334+
// deno-lint-ignore no-explicit-any
3335+
(window as any).__indicator = indicator;
3336+
});
3337+
3338+
await page.locator(".update").click();
3339+
await page.locator(".done").wait();
3340+
3341+
const result = await page.evaluate(() => {
3342+
// deno-lint-ignore no-explicit-any
3343+
const ind = (window as any).__indicator;
3344+
return { wasTrue: ind._wasTrue, currentValue: ind.value };
3345+
});
3346+
expect(result.wasTrue).toBe(true);
3347+
expect(result.currentValue).toBe(false);
3348+
});
3349+
},
3350+
});
3351+
3352+
Deno.test({
3353+
name: "partials - submit form indicator on form element",
3354+
fn: async () => {
3355+
const app = testApp()
3356+
.get("/partial", (ctx) => {
3357+
return ctx.render(
3358+
<Doc>
3359+
<Partial name="foo">
3360+
<p class="done">done</p>
3361+
</Partial>
3362+
</Doc>,
3363+
);
3364+
})
3365+
.get("/", (ctx) => {
3366+
return ctx.render(
3367+
<Doc>
3368+
<div f-client-nav>
3369+
<form action="/partial" class="myform">
3370+
<Partial name="foo">
3371+
<p class="init">init</p>
3372+
</Partial>
3373+
<button type="submit" class="update">
3374+
update
3375+
</button>
3376+
</form>
3377+
</div>
3378+
</Doc>,
3379+
);
3380+
});
3381+
3382+
await withBrowserApp(app, async (page, address) => {
3383+
await page.goto(address, { waitUntil: "load" });
3384+
await page.locator(".init").wait();
3385+
3386+
// Attach indicator to the form element (not the button)
3387+
await page.evaluate(() => {
3388+
const form = document.querySelector(".myform")!;
3389+
const indicator = { _wasTrue: false, _value: false };
3390+
Object.defineProperty(indicator, "value", {
3391+
get() {
3392+
return this._value;
3393+
},
3394+
set(v: boolean) {
3395+
this._value = v;
3396+
if (v) this._wasTrue = true;
3397+
},
3398+
});
3399+
// deno-lint-ignore no-explicit-any
3400+
(form as any)._freshIndicator = indicator;
3401+
// deno-lint-ignore no-explicit-any
3402+
(window as any).__indicator = indicator;
3403+
});
3404+
3405+
await page.locator(".update").click();
3406+
await page.locator(".done").wait();
3407+
3408+
const result = await page.evaluate(() => {
3409+
// deno-lint-ignore no-explicit-any
3410+
const ind = (window as any).__indicator;
3411+
return { wasTrue: ind._wasTrue, currentValue: ind.value };
3412+
});
3413+
expect(result.wasTrue).toBe(true);
3414+
expect(result.currentValue).toBe(false);
3415+
});
3416+
},
3417+
});
3418+
3419+
Deno.test({
3420+
name: "partials - submit form indicator prefers submitter over form",
3421+
fn: async () => {
3422+
const app = testApp()
3423+
.get("/partial", (ctx) => {
3424+
return ctx.render(
3425+
<Doc>
3426+
<Partial name="foo">
3427+
<p class="done">done</p>
3428+
</Partial>
3429+
</Doc>,
3430+
);
3431+
})
3432+
.get("/", (ctx) => {
3433+
return ctx.render(
3434+
<Doc>
3435+
<div f-client-nav>
3436+
<form action="/partial" class="myform">
3437+
<Partial name="foo">
3438+
<p class="init">init</p>
3439+
</Partial>
3440+
<button type="submit" class="update">
3441+
update
3442+
</button>
3443+
</form>
3444+
</div>
3445+
</Doc>,
3446+
);
3447+
});
3448+
3449+
await withBrowserApp(app, async (page, address) => {
3450+
await page.goto(address, { waitUntil: "load" });
3451+
await page.locator(".init").wait();
3452+
3453+
// Attach indicators to both form and button
3454+
await page.evaluate(() => {
3455+
function makeIndicator(name: string) {
3456+
const indicator = { _wasTrue: false, _value: false };
3457+
Object.defineProperty(indicator, "value", {
3458+
get() {
3459+
return this._value;
3460+
},
3461+
set(v: boolean) {
3462+
this._value = v;
3463+
if (v) this._wasTrue = true;
3464+
},
3465+
});
3466+
// deno-lint-ignore no-explicit-any
3467+
(window as any)[name] = indicator;
3468+
return indicator;
3469+
}
3470+
3471+
const btn = document.querySelector(".update")!;
3472+
const form = document.querySelector(".myform")!;
3473+
// deno-lint-ignore no-explicit-any
3474+
(btn as any)._freshIndicator = makeIndicator("__btnIndicator");
3475+
// deno-lint-ignore no-explicit-any
3476+
(form as any)._freshIndicator = makeIndicator("__formIndicator");
3477+
});
3478+
3479+
await page.locator(".update").click();
3480+
await page.locator(".done").wait();
3481+
3482+
const result = await page.evaluate(() => {
3483+
// deno-lint-ignore no-explicit-any
3484+
const w = window as any;
3485+
return {
3486+
btnWasTrue: w.__btnIndicator._wasTrue,
3487+
btnCurrent: w.__btnIndicator.value,
3488+
formWasTrue: w.__formIndicator._wasTrue,
3489+
formCurrent: w.__formIndicator.value,
3490+
};
3491+
});
3492+
// Submitter indicator should have been toggled
3493+
expect(result.btnWasTrue).toBe(true);
3494+
expect(result.btnCurrent).toBe(false);
3495+
// Form indicator should NOT have been used
3496+
expect(result.formWasTrue).toBe(false);
3497+
expect(result.formCurrent).toBe(false);
3498+
});
3499+
},
3500+
});

0 commit comments

Comments
 (0)