@@ -632,6 +632,22 @@ function pushStringAttribute(
632632 }
633633}
634634
635+ type CustomFormAction = {
636+ name ?: string ,
637+ action ?: string ,
638+ encType ?: string ,
639+ method ?: string ,
640+ target ?: string ,
641+ data ?: FormData ,
642+ } ;
643+
644+ function makeFormFieldPrefix ( responseState : ResponseState ) : string {
645+ // I'm just reusing this counter. It's not really the same namespace as "name".
646+ // It could just be its own counter.
647+ const id = responseState . nextSuspenseID ++ ;
648+ return responseState . idPrefix + '$ACTION:' + id + ':' ;
649+ }
650+
635651// Since this will likely be repeated a lot in the HTML, we use a more concise message
636652// than on the client and hopefully it's googleable.
637653const actionJavaScriptURL = stringToPrecomputedChunk (
@@ -641,6 +657,36 @@ const actionJavaScriptURL = stringToPrecomputedChunk(
641657 ) ,
642658) ;
643659
660+ const startHiddenInputChunk = stringToPrecomputedChunk ( '<input type="hidden"' ) ;
661+
662+ function pushAdditionalFormField (
663+ this : Array < Chunk | PrecomputedChunk > ,
664+ value : string | File ,
665+ key : string ,
666+ ) : void {
667+ const target : Array < Chunk | PrecomputedChunk > = this ;
668+ target . push ( startHiddenInputChunk ) ;
669+ if ( typeof value !== 'string' ) {
670+ throw new Error (
671+ 'File/Blob fields are not yet supported in progressive forms. ' +
672+ 'It probably means you are closing over binary data or FormData in a Server Action.' ,
673+ ) ;
674+ }
675+ pushStringAttribute ( target , 'name' , key ) ;
676+ pushStringAttribute ( target , 'value' , value ) ;
677+ target . push ( endOfStartTagSelfClosing ) ;
678+ }
679+
680+ function pushAdditionalFormFields (
681+ target : Array < Chunk | PrecomputedChunk > ,
682+ formData : null | FormData ,
683+ ) {
684+ if ( formData !== null ) {
685+ // $FlowFixMe[prop-missing]: FormData has forEach.
686+ formData . forEach ( pushAdditionalFormField , target ) ;
687+ }
688+ }
689+
644690function pushFormActionAttribute (
645691 target : Array < Chunk | PrecomputedChunk > ,
646692 responseState : ResponseState ,
@@ -649,7 +695,8 @@ function pushFormActionAttribute(
649695 formMethod : any ,
650696 formTarget : any ,
651697 name : any ,
652- ) : void {
698+ ) : null | FormData {
699+ let formData = null ;
653700 if ( enableFormActions && typeof formAction === 'function' ) {
654701 // Function form actions cannot control the form properties
655702 if ( __DEV__ ) {
@@ -678,37 +725,55 @@ function pushFormActionAttribute(
678725 ) ;
679726 }
680727 }
681- // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
682- // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
683- // manually submitted or if someone calls stopPropagation before React gets the event.
684- // If CSP is used to block javascript: URLs that's fine too. It just won't show this
685- // error message but the URL will be logged.
686- target . push (
687- attributeSeparator ,
688- stringToChunk ( 'formAction' ) ,
689- attributeAssign ,
690- actionJavaScriptURL ,
691- attributeEnd ,
692- ) ;
693- injectFormReplayingRuntime ( responseState ) ;
694- } else {
695- // Plain form actions support all the properties, so we have to emit them.
696- if ( name !== null ) {
697- pushAttribute ( target , 'name' , name ) ;
698- }
699- if ( formAction !== null ) {
700- pushAttribute ( target , 'formAction' , formAction ) ;
701- }
702- if ( formEncType !== null ) {
703- pushAttribute ( target , 'formEncType' , formEncType ) ;
704- }
705- if ( formMethod !== null ) {
706- pushAttribute ( target , 'formMethod' , formMethod ) ;
707- }
708- if ( formTarget !== null ) {
709- pushAttribute ( target , 'formTarget' , formTarget ) ;
728+ const customAction : CustomFormAction = formAction . $$FORM_ACTION ;
729+ if ( typeof customAction === 'function' ) {
730+ // This action has a custom progressive enhancement form that can submit the form
731+ // back to the server if it's invoked before hydration. Such as a Server Action.
732+ const prefix = makeFormFieldPrefix ( responseState ) ;
733+ const customFields = customAction ( prefix ) ;
734+ name = customFields . name ;
735+ formAction = customFields . action || '' ;
736+ formEncType = customFields . encType ;
737+ formMethod = customFields . method ;
738+ formTarget = customFields . target ;
739+ formData = customFields . data ;
740+ } else {
741+ // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
742+ // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
743+ // manually submitted or if someone calls stopPropagation before React gets the event.
744+ // If CSP is used to block javascript: URLs that's fine too. It just won't show this
745+ // error message but the URL will be logged.
746+ target . push (
747+ attributeSeparator ,
748+ stringToChunk ( 'formAction' ) ,
749+ attributeAssign ,
750+ actionJavaScriptURL ,
751+ attributeEnd ,
752+ ) ;
753+ name = null ;
754+ formAction = null ;
755+ formEncType = null ;
756+ formMethod = null ;
757+ formTarget = null ;
758+ injectFormReplayingRuntime ( responseState ) ;
710759 }
711760 }
761+ if ( name !== null ) {
762+ pushAttribute ( target , 'name' , name ) ;
763+ }
764+ if ( formAction !== null ) {
765+ pushAttribute ( target , 'formAction' , formAction ) ;
766+ }
767+ if ( formEncType !== null ) {
768+ pushAttribute ( target , 'formEncType' , formEncType ) ;
769+ }
770+ if ( formMethod !== null ) {
771+ pushAttribute ( target , 'formMethod' , formMethod ) ;
772+ }
773+ if ( formTarget !== null ) {
774+ pushAttribute ( target , 'formTarget' , formTarget ) ;
775+ }
776+ return formData ;
712777}
713778
714779function pushAttribute (
@@ -1330,6 +1395,8 @@ function pushStartForm(
13301395 }
13311396 }
13321397
1398+ let formData = null ;
1399+ let formActionName = null ;
13331400 if ( enableFormActions && typeof formAction === 'function' ) {
13341401 // Function form actions cannot control the form properties
13351402 if ( __DEV__ ) {
@@ -1352,36 +1419,60 @@ function pushStartForm(
13521419 ) ;
13531420 }
13541421 }
1355- // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
1356- // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
1357- // manually submitted or if someone calls stopPropagation before React gets the event.
1358- // If CSP is used to block javascript: URLs that's fine too. It just won't show this
1359- // error message but the URL will be logged.
1360- target . push (
1361- attributeSeparator ,
1362- stringToChunk ( 'action' ) ,
1363- attributeAssign ,
1364- actionJavaScriptURL ,
1365- attributeEnd ,
1366- ) ;
1367- injectFormReplayingRuntime ( responseState ) ;
1368- } else {
1369- // Plain form actions support all the properties, so we have to emit them.
1370- if ( formAction !== null ) {
1371- pushAttribute ( target , 'action' , formAction ) ;
1372- }
1373- if ( formEncType !== null ) {
1374- pushAttribute ( target , 'encType' , formEncType ) ;
1375- }
1376- if ( formMethod !== null ) {
1377- pushAttribute ( target , 'method' , formMethod ) ;
1378- }
1379- if ( formTarget !== null ) {
1380- pushAttribute ( target , 'target' , formTarget ) ;
1422+ const customAction : CustomFormAction = formAction . $$FORM_ACTION ;
1423+ if ( typeof customAction === 'function' ) {
1424+ // This action has a custom progressive enhancement form that can submit the form
1425+ // back to the server if it's invoked before hydration. Such as a Server Action.
1426+ const prefix = makeFormFieldPrefix ( responseState ) ;
1427+ const customFields = customAction ( prefix ) ;
1428+ formAction = customFields . action || '' ;
1429+ formEncType = customFields . encType ;
1430+ formMethod = customFields . method ;
1431+ formTarget = customFields . target ;
1432+ formData = customFields . data ;
1433+ formActionName = customFields . name ;
1434+ } else {
1435+ // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
1436+ // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
1437+ // manually submitted or if someone calls stopPropagation before React gets the event.
1438+ // If CSP is used to block javascript: URLs that's fine too. It just won't show this
1439+ // error message but the URL will be logged.
1440+ target . push (
1441+ attributeSeparator ,
1442+ stringToChunk ( 'action' ) ,
1443+ attributeAssign ,
1444+ actionJavaScriptURL ,
1445+ attributeEnd ,
1446+ ) ;
1447+ formAction = null ;
1448+ formEncType = null ;
1449+ formMethod = null ;
1450+ formTarget = null ;
1451+ injectFormReplayingRuntime ( responseState ) ;
13811452 }
13821453 }
1454+ if ( formAction !== null ) {
1455+ pushAttribute ( target , 'action' , formAction ) ;
1456+ }
1457+ if ( formEncType !== null ) {
1458+ pushAttribute ( target , 'encType' , formEncType ) ;
1459+ }
1460+ if ( formMethod !== null ) {
1461+ pushAttribute ( target , 'method' , formMethod ) ;
1462+ }
1463+ if ( formTarget !== null ) {
1464+ pushAttribute ( target , 'target' , formTarget ) ;
1465+ }
13831466
13841467 target . push ( endOfStartTag ) ;
1468+
1469+ if ( formActionName !== null ) {
1470+ target . push ( startHiddenInputChunk ) ;
1471+ pushStringAttribute ( target , 'name' , formActionName ) ;
1472+ target . push ( endOfStartTagSelfClosing ) ;
1473+ pushAdditionalFormFields ( target , formData ) ;
1474+ }
1475+
13851476 pushInnerHTML ( target , innerHTML , children ) ;
13861477 if ( typeof children === 'string' ) {
13871478 // Special case children as a string to avoid the unnecessary comment.
@@ -1474,7 +1565,7 @@ function pushInput(
14741565 }
14751566 }
14761567
1477- pushFormActionAttribute (
1568+ const formData = pushFormActionAttribute (
14781569 target ,
14791570 responseState ,
14801571 formAction ,
@@ -1525,6 +1616,10 @@ function pushInput(
15251616 }
15261617
15271618 target . push ( endOfStartTagSelfClosing ) ;
1619+
1620+ // We place any additional hidden form fields after the input.
1621+ pushAdditionalFormFields ( target , formData ) ;
1622+
15281623 return null ;
15291624}
15301625
@@ -1592,7 +1687,7 @@ function pushStartButton(
15921687 }
15931688 }
15941689
1595- pushFormActionAttribute (
1690+ const formData = pushFormActionAttribute (
15961691 target ,
15971692 responseState ,
15981693 formAction ,
@@ -1603,13 +1698,18 @@ function pushStartButton(
16031698 ) ;
16041699
16051700 target . push ( endOfStartTag ) ;
1701+
1702+ // We place any additional hidden form fields we need to include inside the button itself.
1703+ pushAdditionalFormFields ( target , formData ) ;
1704+
16061705 pushInnerHTML ( target , innerHTML , children ) ;
16071706 if ( typeof children === 'string' ) {
16081707 // Special case children as a string to avoid the unnecessary comment.
16091708 // TODO: Remove this special case after the general optimization is in place.
16101709 target . push ( stringToChunk ( encodeHTMLTextNode ( children ) ) ) ;
16111710 return null ;
16121711 }
1712+
16131713 return children ;
16141714}
16151715
0 commit comments