mirror of
https://github.com/elyby/accounts-frontend.git
synced 2024-12-25 06:30:00 +05:30
51fb3afe76
* Clear form errors before submitting * Move error clearing logic to FormModel * Revert setError signature to the previous state * Expose clearErrors proxy method on Form like setErrors
201 lines
5.3 KiB
TypeScript
201 lines
5.3 KiB
TypeScript
import React from 'react';
|
|
import clsx from 'clsx';
|
|
|
|
import logger from 'app/services/logger';
|
|
|
|
import FormModel from './FormModel';
|
|
import styles from './form.scss';
|
|
|
|
interface BaseProps {
|
|
id: string;
|
|
isLoading: boolean;
|
|
onInvalid: (errors: Record<string, string>) => void;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
interface PropsWithoutForm extends BaseProps {
|
|
onSubmit?: (form: FormData) => Promise<void> | void;
|
|
}
|
|
|
|
interface PropsWithForm extends BaseProps {
|
|
form: FormModel;
|
|
onSubmit?: (form: FormModel) => Promise<void> | void;
|
|
}
|
|
|
|
type Props = PropsWithoutForm | PropsWithForm;
|
|
|
|
function hasForm(props: Props): props is PropsWithForm {
|
|
return 'form' in props;
|
|
}
|
|
|
|
interface State {
|
|
id: string; // just to track value for derived updates
|
|
isTouched: boolean;
|
|
isLoading: boolean;
|
|
}
|
|
type InputElement = HTMLInputElement | HTMLTextAreaElement;
|
|
|
|
export default class Form extends React.Component<Props, State> {
|
|
static defaultProps = {
|
|
id: 'default',
|
|
isLoading: false,
|
|
onSubmit() {},
|
|
onInvalid() {},
|
|
};
|
|
|
|
state: State = {
|
|
id: this.props.id,
|
|
isTouched: false,
|
|
isLoading: this.props.isLoading || false,
|
|
};
|
|
|
|
formEl: HTMLFormElement | null;
|
|
|
|
mounted = false;
|
|
|
|
componentDidMount() {
|
|
if (hasForm(this.props)) {
|
|
this.props.form.addLoadingListener(this.onLoading);
|
|
}
|
|
|
|
this.mounted = true;
|
|
}
|
|
|
|
static getDerivedStateFromProps(props: Props, state: State) {
|
|
const patch: Partial<State> = {};
|
|
|
|
if (typeof props.isLoading !== 'undefined' && props.isLoading !== state.isLoading) {
|
|
patch.isLoading = props.isLoading;
|
|
}
|
|
|
|
if (props.id !== state.id) {
|
|
patch.id = props.id;
|
|
patch.isTouched = true;
|
|
}
|
|
|
|
return patch;
|
|
}
|
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
const nextForm = hasForm(this.props) ? this.props.form : undefined;
|
|
const prevForm = hasForm(prevProps) ? prevProps.form : undefined;
|
|
|
|
if (nextForm !== prevForm) {
|
|
if (prevForm) {
|
|
prevForm.removeLoadingListener(this.onLoading);
|
|
}
|
|
|
|
if (nextForm) {
|
|
nextForm.addLoadingListener(this.onLoading);
|
|
}
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (hasForm(this.props)) {
|
|
this.props.form.removeLoadingListener(this.onLoading);
|
|
}
|
|
|
|
this.mounted = false;
|
|
}
|
|
|
|
render() {
|
|
const { isLoading } = this.state;
|
|
|
|
return (
|
|
<form
|
|
className={clsx(styles.form, {
|
|
[styles.isFormLoading]: isLoading,
|
|
[styles.formTouched]: this.state.isTouched,
|
|
})}
|
|
onSubmit={this.onFormSubmit}
|
|
ref={(el: HTMLFormElement | null) => (this.formEl = el)}
|
|
noValidate
|
|
>
|
|
{this.props.children}
|
|
</form>
|
|
);
|
|
}
|
|
|
|
submit() {
|
|
if (!this.state.isTouched) {
|
|
this.setState({
|
|
isTouched: true,
|
|
});
|
|
}
|
|
|
|
const form = this.formEl;
|
|
|
|
if (!form) {
|
|
return;
|
|
}
|
|
|
|
if (form.checkValidity()) {
|
|
this.clearErrors();
|
|
let result: Promise<void> | void;
|
|
|
|
if (hasForm(this.props)) {
|
|
// @ts-ignore this prop has default value
|
|
result = this.props.onSubmit(this.props.form);
|
|
} else {
|
|
// @ts-ignore this prop has default value
|
|
result = this.props.onSubmit(new FormData(form));
|
|
}
|
|
|
|
if (result && result.then) {
|
|
this.setState({ isLoading: true });
|
|
|
|
result
|
|
.catch((errors: Record<string, string>) => {
|
|
this.setErrors(errors);
|
|
})
|
|
.finally(() => this.mounted && this.setState({ isLoading: false }));
|
|
}
|
|
} else {
|
|
const invalidEls: NodeListOf<InputElement> = form.querySelectorAll(':invalid');
|
|
const errors: Record<string, string> = {};
|
|
invalidEls[0].focus(); // focus on first error
|
|
|
|
Array.from(invalidEls).reduce((acc, el) => {
|
|
if (!el.name) {
|
|
logger.warn('Found an element without name', { el });
|
|
|
|
return acc;
|
|
}
|
|
|
|
let errorMessage = el.validationMessage;
|
|
|
|
if (el.validity.valueMissing) {
|
|
errorMessage = `error.${el.name}_required`;
|
|
} else if (el.validity.typeMismatch) {
|
|
errorMessage = `error.${el.name}_invalid`;
|
|
}
|
|
|
|
acc[el.name] = errorMessage;
|
|
|
|
return acc;
|
|
}, errors);
|
|
|
|
this.setErrors(errors);
|
|
}
|
|
}
|
|
|
|
setErrors(errors: { [key: string]: string }) {
|
|
if (hasForm(this.props)) {
|
|
this.props.form.setErrors(errors);
|
|
}
|
|
|
|
this.props.onInvalid(errors);
|
|
}
|
|
|
|
clearErrors = () => hasForm(this.props) && this.props.form.clearErrors();
|
|
|
|
onFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
|
|
this.submit();
|
|
};
|
|
|
|
onLoading = (isLoading: boolean) => this.setState({ isLoading });
|
|
}
|