@@ -632,6 +632,22 @@ function pushStringAttribute(
632
632
}
633
633
}
634
634
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
+
635
651
// Since this will likely be repeated a lot in the HTML, we use a more concise message
636
652
// than on the client and hopefully it's googleable.
637
653
const actionJavaScriptURL = stringToPrecomputedChunk (
@@ -641,6 +657,36 @@ const actionJavaScriptURL = stringToPrecomputedChunk(
641
657
) ,
642
658
) ;
643
659
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
+
644
690
function pushFormActionAttribute (
645
691
target : Array < Chunk | PrecomputedChunk > ,
646
692
responseState : ResponseState ,
@@ -649,7 +695,8 @@ function pushFormActionAttribute(
649
695
formMethod : any ,
650
696
formTarget : any ,
651
697
name : any ,
652
- ) : void {
698
+ ) : null | FormData {
699
+ let formData = null ;
653
700
if ( enableFormActions && typeof formAction === 'function' ) {
654
701
// Function form actions cannot control the form properties
655
702
if ( __DEV__ ) {
@@ -678,37 +725,55 @@ function pushFormActionAttribute(
678
725
) ;
679
726
}
680
727
}
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 ) ;
710
759
}
711
760
}
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 ;
712
777
}
713
778
714
779
function pushAttribute (
@@ -1330,6 +1395,8 @@ function pushStartForm(
1330
1395
}
1331
1396
}
1332
1397
1398
+ let formData = null ;
1399
+ let formActionName = null ;
1333
1400
if ( enableFormActions && typeof formAction === 'function' ) {
1334
1401
// Function form actions cannot control the form properties
1335
1402
if ( __DEV__ ) {
@@ -1352,36 +1419,60 @@ function pushStartForm(
1352
1419
) ;
1353
1420
}
1354
1421
}
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 ) ;
1381
1452
}
1382
1453
}
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
+ }
1383
1466
1384
1467
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
+
1385
1476
pushInnerHTML ( target , innerHTML , children ) ;
1386
1477
if ( typeof children === 'string' ) {
1387
1478
// Special case children as a string to avoid the unnecessary comment.
@@ -1474,7 +1565,7 @@ function pushInput(
1474
1565
}
1475
1566
}
1476
1567
1477
- pushFormActionAttribute (
1568
+ const formData = pushFormActionAttribute (
1478
1569
target ,
1479
1570
responseState ,
1480
1571
formAction ,
@@ -1525,6 +1616,10 @@ function pushInput(
1525
1616
}
1526
1617
1527
1618
target . push ( endOfStartTagSelfClosing ) ;
1619
+
1620
+ // We place any additional hidden form fields after the input.
1621
+ pushAdditionalFormFields ( target , formData ) ;
1622
+
1528
1623
return null ;
1529
1624
}
1530
1625
@@ -1592,7 +1687,7 @@ function pushStartButton(
1592
1687
}
1593
1688
}
1594
1689
1595
- pushFormActionAttribute (
1690
+ const formData = pushFormActionAttribute (
1596
1691
target ,
1597
1692
responseState ,
1598
1693
formAction ,
@@ -1603,13 +1698,18 @@ function pushStartButton(
1603
1698
) ;
1604
1699
1605
1700
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
+
1606
1705
pushInnerHTML ( target , innerHTML , children ) ;
1607
1706
if ( typeof children === 'string' ) {
1608
1707
// Special case children as a string to avoid the unnecessary comment.
1609
1708
// TODO: Remove this special case after the general optimization is in place.
1610
1709
target . push ( stringToChunk ( encodeHTMLTextNode ( children ) ) ) ;
1611
1710
return null ;
1612
1711
}
1712
+
1613
1713
return children ;
1614
1714
}
1615
1715
0 commit comments