import React , { FormEvent , ComponentType , Component } from 'react' ; import { serializeForm , submitForm , instanceOfValueFormField , getFieldValueFromModel , SitecoreForm , FormField , FormTracker , FormFetcher , TrackerFetcher , } from '@sitecore-jss/sitecore-jss-forms' ; import FieldFactory from '../field-factory' ; import { FieldWithValueProps , LabelProps } from '../FieldProps' ; import DefaultFieldFactory from '../default-field-factory' ; import { DefaultError } from './default-error' ; export interface ErrorComponentProps { form : SitecoreForm ; formErrors : string [ ] ; fieldErrors : Array < { fieldName : string ; state : FieldState ; } > ; } export interface FormProps { form : SitecoreForm ; className ? : string ; fieldFactory ? : FieldFactory ; sitecoreApiHost : string ; sitecoreApiKey : string ; onRedirect ? : ( url : string ) => void ; errorComponent ? : ComponentType < ErrorComponentProps > ; fieldWrapperComponent ? : ComponentType < FieldWithValueProps > ; /** Optionally override the label component for any field components that render a label */ labelComponent ? : ComponentType < LabelProps > ; /** Optionally override the field validation errors display component for any field components that render validation errors */ fieldValidationErrorsComponent ? : ComponentType < LabelProps > ; /** Fetch function used when submitting the form (defaults to using `fetch`) */ formFetcher ? : FormFetcher ; /** Fetch function used when posting form field tracking data (defaults to using `fetch`) */ trackerFetcher ? : TrackerFetcher ; } export interface FieldState { value ? : string | string [ ] | File [ ] | boolean ; isValid : boolean ; errors : string [ ] ; } export interface FormState { errors : string [ ] ; nextForm : SitecoreForm | null ; submitButton : string | null ; } export interface FieldStateCollection { [ key ] : FieldState ; } export class Form extends Component < FormProps , FormState & FieldStateCollection > { private _tracker : FormTracker ; constructor ( props ) { super ( props ) ; this . state = ( { errors : [ ] , nextForm : null , submitButton : null , } as unknown ) as FieldStateCollection & FormState ; this . createFieldComponent = this . createFieldComponent . bind ( this ) ; this . getCurrentFieldState = this . getCurrentFieldState . bind ( this ) ; this . collectCurrentFieldValues = this . collectCurrentFieldValues . bind ( this ) ; this . _tracker = new FormTracker ( { endpoint : `${ this . props . sitecoreApiHost }/api/jss/fieldtracking/register?sc_apikey=${ this . props . sitecoreApiKey }` , fetcher : this . props . trackerFetcher , } ) ; } render ( ) { const form = this . state . nextForm || this . props . form ; if ( ! form ) { return < div > No form data was provided . Need to set a datasource ? < /div>; } if ( ! form . metadata ) { return < div > Form data invalid . Forget to set the rendering contents resolver ? < /div>; } const action = `${ this . props . sitecoreApiHost }/api/jss/formbuilder?fxb.FormItemId=${ form . metadata . itemId }&fxb.HtmlPrefix=${ form . htmlPrefix }&sc_apikey=${ this . props . sitecoreApiKey }&sc_itemid=${ form . contextItemId }` ; this . _tracker . setFormData ( form . formItemId . value , form . formSessionId . value , form . metadata . isTrackingEnabled ) ; const fieldComponents = form . fields . map ( this . createFieldComponent ) ; const ErrorComponent = this . props . errorComponent || DefaultError ; const fieldErrors = this . collectCurrentFieldValues ( ) . filter ( ( field ) => ! field . state . isValid ) ; return ( < form className = { this . props . className } action = { action } method = "POST" onSubmit = { this . onSubmit . bind ( this ) } > < ErrorComponent form = { form } formErrors = { this . state . errors } fieldErrors = { fieldErrors } / > { fieldComponents } < /form>); } param { FormField } field returns { React . ReactNode } createFieldComponent ( field ) { const props = { field , key : field . model . itemId , onChange : this . onFieldChange . bind ( this ) , onButtonClick : this . onButtonClick . bind ( this ) , fieldFactory : this . createFieldComponent , fieldValidationErrorsComponent : this . props . fieldValidationErrorsComponent , labelComponent : this . props . labelComponent , tracker : this . _tracker , ... this . getCurrentFieldState ( field ) , } as FieldWithValueProps ; const component = ( this . props . fieldFactory || DefaultFieldFactory ) . get ( field , props ) ; if ( this . props . fieldWrapperComponent ) { const Wrapper = this . props . fieldWrapperComponent ; return < Wrapper { ... props } > { component } < /Wrapper>; } return component ; } param { FormField } field returns { Object | null } getCurrentFieldState ( field ) { if ( ! instanceOfValueFormField ( field ) ) { return null ; } const fieldName = field . valueField . name || null ; if ( ! fieldName ) { return null ; } const fieldState = this . state [ fieldName ] ; if ( fieldState ) { const result = { isValid : fieldState . isValid , errors : fieldState . errors || [ ] , } ; if ( typeof fieldState . value !== 'undefined' ) { result . value = fieldState . value ; } else { result . value = getFieldValueFromModel ( field ) ; } return result ; } return { isValid : true , errors : [ ] , value : getFieldValueFromModel ( field ) , } ; } param { string } buttonName onButtonClick ( buttonName ) { this . setState ( { submitButton : buttonName } ) ; } param { string } key param { string | string [ ] | File [ ] } value param { boolean } isValid param { string [ ] } errors onFieldChange ( key , value , isValid , errors ) { this . setState ( { [ key ] : { value , isValid , errors } , } ) ; } param { FormEvent } e onSubmit ( e ) { e . preventDefault ( ) ; const form = this . state . nextForm || this . props . form ; const fieldValues = { } ; const currentFieldValues = this . collectCurrentFieldValues ( ) ; currentFieldValues . forEach ( ( field ) => { if ( typeof field . state . value !== 'undefined' ) { fieldValues [ field . fieldName ] = field . state . value ; } } ) ; const formData = serializeForm ( form , { submitButtonName : this . state . submitButton } ) ; formData . mergeOverwritingExisting ( fieldValues ) ; const submitUrl = ( e . target as HTMLFormElement ) . action ; if ( ! submitUrl ) { throw new Error ( 'Submit URL was not defined. Ensure the form has an action attribute.' ) ; } submitForm ( formData , submitUrl , { fetcher : this . props . formFetcher } ) . then ( ( result ) => { if ( result . success && result . redirectUrl ) { if ( this . props . onRedirect ) { this . props . onRedirect ( result . redirectUrl ) ; } else { window . location . href = result . redirectUrl ; } } if ( result . validationErrors ) { const stateUpdate = { } ; Object . keys ( result . validationErrors ) . forEach ( ( fieldKey ) => { stateUpdate [ fieldKey ] = { value : ( this . state [ fieldKey ] || { } ) . value , isValid : false , errors : result . validationErrors [ fieldKey ] , } ; } ) ; this . setState ( stateUpdate ) ; } if ( result . nextForm ) { this . setState ( { nextForm : result . nextForm } ) ; } if ( result . success ) { this . resetFieldsState ( ) ; } if ( result . errors && result . errors . length > 0 ) { throw result . errors ; } this . setState ( { errors : [ ] } ) ; } ) . catch ( ( error ) => { if ( Array . isArray ( error ) ) { this . setState ( { errors : error } ) ; } else if ( typeof error === 'string' ) { console . log ( 'Form submit error' , error ) ; this . setState ( { errors : [ error ] } ) ; } else { console . log ( 'Form submit error' , error ) ; this . setState ( { errors : [ error . message ] } ) ; } } ) ; } collectCurrentFieldValues ( ) { return Object . keys ( this . state ) . filter ( ( fieldName ) => this . state [ fieldName ] && typeof this . state [ fieldName ] . isValid !== 'undefined' ) . map ( ( fieldName ) => ( { fieldName : fieldName , state : this . state [ fieldName ] as FieldState } ) ) ; } /**
     * Removes the current fields' mutated state from this.state,
     * which prevents validation issues and mutable field state from following us
     * across steps in a multistep form.
     */ resetFieldsState ( ) { const keys = Object . keys ( this . state ) . filter ( ( key ) => key !== 'nextForm' && key !== 'errors' && key !== 'submitButton' ) ; const stateReset = keys . reduce ( ( acc , v ) => ( { ... acc , [ v ] : undefined } ) , { } ) ; this . setState ( { ... stateReset , errors : [ ] } ) ; } }