diff --git a/.vscode/settings.json b/.vscode/settings.json index 0e8dd84..2fea7b8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "jestrunner.jestCommand": "npm run test --", "jest.rootPath": "src/@optimizely/forms-sdk", + "jest.jestCommandLine": "npm run test --", } \ No newline at end of file diff --git a/samples/sample-react-app/src/App.test.tsx b/samples/sample-react-app/src/App.test.tsx deleted file mode 100644 index 2a68616..0000000 --- a/samples/sample-react-app/src/App.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/@optimizely/forms-react/src/components/FormBody.tsx b/src/@optimizely/forms-react/src/components/FormBody.tsx new file mode 100644 index 0000000..01faafb --- /dev/null +++ b/src/@optimizely/forms-react/src/components/FormBody.tsx @@ -0,0 +1,129 @@ +import React, { useRef } from "react"; +import { useForms } from "../context/store"; +import { FormContainer, FormSubmit, StepBuilder, SubmitButtonType, isInArray } from "@optimizely/forms-sdk"; +import { RenderElementInStep } from "./RenderElementInStep"; + +export const FormBody = () => { + const formContext = useForms(); + const form = formContext?.formContainer ?? {} as FormContainer; + const formSubmit = new FormSubmit(formContext?.formContainer ?? {} as FormContainer); + + const formTitleId = `${form.key}_label`; + const statusDisplay = useRef("hide"); + const stepCount = form.steps.length; + const statusMessage = useRef(""); + + //TODO: these variables should be get from api or sdk + const validateFail = false, + formFinalized = false, + isProgressiveSubmit = false, + isSuccess = false, + submittable = true, + submissionWarning = false, + message = "", + isReadOnlyMode = false, + readOnlyModeMessage = "", + currentStepIndex = 0, + isStepValidToDisplay = true; + + if(formFinalized || isProgressiveSubmit) + { + statusDisplay.current = "Form__Success__Message"; + statusMessage.current = form.properties.submitSuccessMessage ?? message; + } + else if((submissionWarning || (!submittable && !isSuccess)) + && message) + { + statusDisplay.current = "Form__Warning__Message"; + statusMessage.current = message; + } + const validationCssClass = validateFail ? "ValidationFail" : "ValidationSuccess"; + const isShowStepNavigation = stepCount > 1 && currentStepIndex > -1 && currentStepIndex < stepCount && !formFinalized; + const prevButtonDisableState = (currentStepIndex == 0) || !submittable; + const nextButtonDisableState = (currentStepIndex == stepCount - 1) || !submittable; + const currentDisplayStepIndex = currentStepIndex + 1; + const progressWidth = (100 * currentDisplayStepIndex / stepCount) + "%"; + + const handleSubmit = (e: any) => { + e.preventDefault(); + + let formSubmissions = (formContext?.formSubmissions ?? []) + //only post value of active elements + .filter(fs => !isInArray(fs.elementKey, formContext?.dependencyInactiveElements ?? [])); + + formSubmit.doSubmit(formSubmissions); + } + + return ( +
+ {form.properties.title && +

+ {form.properties.title} +

+ } + {form.properties.description && + + } + {isReadOnlyMode && readOnlyModeMessage && +
+ + {readOnlyModeMessage} + +
+ } + {/* area for showing Form's status or validation */} +
+
+
+
+
+ +
+ {/* render element */} + {form.steps.map((e, i)=>{ + let stepDisplaying = (currentStepIndex === i && !formFinalized && isStepValidToDisplay) ? "" : "hide"; + return ( +
+ +
+ ); + })} + + {/* render step navigation */} + {isShowStepNavigation && + + } +
+
+ ) +} \ No newline at end of file diff --git a/src/@optimizely/forms-react/src/components/FormContainerBlock.tsx b/src/@optimizely/forms-react/src/components/FormContainerBlock.tsx index f256438..847a3a6 100644 --- a/src/@optimizely/forms-react/src/components/FormContainerBlock.tsx +++ b/src/@optimizely/forms-react/src/components/FormContainerBlock.tsx @@ -1,9 +1,8 @@ -import React, { useRef } from "react"; +import React from "react"; import { FormContainer, StepBuilder } from "@optimizely/forms-sdk"; -import { RenderElementInStep } from "./RenderElementInStep"; -import { SubmitButtonType } from "../models/SubmitButtonType"; import { FormProvider } from "../context/FormProvider"; import { initState } from "../context/initState"; +import { FormBody } from "./FormBody"; export interface FormContainerProps { form: FormContainer @@ -12,123 +11,12 @@ export interface FormContainerProps { export function FormContainerBlock(props: FormContainerProps){ const stepBuilder = new StepBuilder(props.form); const form = stepBuilder.buildForm(); - const formTitleId = `${form.key}_label`; - const statusDisplay = useRef("hide"); - const stepCount = form.steps.length; - const statusMessage = useRef(""); - - //TODO: these variables should be get from api or sdk - const validateFail = false, - formFinalized = false, - isProgressiveSubmit = false, - isSuccess = false, - submittable = true, - submissionWarning = false, - message = "", - isReadOnlyMode = false, - readOnlyModeMessage = "", - currentStepIndex = 0, - isStepValidToDisplay = true; - - if(formFinalized || isProgressiveSubmit) - { - statusDisplay.current = "Form__Success__Message"; - statusMessage.current = form.properties.submitSuccessMessage ?? message; - } - else if((submissionWarning || (!submittable && !isSuccess)) - && message) - { - statusDisplay.current = "Form__Warning__Message"; - statusMessage.current = message; - } - const validationCssClass = validateFail ? "ValidationFail" : "ValidationSuccess"; - const isShowStepNavigation = stepCount > 1 && currentStepIndex > -1 && currentStepIndex < stepCount && !formFinalized; - const prevButtonDisableState = (currentStepIndex == 0) || !submittable; - const nextButtonDisableState = (currentStepIndex == stepCount - 1) || !submittable; - const currentDisplayStepIndex = currentStepIndex + 1; - const progressWidth = (100 * currentDisplayStepIndex / stepCount) + "%"; - - const FormBody = () => { - return ( - <> - {form.properties.title && -

- {form.properties.title} -

- } - {form.properties.description && - - } - {isReadOnlyMode && readOnlyModeMessage && -
- - {readOnlyModeMessage} - -
- } - {/* area for showing Form's status or validation */} -
-
-
-
-
- -
- {/* render element */} - {form.steps.map((e, i)=>{ - let stepDisplaying = (currentStepIndex === i && !formFinalized && isStepValidToDisplay) ? "" : "hide"; - return ( -
- -
- ); - })} - - {/* render step navigation */} - {isShowStepNavigation && - - } -
- - ) - } - const state = initState({formContainer: form}); {/* finally return the form */} return ( -
- - +
) } \ No newline at end of file diff --git a/src/@optimizely/forms-react/src/context/initState.ts b/src/@optimizely/forms-react/src/context/initState.ts index 9d393ca..9457586 100644 --- a/src/@optimizely/forms-react/src/context/initState.ts +++ b/src/@optimizely/forms-react/src/context/initState.ts @@ -7,7 +7,6 @@ import { ConditionProperties, FormSubmission, FormValidation, FormValidationResult, - FormDependencies, StepDependencies } from "@optimizely/forms-sdk"; interface InitStateProps{ @@ -19,7 +18,6 @@ export function initState(props: InitStateProps): FormState{ let formSubmissions = [] as FormSubmission[]; let formValidations = [] as FormValidation[]; - let formDependencies = [] as FormDependencies[]; let stepDependencies = [] as StepDependencies[]; formContainer?.steps.forEach(s => { @@ -40,16 +38,6 @@ export function initState(props: InitStateProps): FormState{ } formValidations = formValidations.concat({elementKey: e.key, results: formValidationResults}); - - //init form dependencies - let conditionProps = (e.properties as unknown) as ConditionProperties; - - //Captcha, ResetButton don't have condition - if(!isNull(conditionProps.conditions)){ - conditionProps.conditions.forEach(c => { - formDependencies = formDependencies.concat({ elementKey: e.key, isSatisfied: false }); //default isSatisfied = false to do reverse action - }); - } }); stepDependencies = stepDependencies.concat({elementKey: s.formStep.key, isSatisfied: false }); }); @@ -57,6 +45,6 @@ export function initState(props: InitStateProps): FormState{ return { - isReset: false, formSubmissions, formDependencies, formValidations, stepDependencies, formContainer + isReset: false, formSubmissions, formValidations, stepDependencies, formContainer, dependencyInactiveElements: [] } as FormState; } \ No newline at end of file diff --git a/src/@optimizely/forms-react/src/context/reducer.ts b/src/@optimizely/forms-react/src/context/reducer.ts index 8cd0a42..5ce15b0 100644 --- a/src/@optimizely/forms-react/src/context/reducer.ts +++ b/src/@optimizely/forms-react/src/context/reducer.ts @@ -1,6 +1,5 @@ import { equals, FormState, - FormDependencies, FormSubmission, FormValidation } from "@optimizely/forms-sdk"; @@ -35,10 +34,7 @@ export function formReducer(formState: FormState, action: any) { case ActionType.UpdateDependencies: { return { ...formState, - formDependencies: formState.formDependencies.map(fd => equals(fd.elementKey, action.elementKey) ? { - elementKey: action.elementKey, - isSatisfied: action.isSatisfied - } as FormDependencies : fd) + dependencyInactiveElements: action.dependencyInactiveElements } as FormState; } case ActionType.ResetForm: { diff --git a/src/@optimizely/forms-react/src/hooks/useElement.ts b/src/@optimizely/forms-react/src/hooks/useElement.ts index 58b0e94..78d6aa4 100644 --- a/src/@optimizely/forms-react/src/hooks/useElement.ts +++ b/src/@optimizely/forms-react/src/hooks/useElement.ts @@ -37,6 +37,7 @@ export const useElement = (element: FormElementBase) => { const formCondition = new FormDependConditions(element) const defaultValue = getDefaultValue(element); const failClass = "ValidationFail"; + const isVisible = useRef(true); //build element state const value = (formContext?.formSubmissions ?? []) @@ -94,6 +95,36 @@ export const useElement = (element: FormElementBase) => { } },[formContext?.isReset]); + //update visible + useEffect(()=>{ + const conditionProps = (element.properties as unknown) as ConditionProperties; + + if (isNull(conditionProps.satisfiedAction)) { + return; + } + + //check form field dependencies + const checkConditions = formCondition.checkConditions(formContext?.formSubmissions as FormSubmission[]); + if (checkConditions) { + //if isDependenciesSatisfied = true, and if SatisfiedAction = show, then show element. otherwise hide element. + isVisible.current = equals(conditionProps.satisfiedAction, SatisfiedActionType.Show); + } + else { + //if isDependenciesSatisfied = false, and if SatisfiedAction = hide, then show element. otherwise hide element. + isVisible.current = equals(conditionProps.satisfiedAction, SatisfiedActionType.Hide); + } + + //update form state + let inactives = formContext?.dependencyInactiveElements ?? []; + if(isVisible.current){ + inactives = inactives.filter(ek => !equals(ek, element.key)); + } + else { + !isInArray(element.key, inactives) && inactives.push(element.key); + } + dispatchUpdateDependencies(inactives); + },[formContext?.formSubmissions]); + const dispatchUpdateValidation = (validationResults: FormValidationResult[]) => { dispatch({ type: ActionType.UpdateValidation, @@ -110,6 +141,13 @@ export const useElement = (element: FormElementBase) => { }); } + const dispatchUpdateDependencies = (dependencyInactiveElements: string[]) => { + dispatch({ + type: ActionType.UpdateDependencies, + dependencyInactiveElements + }); + } + const handleChange = (e: any) => { const { name, value, type, checked, files } = e.target; let submissionValue = value; @@ -153,8 +191,6 @@ export const useElement = (element: FormElementBase) => { //call validation from form-sdk let validationResults = doValidate(elementContext.value); - //call dependencies from form-sdk - //update form context dispatchUpdateValidation(validationResults); @@ -198,21 +234,7 @@ export const useElement = (element: FormElementBase) => { } const checkVisible = (): boolean => { - const conditionProps = (element.properties as unknown) as ConditionProperties; - - if (isNull(conditionProps.satisfiedAction)) { - return true; - } - - const checkConditions = formCondition.checkConditions(formContext?.formSubmissions as FormSubmission[]); - if (checkConditions) { - //if isDependenciesSatisfied = true, and if SatisfiedAction = show, then show element. otherwise hide element. - return equals(conditionProps.satisfiedAction, SatisfiedActionType.Show); - } - else { - //if isDependenciesSatisfied = false, and if SatisfiedAction = hide, then show element. otherwise hide element. - return equals(conditionProps.satisfiedAction, SatisfiedActionType.Hide); - } + return isVisible.current; } return { diff --git a/src/@optimizely/forms-react/src/models/index.ts b/src/@optimizely/forms-react/src/models/index.ts deleted file mode 100644 index 9d289d1..0000000 --- a/src/@optimizely/forms-react/src/models/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./SubmitButtonType"; \ No newline at end of file diff --git a/src/@optimizely/forms-sdk/src/form-submit/formSubmit.ts b/src/@optimizely/forms-sdk/src/form-submit/formSubmit.ts new file mode 100644 index 0000000..40c0159 --- /dev/null +++ b/src/@optimizely/forms-sdk/src/form-submit/formSubmit.ts @@ -0,0 +1,28 @@ +import { FormStorage } from "../form-storage"; +import { FormContainer, FormSubmission } from "../models"; + +/** + * Class to submit form submission to Headless Form API + */ +export class FormSubmit { + readonly _form: FormContainer + + constructor(form: FormContainer){ + this._form = form; + } + + /** + * Post an array of form submission to the Headless Form API + * @param formSubmission the array of form submission to post + */ + async doSubmit(formSubmissions: FormSubmission[]): Promise { + return new Promise((resolve, reject)=>{ + let formStorage = new FormStorage(this._form); + + //save data to storage of browser + formStorage.saveFormDataToStorage(formSubmissions); + + //TODO: post to API + }); + } +} \ No newline at end of file diff --git a/src/@optimizely/forms-sdk/src/form-submit/index.ts b/src/@optimizely/forms-sdk/src/form-submit/index.ts new file mode 100644 index 0000000..5bafb13 --- /dev/null +++ b/src/@optimizely/forms-sdk/src/form-submit/index.ts @@ -0,0 +1 @@ +export * from "./formSubmit"; \ No newline at end of file diff --git a/src/@optimizely/forms-sdk/src/index.ts b/src/@optimizely/forms-sdk/src/index.ts index dc0f96e..42297ff 100644 --- a/src/@optimizely/forms-sdk/src/index.ts +++ b/src/@optimizely/forms-sdk/src/index.ts @@ -4,3 +4,5 @@ export * from "./form-step"; export * from "./helpers"; export * from "./form-validator"; export * from "./form-depend-conditions"; +export * from "./form-storage"; +export * from "./form-submit"; \ No newline at end of file diff --git a/src/@optimizely/forms-react/src/models/SubmitButtonType.ts b/src/@optimizely/forms-sdk/src/models/enums/SubmitButtonType.ts similarity index 100% rename from src/@optimizely/forms-react/src/models/SubmitButtonType.ts rename to src/@optimizely/forms-sdk/src/models/enums/SubmitButtonType.ts diff --git a/src/@optimizely/forms-sdk/src/models/enums/index.ts b/src/@optimizely/forms-sdk/src/models/enums/index.ts index 2f723a6..dfffd49 100644 --- a/src/@optimizely/forms-sdk/src/models/enums/index.ts +++ b/src/@optimizely/forms-sdk/src/models/enums/index.ts @@ -1,4 +1,5 @@ export * from "./ValidatorType"; export * from "./ConditionCombinationType"; export * from "./SatisfiedActionType"; -export * from "./ConditionFunctionType" \ No newline at end of file +export * from "./ConditionFunctionType"; +export * from "./SubmitButtonType"; \ No newline at end of file diff --git a/src/@optimizely/forms-sdk/src/models/states/FormDependencies.ts b/src/@optimizely/forms-sdk/src/models/states/FormDependencies.ts deleted file mode 100644 index fbab3e7..0000000 --- a/src/@optimizely/forms-sdk/src/models/states/FormDependencies.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface FormDependencies { - elementKey: string - isSatisfied: boolean -} \ No newline at end of file diff --git a/src/@optimizely/forms-sdk/src/models/states/FormState.ts b/src/@optimizely/forms-sdk/src/models/states/FormState.ts index 5b07462..b498be2 100644 --- a/src/@optimizely/forms-sdk/src/models/states/FormState.ts +++ b/src/@optimizely/forms-sdk/src/models/states/FormState.ts @@ -1,5 +1,4 @@ import { FormContainer } from "../FormContainer" -import { FormDependencies } from "./FormDependencies" import { FormSubmission } from "./FormSubmission" import { FormValidation } from "./FormValidation" import { StepDependencies } from "./StepDependencies" @@ -8,7 +7,7 @@ export interface FormState { isReset: boolean formSubmissions: FormSubmission[] formValidations: FormValidation[] - formDependencies: FormDependencies[] stepDependencies: StepDependencies[] formContainer: FormContainer + dependencyInactiveElements: string[] } \ No newline at end of file diff --git a/src/@optimizely/forms-sdk/src/models/states/StepDependencies.ts b/src/@optimizely/forms-sdk/src/models/states/StepDependencies.ts index 6eaf39d..8ee2061 100644 --- a/src/@optimizely/forms-sdk/src/models/states/StepDependencies.ts +++ b/src/@optimizely/forms-sdk/src/models/states/StepDependencies.ts @@ -1,4 +1,4 @@ -import { FormDependencies } from "./FormDependencies" - -export interface StepDependencies extends FormDependencies{ +export interface StepDependencies{ + elementKey: string + isSatisfied: boolean } \ No newline at end of file diff --git a/src/@optimizely/forms-sdk/src/models/states/index.ts b/src/@optimizely/forms-sdk/src/models/states/index.ts index fec6775..325f493 100644 --- a/src/@optimizely/forms-sdk/src/models/states/index.ts +++ b/src/@optimizely/forms-sdk/src/models/states/index.ts @@ -1,5 +1,4 @@ export * from "./FormState"; -export * from "./FormDependencies"; export * from "./FormSubmission"; export * from "./FormValidation"; export * from "./StepDependencies"; \ No newline at end of file