Skip to content

Instantly share code, notes, and snippets.

@devstojko
Last active September 29, 2023 18:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save devstojko/b461349507ff7aa9b5d38c011db8a922 to your computer and use it in GitHub Desktop.
Save devstojko/b461349507ff7aa9b5d38c011db8a922 to your computer and use it in GitHub Desktop.
remix-validated-form shadcn/ui
import type * as LabelPrimitive from "@radix-ui/react-label";
import { ValidatedForm, useField } from "remix-validated-form";
import React from "react";
import { Label } from "./label";
import { Slot } from "@radix-ui/react-slot";
import { cn } from "~/lib/utils";
const AppValidatedFormContext = React.createContext<{ id: string }>(
{} as { id: string }
);
interface CustomValidateFormProps
extends React.ComponentPropsWithoutRef<typeof ValidatedForm> {
id: string;
}
const AppValidatedForm = React.forwardRef<
React.ElementRef<typeof ValidatedForm>,
CustomValidateFormProps
>(({ ...props }, ref) => {
return (
<AppValidatedFormContext.Provider value={{ id: props.id }}>
<ValidatedForm {...props} ref={ref} />
</AppValidatedFormContext.Provider>
);
});
AppValidatedForm.displayName = "appValidatedForm";
type FormFieldContextValue = {
formId: string;
name: string;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
);
type AppValidatedFormFieldProps = {
name: string;
children: React.ReactNode;
};
const AppValidatedFormField = (props: AppValidatedFormFieldProps) => {
const ctx = React.useContext(AppValidatedFormContext);
return (
<FormFieldContext.Provider
value={{
formId: ctx.id,
name: props.name,
}}
{...props}
/>
);
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
);
const AppValidatedFormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
AppValidatedFormItem.displayName = "AppValidatedFormItem";
const useAppValidatedFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const field = useField(fieldContext.name);
return {
id: itemContext.id,
name: fieldContext.name,
formItemId: `${itemContext.id}-form-item`,
formDescriptionId: `${itemContext.id}-form-item-description`,
formMessageId: `${itemContext.id}-form-item-message`,
...field,
};
};
const AppValidatedFormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useAppValidatedFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
AppValidatedFormLabel.displayName = "AppValidatedFormLabel";
const AppValidatedFormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId, getInputProps } =
useAppValidatedFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...getInputProps()}
{...props}
/>
);
});
AppValidatedFormControl.displayName = "AppValidatedFormControl";
const AppValidatedFormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useAppValidatedFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
});
AppValidatedFormDescription.displayName = "AppValidatedFormDescription";
const AppValidatedFormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useAppValidatedFormField();
const body = error ? String(error) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
AppValidatedFormMessage.displayName = "AppValidatedFormMessage";
export {
AppValidatedForm,
AppValidatedFormItem,
AppValidatedFormLabel,
AppValidatedFormControl,
AppValidatedFormDescription,
AppValidatedFormMessage,
AppValidatedFormField,
};
@devstojko
Copy link
Author

devstojko commented Sep 29, 2023

I added App prefix for all exported components so it doesn't conflict with original one. You can rename it if you want.

Usage example:


export const signInFormSchema = z.object({
  email: z.string().email(),
  password: z.string().min(7),
});

export const signInFormValidator = withZod(signInFormSchema);

<AppValidatedForm
          id="sign-up-form"
          validator={signInFormValidator}
        >
          <fieldset className="mb-8 space-y-4">
            <AppValidatedFormField name="email">
              <AppValidatedFormItem>
                <AppValidatedFormLabel>Email</AppValidatedFormLabel>
                <AppValidatedFormControl>
                  <Input type="text" />
                </AppValidatedFormControl>
                <AppValidatedFormMessage />
              </AppValidatedFormItem>
            </AppValidatedFormField>
            <AppValidatedFormField name="password">
              <AppValidatedFormItem>
                <AppValidatedFormLabel>Password</AppValidatedFormLabel>
                <AppValidatedFormControl>
                  <Input type="password" />
                </AppValidatedFormControl>
                <AppValidatedFormMessage />
              </AppValidatedFormItem>
            </AppValidatedFormField>
          </fieldset>

          <Button disabled={isLoading} type="submit" className="w-full">
            {isLoading ? <Loader2 className="animate-spin" /> : "Sign in"}
          </Button>
        </AppValidatedForm>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment