@aglyn/plugins-forms 1.0.0-beta.246 → 1.0.0-beta.249
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/lib/components/form.d.ts +9 -0
- package/src/lib/components/form.js +138 -90
- package/src/lib/components/form.js.map +1 -1
- package/src/lib/model/form-field-elements.d.ts +132 -0
- package/src/lib/model/form-field-elements.js +511 -0
- package/src/lib/model/form-field-elements.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-forms",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.249",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
40
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.249",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.249",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.249",
|
|
31
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.249",
|
|
32
|
+
"@aglyn/shared-ui-jsx-forms": "1.0.0-beta.249",
|
|
33
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.249",
|
|
34
|
+
"@aglyn/shared-util-email": "1.0.0-beta.249",
|
|
35
|
+
"@aglyn/shared-util-http": "1.0.0-beta.249",
|
|
36
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.249",
|
|
37
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.249",
|
|
38
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.249",
|
|
39
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.249",
|
|
40
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.249",
|
|
41
41
|
"@swc/helpers": "0.5.23"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -201,6 +201,15 @@ export declare const parseFieldOptions: (options?: string) => string[];
|
|
|
201
201
|
declare const FormField: import("react").ForwardRefExoticComponent<FormFieldProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
202
202
|
export declare const formSchema: Aglyn.ComponentSchema<FormProps>;
|
|
203
203
|
export declare const formFieldSchema: Aglyn.ComponentSchema<FormFieldProps>;
|
|
204
|
+
/**
|
|
205
|
+
* A field as a preset: the all-in-one Form Field's props rebuilt as MUI's
|
|
206
|
+
* separate form elements by `formFieldToElements` — the same function the
|
|
207
|
+
* backfill migrates saved fields with, so a field added today and a field
|
|
208
|
+
* migrated from a Form Field are the same tree. Ids are left `null` (fresh
|
|
209
|
+
* ones are minted at insertion) and every node names `mui`, the bundle that
|
|
210
|
+
* registers it.
|
|
211
|
+
*/
|
|
212
|
+
export declare function fieldPresetData(props: FormFieldProps): Aglyn.NodePresetData;
|
|
204
213
|
export declare const formPresets: Aglyn.PresetSchema[];
|
|
205
214
|
/**
|
|
206
215
|
* The composed section a form usually arrives inside.
|
|
@@ -27,6 +27,7 @@ import { parseFormUnavailableRefusal } from "../model/form-unavailable.js";
|
|
|
27
27
|
import { mdiEmailFastOutline, mdiEmailOutline, mdiFormTextbox } from "@aglyn/shared-data-mdi";
|
|
28
28
|
import { isSameOriginPath } from "@aglyn/shared-util-http/safe-redirect";
|
|
29
29
|
import Alert from "@mui/material/Alert";
|
|
30
|
+
import Box from "@mui/material/Box";
|
|
30
31
|
import Button from "@mui/material/Button";
|
|
31
32
|
import Checkbox from "@mui/material/Checkbox";
|
|
32
33
|
import FormControl from "@mui/material/FormControl";
|
|
@@ -40,9 +41,11 @@ import Radio from "@mui/material/Radio";
|
|
|
40
41
|
import RadioGroup from "@mui/material/RadioGroup";
|
|
41
42
|
import Rating from "@mui/material/Rating";
|
|
42
43
|
import Stack from "@mui/material/Stack";
|
|
44
|
+
import { useForkRef } from "@mui/material/utils";
|
|
43
45
|
import TextField from "@mui/material/TextField";
|
|
44
46
|
import { createContext, forwardRef, useCallback, useContext, useEffect, useRef, useState } from "react";
|
|
45
47
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
48
|
+
import { formFieldToElements, LEGACY_FORM_FIELD_COMPONENT_ID } from "../model/form-field-elements.js";
|
|
46
49
|
import { generatePresetId } from "../utils/generate-preset-id.js";
|
|
47
50
|
/*
|
|
48
51
|
* The heading and the stack this block's form sits inside are mui elements,
|
|
@@ -73,6 +76,39 @@ export const FORM_FIELD_ID = 'formField';
|
|
|
73
76
|
* A form whose consent field never mounted posts no key, and its opt-in is
|
|
74
77
|
* recorded for the one site the visitor is on.
|
|
75
78
|
*/ export const CONSENT_GROUP_INPUT = '__consentGroup';
|
|
79
|
+
/**
|
|
80
|
+
* The class on a form's built-in submit button. It hides while the form holds
|
|
81
|
+
* a Button the author placed with Type: Submit (see the Form's `:has` rule).
|
|
82
|
+
*/ const BUILT_IN_SUBMIT_CLASS = 'aglyn-form-submit-built-in';
|
|
83
|
+
/**
|
|
84
|
+
* The consent-group sentence (AGL-3320), as the all-in-one Form Field draws it
|
|
85
|
+
* under the opt-in, and as the Form draws it for a form built from MUI's own
|
|
86
|
+
* form elements, which know nothing of consent. The Form's copy hides while a
|
|
87
|
+
* Form Field's is on the page, so a form never shows it twice.
|
|
88
|
+
*/ const FIELD_CONSENT_NOTE_CLASS = 'aglyn-form-consent-note-field';
|
|
89
|
+
const FORM_CONSENT_NOTE_CLASS = 'aglyn-form-consent-note';
|
|
90
|
+
/**
|
|
91
|
+
* Whether the form's opt-in is one of MUI's form elements (an input named as
|
|
92
|
+
* the declared opt-in field) rather than an all-in-one Form Field, which
|
|
93
|
+
* draws its own sentence. Read off the rendered form after mount: the
|
|
94
|
+
* elements know nothing of consent, and until it is read the form shows no
|
|
95
|
+
* sentence and posts no key — the safe way to be early.
|
|
96
|
+
*/ function useOptInDrawnByElements(formRef, fieldName) {
|
|
97
|
+
const [drawn, setDrawn] = useState(false);
|
|
98
|
+
useEffect(()=>{
|
|
99
|
+
const form = formRef.current;
|
|
100
|
+
if (!form || !fieldName) {
|
|
101
|
+
setDrawn(false);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const hasInput = Array.from(form.elements).some((element)=>element.name === fieldName);
|
|
105
|
+
setDrawn(hasInput && !form.querySelector(`.${FIELD_CONSENT_NOTE_CLASS}`));
|
|
106
|
+
}, [
|
|
107
|
+
formRef,
|
|
108
|
+
fieldName
|
|
109
|
+
]);
|
|
110
|
+
return drawn;
|
|
111
|
+
}
|
|
76
112
|
/**
|
|
77
113
|
* The disclosure a Form fetched, handed to the fields inside it. A context
|
|
78
114
|
* because the answer flows DOWN, from the one request the Form makes to the
|
|
@@ -179,6 +215,8 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
179
215
|
]);
|
|
180
216
|
const { hostId } = Aglyn.useSite();
|
|
181
217
|
const siteFetch = Aglyn.useSiteFetch();
|
|
218
|
+
const formRef = useRef(null);
|
|
219
|
+
const handleRef = useForkRef(ref, formRef);
|
|
182
220
|
// Resolves the redirect screen like ScreenLink does (rename-safe) and
|
|
183
221
|
// flags editing surfaces, where outcomes must never fire (AGL-557).
|
|
184
222
|
const { href: redirectScreenHref, suppressNavigation } = Aglyn.useScreenLink(afterSubmit === 'redirect' ? redirectScreenId : undefined);
|
|
@@ -265,6 +303,7 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
265
303
|
formId,
|
|
266
304
|
siteFetch
|
|
267
305
|
]);
|
|
306
|
+
const optInDrawnByElements = useOptInDrawnByElements(formRef, disclosure == null ? void 0 : disclosure.fieldName);
|
|
268
307
|
const handleSubmit = useCallback(async (event)=>{
|
|
269
308
|
event.preventDefault();
|
|
270
309
|
if (!hostId || status === 'sending') return;
|
|
@@ -437,12 +476,30 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
437
476
|
});
|
|
438
477
|
}
|
|
439
478
|
return /*#__PURE__*/ _jsxs(Stack, _extends({
|
|
440
|
-
ref:
|
|
479
|
+
ref: handleRef,
|
|
441
480
|
component: "form",
|
|
442
481
|
spacing: 2,
|
|
482
|
+
// Gap, not margins, between children: a form turned into a row with
|
|
483
|
+
// the Styles panel's Flex direction lines its input and button up
|
|
484
|
+
// instead of pushing every item after the first down a step.
|
|
485
|
+
useFlexGap: true,
|
|
443
486
|
onSubmit: handleSubmit,
|
|
444
487
|
onInput: handleFirstInput
|
|
445
488
|
}, rest, {
|
|
489
|
+
sx: [
|
|
490
|
+
{
|
|
491
|
+
// A Button placed in the form with Type: Submit is the form's
|
|
492
|
+
// button — styled like any element — so the built-in one steps
|
|
493
|
+
// aside. CSS rather than a registration, so the server's first
|
|
494
|
+
// paint is already right.
|
|
495
|
+
[`&:has(button[type="submit"]:not(.${BUILT_IN_SUBMIT_CLASS})) .${BUILT_IN_SUBMIT_CLASS}`]: {
|
|
496
|
+
display: 'none'
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
...Array.isArray(rest['sx']) ? rest['sx'] : [
|
|
500
|
+
rest['sx']
|
|
501
|
+
]
|
|
502
|
+
],
|
|
446
503
|
children: [
|
|
447
504
|
revealSelector ? /*#__PURE__*/ _jsx("style", {
|
|
448
505
|
children: `${revealSelector}{display:none !important}`
|
|
@@ -451,6 +508,20 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
451
508
|
value: disclosure,
|
|
452
509
|
children: children
|
|
453
510
|
}),
|
|
511
|
+
disclosure && optInDrawnByElements ? /*#__PURE__*/ _jsxs(Box, {
|
|
512
|
+
className: FORM_CONSENT_NOTE_CLASS,
|
|
513
|
+
children: [
|
|
514
|
+
/*#__PURE__*/ _jsx(FormHelperText, {
|
|
515
|
+
children: disclosure.text
|
|
516
|
+
}),
|
|
517
|
+
/*#__PURE__*/ _jsx("input", {
|
|
518
|
+
type: "hidden",
|
|
519
|
+
name: CONSENT_GROUP_INPUT,
|
|
520
|
+
value: disclosure.key,
|
|
521
|
+
readOnly: true
|
|
522
|
+
})
|
|
523
|
+
]
|
|
524
|
+
}) : null,
|
|
454
525
|
/*#__PURE__*/ _jsx("input", {
|
|
455
526
|
type: "text",
|
|
456
527
|
name: "website",
|
|
@@ -511,6 +582,7 @@ const REVEAL_NODE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
|
511
582
|
/*#__PURE__*/ _jsx(Button, {
|
|
512
583
|
type: "submit",
|
|
513
584
|
variant: "contained",
|
|
585
|
+
className: BUILT_IN_SUBMIT_CLASS,
|
|
514
586
|
disabled: status === 'sending',
|
|
515
587
|
sx: {
|
|
516
588
|
alignSelf: 'flex-start'
|
|
@@ -586,6 +658,7 @@ Form.displayName = 'AglynForm';
|
|
|
586
658
|
const consentNote = disclosure && describesOptIn ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
587
659
|
children: [
|
|
588
660
|
/*#__PURE__*/ _jsx(FormHelperText, {
|
|
661
|
+
className: FIELD_CONSENT_NOTE_CLASS,
|
|
589
662
|
children: disclosure.text
|
|
590
663
|
}),
|
|
591
664
|
/*#__PURE__*/ _jsx("input", {
|
|
@@ -937,7 +1010,61 @@ export const formFieldSchema = {
|
|
|
937
1010
|
}
|
|
938
1011
|
]
|
|
939
1012
|
};
|
|
940
|
-
|
|
1013
|
+
/**
|
|
1014
|
+
* A field as a preset: the all-in-one Form Field's props rebuilt as MUI's
|
|
1015
|
+
* separate form elements by `formFieldToElements` — the same function the
|
|
1016
|
+
* backfill migrates saved fields with, so a field added today and a field
|
|
1017
|
+
* migrated from a Form Field are the same tree. Ids are left `null` (fresh
|
|
1018
|
+
* ones are minted at insertion) and every node names `mui`, the bundle that
|
|
1019
|
+
* registers it.
|
|
1020
|
+
*/ export function fieldPresetData(props) {
|
|
1021
|
+
const built = formFieldToElements({
|
|
1022
|
+
$id: 'field',
|
|
1023
|
+
componentId: LEGACY_FORM_FIELD_COMPONENT_ID,
|
|
1024
|
+
props: props
|
|
1025
|
+
});
|
|
1026
|
+
if (!built) throw new Error('formFieldToElements refused a Form Field');
|
|
1027
|
+
const nest = (id)=>{
|
|
1028
|
+
var _node_nodes, _node_props;
|
|
1029
|
+
const node = built.nodes[id];
|
|
1030
|
+
const children = ((_node_nodes = node.nodes) != null ? _node_nodes : []).map(String);
|
|
1031
|
+
return _extends({
|
|
1032
|
+
$id: null,
|
|
1033
|
+
componentId: node.componentId,
|
|
1034
|
+
pluginId: node.pluginId,
|
|
1035
|
+
props: (_node_props = node.props) != null ? _node_props : {}
|
|
1036
|
+
}, children.length ? {
|
|
1037
|
+
nodes: children.map(nest)
|
|
1038
|
+
} : {});
|
|
1039
|
+
};
|
|
1040
|
+
return nest(built.rootId);
|
|
1041
|
+
}
|
|
1042
|
+
/** The name/email/message fields every contact form starts with. */ const contactFields = ()=>[
|
|
1043
|
+
fieldPresetData({
|
|
1044
|
+
fieldName: 'name',
|
|
1045
|
+
label: 'Name',
|
|
1046
|
+
required: true
|
|
1047
|
+
}),
|
|
1048
|
+
fieldPresetData({
|
|
1049
|
+
fieldName: 'email',
|
|
1050
|
+
label: 'Email',
|
|
1051
|
+
fieldType: 'email',
|
|
1052
|
+
required: true
|
|
1053
|
+
}),
|
|
1054
|
+
fieldPresetData({
|
|
1055
|
+
fieldName: 'message',
|
|
1056
|
+
label: 'Message',
|
|
1057
|
+
fieldType: 'textarea',
|
|
1058
|
+
required: true
|
|
1059
|
+
})
|
|
1060
|
+
];
|
|
1061
|
+
/*
|
|
1062
|
+
* The all-in-one Form Field has no preset: every piece of a field is its own
|
|
1063
|
+
* element in the mui plugin now (Form Control, Form Label, Input, …, with
|
|
1064
|
+
* Text/Email/Message/Dropdown/Radio/Checkbox/Rating field presets there), so
|
|
1065
|
+
* the Add Element picker no longer offers it. Its component and schema stay
|
|
1066
|
+
* registered so a document nobody has migrated keeps drawing and editing.
|
|
1067
|
+
*/ export const formPresets = [
|
|
941
1068
|
{
|
|
942
1069
|
$id: generatePresetId(FORM_ID, 'contact'),
|
|
943
1070
|
type: Aglyn.NodeType.PRESET,
|
|
@@ -958,70 +1085,16 @@ export const formPresets = [
|
|
|
958
1085
|
props: {
|
|
959
1086
|
formName: 'Contact'
|
|
960
1087
|
},
|
|
961
|
-
nodes:
|
|
962
|
-
{
|
|
963
|
-
$id: null,
|
|
964
|
-
componentId: FORM_FIELD_ID,
|
|
965
|
-
pluginId: BUNDLE_ID,
|
|
966
|
-
props: {
|
|
967
|
-
fieldName: 'name',
|
|
968
|
-
label: 'Name',
|
|
969
|
-
required: true
|
|
970
|
-
}
|
|
971
|
-
},
|
|
972
|
-
{
|
|
973
|
-
$id: null,
|
|
974
|
-
componentId: FORM_FIELD_ID,
|
|
975
|
-
pluginId: BUNDLE_ID,
|
|
976
|
-
props: {
|
|
977
|
-
fieldName: 'email',
|
|
978
|
-
label: 'Email',
|
|
979
|
-
fieldType: 'email',
|
|
980
|
-
required: true
|
|
981
|
-
}
|
|
982
|
-
},
|
|
983
|
-
{
|
|
984
|
-
$id: null,
|
|
985
|
-
componentId: FORM_FIELD_ID,
|
|
986
|
-
pluginId: BUNDLE_ID,
|
|
987
|
-
props: {
|
|
988
|
-
fieldName: 'message',
|
|
989
|
-
label: 'Message',
|
|
990
|
-
fieldType: 'textarea',
|
|
991
|
-
required: true
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
]
|
|
995
|
-
}
|
|
996
|
-
},
|
|
997
|
-
{
|
|
998
|
-
$id: generatePresetId(FORM_FIELD_ID),
|
|
999
|
-
type: Aglyn.NodeType.PRESET,
|
|
1000
|
-
displayName: 'Form Field',
|
|
1001
|
-
pluginId: BUNDLE_ID,
|
|
1002
|
-
description: 'Single input inside a form',
|
|
1003
|
-
category: Aglyn.ComponentCategory.FORMS,
|
|
1004
|
-
icon: {
|
|
1005
|
-
path: mdiFormTextbox.path,
|
|
1006
|
-
sx: {
|
|
1007
|
-
color: 'info.main'
|
|
1008
|
-
}
|
|
1009
|
-
},
|
|
1010
|
-
data: {
|
|
1011
|
-
$id: null,
|
|
1012
|
-
componentId: FORM_FIELD_ID,
|
|
1013
|
-
pluginId: BUNDLE_ID,
|
|
1014
|
-
props: {
|
|
1015
|
-
fieldName: 'field',
|
|
1016
|
-
label: 'Field'
|
|
1017
|
-
}
|
|
1088
|
+
nodes: contactFields()
|
|
1018
1089
|
}
|
|
1019
1090
|
},
|
|
1020
1091
|
/*
|
|
1021
|
-
* The marketing consent field as one preset rather than a recipe.
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1092
|
+
* The marketing consent field as one preset rather than a recipe. It is the
|
|
1093
|
+
* platform's `MARKETING_CONSENT_FORM_FIELD` rebuilt as elements — a lone,
|
|
1094
|
+
* unticked, not-required Checkbox named `marketingConsent` in a Choice
|
|
1095
|
+
* Label whose words say what the person agrees to and are also what the box
|
|
1096
|
+
* posts — so every place that adds this field adds the same one. It records
|
|
1097
|
+
* consent once the form names it as its Marketing consent field.
|
|
1025
1098
|
*/ {
|
|
1026
1099
|
$id: generatePresetId(FORM_FIELD_ID, 'marketing-consent'),
|
|
1027
1100
|
type: Aglyn.NodeType.PRESET,
|
|
@@ -1035,12 +1108,7 @@ export const formPresets = [
|
|
|
1035
1108
|
color: 'info.main'
|
|
1036
1109
|
}
|
|
1037
1110
|
},
|
|
1038
|
-
data: {
|
|
1039
|
-
$id: null,
|
|
1040
|
-
componentId: FORM_FIELD_ID,
|
|
1041
|
-
pluginId: BUNDLE_ID,
|
|
1042
|
-
props: _extends({}, Aglyn.MARKETING_CONSENT_FORM_FIELD)
|
|
1043
|
-
}
|
|
1111
|
+
data: fieldPresetData(_extends({}, Aglyn.MARKETING_CONSENT_FORM_FIELD))
|
|
1044
1112
|
}
|
|
1045
1113
|
];
|
|
1046
1114
|
/**
|
|
@@ -1099,32 +1167,12 @@ export const formPresets = [
|
|
|
1099
1167
|
submitLabel: 'Send message',
|
|
1100
1168
|
successMessage: 'Thanks — we will get back to you soon.'
|
|
1101
1169
|
},
|
|
1102
|
-
nodes:
|
|
1103
|
-
contactField('name', 'Name'),
|
|
1104
|
-
contactField('email', 'Email', {
|
|
1105
|
-
fieldType: 'email'
|
|
1106
|
-
}),
|
|
1107
|
-
contactField('message', 'Message', {
|
|
1108
|
-
fieldType: 'textarea'
|
|
1109
|
-
})
|
|
1110
|
-
]
|
|
1170
|
+
nodes: contactFields()
|
|
1111
1171
|
}
|
|
1112
1172
|
]
|
|
1113
1173
|
}
|
|
1114
1174
|
}
|
|
1115
1175
|
];
|
|
1116
|
-
function contactField(fieldName, label, extra) {
|
|
1117
|
-
return {
|
|
1118
|
-
$id: null,
|
|
1119
|
-
componentId: FORM_FIELD_ID,
|
|
1120
|
-
pluginId: BUNDLE_ID,
|
|
1121
|
-
props: _extends({
|
|
1122
|
-
fieldName,
|
|
1123
|
-
label,
|
|
1124
|
-
required: true
|
|
1125
|
-
}, extra)
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
1176
|
export { Form, FormField };
|
|
1129
1177
|
export default Form;
|
|
1130
1178
|
|