Skip to content

Commit

Permalink
fix: refactor forms and validations
Browse files Browse the repository at this point in the history
  • Loading branch information
Jozwiaczek committed Nov 9, 2020
1 parent 53864b5 commit 74835ec
Show file tree
Hide file tree
Showing 11 changed files with 431 additions and 455 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styled from 'styled-components';
import { Typography } from '@material-ui/core';
import { Typography, TypographyProps } from '@material-ui/core';

export const FormActionsContainer = styled.div`
display: flex;
Expand All @@ -21,12 +21,12 @@ export const FormLink = styled(Typography)`
cursor: pointer;
`;

export const FormSectionSubtitle = styled.div`
export const FormSubheaderContent = styled.div`
display: flex;
`;

export const FormSectionTitle = styled(Typography)`
font-weight: 900;
export const FormSubheaderTitle = styled(Typography)<TypographyProps>`
font-weight: ${({ variant }) => variant === 'h5' && 900};
`;

export const SubHeader = styled.div`
Expand Down
184 changes: 184 additions & 0 deletions src/components/Form/Form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React, {
Children,
cloneElement,
FC,
isValidElement,
useCallback,
useEffect,
useState,
} from 'react';
import { PropTypes } from '@material-ui/core';
import { Form as FinalForm } from 'react-final-form';
import { ValidationErrors } from 'final-form';
import clsx from 'clsx';
import { Variant } from '@material-ui/core/styles/createTypography';
import { FormButton } from '../FormButton';
import {
FormActionsContainer,
FormInputsContainer,
FormSubheaderContent,
FormSubheaderTitle,
SubHeader,
} from './Form.styled';
import { Header } from '../Header';
import { defaultAuthValidation, DefaultAuthValidationOptions } from '../../utils/validations';
import { isObjectEmpty } from '../../utils/isObjectEmpty';
import { CognitoError } from '../../common/interfaces/CognitoError';
import { useSnackbar } from '../SnackbarProvider';
import { useSafeSetState } from '../../hooks/useSafeSetState';

export interface FormProps {
onSubmit: (values: any) => Promise<string>;
onSubmitResult?: (values: any) => Promise<void>;
title?: string;
titleAlign?: PropTypes.Alignment;
autoFocus?: boolean;
onSuccessLoginMsg?: string;
initialValues?: any;
inputVariant?: 'filled' | 'outlined' | 'standard';
defaultValidationFields: DefaultAuthValidationOptions;
validate?: (
values: any,
requiredChildren: Array<string>,
) => ValidationErrors | Promise<ValidationErrors>;
replacementValidate?: (values: any) => ValidationErrors | Promise<ValidationErrors>;
loading?: boolean;
className?: string;
confirmButtonLabel: string;
subheaderTitle?: string;
subheaderTitleVariant?: Variant;
subheaderContent?: string | JSX.Element;
formActions?: JSX.Element;
}

export interface FormWithDefaultsProps extends FormProps {
codeLabel?: string;
emailLabel?: string;
passwordLabel?: string;
passwordPreview?: boolean;
passwordRepeat?: boolean;
}

export const Form: FC<FormProps> = ({
onSubmit,
title,
titleAlign = 'center',
initialValues = {},
inputVariant = 'outlined',
defaultValidationFields,
validate,
replacementValidate,
loading = false,
className,
children,
confirmButtonLabel,
subheaderTitle,
subheaderTitleVariant = 'h5',
subheaderContent,
formActions,
}) => {
const [internalLoading, setInternalLoading] = useSafeSetState<boolean>(loading);
const [requiredChildren, setRequiredChildren] = useState<Array<string>>([]);
const showSnackbar = useSnackbar();

useEffect(() => {
setInternalLoading(loading);
}, [loading]);

const handleOnSubmit = useCallback((values) => {
setInternalLoading(true);
return onSubmit(values)
.then(async (message) => {
setInternalLoading(false);
showSnackbar({ message, severity: 'success' });
})
.catch(({ message }: CognitoError) => {
setInternalLoading(false);
showSnackbar({ message, severity: 'error' });
});
}, []);

const defaultValidate = useCallback(
(values: any) => {
const errors: any = defaultAuthValidation(
values,
requiredChildren,
false,
defaultValidationFields,
);
return validate ? { ...errors, ...validate(values, requiredChildren) } : errors;
},
[requiredChildren, validate],
);

return (
<div className={clsx(className, 'form')}>
{title && (
<Header className="form__header" align={titleAlign}>
{title}
</Header>
)}
{(subheaderTitle || subheaderContent) && (
<SubHeader className="form__subheader">
{subheaderTitle && (
<FormSubheaderTitle
className="form__subheader__title"
variant={subheaderTitleVariant}
gutterBottom
>
{subheaderTitle}
</FormSubheaderTitle>
)}
{subheaderContent && (
<FormSubheaderContent className="form__subheader__content">
{subheaderContent}
</FormSubheaderContent>
)}
</SubHeader>
)}
<FinalForm
onSubmit={handleOnSubmit}
initialValues={initialValues}
validate={replacementValidate || defaultValidate}
render={({ handleSubmit, pristine, errors }) => (
<form onSubmit={handleSubmit} className="form__container">
<FormInputsContainer className="form__container__inputs">
{Children.map(children, (child) => {
if (isValidElement(child)) {
const { props } = child;
if (props.required) {
setRequiredChildren((prevState) => {
prevState.push(props.id || props.source);
return prevState;
});
}
return cloneElement(
child,
{
disabled: internalLoading,
variant: inputVariant,
},
null,
);
}
return child;
})}
</FormInputsContainer>
<FormActionsContainer className="form__container__actions">
{formActions && formActions}
<FormButton
type="submit"
className="form__action__btn--submit"
loading={internalLoading}
disabled={internalLoading || pristine || !isObjectEmpty(errors)}
fullWidth
>
{confirmButtonLabel}
</FormButton>
</FormActionsContainer>
</form>
)}
/>
</div>
);
};
2 changes: 1 addition & 1 deletion src/components/SnackbarProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const SnackbarContextProvider: FC = ({ children }) => {
message,
severity = 'info',
position = initPosition,
duration = getDisplayDuration(messageInternal),
duration = getDisplayDuration(message),
}: ShowSnackbarProps) => {
setMessageInternal(message);
setSeverityInternal(severity);
Expand Down
46 changes: 46 additions & 0 deletions src/containers/ConfirmSignUp/ConfirmSignUp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { FC, useCallback } from 'react';
import { Auth } from 'aws-amplify';
import clsx from 'clsx';
import { FormInput } from '../../components/FormInput/FormInput';
import { Form, FormWithDefaultsProps } from '../../components/Form/Form';

export const ConfirmSignUp: FC<FormWithDefaultsProps> = ({
onSubmitResult,
title = 'Confirm Sign Up',
subheaderTitle = 'Please check your email for the one-time code to confirm your account.',
subheaderTitleVariant = 'body1',
confirmButtonLabel = 'Confirm sign up',
onSuccessLoginMsg = 'Sign up confirmed.',
codeLabel = 'Code',
emailLabel = 'Email',
autoFocus = false,
children,
className,
...rest
}) => {
const confirmSignUp = useCallback(
async (values: any) =>
Auth.confirmSignUp(values.email, values.code).then(async (result) => {
onSubmitResult && (await onSubmitResult(result));
return onSuccessLoginMsg;
}),
[],
);

return (
<Form
{...rest}
title={title}
subheaderTitle={subheaderTitle}
subheaderTitleVariant={subheaderTitleVariant}
onSubmit={confirmSignUp}
defaultValidationFields={['email', 'code']}
className={clsx(className, 'confirmSignUp')}
confirmButtonLabel={confirmButtonLabel}
>
<FormInput autoFocus={autoFocus} source="email" required label={emailLabel} />
<FormInput source="code" required label={codeLabel} />
{children}
</Form>
);
};

0 comments on commit 74835ec

Please sign in to comment.