21
enableFilterEmptyStringAttributesDOM,
22
enableCustomElementPropertySupport,
23
enableFloat,
24
+ enableFormActions,
25
enableFizzExternalRuntime,
26
} from 'shared/ReactFeatureFlags';
27
636
}
637
}
638
639
+// Since this will likely be repeated a lot in the HTML, we use a more concise message
640
+// than on the client and hopefully it's googleable.
641
+const actionJavaScriptURL = stringToPrecomputedChunk(
642
+ escapeTextForBrowser(
643
+ // eslint-disable-next-line no-script-url
644
+ "javascript:throw new Error('A React form was unexpectedly submitted.')",
645
+ ),
646
+);
647
+
648
+function pushFormActionAttribute(
649
+ target: Array<Chunk | PrecomputedChunk>,
650
+ formAction: any,
651
+ formEncType: any,
652
+ formMethod: any,
653
+ formTarget: any,
654
+ name: any,
655
+): void {
656
+ if (enableFormActions && typeof formAction === 'function') {
657
+ // Function form actions cannot control the form properties
658
+ if (__DEV__) {
659
+ if (name !== null && !didWarnFormActionName) {
660
+ didWarnFormActionName = true;
661
+ console.error(
662
+ 'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +
663
+ 'React needs it to encode which action should be invoked. It will get overridden.',
664
+ );
665
+ }
666
+ if (
667
+ (formEncType !== null || formMethod !== null) &&
668
+ !didWarnFormActionMethod
669
+ ) {
670
+ didWarnFormActionMethod = true;
671
+ console.error(
672
+ 'Cannot specify a formEncType or formMethod for a button that specifies a ' +
673
+ 'function as a formAction. React provides those automatically. They will get overridden.',
674
+ );
675
+ }
676
+ if (formTarget !== null && !didWarnFormActionTarget) {
677
+ didWarnFormActionTarget = true;
678
+ console.error(
679
+ 'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +
680
+ 'The function will always be executed in the same window.',
681
+ );
682
+ }
683
+ }
684
+ // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
685
+ // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
686
+ // manually submitted or if someone calls stopPropagation before React gets the event.
687
+ // If CSP is used to block javascript: URLs that's fine too. It just won't show this
688
+ // error message but the URL will be logged.
689
+ target.push(
690
+ attributeSeparator,
691
+ stringToChunk('formAction'),
692
+ attributeAssign,
693
+ actionJavaScriptURL,
694
+ attributeEnd,
695
+ );
696
+ } else {
697
+ // Plain form actions support all the properties, so we have to emit them.
698
+ if (name !== null) {
699
+ pushAttribute(target, 'name', name);
700
+ }
701
+ if (formAction !== null) {
702
+ pushAttribute(target, 'formAction', formAction);
703
+ }
704
+ if (formEncType !== null) {
705
+ pushAttribute(target, 'formEncType', formEncType);
706
+ }
707
+ if (formMethod !== null) {
708
+ pushAttribute(target, 'formMethod', formMethod);
709
+ }
710
+ if (formTarget !== null) {
711
+ pushAttribute(target, 'formTarget', formTarget);
712
+ }
713
+ }
714
+}
715
+
716
function pushAttribute(
717
target: Array<Chunk | PrecomputedChunk>,
718
name: string,
743
return;
744
}
745
case 'src':
668
- case 'href':
669
- case 'action':
746
+ case 'href': {
747
if (enableFilterEmptyStringAttributesDOM) {
748
if (value === '') {
749
if (__DEV__) {
769
return;
770
}
771
}
772
+ }
773
// Fall through to the last case which shouldn't remove empty strings.
774
+ case 'action':
775
case 'formAction': {
776
+ // TODO: Consider only special casing these for each tag.
777
if (
778
value == null ||
779
typeof value === 'function' ||
1050
let didWarnInvalidOptionChildren = false;
1051
let didWarnInvalidOptionInnerHTML = false;
1052
let didWarnSelectedSetOnOption = false;
1053
+let didWarnFormActionType = false;
1054
+let didWarnFormActionName = false;
1055
+let didWarnFormActionTarget = false;
1056
+let didWarnFormActionMethod = false;
1057
1058
function checkSelectProp(props: any, propName: string) {
1059
if (__DEV__) {
1266
return children;
1267
}
1268
1185
-function pushInput(
1269
+function pushStartForm(
1270
target: Array<Chunk | PrecomputedChunk>,
1271
props: Object,
1272
): ReactNodeList {
1189
- if (__DEV__) {
1190
- checkControlledValueProps('input', props);
1273
+ target.push(startChunkForTag('form'));
1274
1192
- if (
1193
- props.checked !== undefined &&
1194
- props.defaultChecked !== undefined &&
1195
- !didWarnDefaultChecked
1196
- ) {
1197
- console.error(
1198
- '%s contains an input of type %s with both checked and defaultChecked props. ' +
1199
- 'Input elements must be either controlled or uncontrolled ' +
1200
- '(specify either the checked prop, or the defaultChecked prop, but not ' +
1201
- 'both). Decide between using a controlled or uncontrolled input ' +
1202
- 'element and remove one of these props. More info: ' +
1203
- 'https://reactjs.org/link/controlled-components',
1204
- 'A component',
1205
- props.type,
1206
- );
1207
- didWarnDefaultChecked = true;
1275
+ let children = null;
1276
+ let innerHTML = null;
1277
+ let formAction = null;
1278
+ let formEncType = null;
1279
+ let formMethod = null;
1280
+ let formTarget = null;
1281
+
1282
+ for (const propKey in props) {
1283
+ if (hasOwnProperty.call(props, propKey)) {
1284
+ const propValue = props[propKey];
1285
+ if (propValue == null) {
1286
+ continue;
1287
+ }
1288
+ switch (propKey) {
1289
+ case 'children':
1290
+ children = propValue;
1291
+ break;
1292
+ case 'dangerouslySetInnerHTML':
1293
+ innerHTML = propValue;
1294
+ break;
1295
+ case 'action':
1296
+ formAction = propValue;
1297
+ break;
1298
+ case 'encType':
1299
+ formEncType = propValue;
1300
+ break;
1301
+ case 'method':
1302
+ formMethod = propValue;
1303
+ break;
1304
+ case 'target':
1305
+ formTarget = propValue;
1306
+ break;
1307
+ default:
1308
+ pushAttribute(target, propKey, propValue);
1309
+ break;
1310
+ }
1311
}
1209
- if (
1210
- props.value !== undefined &&
1211
- props.defaultValue !== undefined &&
1212
- !didWarnDefaultInputValue
1213
- ) {
1214
- console.error(
1215
- '%s contains an input of type %s with both value and defaultValue props. ' +
1216
- 'Input elements must be either controlled or uncontrolled ' +
1217
- '(specify either the value prop, or the defaultValue prop, but not ' +
1218
- 'both). Decide between using a controlled or uncontrolled input ' +
1219
- 'element and remove one of these props. More info: ' +
1220
- 'https://reactjs.org/link/controlled-components',
1221
- 'A component',
1222
- props.type,
1223
- );
1224
- didWarnDefaultInputValue = true;
1312
+ }
1313
+
1314
+ if (enableFormActions && typeof formAction === 'function') {
1315
+ // Function form actions cannot control the form properties
1316
+ if (__DEV__) {
1317
+ if (
1318
+ (formEncType !== null || formMethod !== null) &&
1319
+ !didWarnFormActionMethod
1320
+ ) {
1321
+ didWarnFormActionMethod = true;
1322
+ console.error(
1323
+ 'Cannot specify a encType or method for a form that specifies a ' +
1324
+ 'function as the action. React provides those automatically. ' +
1325
+ 'They will get overridden.',
1326
+ );
1327
+ }
1328
+ if (formTarget !== null && !didWarnFormActionTarget) {
1329
+ didWarnFormActionTarget = true;
1330
+ console.error(
1331
+ 'Cannot specify a target for a form that specifies a function as the action. ' +
1332
+ 'The function will always be executed in the same window.',
1333
+ );
1334
+ }
1335
+ }
1336
+ // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
1337
+ // because we'll preventDefault in the Fizz runtime, but it can happen if a form is
1338
+ // manually submitted or if someone calls stopPropagation before React gets the event.
1339
+ // If CSP is used to block javascript: URLs that's fine too. It just won't show this
1340
+ // error message but the URL will be logged.
1341
+ target.push(
1342
+ attributeSeparator,
1343
+ stringToChunk('action'),
1344
+ attributeAssign,
1345
+ actionJavaScriptURL,
1346
+ attributeEnd,
1347
+ );
1348
+ } else {
1349
+ // Plain form actions support all the properties, so we have to emit them.
1350
+ if (formAction !== null) {
1351
+ pushAttribute(target, 'action', formAction);
1352
+ }
1353
+ if (formEncType !== null) {
1354
+ pushAttribute(target, 'encType', formEncType);
1355
+ }
1356
+ if (formMethod !== null) {
1357
+ pushAttribute(target, 'method', formMethod);
1358
}
1359
+ if (formTarget !== null) {
1360
+ pushAttribute(target, 'target', formTarget);
1361
+ }
1362
+ }
1363
+
1364
+ target.push(endOfStartTag);
1365
+ pushInnerHTML(target, innerHTML, children);
1366
+ if (typeof children === 'string') {
1367
+ // Special case children as a string to avoid the unnecessary comment.
1368
+ // TODO: Remove this special case after the general optimization is in place.
1369
+ target.push(stringToChunk(encodeHTMLTextNode(children)));
1370
+ return null;
1371
+ }
1372
+ return children;
1373
+}
1374
+
1375
+function pushInput(
1376
+ target: Array<Chunk | PrecomputedChunk>,
1377
+ props: Object,
1378
+): ReactNodeList {
1379
+ if (__DEV__) {
1380
+ checkControlledValueProps('input', props);
1381
}
1382
1383
target.push(startChunkForTag('input'));
1384
1385
+ let name = null;
1386
+ let formAction = null;
1387
+ let formEncType = null;
1388
+ let formMethod = null;
1389
+ let formTarget = null;
1390
let value = null;
1391
let defaultValue = null;
1392
let checked = null;
1405
`${'input'} is a self-closing tag and must neither have \`children\` nor ` +
1406
'use `dangerouslySetInnerHTML`.',
1407
);
1408
+ case 'name':
1409
+ name = propValue;
1410
+ break;
1411
+ case 'formAction':
1412
+ formAction = propValue;
1413
+ break;
1414
+ case 'formEncType':
1415
+ formEncType = propValue;
1416
+ break;
1417
+ case 'formMethod':
1418
+ formMethod = propValue;
1419
+ break;
1420
+ case 'formTarget':
1421
+ formTarget = propValue;
1422
+ break;
1423
case 'defaultChecked':
1424
defaultChecked = propValue;
1425
break;
1439
}
1440
}
1441
1442
+ if (__DEV__) {
1443
+ if (
1444
+ formAction !== null &&
1445
+ props.type !== 'image' &&
1446
+ props.type !== 'submit' &&
1447
+ !didWarnFormActionType
1448
+ ) {
1449
+ didWarnFormActionType = true;
1450
+ console.error(
1451
+ 'An input can only specify a formAction along with type="submit" or type="image".',
1452
+ );
1453
+ }
1454
+ }
1455
+
1456
+ pushFormActionAttribute(
1457
+ target,
1458
+ formAction,
1459
+ formEncType,
1460
+ formMethod,
1461
+ formTarget,
1462
+ name,
1463
+ );
1464
+
1465
+ if (__DEV__) {
1466
+ if (checked !== null && defaultChecked !== null && !didWarnDefaultChecked) {
1467
+ console.error(
1468
+ '%s contains an input of type %s with both checked and defaultChecked props. ' +
1469
+ 'Input elements must be either controlled or uncontrolled ' +
1470
+ '(specify either the checked prop, or the defaultChecked prop, but not ' +
1471
+ 'both). Decide between using a controlled or uncontrolled input ' +
1472
+ 'element and remove one of these props. More info: ' +
1473
+ 'https://reactjs.org/link/controlled-components',
1474
+ 'A component',
1475
+ props.type,
1476
+ );
1477
+ didWarnDefaultChecked = true;
1478
+ }
1479
+ if (value !== null && defaultValue !== null && !didWarnDefaultInputValue) {
1480
+ console.error(
1481
+ '%s contains an input of type %s with both value and defaultValue props. ' +
1482
+ 'Input elements must be either controlled or uncontrolled ' +
1483
+ '(specify either the value prop, or the defaultValue prop, but not ' +
1484
+ 'both). Decide between using a controlled or uncontrolled input ' +
1485
+ 'element and remove one of these props. More info: ' +
1486
+ 'https://reactjs.org/link/controlled-components',
1487
+ 'A component',
1488
+ props.type,
1489
+ );
1490
+ didWarnDefaultInputValue = true;
1491
+ }
1492
+ }
1493
+
1494
if (checked !== null) {
1495
pushBooleanAttribute(target, 'checked', checked);
1496
} else if (defaultChecked !== null) {
1506
return null;
1507
}
1508
1509
+function pushStartButton(
1510
+ target: Array<Chunk | PrecomputedChunk>,
1511
+ props: Object,
1512
+): ReactNodeList {
1513
+ target.push(startChunkForTag('button'));
1514
+
1515
+ let children = null;
1516
+ let innerHTML = null;
1517
+ let name = null;
1518
+ let formAction = null;
1519
+ let formEncType = null;
1520
+ let formMethod = null;
1521
+ let formTarget = null;
1522
+
1523
+ for (const propKey in props) {
1524
+ if (hasOwnProperty.call(props, propKey)) {
1525
+ const propValue = props[propKey];
1526
+ if (propValue == null) {
1527
+ continue;
1528
+ }
1529
+ switch (propKey) {
1530
+ case 'children':
1531
+ children = propValue;
1532
+ break;
1533
+ case 'dangerouslySetInnerHTML':
1534
+ innerHTML = propValue;
1535
+ break;
1536
+ case 'name':
1537
+ name = propValue;
1538
+ break;
1539
+ case 'formAction':
1540
+ formAction = propValue;
1541
+ break;
1542
+ case 'formEncType':
1543
+ formEncType = propValue;
1544
+ break;
1545
+ case 'formMethod':
1546
+ formMethod = propValue;
1547
+ break;
1548
+ case 'formTarget':
1549
+ formTarget = propValue;
1550
+ break;
1551
+ default:
1552
+ pushAttribute(target, propKey, propValue);
1553
+ break;
1554
+ }
1555
+ }
1556
+ }
1557
+
1558
+ if (__DEV__) {
1559
+ if (
1560
+ formAction !== null &&
1561
+ props.type != null &&
1562
+ props.type !== 'submit' &&
1563
+ !didWarnFormActionType
1564
+ ) {
1565
+ didWarnFormActionType = true;
1566
+ console.error(
1567
+ 'A button can only specify a formAction along with type="submit" or no type.',
1568
+ );
1569
+ }
1570
+ }
1571
+
1572
+ pushFormActionAttribute(
1573
+ target,
1574
+ formAction,
1575
+ formEncType,
1576
+ formMethod,
1577
+ formTarget,
1578
+ name,
1579
+ );
1580
+
1581
+ target.push(endOfStartTag);
1582
+ pushInnerHTML(target, innerHTML, children);
1583
+ if (typeof children === 'string') {
1584
+ // Special case children as a string to avoid the unnecessary comment.
1585
+ // TODO: Remove this special case after the general optimization is in place.
1586
+ target.push(stringToChunk(encodeHTMLTextNode(children)));
1587
+ return null;
1588
+ }
1589
+ return children;
1590
+}
1591
+
1592
function pushStartTextArea(
1593
target: Array<Chunk | PrecomputedChunk>,
1594
props: Object,
2958
return pushStartTextArea(target, props);
2959
case 'input':
2960
return pushInput(target, props);
2961
+ case 'button':
2962
+ return pushStartButton(target, props);
2963
+ case 'form':
2964
+ return pushStartForm(target, props);
2965
case 'menuitem':
2966
return pushStartMenuItem(target, props);
2967
case 'title':
3043
case 'font-face-format':
3044
case 'font-face-name':
3045
case 'missing-glyph': {
2732
- return pushStartGenericElement(target, props, type);
3046
+ break;
3047
}
3048
// Preamble start tags
3049
case 'head':