diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index 8d2402e26ba9b..bd7dfc0d53738 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -132,6 +132,9 @@ function validateFormActionInDevelopment( props: any, ) { if (__DEV__) { + if (value == null) { + return; + } if (tag === 'form') { if (key === 'formAction') { console.error( @@ -483,6 +486,9 @@ function setProp( case 'action': case 'formAction': { // TODO: Consider moving these special cases to the form, input and button tags. + if (__DEV__) { + validateFormActionInDevelopment(tag, key, value, props); + } if (enableFormActions) { if (typeof value === 'function') { // Set a javascript URL that doesn't do anything. We don't expect this to be invoked @@ -554,9 +560,6 @@ function setProp( domElement.removeAttribute(key); break; } - if (__DEV__) { - validateFormActionInDevelopment(tag, key, value, props); - } // `setAttribute` with objects becomes only `[object]` in IE8/9, // ('' + value) makes it output the correct toString()-value. if (__DEV__) { diff --git a/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js b/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js index 4ef703ff8c529..1fc0091b1c1cd 100644 --- a/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js +++ b/packages/react-dom-bindings/src/events/plugins/FormActionEventPlugin.js @@ -42,7 +42,7 @@ function extractEvents( const formInst = maybeTargetInst; const form: HTMLFormElement = (nativeEventTarget: any); let action = (getFiberCurrentPropsFromNode(form): any).action; - const submitter: null | HTMLInputElement | HTMLButtonElement = + let submitter: null | HTMLInputElement | HTMLButtonElement = (nativeEvent: any).submitter; let submitterAction; if (submitter) { @@ -53,6 +53,9 @@ function extractEvents( if (submitterAction != null) { // The submitter overrides the form action. action = submitterAction; + // If the action is a function, we don't want to pass its name + // value to the FormData since it's controlled by the server. + submitter = null; } } @@ -81,18 +84,16 @@ function extractEvents( // 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. The easiest way to do this is to switch the form field to hidden, - // which is always included, and then back again. This does means that this - // is observable from the formdata event though. - // TODO: This tricky doesn't work on button elements. Consider inserting - // a fake node instead for that case. + // 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 type = submitter.type; - submitter.type = 'hidden'; + const temp = submitter.ownerDocument.createElement('input'); + temp.name = submitter.name; + temp.value = submitter.value; + (submitter.parentNode: any).insertBefore(temp, submitter); formData = new FormData(form); - submitter.type = type; + (temp.parentNode: any).removeChild(temp); } else { formData = new FormData(form); } diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js index a0076f70c90b3..267d19410bdd2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js @@ -475,7 +475,8 @@ describe('ReactDOMForm', () => { // @gate enableFormActions it('can read the clicked button in the formdata event', async () => { - const ref = React.createRef(); + const inputRef = React.createRef(); + const buttonRef = React.createRef(); let button; let title; @@ -487,11 +488,13 @@ describe('ReactDOMForm', () => { const root = ReactDOMClient.createRoot(container); await act(async () => { root.render( - // TODO: Test button element too.
- + +
, ); }); @@ -503,10 +506,70 @@ describe('ReactDOMForm', () => { } }); - await submit(ref.current); + await submit(inputRef.current); expect(button).toBe('delete'); expect(title).toBe(null); + + await submit(buttonRef.current); + + expect(button).toBe('edit'); + 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); + }); + + // @gate enableFormActions + it('excludes the submitter name when the submitter is a function action', async () => { + const inputRef = React.createRef(); + const buttonRef = React.createRef(); + let button; + + function action(formData) { + // A function action cannot control the name since it might be controlled by the server + // so we need to make sure it doesn't get into the FormData. + button = formData.get('button'); + } + + const root = ReactDOMClient.createRoot(container); + await expect(async () => { + await act(async () => { + root.render( +
+ + +
, + ); + }); + }).toErrorDev([ + 'Cannot specify a "name" prop for a button that specifies a function as a formAction.', + ]); + + await submit(inputRef.current); + + expect(button).toBe(null); + + await submit(buttonRef.current); + + expect(button).toBe(null); + + // Ensure that the type field got correctly restored + expect(inputRef.current.getAttribute('type')).toBe('submit'); + expect(buttonRef.current.getAttribute('type')).toBe(null); }); // @gate enableFormActions || !__DEV__