main
js 185 lines 5.83 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {AnyNativeEvent} from '../PluginModuleType';
11 import type {DOMEventName} from '../DOMEventNames';
12 import type {DispatchQueue} from '../DOMPluginEventSystem';
13 import type {EventSystemFlags} from '../EventSystemFlags';
14 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
15 import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
16
17 import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
18 import {getFiberCurrentPropsFromNode} from '../../client/ReactDOMComponentTree';
19 import {startHostTransition} from 'react-reconciler/src/ReactFiberReconciler';
20 import {didCurrentEventScheduleTransition} from 'react-reconciler/src/ReactFiberRootScheduler';
21 import sanitizeURL from 'react-dom-bindings/src/shared/sanitizeURL';
22 import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
23
24 import {SyntheticEvent} from '../SyntheticEvent';
25
26 function coerceFormActionProp(
27 actionProp: mixed,
28 ): string | (FormData => void | Promise<void>) | null {
29 // This should match the logic in ReactDOMComponent
30 if (
31 actionProp == null ||
32 typeof actionProp === 'symbol' ||
33 typeof actionProp === 'boolean'
34 ) {
35 return null;
36 } else if (typeof actionProp === 'function') {
37 return actionProp as any;
38 } else {
39 if (__DEV__) {
40 checkAttributeStringCoercion(actionProp, 'action');
41 }
42 return sanitizeURL(
43 enableTrustedTypesIntegration ? actionProp : '' + (actionProp as any),
44 ) as any;
45 }
46 }
47
48 /**
49 * This plugin invokes action functions on forms, inputs and buttons if
50 * the form doesn't prevent default.
51 */
52 function extractEvents(
53 dispatchQueue: DispatchQueue,
54 domEventName: DOMEventName,
55 maybeTargetInst: null | Fiber,
56 nativeEvent: AnyNativeEvent,
57 nativeEventTarget: null | EventTarget,
58 eventSystemFlags: EventSystemFlags,
59 targetContainer: EventTarget,
60 ) {
61 if (domEventName !== 'submit') {
62 return;
63 }
64 if (!maybeTargetInst || maybeTargetInst.stateNode !== nativeEventTarget) {
65 // If we're inside a parent root that itself is a parent of this root, then
66 // its deepest target won't be the actual form that's being submitted.
67 return;
68 }
69 const formInst = maybeTargetInst;
70 const form: HTMLFormElement = nativeEventTarget as any;
71 let action = coerceFormActionProp(
72 (getFiberCurrentPropsFromNode(form) as any).action,
73 );
74 let submitter: null | void | HTMLInputElement | HTMLButtonElement = (
75 nativeEvent as any
76 ).submitter;
77 let submitterAction;
78 if (submitter) {
79 const submitterProps = getFiberCurrentPropsFromNode(submitter);
80 submitterAction = submitterProps
81 ? coerceFormActionProp((submitterProps as any).formAction)
82 : // The built-in Flow type is ?string, wider than the spec
83 (submitter.getAttribute('formAction') as any as string | null);
84 if (submitterAction !== null) {
85 // The submitter overrides the form action.
86 action = submitterAction;
87 // If the action is a function, we don't want to pass its name
88 // value to the FormData since it's controlled by the server.
89 submitter = null;
90 }
91 }
92
93 const event = new SyntheticEvent(
94 'action',
95 'action',
96 null,
97 nativeEvent,
98 nativeEventTarget,
99 );
100
101 function submitForm() {
102 if (nativeEvent.defaultPrevented) {
103 // An earlier event prevented form submission. If a transition update was
104 // also scheduled, we should trigger a pending form status — even if
105 // no action function was provided.
106 if (didCurrentEventScheduleTransition()) {
107 // We're going to set the pending form status, but because the submission
108 // was prevented, we should not fire the action function.
109 const formData = new FormData(form, submitter);
110 const pendingState: FormStatus = {
111 pending: true,
112 data: formData,
113 method: form.method,
114 action: action,
115 };
116 if (__DEV__) {
117 Object.freeze(pendingState);
118 }
119 startHostTransition(
120 formInst,
121 pendingState,
122 // Pass `null` as the action
123 // TODO: Consider splitting up startHostTransition into two separate
124 // functions, one that sets the form status and one that invokes
125 // the action.
126 null,
127 formData,
128 );
129 } else {
130 // No earlier event scheduled a transition. Exit without setting a
131 // pending form status.
132 }
133 } else if (typeof action === 'function') {
134 // A form action was provided. Prevent native navigation.
135 event.preventDefault();
136
137 // Dispatch the action and set a pending form status.
138 const formData = new FormData(form, submitter);
139 const pendingState: FormStatus = {
140 pending: true,
141 data: formData,
142 method: form.method,
143 action: action,
144 };
145 if (__DEV__) {
146 Object.freeze(pendingState);
147 }
148 startHostTransition(formInst, pendingState, action, formData);
149 } else {
150 // No earlier event prevented the default submission, and no action was
151 // provided. Exit without setting a pending form status.
152 }
153 }
154
155 dispatchQueue.push({
156 event,
157 listeners: [
158 {
159 instance: null,
160 listener: submitForm,
161 currentTarget: form,
162 },
163 ],
164 });
165 }
166
167 export {extractEvents};
168
169 export function dispatchReplayedFormAction(
170 formInst: Fiber,
171 form: HTMLFormElement,
172 action: FormData => void | Promise<void>,
173 formData: FormData,
174 ): void {
175 const pendingState: FormStatus = {
176 pending: true,
177 data: formData,
178 method: form.method,
179 action: action,
180 };
181 if (__DEV__) {
182 Object.freeze(pendingState);
183 }
184 startHostTransition(formInst, pendingState, action, formData);
185 }