Open a0m0rajab opened 3 months ago
"use client";
import { Button } from "@/components/ui/button";
import { Form, FormMessage } from "@/components/ui/form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useHookFormActionErrorMapper } from "@next-safe-action/adapter-react-hook-form/hooks";
import { Icons, cn } from "@watt/common";
import type {
BindArgsValidationErrors,
ValidationErrors
} from "next-safe-action";
import type { Infer, Schema } from "next-safe-action/adapters/types";
import type {
HookBaseUtils,
HookCallbacks,
HookSafeActionFn,
UseActionHookReturn
} from "next-safe-action/hooks";
import React from "react";
import { type DefaultValues, type FormState, useForm } from "react-hook-form";
import AutoFormObject from "./fields/object";
import type { Dependency, FieldConfig } from "./types";
import { getDefaultValues, getObjectFormSchema } from "./utils";
export function AutoFormButton({
children,
className,
disabled,
isSubmitting,
...props
}: React.ComponentProps<typeof Button> & {
isSubmitting?: boolean;
}) {
return (
<Button
type="submit"
disabled={disabled || isSubmitting}
className={className}
{...props}
>
{isSubmitting ? (
<Icons.spinner className="h-4 w-4 animate-spin" />
) : (
children ?? "Submit"
)}
</Button>
);
}
// TFieldValues extends FieldValues Maybe?
function AutoForm<SchemaType extends Schema>({
formSchema,
values: valuesProp,
onValuesChange: onValuesChangeProp,
onParsedValuesChange,
formAction: formActionProp,
fieldConfig,
children,
className,
dependencies
}: {
formSchema: SchemaType;
values?: Infer<SchemaType>;
onValuesChange?: (values: Partial<Infer<SchemaType>>) => void;
onParsedValuesChange?: (values: Partial<Infer<SchemaType>>) => void;
formAction:
| ReturnType<
<
ServerError extends never,
S extends SchemaType | undefined,
const BAS extends readonly SchemaType[],
CVE extends ValidationErrors<SchemaType>,
CBAVE extends BindArgsValidationErrors<BAS>,
Data
>(
safeActionFn: HookSafeActionFn<ServerError, S, BAS, CVE, CBAVE, Data>,
utils?: HookBaseUtils<S> &
HookCallbacks<ServerError, S, BAS, CVE, CBAVE, Data>
) => UseActionHookReturn<ServerError, S, BAS, CVE, CBAVE, Data>
>
// biome-ignore lint/suspicious/noExplicitAny: <fix later>
| any;
fieldConfig?: FieldConfig<Infer<SchemaType>>;
children?:
| React.ReactNode
| ((formState: FormState<Infer<SchemaType>>) => React.ReactNode);
className?: string;
dependencies?: Dependency<Infer<SchemaType>>[];
}) {
type Y = typeof formActionProp.execute;
const objectFormSchema = getObjectFormSchema(formSchema);
type FormType = Infer<typeof objectFormSchema>;
const defaultValues: DefaultValues<FormType> | null = getDefaultValues(
objectFormSchema,
fieldConfig
);
const X = useHookFormActionErrorMapper<typeof formSchema>(
formActionProp.result?.validationErrors,
{ joinBy: "\n" }
);
const { hookFormValidationErrors } = X;
const form = useForm<FormType>({
resolver: zodResolver(objectFormSchema),
defaultValues: defaultValues ?? undefined,
values: valuesProp,
errors: hookFormValidationErrors
});
async function onSubmit(values: Infer<SchemaType>) {
const parsedValues = formSchema.safeParse(values);
if (parsedValues.success) {
try {
await formActionProp.execute(parsedValues.data);
} catch (error) {
form.setError("root.server", {
message: (error as Error)?.message ?? "Unknown error",
type: "server"
});
}
}
}
const values = form.watch();
const valuesString = JSON.stringify(values);
React.useEffect(() => {
// biome-ignore lint/suspicious/noExplicitAny: <fix later>
onValuesChangeProp?.(values as any);
const parsedValues = formSchema.safeParse(values);
if (parsedValues.success) {
onParsedValuesChange?.(parsedValues.data);
}
}, [values, onValuesChangeProp, onParsedValuesChange, formSchema]);
return (
<div className="w-full">
<Form {...form}>
<form
// biome-ignore lint/suspicious/noExplicitAny: <fix later>
onSubmit={form.handleSubmit(onSubmit as any)}
className={cn("space-y-5", className)}
>
<AutoFormObject
schema={objectFormSchema}
form={form}
dependencies={dependencies}
fieldConfig={fieldConfig}
/>
{typeof children === "function"
? children(form.formState as FormState<Infer<SchemaType>>)
: children}
{form.formState.errors.root?.server && (
<FormMessage>
{form.formState.errors.root.server.message}
</FormMessage>
)}
</form>
</Form>
</div>
);
}
export default AutoForm;
Usage
"use client";
import AutoForm, { AutoFormButton } from "@/components/ui/auto-form";
import { toast } from "@/components/ui/use-toast";
import { useAction } from "next-safe-action/hooks";
import { useEffect } from "react";
import { signIn } from "../../action";
import { UserAuthSchema } from "../../schema";
export function UserLoginForm() {
const formAction = useAction(signIn, {
onError: ({ error }) => {
toast({
title: "Sign-in failed",
description: error.fetchError ?? "An error occurred during sign-in.",
variant: "destructive"
});
}
});
useEffect(() => {
if (
Notification.permission !== "granted" &&
Notification.permission !== "denied"
) {
Notification.requestPermission().then(permission => {
if (permission === "granted") {
console.info("Notification permission granted.");
}
});
}
}, []);
return (
<AutoForm
formSchema={UserAuthSchema}
formAction={formAction}
fieldConfig={{
email: {
inputProps: {
placeholder: "you@watt.com",
autoCapitalize: "none",
autoComplete: "email",
autoCorrect: "off"
}
},
password: {
inputProps: {
type: "password",
placeholder: "••••••••",
autoComplete: "current-password",
autoCapitalize: "none",
autoCorrect: "off"
}
}
}}
>
{({ isSubmitting }) => (
<AutoFormButton
disabled={isSubmitting}
isSubmitting={isSubmitting}
className="w-full"
>
Login
</AutoFormButton>
)}
</AutoForm>
);
}
Indeeed @gruckion , isSubmitting is not working. Turning onSubmit as async works for me. Can you look into it @vantezzen ?
It would be great to have a loading state which shows a dummy form based on the zod schema or just blank one.