@@ -668,6 +668,22 @@ function pushStringAttribute(
668668 }
669669}
670670
671+ type CustomFormAction = {
672+ name ?: string ,
673+ action ?: string ,
674+ encType ?: string ,
675+ method ?: string ,
676+ target ?: string ,
677+ data ?: FormData ,
678+ } ;
679+
680+ function makeFormFieldPrefix ( responseState : ResponseState ) : string {
681+ // I'm just reusing this counter. It's not really the same namespace as "name".
682+ // It could just be its own counter.
683+ const id = responseState . nextSuspenseID ++ ;
684+ return responseState . idPrefix + '$ACTION:' + id + ':' ;
685+ }
686+
671687// Since this will likely be repeated a lot in the HTML, we use a more concise message
672688// than on the client and hopefully it's googleable.
673689const actionJavaScriptURL = stringToPrecomputedChunk (
@@ -677,6 +693,36 @@ const actionJavaScriptURL = stringToPrecomputedChunk(
677693 ) ,
678694) ;
679695
696+ const startHiddenInputChunk = stringToPrecomputedChunk ( '<input type="hidden"' ) ;
697+
698+ function pushAdditionalFormField (
699+ this : Array < Chunk | PrecomputedChunk > ,
700+ value : string | File ,
701+ key : string ,
702+ ) : void {
703+ const target : Array < Chunk | PrecomputedChunk > = this ;
704+ target . push ( startHiddenInputChunk ) ;
705+ if ( typeof value !== 'string' ) {
706+ throw new Error (
707+ 'File/Blob fields are not yet supported in progressive forms. ' +
708+ 'It probably means you are closing over binary data or FormData in a Server Action.' ,
709+ ) ;
710+ }
711+ pushStringAttribute ( target , 'name' , key ) ;
712+ pushStringAttribute ( target , 'value' , value ) ;
713+ target . push ( endOfStartTagSelfClosing ) ;
714+ }
715+
716+ function pushAdditionalFormFields (
717+ target : Array < Chunk | PrecomputedChunk > ,
718+ formData : null | FormData ,
719+ ) {
720+ if ( formData !== null ) {
721+ // $FlowFixMe[prop-missing]: FormData has forEach.
722+ formData . forEach ( pushAdditionalFormField , target ) ;
723+ }
724+ }
725+
680726function pushFormActionAttribute (
681727 target : Array < Chunk | PrecomputedChunk > ,
682728 responseState : ResponseState ,
@@ -685,7 +731,8 @@ function pushFormActionAttribute(
685731 formMethod : any ,
686732 formTarget : any ,
687733 name : any ,
688- ) : void {
734+ ) : null | FormData {
735+ let formData = null ;
689736 if ( enableFormActions && typeof formAction === 'function' ) {
690737 // Function form actions cannot control the form properties
691738 if ( __DEV__ ) {
@@ -714,37 +761,55 @@ function pushFormActionAttribute(
714761 ) ;
715762 }
716763 }
717- // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
718- // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
719- // manually submitted or if someone calls stopPropagation before React gets the event.
720- // If CSP is used to block javascript: URLs that's fine too. It just won't show this
721- // error message but the URL will be logged.
722- target . push (
723- attributeSeparator ,
724- stringToChunk ( 'formAction' ) ,
725- attributeAssign ,
726- actionJavaScriptURL ,
727- attributeEnd ,
728- ) ;
729- injectFormReplayingRuntime ( responseState ) ;
730- } else {
731- // Plain form actions support all the properties, so we have to emit them.
732- if ( name !== null ) {
733- pushAttribute ( target , 'name' , name ) ;
734- }
735- if ( formAction !== null ) {
736- pushAttribute ( target , 'formAction' , formAction ) ;
737- }
738- if ( formEncType !== null ) {
739- pushAttribute ( target , 'formEncType' , formEncType ) ;
740- }
741- if ( formMethod !== null ) {
742- pushAttribute ( target , 'formMethod' , formMethod ) ;
743- }
744- if ( formTarget !== null ) {
745- pushAttribute ( target , 'formTarget' , formTarget ) ;
764+ const customAction : CustomFormAction = formAction . $$FORM_ACTION ;
765+ if ( typeof customAction === 'function' ) {
766+ // This action has a custom progressive enhancement form that can submit the form
767+ // back to the server if it's invoked before hydration. Such as a Server Action.
768+ const prefix = makeFormFieldPrefix ( responseState ) ;
769+ const customFields = customAction ( prefix ) ;
770+ name = customFields . name ;
771+ formAction = customFields . action || '' ;
772+ formEncType = customFields . encType ;
773+ formMethod = customFields . method ;
774+ formTarget = customFields . target ;
775+ formData = customFields . data ;
776+ } else {
777+ // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
778+ // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
779+ // manually submitted or if someone calls stopPropagation before React gets the event.
780+ // If CSP is used to block javascript: URLs that's fine too. It just won't show this
781+ // error message but the URL will be logged.
782+ target . push (
783+ attributeSeparator ,
784+ stringToChunk ( 'formAction' ) ,
785+ attributeAssign ,
786+ actionJavaScriptURL ,
787+ attributeEnd ,
788+ ) ;
789+ name = null ;
790+ formAction = null ;
791+ formEncType = null ;
792+ formMethod = null ;
793+ formTarget = null ;
794+ injectFormReplayingRuntime ( responseState ) ;
746795 }
747796 }
797+ if ( name !== null ) {
798+ pushAttribute ( target , 'name' , name ) ;
799+ }
800+ if ( formAction !== null ) {
801+ pushAttribute ( target , 'formAction' , formAction ) ;
802+ }
803+ if ( formEncType !== null ) {
804+ pushAttribute ( target , 'formEncType' , formEncType ) ;
805+ }
806+ if ( formMethod !== null ) {
807+ pushAttribute ( target , 'formMethod' , formMethod ) ;
808+ }
809+ if ( formTarget !== null ) {
810+ pushAttribute ( target , 'formTarget' , formTarget ) ;
811+ }
812+ return formData ;
748813}
749814
750815function pushAttribute (
@@ -1366,6 +1431,8 @@ function pushStartForm(
13661431 }
13671432 }
13681433
1434+ let formData = null ;
1435+ let formActionName = null ;
13691436 if ( enableFormActions && typeof formAction === 'function' ) {
13701437 // Function form actions cannot control the form properties
13711438 if ( __DEV__ ) {
@@ -1388,36 +1455,60 @@ function pushStartForm(
13881455 ) ;
13891456 }
13901457 }
1391- // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
1392- // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
1393- // manually submitted or if someone calls stopPropagation before React gets the event.
1394- // If CSP is used to block javascript: URLs that's fine too. It just won't show this
1395- // error message but the URL will be logged.
1396- target . push (
1397- attributeSeparator ,
1398- stringToChunk ( 'action' ) ,
1399- attributeAssign ,
1400- actionJavaScriptURL ,
1401- attributeEnd ,
1402- ) ;
1403- injectFormReplayingRuntime ( responseState ) ;
1404- } else {
1405- // Plain form actions support all the properties, so we have to emit them.
1406- if ( formAction !== null ) {
1407- pushAttribute ( target , 'action' , formAction ) ;
1408- }
1409- if ( formEncType !== null ) {
1410- pushAttribute ( target , 'encType' , formEncType ) ;
1411- }
1412- if ( formMethod !== null ) {
1413- pushAttribute ( target , 'method' , formMethod ) ;
1414- }
1415- if ( formTarget !== null ) {
1416- pushAttribute ( target , 'target' , formTarget ) ;
1458+ const customAction : CustomFormAction = formAction . $$FORM_ACTION ;
1459+ if ( typeof customAction === 'function' ) {
1460+ // This action has a custom progressive enhancement form that can submit the form
1461+ // back to the server if it's invoked before hydration. Such as a Server Action.
1462+ const prefix = makeFormFieldPrefix ( responseState ) ;
1463+ const customFields = customAction ( prefix ) ;
1464+ formAction = customFields . action || '' ;
1465+ formEncType = customFields . encType ;
1466+ formMethod = customFields . method ;
1467+ formTarget = customFields . target ;
1468+ formData = customFields . data ;
1469+ formActionName = customFields . name ;
1470+ } else {
1471+ // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
1472+ // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
1473+ // manually submitted or if someone calls stopPropagation before React gets the event.
1474+ // If CSP is used to block javascript: URLs that's fine too. It just won't show this
1475+ // error message but the URL will be logged.
1476+ target . push (
1477+ attributeSeparator ,
1478+ stringToChunk ( 'action' ) ,
1479+ attributeAssign ,
1480+ actionJavaScriptURL ,
1481+ attributeEnd ,
1482+ ) ;
1483+ formAction = null ;
1484+ formEncType = null ;
1485+ formMethod = null ;
1486+ formTarget = null ;
1487+ injectFormReplayingRuntime ( responseState ) ;
14171488 }
14181489 }
1490+ if ( formAction !== null ) {
1491+ pushAttribute ( target , 'action' , formAction ) ;
1492+ }
1493+ if ( formEncType !== null ) {
1494+ pushAttribute ( target , 'encType' , formEncType ) ;
1495+ }
1496+ if ( formMethod !== null ) {
1497+ pushAttribute ( target , 'method' , formMethod ) ;
1498+ }
1499+ if ( formTarget !== null ) {
1500+ pushAttribute ( target , 'target' , formTarget ) ;
1501+ }
14191502
14201503 target . push ( endOfStartTag ) ;
1504+
1505+ if ( formActionName !== null ) {
1506+ target . push ( startHiddenInputChunk ) ;
1507+ pushStringAttribute ( target , 'name' , formActionName ) ;
1508+ target . push ( endOfStartTagSelfClosing ) ;
1509+ pushAdditionalFormFields ( target , formData ) ;
1510+ }
1511+
14211512 pushInnerHTML ( target , innerHTML , children ) ;
14221513 if ( typeof children === 'string' ) {
14231514 // Special case children as a string to avoid the unnecessary comment.
@@ -1510,7 +1601,7 @@ function pushInput(
15101601 }
15111602 }
15121603
1513- pushFormActionAttribute (
1604+ const formData = pushFormActionAttribute (
15141605 target ,
15151606 responseState ,
15161607 formAction ,
@@ -1561,6 +1652,10 @@ function pushInput(
15611652 }
15621653
15631654 target . push ( endOfStartTagSelfClosing ) ;
1655+
1656+ // We place any additional hidden form fields after the input.
1657+ pushAdditionalFormFields ( target , formData ) ;
1658+
15641659 return null ;
15651660}
15661661
@@ -1628,7 +1723,7 @@ function pushStartButton(
16281723 }
16291724 }
16301725
1631- pushFormActionAttribute (
1726+ const formData = pushFormActionAttribute (
16321727 target ,
16331728 responseState ,
16341729 formAction ,
@@ -1639,13 +1734,18 @@ function pushStartButton(
16391734 ) ;
16401735
16411736 target . push ( endOfStartTag ) ;
1737+
1738+ // We place any additional hidden form fields we need to include inside the button itself.
1739+ pushAdditionalFormFields ( target , formData ) ;
1740+
16421741 pushInnerHTML ( target , innerHTML , children ) ;
16431742 if ( typeof children === 'string' ) {
16441743 // Special case children as a string to avoid the unnecessary comment.
16451744 // TODO: Remove this special case after the general optimization is in place.
16461745 target . push ( stringToChunk ( encodeHTMLTextNode ( children ) ) ) ;
16471746 return null ;
16481747 }
1748+
16491749 return children ;
16501750}
16511751
0 commit comments