Use FormData submitter parameter (#29028)

This commit is contained in:
Jon Jensen
2025-12-18 03:34:15 -07:00
committed by GitHub
parent 454fc41fc7
commit 65eec428c4
5 changed files with 24 additions and 66 deletions

View File

@@ -45,30 +45,6 @@ function coerceFormActionProp(
}
}
function createFormDataWithSubmitter(
form: HTMLFormElement,
submitter: HTMLInputElement | HTMLButtonElement,
) {
// The submitter's value should be included in the FormData.
// It should be in the document order in the form.
// Since the FormData constructor invokes the formdata event it also
// needs to be available before that happens so after construction it's too
// late. We use a temporary fake node for the duration of this event.
// TODO: FormData takes a second argument that it's the submitter but this
// is fairly new so not all browsers support it yet. Switch to that technique
// when available.
const temp = submitter.ownerDocument.createElement('input');
temp.name = submitter.name;
temp.value = submitter.value;
if (form.id) {
temp.setAttribute('form', form.id);
}
(submitter.parentNode: any).insertBefore(temp, submitter);
const formData = new FormData(form);
(temp.parentNode: any).removeChild(temp);
return formData;
}
/**
* This plugin invokes action functions on forms, inputs and buttons if
* the form doesn't prevent default.
@@ -129,9 +105,7 @@ function extractEvents(
if (didCurrentEventScheduleTransition()) {
// We're going to set the pending form status, but because the submission
// was prevented, we should not fire the action function.
const formData = submitter
? createFormDataWithSubmitter(form, submitter)
: new FormData(form);
const formData = new FormData(form, submitter);
const pendingState: FormStatus = {
pending: true,
data: formData,
@@ -160,9 +134,7 @@ function extractEvents(
event.preventDefault();
// Dispatch the action and set a pending form status.
const formData = submitter
? createFormDataWithSubmitter(form, submitter)
: new FormData(form);
const formData = new FormData(form, submitter);
const pendingState: FormStatus = {
pending: true,
data: formData,

View File

@@ -14,4 +14,4 @@ export const completeBoundaryWithStyles =
export const completeSegment =
'$RS=function(a,b){a=document.getElementById(a);b=document.getElementById(b);for(a.parentNode.removeChild(a);a.firstChild;)b.parentNode.insertBefore(a.firstChild,b);b.parentNode.removeChild(b)};';
export const formReplaying =
'addEventListener("submit",function(a){if(!a.defaultPrevented){var c=a.target,d=a.submitter,e=c.action,b=d;if(d){var f=d.getAttribute("formAction");null!=f&&(e=f,b=null)}"javascript:throw new Error(\'React form unexpectedly submitted.\')"===e&&(a.preventDefault(),b?(a=document.createElement("input"),a.name=b.name,a.value=b.value,b.parentNode.insertBefore(a,b),b=new FormData(c),a.parentNode.removeChild(a)):b=new FormData(c),a=c.ownerDocument||c,(a.$$reactFormReplay=a.$$reactFormReplay||[]).push(c,d,b))}});';
'addEventListener("submit",function(a){if(!a.defaultPrevented){var b=a.target,d=a.submitter,c=b.action,e=d;if(d){var f=d.getAttribute("formAction");null!=f&&(c=f,e=null)}"javascript:throw new Error(\'React form unexpectedly submitted.\')"===c&&(a.preventDefault(),a=new FormData(b,e),c=b.ownerDocument||b,(c.$$reactFormReplay=c.$$reactFormReplay||[]).push(b,d,a))}});';

View File

@@ -634,25 +634,7 @@ export function listenToFormSubmissionsForReplaying() {
event.preventDefault();
// Take a snapshot of the FormData at the time of the event.
let formData;
if (formDataSubmitter) {
// The submitter's value should be included in the FormData.
// It should be in the document order in the form.
// Since the FormData constructor invokes the formdata event it also
// needs to be available before that happens so after construction it's too
// late. We use a temporary fake node for the duration of this event.
// TODO: FormData takes a second argument that it's the submitter but this
// is fairly new so not all browsers support it yet. Switch to that technique
// when available.
const temp = document.createElement('input');
temp.name = formDataSubmitter.name;
temp.value = formDataSubmitter.value;
formDataSubmitter.parentNode.insertBefore(temp, formDataSubmitter);
formData = new FormData(form);
temp.parentNode.removeChild(temp);
} else {
formData = new FormData(form);
}
const formData = new FormData(form, formDataSubmitter);
// Queue for replaying later. This field could potentially be shared with multiple
// Reacts on the same page since each one will preventDefault for the next one.

View File

@@ -14,8 +14,8 @@ global.IS_REACT_ACT_ENVIRONMENT = true;
// Our current version of JSDOM doesn't implement the event dispatching
// so we polyfill it.
const NativeFormData = global.FormData;
const FormDataPolyfill = function FormData(form) {
const formData = new NativeFormData(form);
const FormDataPolyfill = function FormData(form, submitter) {
const formData = new NativeFormData(form, submitter);
const formDataEvent = new Event('formdata', {
bubbles: true,
cancelable: false,
@@ -489,11 +489,16 @@ describe('ReactDOMForm', () => {
const inputRef = React.createRef();
const buttonRef = React.createRef();
const outsideButtonRef = React.createRef();
const imageButtonRef = React.createRef();
let button;
let buttonX;
let buttonY;
let title;
function action(formData) {
button = formData.get('button');
buttonX = formData.get('button.x');
buttonY = formData.get('button.y');
title = formData.get('title');
}
@@ -508,6 +513,12 @@ describe('ReactDOMForm', () => {
<button name="button" value="edit" ref={buttonRef}>
Edit
</button>
<input
type="image"
name="button"
href="/some/image.png"
ref={imageButtonRef}
/>
</form>
<form id="form" action={action}>
<input type="text" name="title" defaultValue="hello" />
@@ -546,9 +557,12 @@ describe('ReactDOMForm', () => {
expect(button).toBe('outside');
expect(title).toBe('hello');
// Ensure that the type field got correctly restored
expect(inputRef.current.getAttribute('type')).toBe('submit');
expect(buttonRef.current.getAttribute('type')).toBe(null);
await submit(imageButtonRef.current);
expect(button).toBe(null);
expect(buttonX).toBe('0');
expect(buttonY).toBe('0');
expect(title).toBe('hello');
});
it('excludes the submitter name when the submitter is a function action', async () => {

View File

@@ -121,17 +121,7 @@ describe('ReactFlightDOMForm', () => {
const method = (submitter && submitter.formMethod) || form.method;
const encType = (submitter && submitter.formEnctype) || form.enctype;
if (method === 'post' && encType === 'multipart/form-data') {
let formData;
if (submitter) {
const temp = document.createElement('input');
temp.name = submitter.name;
temp.value = submitter.value;
submitter.parentNode.insertBefore(temp, submitter);
formData = new FormData(form);
temp.parentNode.removeChild(temp);
} else {
formData = new FormData(form);
}
const formData = new FormData(form, submitter);
return POST(formData);
}
throw new Error('Navigate to: ' + action);