@samitouri / QOS-React-2 / commits / 65eec428c4

Use `FormData` `submitter` parameter (#29028)

Jon Jensen committed Dec 18, 2025 at 03:34 UTC 65eec428c40d542d4d5a9c1af5c3f406aecf3440
5 files changed +24 -66
packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js
+2 -30
@@ -45,30 +45,6 @@ function coerceFormActionProp(
45 }
46 }
47
48 -function createFormDataWithSubmitter(
49 - form: HTMLFormElement,
50 - submitter: HTMLInputElement | HTMLButtonElement,
51 -) {
52 - // The submitter's value should be included in the FormData.
53 - // It should be in the document order in the form.
54 - // Since the FormData constructor invokes the formdata event it also
55 - // needs to be available before that happens so after construction it's too
56 - // late. We use a temporary fake node for the duration of this event.
57 - // TODO: FormData takes a second argument that it's the submitter but this
58 - // is fairly new so not all browsers support it yet. Switch to that technique
59 - // when available.
60 - const temp = submitter.ownerDocument.createElement('input');
61 - temp.name = submitter.name;
62 - temp.value = submitter.value;
63 - if (form.id) {
64 - temp.setAttribute('form', form.id);
65 - }
66 - (submitter.parentNode: any).insertBefore(temp, submitter);
67 - const formData = new FormData(form);
68 - (temp.parentNode: any).removeChild(temp);
69 - return formData;
70 -}
71 -
48 /**
49 * This plugin invokes action functions on forms, inputs and buttons if
50 * the form doesn't prevent default.
@@ -129,9 +105,7 @@ function extractEvents(
105 if (didCurrentEventScheduleTransition()) {
106 // We're going to set the pending form status, but because the submission
107 // was prevented, we should not fire the action function.
132 - const formData = submitter
133 - ? createFormDataWithSubmitter(form, submitter)
134 - : new FormData(form);
108 + const formData = new FormData(form, submitter);
109 const pendingState: FormStatus = {
110 pending: true,
111 data: formData,
@@ -160,9 +134,7 @@ function extractEvents(
134 event.preventDefault();
135
136 // Dispatch the action and set a pending form status.
163 - const formData = submitter
164 - ? createFormDataWithSubmitter(form, submitter)
165 - : new FormData(form);
137 + const formData = new FormData(form, submitter);
138 const pendingState: FormStatus = {
139 pending: true,
140 data: formData,
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+1 -1
@@ -14,4 +14,4 @@ export const completeBoundaryWithStyles =
14 export const completeSegment =
15 '$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)};';
16 export const formReplaying =
17 - '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))}});';
17 + '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))}});';
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
+1 -19
@@ -634,25 +634,7 @@ export function listenToFormSubmissionsForReplaying() {
634 event.preventDefault();
635
636 // Take a snapshot of the FormData at the time of the event.
637 - let formData;
638 - if (formDataSubmitter) {
639 - // The submitter's value should be included in the FormData.
640 - // It should be in the document order in the form.
641 - // Since the FormData constructor invokes the formdata event it also
642 - // needs to be available before that happens so after construction it's too
643 - // late. We use a temporary fake node for the duration of this event.
644 - // TODO: FormData takes a second argument that it's the submitter but this
645 - // is fairly new so not all browsers support it yet. Switch to that technique
646 - // when available.
647 - const temp = document.createElement('input');
648 - temp.name = formDataSubmitter.name;
649 - temp.value = formDataSubmitter.value;
650 - formDataSubmitter.parentNode.insertBefore(temp, formDataSubmitter);
651 - formData = new FormData(form);
652 - temp.parentNode.removeChild(temp);
653 - } else {
654 - formData = new FormData(form);
655 - }
637 + const formData = new FormData(form, formDataSubmitter);
638
639 // Queue for replaying later. This field could potentially be shared with multiple
640 // Reacts on the same page since each one will preventDefault for the next one.
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+19 -5
@@ -14,8 +14,8 @@ global.IS_REACT_ACT_ENVIRONMENT = true;
14 // Our current version of JSDOM doesn't implement the event dispatching
15 // so we polyfill it.
16 const NativeFormData = global.FormData;
17 -const FormDataPolyfill = function FormData(form) {
18 - const formData = new NativeFormData(form);
17 +const FormDataPolyfill = function FormData(form, submitter) {
18 + const formData = new NativeFormData(form, submitter);
19 const formDataEvent = new Event('formdata', {
20 bubbles: true,
21 cancelable: false,
@@ -489,11 +489,16 @@ describe('ReactDOMForm', () => {
489 const inputRef = React.createRef();
490 const buttonRef = React.createRef();
491 const outsideButtonRef = React.createRef();
492 + const imageButtonRef = React.createRef();
493 let button;
494 + let buttonX;
495 + let buttonY;
496 let title;
497
498 function action(formData) {
499 button = formData.get('button');
500 + buttonX = formData.get('button.x');
501 + buttonY = formData.get('button.y');
502 title = formData.get('title');
503 }
504
@@ -508,6 +513,12 @@ describe('ReactDOMForm', () => {
513 <button name="button" value="edit" ref={buttonRef}>
514 Edit
515 </button>
516 + <input
517 + type="image"
518 + name="button"
519 + href="/some/image.png"
520 + ref={imageButtonRef}
521 + />
522 </form>
523 <form id="form" action={action}>
524 <input type="text" name="title" defaultValue="hello" />
@@ -546,9 +557,12 @@ describe('ReactDOMForm', () => {
557 expect(button).toBe('outside');
558 expect(title).toBe('hello');
559
549 - // Ensure that the type field got correctly restored
550 - expect(inputRef.current.getAttribute('type')).toBe('submit');
551 - expect(buttonRef.current.getAttribute('type')).toBe(null);
560 + await submit(imageButtonRef.current);
561 +
562 + expect(button).toBe(null);
563 + expect(buttonX).toBe('0');
564 + expect(buttonY).toBe('0');
565 + expect(title).toBe('hello');
566 });
567
568 it('excludes the submitter name when the submitter is a function action', async () => {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMForm-test.js
+1 -11
@@ -121,17 +121,7 @@ describe('ReactFlightDOMForm', () => {
121 const method = (submitter && submitter.formMethod) || form.method;
122 const encType = (submitter && submitter.formEnctype) || form.enctype;
123 if (method === 'post' && encType === 'multipart/form-data') {
124 - let formData;
125 - if (submitter) {
126 - const temp = document.createElement('input');
127 - temp.name = submitter.name;
128 - temp.value = submitter.value;
129 - submitter.parentNode.insertBefore(temp, submitter);
130 - formData = new FormData(form);
131 - temp.parentNode.removeChild(temp);
132 - } else {
133 - formData = new FormData(form);
134 - }
124 + const formData = new FormData(form, submitter);
125 return POST(formData);
126 }
127 throw new Error('Navigate to: ' + action);