{
componentName: 'ContentMain',
content: [{
uid: '',
componentName: 'Single Step Form',
dataSource: '',
fields: {
ModelJson: {
value: '[ { "title":"Form Name", "fields":{ "Name":{ "label":"Form Name", "type":"input", "value":"Form Name", "name":"Name" }, "Id":{ "label":"Id", "type":"hidden", "value":"", "name":"Id" } }, "fresh":true }, { "title":"Text Input", "fields":{ "FormId":{ "label":"FormId", "type":"hidden", "value":"stepOne", "name":"FormId" }, "Name":{ "label":"Name", "type":"hidden", "value":"textinput", "name":"Name" }, "Id":{ "label":"Id", "type":"input", "value":"fixtureName", "name":"Id" }, "Label":{ "label":"Label", "type":"input", "value":"Fixture Selection", "name":"Label" }, "BackEndLabel":{ "label":"BackEnd Label", "type":"input", "value":"Fixture Selection", "name":"BackEndLabel" }, "Value":{ "label":"Default Value", "type":"input", "value":"", "name":"Value" }, "DefaultValueSource":{ "label":"Default Value Source", "type":"select", "value":[ { "value":"None", "selected":false, "label":"None" }, { "value":"cookie", "selected":true, "label":"Cookie" } ], "name":"DefaultValueSource" }, "DefaultValueKey":{ "label":"Default Value Source Key", "type":"input", "value":"fixtureName", "name":"DefaultValueKey"}]
}
}
}]
} [
{
"title": "Radio List",
"fields": {
"FormId": {
"label": "FormId",
"type": "hidden",
"value": "stepOne",
"name": "FormId"
},
"Name": {
"label": "Name",
"type": "input",
"value": "Are you a customer requesting a single light for your yard or property?",
"name": "Name"
},
"Label": {
"label": "Label",
"type": "input",
"value": "Are you a customer requesting a single light for your yard or property?",
"name": "Label"
},
"BackEndLabel": {
"label": "BackEnd Label",
"type": "input",
"value": "Are you a customer requesting a single light for your yard or property?",
"name": "BackEndLabel"
},
"GroupName": {
"label": "Group Name",
"type": "input",
"value": "GroupName",
"name": "GroupName"
},
"Value": {
"label": "Default Value",
"type": "input",
"value": "",
"name": "Value"
},
"Id": {
"label": "Id",
"type": "input",
"value": "radiolist",
"name": "Id"
},
"TooltipText": {
"label": "Tooltip Text",
"type": "input",
"value": "",
"name": "TooltipText"
},
"CustomValidationErrorMsg": {
"label": "Custom Validation Err. Msg.",
"type": "input",
"value": "Please pick an option.",
"name": "CustomValidationErrorMsg"
},
"AppearsOnFormConfirmation": {
"label": "Appear on Confirmation",
"type": "checkbox",
"value": true,
"name": "AppearsOnFormConfirmation"
},
"Required": {
"label": "Required",
"type": "checkbox",
"value": true,
"name": "Required"
},
"InputItems": {
"label": "Items",
"type": "radiolist",
"value": "[{\"text\":\"Yes\",\"value\":\"Yes\"},{\"text\":\"No\",\"value\":\"No\"}]",
"name": "InputItems"
}
},
"fresh": true
},
{
"title": "Section Header",
"fields": {
"FormId": {
"label": "FormId",
"type": "hidden",
"value": "stepOne",
"name": "FormId"
},
"Name": {
"label": "Name",
"type": "hidden",
"value": "sectionhdr",
"name": "Name"
},
"Label": {
"label": "Label",
"type": "input",
"value": "Name & Address",
"name": "Label"
},
"BackEndLabel": {
"label": "BackEnd Label",
"type": "input",
"value": "Name & Address",
"name": "BackEndLabel"
},
"Id": {
"label": "Id",
"type": "hidden",
"value": "sectionhdr",
"name": "Id"
}
},
"fresh": true
},
{
"title": "First Name",
"fields": {
"FormId": {
"label": "FormId",
"type": "hidden",
"value": "stepOne",
"name": "FormId"
},
"Name": {
"label": "Name",
"type": "hidden",
"value": "FirstName",
"name": "Name"
},
"Id": {
"label": "Id",
"type": "hidden",
"value": "FirstName",
"name": "Id"
},
"Label": {
"label": "Label",
"type": "hidden",
"value": "First Name",
"name": "Label"
},
"BackEndLabel": {
"label": "BackEnd Label",
"type": "hidden",
"value": "First Name",
"name": "BackEndLabel"
},
"Value": {
"label": "Default Value",
"type": "hidden",
"value": "",
"name": "Value"
},
"ValidationPattern": {
"label": "Validation Rule",
"type": "hidden",
"value": "lettersWhiteSpace",
"name": "ValidationPattern"
},
"TooltipText": {
"label": "Tooltip Text",
"type": "input",
"value": "",
"name": "TooltipText"
},
"CustomValidationErrorMsg": {
"label": "Custom Validation Err. Msg.",
"type": "hidden",
"value": "Please enter a first name.",
"name": "CustomValidationErrorMsg"
},
"AppearsOnFormConfirmation": {
"label": "Appear on Confirmation",
"type": "checkbox",
"value": false,
"name": "AppearsOnFormConfirmation"
},
"Required": {
"label": "Required",
"type": "checkbox",
"value": true,
"name": "Required"
},
"MinimumLength": {
"label": "Min. Length",
"type": "hidden",
"value": "",
"name": "MinimumLength"
},
"MaximumLength": {
"label": "Max. Length",
"type": "hidden",
"value": "40",
"name": "MaximumLength"
},
"Predefined": {
"label": "Predefined",
"type": "hidden",
"value": "true",
"name": "Predefined"
},
"ColumnWidth": {
"label": "Column Width",
"type": "select",
"value": [
{
"value": "2",
"selected": false,
"label": "2"
},
{
"value": "3",
"selected": true,
"label": "3"
},
{
"value": "4",
"selected": false,
"label": "4"
},
{
"value": "5",
"selected": false,
"label": "5"
},
{
"value": "6",
"selected": false,
"label": "6"
}
],
"name": "ColumnWidth"
}
},
"fresh": true
}
]
```
4. `formModel` then gets passed into `createFormInit()` to process all of this data into something more concise and manageable
```js
const createdFields = useMemo(() => createFormInit(formModel, false), []);- `createdFields` is memoized to cache the original value and keep it from re-rendering each cycle
- The `false` parameter signifies that its not to return a multidimensional array, just a single arrayconst createFormInit: {
(arr: Array<ParsedFormModel>, multi: true): CFReturnType[][];
(arr: Array<ParsedFormModel>, multi: false): CFReturnType[];
} = (arr: Array<ParsedFormModel>, multi: boolean): any => {
return multi ? multiStepFormFields(arr) : parseFields(arr);
};
```
- The `formModel` array will get passed into `parseFields()`
- `parseFields()` will then run each of the form objects that are inside the `formModel` through yet another and final parser, `createForm()`. It will then save these objects to a new array and filter out any nulls generated by `createForm()` and return this array back to `<SingleStepForm />`
- This function uses a Typescript overload to determine the correct return type depending on which `multi` boolean is passed in
```js
const parseFields = (arr: Array < ParsedFormModel > ) => {
const items = arr.reduce((acc: Array < CFReturnType | null > , curr) => {
const formField = createForm(curr);
return [...acc, formField];
}, []);
// filter out any null values due to early returns from hidden fields
return items.filter(Boolean) as Array < CFReturnType > ;
};const inputMap: CFMappingType["inputMap"] = {
alternatephone: "phone",
"alternate_phone_#": "phone",
captcha: "recaptcha",
checkbox: "checkbox",
checkbox_list: "checkboxGroup",
// ...
};const dataMap: CFMappingType["dataMap"] = {
// ...
input: {
file: "Input",
props: {
type: "text",
},
},
phone: {
file: "Input",
props: {
type: "tel",
icon: "phone",
mask: masks.tel,
},
},
radio: {
file: "RadioGroup",
},
// ...
};const regexMap: CFMappingType["regexMap"] = {
email: {
message: "Not a valid email format",
value: /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/,
},
lettersWhiteSpace: {
message: "Can only be letters and spaces",
value: /^[a-zA-ZáéíóúüÁÉÍÓÚÜñÑ\-]+(\s+[a-zA-ZáéíóúüÁÉÍÓÚÜñÑ]+)*$/,
},
notSameDigits: {
message: "Can only contain numbers",
value: /^(\d)\d*(?!\1)\d+$/,
},
// ...
};const { file, props } = dataMap[inputType];
const Component = loadable(() => import(`src/components/Form/${file}`));return {
Component,
data: getData(fields, title),
file,
formName: fields?.FormId?.value,
id: uid(32),
props: {
columns: getColumnWidth(fields.ColumnWidth?.value),
...props,
},
validations: getValidations(file, fields, props?.type),
};- Component: The React component that was dynamically imported
- data: An object containing details about the field such as maxLength, minLength, placeholder, required, etc..const getData: GetDataProps = ({ fields, title }) => ({
customValidationErrorMsg: fields?.CustomValidationErrorMsg?.value || 'field is required',
items: parseItems(fields?.InputItems?.value),
label: fields?.Label?.value || '',
maxLength: parseInt(fields?.MaximumLength?.value) || 524288,
minLength: parseInt(fields?.MinimumLength?.value) || 0,
name: fields?.Name?.value,
placeholder: fields?.PlaceholderText?.value,
required: fields?.Required?.value || false,
tabs: fields?.Tabs?.value.split('\n') || [],
title,
toolTipText: fields?.TooltipText?.value,
});
```
- file: The name of the component that was dynamically imported
- id: A unique id for each field since sitecore doesn't return usable id's
- props: The props returned from the `dataMap` lookup along with the column width for each field
- validations: Will either be null or an object that contains if it should show on the confirmation screen and the validationPattern if any
```typescript
const getValidations: GetValidationProps = (file, fields, regex = '') => {
const skipValidation = ['Heading', 'Recaptcha', 'Tabs'];
let pattern;
if (file && skipValidation.includes(file)) return null;
// Validations will usually come through as a string value from sitecore
// but can also come through as an array of objects, these have the type 'select'
// We first need to parse through this array and grab the value of the selected validation pattern
if (fields?.ValidationPattern?.type === 'select') {
pattern = getSelectedValue(fields.ValidationPattern.value);
} else {
pattern = fields?.ValidationPattern?.value;
}
return {
shouldConfirm: fields?.AppearsOnFormConfirmation?.value,
validationPattern:
// 1. by regex in mapping props (phone, ssn)
// 2. by named regex pattern coming from Sitecore
regexMap[regex] || regexMap[pattern],
};
};<FormComponent onSubmit={handleSubmit(onSubmit)}>
{" "}
{createdFields.map(
({ Component, data, id, props, validations, ...rest }: CFReturnType) => {
return (
<FieldWrapper columns={props?.columns} key={id}>
<Component
register={register({
pattern: regexPattern(validations),
required: isRequired(data),
validate: {
match: (value: string) =>
matchingEmails(
name.toLowerCase(),
value,
getValues(["email", "emailconf"])
),
},
})}
{...{
data,
errors,
name,
props,
...rest,
}}
/>{" "}
</FieldWrapper>
);
}
)} <Button type="submit" variant="primary">
submit{" "}
</Button>{" "}
</FormComponent>const FormInput = ({
data,
errors,
name,
props,
register,
validations,
}: FormInputProps) => {
const { mask, type } = props || {};
const { label = "", maxLength, required, toolTipText } = data;
const propData = {
error: {
hasError: Boolean(errors[name]?.message),
errorMessage: errors[name]?.message,
},
id: label,
label,
mask,
maxLength,
name,
register,
required,
type,
validations,
};
return (
<div className="relative">
<InputText {...propData} />{" "}
<div className="hidden lg:block absolute top-0 right-0 mt-4 lg:-mr-48">
<Tooltip error={Boolean(errors[name])} message={toolTipText} />{" "}
</div>{" "}
</div>
);
};[[form stepper element], [...first section fields], [...second section fields] ...etc]const formModel: Array<ParsedFormModel> = JSON.parse(modelJson.value);
const createdFields = useMemo(() => createFormInit(formModel, true), []);
const createdFieldsWithoutTabs = [...createdFields.slice(1), []];
const createdFieldsOnlyTabFields = [
...createdFields[0][0].data.tabs,
"Confirmation",
];
// ... return
<FormComponent onSubmit={handleFormSubmit}>
<FormStepper activeIndex={activeIndex} content={createdFieldsOnlyTabFields} />
{createdFieldsWithoutTabs.map((innerArray, index) =>
innerArray.map(
({ Component, data, id, props, validations, ...rest }: CFReturnType) => {
return (
<FieldWrapper
className={index === activeIndex ? "block" : "hidden"}
columns={props?.columns}
key={id}
>
<Component
register={register({
pattern: regexPattern(validations),
required: isRequired(data),
validate: {
match: (value: string) =>
matchingEmails(
name.toLowerCase(),
value,
getValues(["email", "emailconf"])
),
},
})}
{...{ data, errors, name, props, ...rest }}
/>
</FieldWrapper>
);
}
)
)}
{isLastStep && <ConfirmationStep data={confirmedValues} {...{ fields }} />}
<ButtonWrapper />
</FormComponent>;