@@ -668,6 +668,22 @@ function pushStringAttribute(
668
668
}
669
669
}
670
670
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
+
671
687
// Since this will likely be repeated a lot in the HTML, we use a more concise message
672
688
// than on the client and hopefully it's googleable.
673
689
const actionJavaScriptURL = stringToPrecomputedChunk (
@@ -677,6 +693,36 @@ const actionJavaScriptURL = stringToPrecomputedChunk(
677
693
) ,
678
694
) ;
679
695
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
+
680
726
function pushFormActionAttribute (
681
727
target : Array < Chunk | PrecomputedChunk > ,
682
728
responseState : ResponseState ,
@@ -685,7 +731,8 @@ function pushFormActionAttribute(
685
731
formMethod : any ,
686
732
formTarget : any ,
687
733
name : any ,
688
- ) : void {
734
+ ) : null | FormData {
735
+ let formData = null ;
689
736
if ( enableFormActions && typeof formAction === 'function' ) {
690
737
// Function form actions cannot control the form properties
691
738
if ( __DEV__ ) {
@@ -714,37 +761,55 @@ function pushFormActionAttribute(
714
761
) ;
715
762
}
716
763
}
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 ) ;
746
795
}
747
796
}
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 ;
748
813
}
749
814
750
815
function pushAttribute (
@@ -1366,6 +1431,8 @@ function pushStartForm(
1366
1431
}
1367
1432
}
1368
1433
1434
+ let formData = null ;
1435
+ let formActionName = null ;
1369
1436
if ( enableFormActions && typeof formAction === 'function' ) {
1370
1437
// Function form actions cannot control the form properties
1371
1438
if ( __DEV__ ) {
@@ -1388,36 +1455,60 @@ function pushStartForm(
1388
1455
) ;
1389
1456
}
1390
1457
}
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 ) ;
1417
1488
}
1418
1489
}
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
+ }
1419
1502
1420
1503
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
+
1421
1512
pushInnerHTML ( target , innerHTML , children ) ;
1422
1513
if ( typeof children === 'string' ) {
1423
1514
// Special case children as a string to avoid the unnecessary comment.
@@ -1510,7 +1601,7 @@ function pushInput(
1510
1601
}
1511
1602
}
1512
1603
1513
- pushFormActionAttribute (
1604
+ const formData = pushFormActionAttribute (
1514
1605
target ,
1515
1606
responseState ,
1516
1607
formAction ,
@@ -1561,6 +1652,10 @@ function pushInput(
1561
1652
}
1562
1653
1563
1654
target . push ( endOfStartTagSelfClosing ) ;
1655
+
1656
+ // We place any additional hidden form fields after the input.
1657
+ pushAdditionalFormFields ( target , formData ) ;
1658
+
1564
1659
return null ;
1565
1660
}
1566
1661
@@ -1628,7 +1723,7 @@ function pushStartButton(
1628
1723
}
1629
1724
}
1630
1725
1631
- pushFormActionAttribute (
1726
+ const formData = pushFormActionAttribute (
1632
1727
target ,
1633
1728
responseState ,
1634
1729
formAction ,
@@ -1639,13 +1734,18 @@ function pushStartButton(
1639
1734
) ;
1640
1735
1641
1736
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
+
1642
1741
pushInnerHTML ( target , innerHTML , children ) ;
1643
1742
if ( typeof children === 'string' ) {
1644
1743
// Special case children as a string to avoid the unnecessary comment.
1645
1744
// TODO: Remove this special case after the general optimization is in place.
1646
1745
target . push ( stringToChunk ( encodeHTMLTextNode ( children ) ) ) ;
1647
1746
return null ;
1648
1747
}
1748
+
1649
1749
return children ;
1650
1750
}
1651
1751
0 commit comments