@@ -65,6 +65,7 @@ import sanitizeURL from '../shared/sanitizeURL';
6565import {
6666 enableCustomElementPropertySupport ,
6767 enableClientRenderFallbackOnTextMismatch ,
68+ enableFormActions ,
6869 enableHostSingletons ,
6970 disableIEWorkarounds ,
7071 enableTrustedTypesIntegration ,
@@ -79,6 +80,10 @@ import {
7980let didWarnControlledToUncontrolled = false ;
8081let didWarnUncontrolledToControlled = false ;
8182let didWarnInvalidHydration = false ;
83+ let didWarnFormActionType = false ;
84+ let didWarnFormActionName = false ;
85+ let didWarnFormActionTarget = false ;
86+ let didWarnFormActionMethod = false ;
8287let canDiffStyleForHydrationWarning ;
8388if ( __DEV__ ) {
8489 // IE 11 parses & normalizes the style attribute as opposed to other
@@ -116,6 +121,102 @@ function validatePropertiesInDevelopment(type: string, props: any) {
116121 }
117122}
118123
124+ function validateFormActionInDevelopment (
125+ tag : string ,
126+ key : string ,
127+ value : mixed ,
128+ props : any ,
129+ ) {
130+ if ( __DEV__ ) {
131+ if ( tag === 'form' ) {
132+ if ( key === 'formAction' ) {
133+ console . error (
134+ 'You can only pass the formAction prop to <input> or <button>. Use the action prop on <form>.' ,
135+ ) ;
136+ } else if ( typeof value === 'function' ) {
137+ if (
138+ ( props . encType != null || props . method != null ) &&
139+ ! didWarnFormActionMethod
140+ ) {
141+ didWarnFormActionMethod = true ;
142+ console . error (
143+ 'Cannot specify a encType or method for a form that specifies a ' +
144+ 'function as the action. React provides those automatically. ' +
145+ 'They will get overridden.' ,
146+ ) ;
147+ }
148+ if ( props . target != null && ! didWarnFormActionTarget ) {
149+ didWarnFormActionTarget = true ;
150+ console . error (
151+ 'Cannot specify a target for a form that specifies a function as the action. ' +
152+ 'The function will always be executed in the same window.' ,
153+ ) ;
154+ }
155+ }
156+ } else if ( tag === 'input' || tag === 'button' ) {
157+ if ( key === 'action' ) {
158+ console . error (
159+ 'You can only pass the action prop to <form>. Use the formAction prop on <input> or <button>.' ,
160+ ) ;
161+ } else if (
162+ tag === 'input' &&
163+ props . type !== 'submit' &&
164+ props . type !== 'image' &&
165+ ! didWarnFormActionType
166+ ) {
167+ didWarnFormActionType = true ;
168+ console . error (
169+ 'An input can only specify a formAction along with type="submit" or type="image".' ,
170+ ) ;
171+ } else if (
172+ tag === 'button' &&
173+ props . type != null &&
174+ props . type !== 'submit' &&
175+ ! didWarnFormActionType
176+ ) {
177+ didWarnFormActionType = true ;
178+ console . error (
179+ 'A button can only specify a formAction along with type="submit" or no type.' ,
180+ ) ;
181+ } else if ( typeof value === 'function' ) {
182+ // Function form actions cannot control the form properties
183+ if ( props . name != null && ! didWarnFormActionName ) {
184+ didWarnFormActionName = true ;
185+ console . error (
186+ 'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +
187+ 'React needs it to encode which action should be invoked. It will get overridden.' ,
188+ ) ;
189+ }
190+ if (
191+ ( props . formEncType != null || props . formMethod != null ) &&
192+ ! didWarnFormActionMethod
193+ ) {
194+ didWarnFormActionMethod = true ;
195+ console . error (
196+ 'Cannot specify a formEncType or formMethod for a button that specifies a ' +
197+ 'function as a formAction. React provides those automatically. They will get overridden.' ,
198+ ) ;
199+ }
200+ if ( props . formTarget != null && ! didWarnFormActionTarget ) {
201+ didWarnFormActionTarget = true ;
202+ console . error (
203+ 'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +
204+ 'The function will always be executed in the same window.' ,
205+ ) ;
206+ }
207+ }
208+ } else {
209+ if ( key === 'action' ) {
210+ console . error ( 'You can only pass the action prop to <form>.' ) ;
211+ } else {
212+ console . error (
213+ 'You can only pass the formAction prop to <input> or <button>.' ,
214+ ) ;
215+ }
216+ }
217+ }
218+ }
219+
119220function warnForPropDifference (
120221 propName : string ,
121222 serverValue : mixed ,
@@ -327,8 +428,7 @@ function setProp(
327428 }
328429 // These attributes accept URLs. These must not allow javascript: URLS.
329430 case 'src' :
330- case 'href' :
331- case 'action' :
431+ case 'href' : {
332432 if ( enableFilterEmptyStringAttributesDOM ) {
333433 if ( value === '' ) {
334434 if ( __DEV__ ) {
@@ -355,8 +455,6 @@ function setProp(
355455 break ;
356456 }
357457 }
358- // Fall through to the last case which shouldn't remove empty strings.
359- case 'formAction' : {
360458 if (
361459 value == null ||
362460 typeof value === 'function' ||
@@ -377,6 +475,50 @@ function setProp(
377475 domElement . setAttribute ( key , sanitizedValue ) ;
378476 break ;
379477 }
478+ case 'action' :
479+ case 'formAction' : {
480+ // TODO: Consider moving these special cases to the form, input and button tags.
481+ if (
482+ value == null ||
483+ ( ! enableFormActions && typeof value === 'function' ) ||
484+ typeof value === 'symbol' ||
485+ typeof value === 'boolean'
486+ ) {
487+ domElement . removeAttribute ( key ) ;
488+ break ;
489+ }
490+ if ( __DEV__ ) {
491+ validateFormActionInDevelopment ( tag , key , value , props ) ;
492+ }
493+ if ( enableFormActions && typeof value === 'function' ) {
494+ // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
495+ // because we'll preventDefault, but it can happen if a form is manually submitted or
496+ // if someone calls stopPropagation before React gets the event.
497+ // If CSP is used to block javascript: URLs that's fine too. It just won't show this
498+ // error message but the URL will be logged.
499+ domElement . setAttribute (
500+ key ,
501+ // eslint-disable-next-line no-script-url
502+ "javascript:throw new Error('" +
503+ 'A React form was unexpectedly submitted. If you called form.submit() manually, ' +
504+ "consider using form.requestSubmit() instead. If you're trying to use " +
505+ 'event.stopPropagation() in a submit event handler, consider also calling ' +
506+ 'event.preventDefault().' +
507+ "')" ,
508+ ) ;
509+ break ;
510+ }
511+ // `setAttribute` with objects becomes only `[object]` in IE8/9,
512+ // ('' + value) makes it output the correct toString()-value.
513+ if ( __DEV__ ) {
514+ checkAttributeStringCoercion ( value , key ) ;
515+ }
516+ const sanitizedValue = ( sanitizeURL (
517+ enableTrustedTypesIntegration ? value : '' + ( value : any ) ,
518+ ) : any ) ;
519+ domElement . setAttribute ( key , sanitizedValue ) ;
520+ break ;
521+ }
380522 case 'onClick' : {
381523 // TODO: This cast may not be sound for SVG, MathML or custom elements.
382524 if ( value != null ) {
@@ -2423,6 +2565,13 @@ function diffHydratedCustomComponent(
24232565 }
24242566}
24252567
2568+ // This is the exact URL string we expect that Fizz renders if we provide a function action.
2569+ // We use this for hydration warnings. It needs to be in sync with Fizz. Maybe makes sense
2570+ // as a shared module for that reason.
2571+ const EXPECTED_FORM_ACTION_URL =
2572+ // eslint-disable-next-line no-script-url
2573+ "javascript :throw new Error ( 'A React form was unexpectedly submitted.' ) ";
2574+
24262575function diffHydratedGenericElement (
24272576 domElement : Element ,
24282577 tag : string ,
@@ -2505,7 +2654,6 @@ function diffHydratedGenericElement(
25052654 }
25062655 case 'src ':
25072656 case 'href ':
2508- case 'action ':
25092657 if ( enableFilterEmptyStringAttributesDOM ) {
25102658 if ( value === '' ) {
25112659 if ( __DEV__ ) {
@@ -2546,11 +2694,29 @@ function diffHydratedGenericElement(
25462694 extraAttributes ,
25472695 ) ;
25482696 continue ;
2697+ case 'action ':
25492698 case 'formAction ':
2699+ if ( enableFormActions ) {
2700+ const serverValue = domElement . getAttribute ( propKey ) ;
2701+ const hasFormActionURL = serverValue === EXPECTED_FORM_ACTION_URL ;
2702+ if ( typeof value === 'function' ) {
2703+ extraAttributes . delete ( propKey . toLowerCase ( ) ) ;
2704+ if ( hasFormActionURL ) {
2705+ // Expected
2706+ continue ;
2707+ }
2708+ warnForPropDifference ( propKey , serverValue , value ) ;
2709+ continue ;
2710+ } else if ( hasFormActionURL ) {
2711+ extraAttributes . delete ( propKey . toLowerCase ( ) ) ;
2712+ warnForPropDifference ( propKey , 'function' , value ) ;
2713+ continue ;
2714+ }
2715+ }
25502716 hydrateSanitizedAttribute (
25512717 domElement ,
25522718 propKey ,
2553- 'formaction' ,
2719+ propKey . toLowerCase ( ) ,
25542720 value ,
25552721 extraAttributes ,
25562722 ) ;
0 commit comments