Skip to content

Commit

Permalink
Insert temporary input node to polyfill submitter argument in FormData
Browse files Browse the repository at this point in the history
We 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.
  • Loading branch information
sebmarkbage committed Apr 24, 2023
1 parent cf20908 commit 3469b8a
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -53,6 +53,11 @@ function extractEvents(
if (submitterAction != null) {
// The submitter overrides the form action.
action = submitterAction;
if (typeof action === 'function') {
// 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;
}
}
}

Expand Down Expand Up @@ -81,18 +86,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 = document.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);
}
Expand Down
61 changes: 57 additions & 4 deletions packages/react-dom/src/__tests__/ReactDOMForm-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -487,11 +488,13 @@ describe('ReactDOMForm', () => {
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(
// TODO: Test button element too.
<form action={action}>
<input type="text" name="title" defaultValue="hello" />
<input type="submit" name="button" value="save" />
<input type="submit" name="button" value="delete" ref={ref} />
<input type="submit" name="button" value="delete" ref={inputRef} />
<button name="button" value="edit" ref={buttonRef}>
Edit
</button>
</form>,
);
});
Expand All @@ -503,10 +506,60 @@ 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(
<form>
<input type="submit" name="button" value="delete" ref={inputRef} formAction={action} />
<button name="button" value="edit" ref={buttonRef} formAction={action}>
Edit
</button>
</form>,
);
});
}).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__
Expand Down

0 comments on commit 3469b8a

Please sign in to comment.