Skip to content
Snippets Groups Projects
Unverified Commit 2fcb899c authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Improve forms API (#26181)

parent 65a7b1e3
No related branches found
No related tags found
No related merge requests found
Showing
with 110 additions and 44 deletions
import React from "react";
import { t } from "ttag";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import useForm from "metabase/core/hooks/use-form";
import Form from "metabase/core/components/Form";
import FormProvider from "metabase/core/components/FormProvider";
import FormCheckBox from "metabase/core/components/FormCheckBox";
import FormErrorMessage from "metabase/core/components/FormErrorMessage";
import FormInput from "metabase/core/components/FormInput";
......@@ -37,14 +37,12 @@ const LoginForm = ({
password: "",
remember: !hasSessionCookies,
};
const handleSubmit = useForm(onSubmit);
return (
<Formik
<FormProvider
initialValues={initialValues}
validationSchema={isLdapEnabled ? LDAP_SCHEMA : PASSWORD_SCHEMA}
isInitialValid={false}
onSubmit={handleSubmit}
onSubmit={onSubmit}
>
<Form>
<FormInput
......@@ -67,10 +65,10 @@ const LoginForm = ({
{!hasSessionCookies && (
<FormCheckBox name="remember" title={t`Remember me`} />
)}
<FormSubmitButton title={t`Sign in`} fullWidth />
<FormSubmitButton title={t`Sign in`} primary fullWidth />
<FormErrorMessage />
</Form>
</Formik>
</FormProvider>
);
};
......
import React, {
FormHTMLAttributes,
forwardRef,
Ref,
SyntheticEvent,
} from "react";
import { useFormikContext } from "formik";
export interface FormProps
extends Omit<FormHTMLAttributes<HTMLFormElement>, "onSubmit" | "onReset"> {
disabled?: boolean;
}
const Form = forwardRef(function Form(
{ disabled, ...props }: FormProps,
ref: Ref<HTMLFormElement>,
) {
const { handleSubmit, handleReset } = useFormikContext();
return (
<form
{...props}
ref={ref}
onSubmit={!disabled ? handleSubmit : handleDisabledEvent}
onReset={!disabled ? handleReset : handleDisabledEvent}
/>
);
});
const handleDisabledEvent = (event: SyntheticEvent) => {
event.preventDefault();
event.stopPropagation();
};
export default Form;
export { default } from "./Form";
export type { FormProps } from "./Form";
import React from "react";
import { Formik } from "formik";
import type { FormikConfig } from "formik";
import useFormSubmit from "metabase/core/hooks/use-form-submit";
function FormProvider<T>({ onSubmit, ...props }: FormikConfig<T>): JSX.Element {
const handleSubmit = useFormSubmit(onSubmit);
return <Formik {...props} onSubmit={handleSubmit} />;
}
export default FormProvider;
export { default } from "./FormProvider";
import React, { forwardRef, Ref } from "react";
import { useFormikContext } from "formik";
import { t } from "ttag";
import Button, { ButtonProps } from "metabase/core/components/Button";
import { FormStatus } from "metabase/core/hooks/use-form-state";
import useFormStatus from "metabase/core/hooks/use-form-status";
import useFormSubmitButton from "metabase/core/hooks/use-form-submit-button";
export interface FormSubmitButtonProps extends Omit<ButtonProps, "children"> {
title?: string;
......@@ -13,30 +12,28 @@ export interface FormSubmitButtonProps extends Omit<ButtonProps, "children"> {
}
const FormSubmitButton = forwardRef(function FormSubmitButton(
{ disabled, ...props }: FormSubmitButtonProps,
{ primary, disabled, ...props }: FormSubmitButtonProps,
ref: Ref<HTMLButtonElement>,
) {
const { isValid, isSubmitting } = useFormikContext();
const status = useFormStatus();
const submitText = getSubmitButtonText(status, props);
const isEnabled = isValid && !isSubmitting && !disabled;
const { status, isDisabled } = useFormSubmitButton({ isDisabled: disabled });
const submitTitle = getSubmitButtonTitle(status, props);
return (
<Button
{...props}
ref={ref}
type="submit"
primary={isEnabled}
primary={primary && !isDisabled}
success={status === "fulfilled"}
danger={status === "rejected"}
disabled={!isEnabled}
disabled={isDisabled}
>
{submitText}
{submitTitle}
</Button>
);
});
const getSubmitButtonText = (
const getSubmitButtonTitle = (
status: FormStatus | undefined,
{
title = t`Submit`,
......
import { useLayoutEffect, useState } from "react";
import { useFormikContext } from "formik";
import type { FormikErrors } from "formik";
import { t } from "ttag";
import useFormState from "metabase/core/hooks/use-form-state";
......@@ -8,6 +7,7 @@ const useFormErrorMessage = (): string | undefined => {
const { values, errors } = useFormikContext();
const { status, message } = useFormState();
const [isVisible, setIsVisible] = useState(false);
const hasErrors = Object.keys(errors).length > 0;
useLayoutEffect(() => {
setIsVisible(false);
......@@ -17,13 +17,9 @@ const useFormErrorMessage = (): string | undefined => {
setIsVisible(status === "rejected");
}, [status]);
return isVisible ? getFormErrorMessage(errors, message) : undefined;
};
const getFormErrorMessage = <T>(errors: FormikErrors<T>, message?: string) => {
const hasErrors = Object.keys(errors).length > 0;
if (message) {
if (!isVisible) {
return undefined;
} else if (message) {
return message;
} else if (!hasErrors) {
return t`An error occurred`;
......
export type FormStatus = "pending" | "fulfilled" | "rejected";
export type FormStatus = "idle" | "pending" | "fulfilled" | "rejected";
export interface FormState {
status?: FormStatus;
status: FormStatus;
message?: string;
}
import { useFormikContext } from "formik";
import { FormState } from "./types";
const DEFAULT_STATE: FormState = {
status: "idle",
};
const useFormState = (): FormState => {
const { status } = useFormikContext();
return status ?? {};
return status ?? DEFAULT_STATE;
};
export default useFormState;
export { default } from "./use-form-status";
export { default } from "./use-form-submit-button";
import { useEffect, useLayoutEffect, useState } from "react";
import { useFormikContext } from "formik";
import useFormState, { FormStatus } from "metabase/core/hooks/use-form-state";
const STATUS_TIMEOUT = 5000;
const useFormStatus = (): FormStatus | undefined => {
export interface UseFormSubmitButtonProps {
isDisabled?: boolean;
}
export interface UseFormSubmitButtonResult {
status: FormStatus;
isDisabled: boolean;
}
const useFormSubmitButton = ({
isDisabled = false,
}: UseFormSubmitButtonProps): UseFormSubmitButtonResult => {
const { isValid, isSubmitting } = useFormikContext();
const { status } = useFormState();
const isRecent = useIsRecent(status, STATUS_TIMEOUT);
switch (status) {
case "pending":
return status;
case "fulfilled":
case "rejected":
return isRecent ? status : undefined;
}
return {
status: getFormStatus(status, isRecent),
isDisabled: !isValid || isSubmitting || isDisabled,
};
};
const useIsRecent = (value: unknown, timeout: number) => {
......@@ -31,4 +41,14 @@ const useIsRecent = (value: unknown, timeout: number) => {
return isRecent;
};
export default useFormStatus;
const getFormStatus = (status: FormStatus, isRecent: boolean): FormStatus => {
switch (status) {
case "fulfilled":
case "rejected":
return isRecent ? status : "idle";
default:
return status;
}
};
export default useFormSubmitButton;
export { default } from "./use-form-submit";
......@@ -2,12 +2,14 @@ import { useCallback } from "react";
import type { FormikHelpers } from "formik";
import { FormError } from "./types";
const useForm = <T>(onSubmit: (data: T) => void) => {
const useFormSubmit = <T>(
onSubmit: (data: T, helpers: FormikHelpers<T>) => void,
) => {
return useCallback(
async (data: T, helpers: FormikHelpers<T>) => {
try {
helpers.setStatus({ status: "pending" });
await onSubmit(data);
await onSubmit(data, helpers);
helpers.setStatus({ status: "fulfilled" });
} catch (error) {
helpers.setErrors(getFormErrors(error));
......@@ -33,4 +35,4 @@ const getFormMessage = (error: unknown) => {
return isFormError(error) ? error.data?.message ?? error.message : undefined;
};
export default useForm;
export default useFormSubmit;
export { default } from "./use-form";
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment