accounts-frontend/packages/app/components/ui/form/Form.tsx

198 lines
5.1 KiB
TypeScript
Raw Normal View History

2019-12-07 16:58:52 +05:30
import React from 'react';
2019-12-08 01:13:08 +05:30
import clsx from 'clsx';
import logger from 'app/services/logger';
2019-12-07 16:58:52 +05:30
import FormModel from './FormModel';
import styles from './form.scss';
interface BaseProps {
2020-05-24 04:38:24 +05:30
id: string;
isLoading: boolean;
onInvalid: (errors: Record<string, string>) => void;
children: React.ReactNode;
2019-12-07 16:58:52 +05:30
}
interface PropsWithoutForm extends BaseProps {
onSubmit?: (form: FormData) => Promise<void> | void;
}
interface PropsWithForm extends BaseProps {
2020-05-24 04:38:24 +05:30
form: FormModel;
onSubmit?: (form: FormModel) => Promise<void> | void;
}
type Props = PropsWithoutForm | PropsWithForm;
function hasForm(props: Props): props is PropsWithForm {
2020-05-24 04:38:24 +05:30
return 'form' in props;
}
2019-12-07 16:58:52 +05:30
interface State {
2020-05-24 04:38:24 +05:30
id: string; // just to track value for derived updates
isTouched: boolean;
isLoading: boolean;
2019-12-07 16:58:52 +05:30
}
type InputElement = HTMLInputElement | HTMLTextAreaElement;
2019-12-07 16:58:52 +05:30
export default class Form extends React.Component<Props, State> {
2020-05-24 04:38:24 +05:30
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;
2016-05-28 01:34:17 +05:30
}
2020-05-24 04:38:24 +05:30
static getDerivedStateFromProps(props: Props, state: State) {
const patch: Partial<State> = {};
2016-05-28 01:34:17 +05:30
2020-05-24 04:38:24 +05:30
if (typeof props.isLoading !== 'undefined' && props.isLoading !== state.isLoading) {
patch.isLoading = props.isLoading;
}
2016-05-28 01:34:17 +05:30
2020-05-24 04:38:24 +05:30
if (props.id !== state.id) {
patch.id = props.id;
patch.isTouched = true;
}
2016-05-28 01:34:17 +05:30
2020-05-24 04:38:24 +05:30
return patch;
2019-12-10 13:17:32 +05:30
}
2018-05-08 00:53:26 +05:30
2020-05-24 04:38:24 +05:30
componentDidUpdate(prevProps: Props) {
const nextForm = hasForm(this.props) ? this.props.form : undefined;
const prevForm = hasForm(prevProps) ? prevProps.form : undefined;
2019-12-10 13:17:32 +05:30
2020-05-24 04:38:24 +05:30
if (nextForm !== prevForm) {
if (prevForm) {
prevForm.removeLoadingListener(this.onLoading);
}
2019-12-10 13:17:32 +05:30
2020-05-24 04:38:24 +05:30
if (nextForm) {
nextForm.addLoadingListener(this.onLoading);
}
}
2016-05-02 12:45:42 +05:30
}
2020-05-24 04:38:24 +05:30
componentWillUnmount() {
if (hasForm(this.props)) {
this.props.form.removeLoadingListener(this.onLoading);
}
this.mounted = false;
2016-05-02 12:45:42 +05:30
}
2020-05-24 04:38:24 +05:30
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>
);
}
2016-05-02 12:45:42 +05:30
2020-05-24 04:38:24 +05:30
submit() {
if (!this.state.isTouched) {
this.setState({
isTouched: true,
});
}
2020-05-24 04:38:24 +05:30
const form = this.formEl;
2020-05-24 04:38:24 +05:30
if (!form) {
return;
}
2020-05-24 04:38:24 +05:30
if (form.checkValidity()) {
let result: Promise<void> | void;
2020-05-24 04:38:24 +05:30
if (hasForm(this.props)) {
// @ts-ignore this prop has default value
2020-05-24 04:38:24 +05:30
result = this.props.onSubmit(this.props.form);
} else {
// @ts-ignore this prop has default value
2020-05-24 04:38:24 +05:30
result = this.props.onSubmit(new FormData(form));
}
2017-06-13 01:02:59 +05:30
2020-05-24 04:38:24 +05:30
if (result && result.then) {
this.setState({ isLoading: true });
2020-05-24 04:38:24 +05:30
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
2017-09-09 19:52:19 +05:30
2020-05-24 04:38:24 +05:30
Array.from(invalidEls).reduce((acc, el) => {
if (!el.name) {
logger.warn('Found an element without name', { el });
2020-05-24 04:38:24 +05:30
return acc;
}
2016-05-02 12:45:42 +05:30
2020-05-24 04:38:24 +05:30
let errorMessage = el.validationMessage;
2016-05-02 12:45:42 +05:30
2020-05-24 04:38:24 +05:30
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);
2020-05-24 04:38:24 +05:30
this.setErrors(errors);
}
}
2020-05-24 04:38:24 +05:30
setErrors(errors: { [key: string]: string }) {
if (hasForm(this.props)) {
this.props.form.setErrors(errors);
}
this.props.onInvalid(errors);
}
2020-05-24 04:38:24 +05:30
onFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
2020-05-24 04:38:24 +05:30
this.submit();
};
2016-05-28 01:34:17 +05:30
2020-05-24 04:38:24 +05:30
onLoading = (isLoading: boolean) => this.setState({ isLoading });
2016-05-02 12:45:42 +05:30
}