Insert temporary input node to polyfill submitter argument in FormData (#26714)
Insert temporary input node to polyfill submitter argument in FormData. This works for buttons too and fixes a bug where the type attribute wasn't reset. I also exclude the submitter if it's a function action. This ensures that we don't include the generated "name" when the action is a server action. Conceptually that name doesn't exist.
Sebastian Markbåge committed
Apr 24, 2023 at 14:18 UTC
5e5342b10059bf90738a8d9171fcf0af9d9d5d51
3 files changed
+83
-16
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+6
-3
@@ -132,6 +132,9 @@ function validateFormActionInDevelopment(
132
props: any,
133
) {
134
if (__DEV__) {
135
+ if (value == null) {
136
+ return;
137
+ }
138
if (tag === 'form') {
139
if (key === 'formAction') {
140
console.error(
@@ -483,6 +486,9 @@ function setProp(
486
case 'action':
487
case 'formAction': {
488
// TODO: Consider moving these special cases to the form, input and button tags.
489
+ if (__DEV__) {
490
+ validateFormActionInDevelopment(tag, key, value, props);
491
+ }
492
if (enableFormActions) {
493
if (typeof value === 'function') {
494
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
@@ -554,9 +560,6 @@ function setProp(
560
domElement.removeAttribute(key);
561
break;
562
}
557
- if (__DEV__) {
558
- validateFormActionInDevelopment(tag, key, value, props);
559
- }
563
// `setAttribute` with objects becomes only `[object]` in IE8/9,
564
// ('' + value) makes it output the correct toString()-value.
565
if (__DEV__) {
packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js
+10
-9
@@ -42,7 +42,7 @@ function extractEvents(
42
const formInst = maybeTargetInst;
43
const form: HTMLFormElement = (nativeEventTarget: any);
44
let action = (getFiberCurrentPropsFromNode(form): any).action;
45
- const submitter: null | HTMLInputElement | HTMLButtonElement =
45
+ let submitter: null | HTMLInputElement | HTMLButtonElement =
46
(nativeEvent: any).submitter;
47
let submitterAction;
48
if (submitter) {
@@ -53,6 +53,9 @@ function extractEvents(
53
if (submitterAction != null) {
54
// The submitter overrides the form action.
55
action = submitterAction;
56
+ // If the action is a function, we don't want to pass its name
57
+ // value to the FormData since it's controlled by the server.
58
+ submitter = null;
59
}
60
}
61
@@ -81,18 +84,16 @@ function extractEvents(
84
// It should be in the document order in the form.
85
// Since the FormData constructor invokes the formdata event it also
86
// needs to be available before that happens so after construction it's too
84
- // late. The easiest way to do this is to switch the form field to hidden,
85
- // which is always included, and then back again. This does means that this
86
- // is observable from the formdata event though.
87
- // TODO: This tricky doesn't work on button elements. Consider inserting
88
- // a fake node instead for that case.
87
+ // late. We use a temporary fake node for the duration of this event.
88
// TODO: FormData takes a second argument that it's the submitter but this
89
// is fairly new so not all browsers support it yet. Switch to that technique
90
// when available.
92
- const type = submitter.type;
93
- submitter.type = 'hidden';
91
+ const temp = submitter.ownerDocument.createElement('input');
92
+ temp.name = submitter.name;
93
+ temp.value = submitter.value;
94
+ (submitter.parentNode: any).insertBefore(temp, submitter);
95
formData = new FormData(form);
95
- submitter.type = type;
96
+ (temp.parentNode: any).removeChild(temp);
97
} else {
98
formData = new FormData(form);
99
}
packages/react-dom/src/__tests__/ReactDOMForm-test.js
+67
-4
@@ -475,7 +475,8 @@ describe('ReactDOMForm', () => {
475
476
// @gate enableFormActions
477
it('can read the clicked button in the formdata event', async () => {
478
- const ref = React.createRef();
478
+ const inputRef = React.createRef();
479
+ const buttonRef = React.createRef();
480
let button;
481
let title;
482
@@ -487,11 +488,13 @@ describe('ReactDOMForm', () => {
488
const root = ReactDOMClient.createRoot(container);
489
await act(async () => {
490
root.render(
490
- // TODO: Test button element too.
491
<form action={action}>
492
<input type="text" name="title" defaultValue="hello" />
493
<input type="submit" name="button" value="save" />
494
- <input type="submit" name="button" value="delete" ref={ref} />
494
+ <input type="submit" name="button" value="delete" ref={inputRef} />
495
+ <button name="button" value="edit" ref={buttonRef}>
496
+ Edit
497
+ </button>
498
</form>,
499
);
500
});
@@ -503,10 +506,70 @@ describe('ReactDOMForm', () => {
506
}
507
});
508
506
- await submit(ref.current);
509
+ await submit(inputRef.current);
510
511
expect(button).toBe('delete');
512
expect(title).toBe(null);
513
+
514
+ await submit(buttonRef.current);
515
+
516
+ expect(button).toBe('edit');
517
+ expect(title).toBe('hello');
518
+
519
+ // Ensure that the type field got correctly restored
520
+ expect(inputRef.current.getAttribute('type')).toBe('submit');
521
+ expect(buttonRef.current.getAttribute('type')).toBe(null);
522
+ });
523
+
524
+ // @gate enableFormActions
525
+ it('excludes the submitter name when the submitter is a function action', async () => {
526
+ const inputRef = React.createRef();
527
+ const buttonRef = React.createRef();
528
+ let button;
529
+
530
+ function action(formData) {
531
+ // A function action cannot control the name since it might be controlled by the server
532
+ // so we need to make sure it doesn't get into the FormData.
533
+ button = formData.get('button');
534
+ }
535
+
536
+ const root = ReactDOMClient.createRoot(container);
537
+ await expect(async () => {
538
+ await act(async () => {
539
+ root.render(
540
+ <form>
541
+ <input
542
+ type="submit"
543
+ name="button"
544
+ value="delete"
545
+ ref={inputRef}
546
+ formAction={action}
547
+ />
548
+ <button
549
+ name="button"
550
+ value="edit"
551
+ ref={buttonRef}
552
+ formAction={action}>
553
+ Edit
554
+ </button>
555
+ </form>,
556
+ );
557
+ });
558
+ }).toErrorDev([
559
+ 'Cannot specify a "name" prop for a button that specifies a function as a formAction.',
560
+ ]);
561
+
562
+ await submit(inputRef.current);
563
+
564
+ expect(button).toBe(null);
565
+
566
+ await submit(buttonRef.current);
567
+
568
+ expect(button).toBe(null);
569
+
570
+ // Ensure that the type field got correctly restored
571
+ expect(inputRef.current.getAttribute('type')).toBe('submit');
572
+ expect(buttonRef.current.getAttribute('type')).toBe(null);
573
});
574
575
// @gate enableFormActions || !__DEV__