Skip to content

Commit

Permalink
Prevent submit if data is invalid
Browse files Browse the repository at this point in the history
  • Loading branch information
hungoptimizely committed Nov 22, 2023
1 parent 29706db commit ec8d0e0
Show file tree
Hide file tree
Showing 17 changed files with 141 additions and 50 deletions.
39 changes: 28 additions & 11 deletions src/@optimizely/forms-react/src/components/FormBody.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import React, { useRef } from "react";
import { useForms } from "../context/store";
import { FormContainer, FormSubmit, StepBuilder, SubmitButtonType, isInArray } from "@optimizely/forms-sdk";
import { useForms, useFormsDispatch } from "../context/store";
import { FormContainer, FormSubmit, SubmitButtonType, equals, isInArray, isNull, isNullOrEmpty } from "@optimizely/forms-sdk";
import { RenderElementInStep } from "./RenderElementInStep";
import { DispatchFunctions } from "../context/dispatchFunctions";

export const FormBody = () => {
const formContext = useForms();
const form = formContext?.formContainer ?? {} as FormContainer;
const formSubmit = new FormSubmit(formContext?.formContainer ?? {} as FormContainer);
const dispatch = useFormsDispatch();
const dispatchFunctions = new DispatchFunctions(dispatch);

const formTitleId = `${form.key}_label`;
const statusDisplay = useRef<string>("hide");
const stepCount = form.steps.length;
const statusMessage = useRef<string>("");
const stepLocalizations = useRef<Record<string, string>>(form.steps?.filter(s => !isNull(s.formStep.localizations))[0]?.formStep.localizations);

//TODO: these variables should be get from api or sdk
const validateFail = false,
Expand All @@ -28,14 +32,14 @@ export const FormBody = () => {

if(formFinalized || isProgressiveSubmit)
{
statusDisplay.current = "Form__Success__Message";
statusMessage.current = form.properties.submitSuccessMessage ?? message;
statusDisplay.current = "Form__Success__Message";
statusMessage.current = form.properties.submitSuccessMessage ?? message;
}
else if((submissionWarning || (!submittable && !isSuccess))
&& message)
&& message)
{
statusDisplay.current = "Form__Warning__Message";
statusMessage.current = message;
statusDisplay.current = "Form__Warning__Message";
statusMessage.current = message;
}
const validationCssClass = validateFail ? "ValidationFail" : "ValidationSuccess";
const isShowStepNavigation = stepCount > 1 && currentStepIndex > -1 && currentStepIndex < stepCount && !formFinalized;
Expand All @@ -50,7 +54,20 @@ export const FormBody = () => {
let formSubmissions = (formContext?.formSubmissions ?? [])
//only post value of active elements
.filter(fs => !isInArray(fs.elementKey, formContext?.dependencyInactiveElements ?? []));

//validate all submission data before submit
let formValidationResults = formSubmit.doValidate(formSubmissions);
dispatchFunctions.dispatchUpdateAllValidation(formValidationResults);

//set focus on the 1st invalid element of current step
let invalid = formValidationResults.filter(fv =>
fv.results.some(r => !r.valid) &&
form.steps[currentStepIndex]?.elements?.some(e => equals(e.key, fv.elementKey))
)[0]?.elementKey;
if(!isNullOrEmpty(invalid)){
dispatchFunctions.dispatchFocusOn(invalid);
return;
}

formSubmit.doSubmit(formSubmissions);
}

Expand Down Expand Up @@ -106,20 +123,20 @@ export const FormBody = () => {
<nav role="navigation" className="Form__NavigationBar">
<button type="submit" name="submit" value={SubmitButtonType.PreviousStep} className="Form__NavigationBar__Action FormExcludeDataRebind btnPrev"
disabled={prevButtonDisableState}>
{form.localizations["previousButtonLabel"]}
{stepLocalizations.current["previousButtonLabel"]}
</button>

<div className="Form__NavigationBar__ProgressBar">
<div className="Form__NavigationBar__ProgressBar--Progress" style={{width: progressWidth}}></div>
<div className="Form__NavigationBar__ProgressBar--Text">
<span className="Form__NavigationBar__ProgressBar__ProgressLabel">{form.localizations["pageButtonLabel"]}</span>
<span className="Form__NavigationBar__ProgressBar__ProgressLabel">{stepLocalizations.current["pageButtonLabel"]}</span>
<span className="Form__NavigationBar__ProgressBar__CurrentStep">{currentDisplayStepIndex}</span>/
<span className="Form__NavigationBar__ProgressBar__StepsCount">{stepCount}</span>
</div>
</div>
<button type="submit" name="submit" value={SubmitButtonType.NextStep} className="Form__NavigationBar__Action FormExcludeDataRebind btnNext"
disabled={nextButtonDisableState}>
{form.localizations["nextButtonLabel"]}
{stepLocalizations.current["nextButtonLabel"]}
</button>
</nav>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const FileUploadElementBlock = (props: FileUploadElementBlockProps) => {
ext = ext.trim();
return (ext[0] != ".") ? `.${ext}` : ext;
}).join(",") : "";
const { isVisible, validationResults, extraAttr, validatorClasses } = elementContext;
const { isVisible, validationResults, extraAttr, validatorClasses, elementRef } = elementContext;

return useMemo(()=>(
<ElementWrapper className={`FormFileUpload ${validatorClasses}`} validationResults={validationResults} isVisible={isVisible}>
Expand All @@ -31,6 +31,7 @@ export const FileUploadElementBlock = (props: FileUploadElementBlockProps) => {
accept={allowedTypes}
aria-describedby={element.key + "_desc"}
onChange={handleChange}
ref={elementRef}
/>
<div className="FormFileUpload__PostedFile"></div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface NumberElementBlockProps {
export const NumberElementBlock = (props: NumberElementBlockProps) => {
const { element } = props;
const { elementContext, handleChange, handleBlur } = useElement(element);
const { isVisible, validationResults, value, extraAttr, validatorClasses } = elementContext;
const { isVisible, validationResults, value, extraAttr, validatorClasses, elementRef } = elementContext;

return useMemo(()=>(
<ElementWrapper className={`FormTextbox FormTextbox--Number ${validatorClasses}`} validationResults={validationResults} isVisible={isVisible}>
Expand All @@ -30,6 +30,7 @@ export const NumberElementBlock = (props: NumberElementBlockProps) => {
autoComplete={element.properties.autoComplete}
onChange={handleChange}
onBlur={handleBlur}
ref={elementRef}
/>

<ValidationMessage element={element} validationResults={validationResults} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface SelectionElementBlockProps {
export const SelectionElementBlock = (props: SelectionElementBlockProps) => {
const { element } = props;
const { elementContext, handleChange, handleBlur } = useElement(element);
const { isVisible, validationResults, value, extraAttr, validatorClasses } = elementContext;
const { isVisible, validationResults, value, extraAttr, validatorClasses, elementRef } = elementContext;

return useMemo(()=>(
<ElementWrapper className={`FormSelection ${validatorClasses}`} validationResults={validationResults} isVisible={isVisible}>
Expand All @@ -26,6 +26,7 @@ export const SelectionElementBlock = (props: SelectionElementBlockProps) => {
onChange={handleChange}
onBlur={handleBlur}
value={value}
ref={elementRef}
>
<option value="" disabled={value !== ""}>
{isNullOrEmpty(element.properties.placeHolder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface TextareaElementBlockProps {
export const TextareaElementBlock = (props: TextareaElementBlockProps) => {
const { element } = props;
const { elementContext, handleChange, handleBlur } = useElement(element);
const { isVisible, validationResults, value, extraAttr, validatorClasses } = elementContext;
const { isVisible, validationResults, value, extraAttr, validatorClasses, elementRef } = elementContext;
return useMemo(()=>(
<ElementWrapper className={`FormTextbox FormTextbox--Textarea ${validatorClasses}`} validationResults={validationResults} isVisible={isVisible}>
<ElementCaption element={element} />
Expand All @@ -27,6 +27,7 @@ export const TextareaElementBlock = (props: TextareaElementBlockProps) => {
autoComplete={element.properties.autoComplete}
onChange={handleChange}
onBlur={handleBlur}
ref={elementRef}
>
</textarea>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface TextboxElementBlockProps {
export const TextboxElementBlock = (props: TextboxElementBlockProps) => {
const { element } = props;
const { elementContext, handleChange, handleBlur } = useElement(element);
const { isVisible, validationResults, value, validatorClasses, extraAttr } = elementContext;
const { isVisible, validationResults, value, elementRef, validatorClasses, extraAttr } = elementContext;
return useMemo(()=>(
<ElementWrapper className={`FormTextbox ${validatorClasses}`} validationResults={validationResults} isVisible={isVisible}>
<ElementCaption element={element} />
Expand All @@ -24,6 +24,7 @@ export const TextboxElementBlock = (props: TextboxElementBlockProps) => {
{...extraAttr}
onChange={handleChange}
onBlur={handleBlur}
ref={elementRef}
/>

<ValidationMessage element={element} validationResults={validationResults} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface UrlElementBlockProps {
export const UrlElementBlock = (props: UrlElementBlockProps) => {
const { element } = props;
const { elementContext, handleChange, handleBlur } = useElement(element);
const { isVisible, validationResults, value, extraAttr, validatorClasses } = elementContext;
const { isVisible, validationResults, value, extraAttr, validatorClasses, elementRef } = elementContext;
return useMemo(()=>(
<ElementWrapper className={`FormTextbox__Input FormUrl__Input ${validatorClasses}`} validationResults={validationResults} isVisible={isVisible}>
<ElementCaption element={element} />
Expand All @@ -26,6 +26,7 @@ export const UrlElementBlock = (props: UrlElementBlockProps) => {
aria-describedby={`${element.key}_desc`}
onChange={handleChange}
onBlur={handleBlur}
ref={elementRef}
/>

<ValidationMessage element={element} validationResults={validationResults} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { FormValidationResult, isInArray, isNull } from "@optimizely/forms-sdk";
import { ElementValidationResult } from "@optimizely/forms-sdk";
import React, { ReactNode } from "react";

export interface ElementWrapperProps{
className?: string
isVisible: boolean,
children: ReactNode,
validationResults?: FormValidationResult[]
validationResults?: ElementValidationResult[]
}

export default function ElementWrapper(props: ElementWrapperProps){
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ValidatableElementBase, FormValidationResult, isNull } from "@optimizely/forms-sdk";
import { ValidatableElementBase, ElementValidationResult, isNull } from "@optimizely/forms-sdk";
import React from "react";

interface ValidationMessageProps {
element: ValidatableElementBase,
validationResults: FormValidationResult[]
validationResults: ElementValidationResult[]
}

export const ValidationMessage = (props: ValidationMessageProps) => {
Expand Down
18 changes: 16 additions & 2 deletions src/@optimizely/forms-react/src/context/dispatchFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FormContainer, FormValidationResult } from "@optimizely/forms-sdk";
import { FormContainer, ElementValidationResult, FormValidationResult } from "@optimizely/forms-sdk";
import { ActionType } from "./reducer";
import { initState } from "./initState";

Expand All @@ -8,14 +8,21 @@ export class DispatchFunctions {
this._dispatch = dispatch;
}

dispatchUpdateValidation = (elementKey: string, validationResults: FormValidationResult[]) => {
dispatchUpdateValidation = (elementKey: string, validationResults: ElementValidationResult[]) => {
this._dispatch({
type: ActionType.UpdateValidation,
elementKey: elementKey,
validationResults
});
}

dispatchUpdateAllValidation = (formValidationResults: FormValidationResult[]) => {
this._dispatch({
type: ActionType.UpdateAllValidation,
formValidationResults
});
}

dispatchUpdateValue = (elementKey: string, value: any) => {
this._dispatch({
type: ActionType.UpdateValue,
Expand Down Expand Up @@ -46,4 +53,11 @@ export class DispatchFunctions {
}
});
}

dispatchFocusOn = (focusOn: string) => {
this._dispatch({
type: ActionType.UpdateFocusOn,
focusOn
});
}
}
16 changes: 7 additions & 9 deletions src/@optimizely/forms-react/src/context/initState.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ConditionProperties,
import {
FormContainer,
ValidatableElementBaseProperties,
getDefaultValue,
isNull,
FormState,
FormSubmission,
FormValidation,
ElementValidationResult,
FormValidationResult,
StepDependencies } from "@optimizely/forms-sdk";

Expand All @@ -17,7 +17,7 @@ export function initState(props: InitStateProps): FormState{
const { formContainer } = props;

let formSubmissions = [] as FormSubmission[];
let formValidations = [] as FormValidation[];
let formValidationResults = [] as FormValidationResult[];
let stepDependencies = [] as StepDependencies[];

formContainer?.steps.forEach(s => {
Expand All @@ -27,24 +27,22 @@ export function initState(props: InitStateProps): FormState{

//init form validation
let validatableProps = e.properties as ValidatableElementBaseProperties;
let formValidationResults = [] as FormValidationResult[];
let elementValidationResults = [] as ElementValidationResult[];

//some elements don't have validator
if(!isNull(validatableProps.validators))
{
validatableProps.validators.forEach(v => {
formValidationResults = formValidationResults.concat({type: v.type, valid: true}); //default valid = true to hide message
elementValidationResults = elementValidationResults.concat({type: v.type, valid: true}); //default valid = true to hide message
});
}

formValidations = formValidations.concat({elementKey: e.key, results: formValidationResults});
formValidationResults = formValidationResults.concat({elementKey: e.key, results: elementValidationResults});
});
stepDependencies = stepDependencies.concat({elementKey: s.formStep.key, isSatisfied: false });
});



return {
isReset: false, formSubmissions, formValidations, stepDependencies, formContainer, dependencyInactiveElements: []
isReset: false, formSubmissions, formValidationResults, stepDependencies, formContainer, dependencyInactiveElements: [], focusOn: ""
} as FormState;
}
22 changes: 18 additions & 4 deletions src/@optimizely/forms-react/src/context/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { equals,
FormState,
FormSubmission,
FormValidation } from "@optimizely/forms-sdk";
FormValidationResult } from "@optimizely/forms-sdk";

export enum ActionType {
UpdateValue = "UpdateValue",
UpdateValidation = "UpdateValidation",
UpdateDependencies = "UpdateDependencies",
ResetForm = "ResetForm",
ResetedForm = "ResetedForm"
ResetedForm = "ResetedForm",
UpdateAllValidation = "UpdateAllValidation",
UpdateFocusOn = "UpdateFocusOn"
}

export function formReducer(formState: FormState, action: any) {
Expand All @@ -25,12 +27,18 @@ export function formReducer(formState: FormState, action: any) {
case ActionType.UpdateValidation: {
return {
...formState,
formValidations: formState.formValidations.map(fv => equals(fv.elementKey, action.elementKey) ? {
formValidationResults: formState.formValidationResults.map(fv => equals(fv.elementKey, action.elementKey) ? {
elementKey: action.elementKey,
results: action.validationResults
} as FormValidation : fv)
} as FormValidationResult : fv)
} as FormState;
}
case ActionType.UpdateAllValidation: {
return {
...formState,
formValidationResults: action.formValidationResults
} as FormState;
}
case ActionType.UpdateDependencies: {
return {
...formState,
Expand All @@ -46,6 +54,12 @@ export function formReducer(formState: FormState, action: any) {
isReset: false
} as FormState;
}
case ActionType.UpdateFocusOn: {
return {
...formState,
focusOn: action.focusOn
} as FormState;
}
default: {
return formState;
}
Expand Down
Loading

0 comments on commit ec8d0e0

Please sign in to comment.