@samitouri / QOS-React / commits / 5e5342b100

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__