vantezzen / auto-form

🌟 A React component that automatically creates a @shadcn/ui form based on a zod schema.
https://vantezzen.github.io/auto-form/
MIT License
2.75k stars 99 forks source link

add loading state to autoform #84

Open a0m0rajab opened 3 months ago

a0m0rajab commented 3 months ago

It would be great to have a loading state which shows a dummy form based on the zod schema or just blank one.

gruckion commented 3 weeks 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>
  );
}
tecoad commented 4 days ago

Indeeed @gruckion , isSubmitting is not working. Turning onSubmit as async works for me. Can you look into it @vantezzen ?