Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idea: support for wrapped Redwood field components #3

Open
Benjamin-Lee opened this issue Aug 20, 2023 · 5 comments
Open

Idea: support for wrapped Redwood field components #3

Benjamin-Lee opened this issue Aug 20, 2023 · 5 comments

Comments

@Benjamin-Lee
Copy link

I'm trying to use shadcn/ui inside a Redwood project while also using Redwood Forms. It would be great if there were a way for the <Input> component from shadcn/ui to wrap the Redwood <TextField> and its siblings. Here's my first pass at it if it's of any help:

import * as React from 'react'

import {
  ButtonField,
  CheckboxField,
  ColorField,
  DateField,
  DatetimeLocalField,
  EmailField,
  FileField,
  HiddenField,
  ImageField,
  MonthField,
  NumberField,
  PasswordField,
  RadioField,
  RangeField,
  ResetField,
  SearchField,
  SubmitField,
  TelField,
  TextField,
  TimeField,
  UrlField,
  WeekField,
} from '@redwoodjs/forms'
import { RegisterOptions } from '@redwoodjs/forms/'

import { cn } from 'src/lib/utils'

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {
  validation: RegisterOptions
}

const mapping = {
  button: ButtonField,
  checkbox: CheckboxField,
  color: ColorField,
  date: DateField,
  'datetime-local': DatetimeLocalField,
  email: EmailField,
  file: FileField,
  hidden: HiddenField,
  image: ImageField,
  month: MonthField,
  number: NumberField,
  password: PasswordField,
  radio: RadioField,
  range: RangeField,
  reset: ResetField,
  search: SearchField,
  submit: SubmitField,
  tel: TelField,
  text: TextField,
  time: TimeField,
  url: UrlField,
  week: WeekField,
}

const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, name, type, ...props }, ref) => {
    const Component = mapping[type] || TextField

    return (
      <Component
        name={name}
        className={cn(
          'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)
Input.displayName = 'Input'

export { Input }
@Tobbe
Copy link
Member

Tobbe commented Aug 20, 2023

This is great @Benjamin-Lee! Thank you so much for contributing 😀

I've had more people asking for better forms support, but haven't had a chance to look into it yet. And to be honest it might still be a few days/weeks/years until I do.

@Benjamin-Lee
Copy link
Author

Happy to help. I ended up changing my strategy for doing this to something so much easier: using cva and just pulling the styles out into an input variant along with some other form-related styles into a file:

import { cva } from "class-variance-authority"

import { cn } from "src/lib/utils"

export const inputVariants = cva(
  "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
)

export const labelVariants = cva(
  "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
  {
    variants: {
      error: {
        true: "text-destructive",
      },
    },
  }
)

export const fieldErrorVariants = cva(
  "block text-[0.8rem] font-medium text-destructive"
)

export const FormItem = React.forwardRef<
  HTMLDivElement,
  React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
  return <div ref={ref} className={cn("space-y-2", className)} {...props} />
})
FormItem.displayName = "FormItem"

Then, building a form is as easy as:

        <Form onSubmit={onSubmit} className="space-y-6">
          <div className="flex justify-between">
            <FormItem>
              <Label
                name="email"
                className={labelVariants()}
                errorClassName={labelVariants({ error: true })}
              >
                First Name
              </Label>
              <TextField
                name="firstName"
                className={inputVariants()}
                validation={{
                  required: {
                    value: true,
                    message: 'First name is required',
                  },
                }}
                autoComplete="given-name"
              />
              <FieldError name="firstName" className={fieldErrorVariants()} />
            </FormItem>
            <FormItem>
              <Label
                name="lastName"
                className={labelVariants()}
                errorClassName={labelVariants({ error: true })}
              >
                Last Name
              </Label>
              <TextField
                name="lastName"
                className={inputVariants()}
                validation={{
                  required: {
                    value: true,
                    message: 'Last name is required',
                  },
                }}
                autoComplete="family-name"
              />
              <FieldError name="email" className={fieldErrorVariants()} />
            </FormItem>
          </div>
          <FormItem>
            <Label
              name="email"
              className={labelVariants()}
              errorClassName={labelVariants({ error: true })}
            >
              Email address
            </Label>
            <EmailField
              name="email"
              className={inputVariants()}
              ref={emailRef}
              validation={{
                required: {
                  value: true,
                  message: 'Email is required',
                },
              }}
            />
            <FieldError name="email" className={fieldErrorVariants()} />
          </FormItem>
          <FormItem>
            <Label
              name="password"
              className={labelVariants()}
              errorClassName={labelVariants({ error: true })}
            >
              Password
            </Label>
            <PasswordField
              name="password"
              className={inputVariants()}
              autoComplete="current-password"
              validation={{
                required: {
                  value: true,
                  message: 'Password is required',
                },
                minLength: {
                  value: 8,
                  message: 'Password must be at least 8 characters',
                },
              }}
            />
            <FieldError name="password" className={fieldErrorVariants()} />
          </FormItem>

          <div>
            <Submit className={cn(buttonVariants(), 'w-full')}>Login</Submit>
          </div>
        </Form>

@Quelu
Copy link

Quelu commented Sep 30, 2024

Hello, I was wondering if you have a solution for the Checkbox and Switch components?
Shadcn generates more than just a single component (e.g., a button with an icon inside) for the Checkbox/Switch, so I can't simply apply the class (generated by cva) to Redwood's CheckboxField component. I also tried using Redwood's useRegister, but it doesn't seem to work with the Shadcn component either

@Tobbe
Copy link
Member

Tobbe commented Oct 1, 2024

@Quelu I've just used shad's <Switch /> component directly

<FormField
  control={form.control}
  name="fanControl"
  render={({ field }) => (
    <FormItem className="flex flex-row items-center justify-between">
      <div className="space-y-0.5">
        <FormLabel className="text-base">Fan Control</FormLabel>
        <FormDescription>
          Changes will apply to next refresh cycle
        </FormDescription>
      </div>
      <FormControl>
        <Switch
          checked={field.value}
          onCheckedChange={field.onChange}
        />
      </FormControl>
    </FormItem>
  )}
/>

It works, but you don't get the full RW form integration with its error handling unfortunately

I want a better experience, but haven't prioritized that yet

@Quelu
Copy link

Quelu commented Oct 1, 2024

Thank you so much for the response!

However, it doesn't work with the Checkbox component when it's set up to have multiple values other than a boolean.

Based on what you provided, I was able to better understand how Shadcn works with react-hook-form. I was then able to create these components, which I can now directly use in my Redwood forms:

export const CheckboxField = ({
  label,
  value,
  name,
}: InputFieldProps & {
  label: React.ReactNode;
  value: string;
}) => {
  return (
    <Controller
      name={name}
      render={({ field }) => (
        <div className="flex gap-4" key={name}>
          <Checkbox
            id={`checkbox-${value}`}
            checked={field.value?.includes(value)}
            onCheckedChange={(checked) => {
              return checked
                ? field.onChange([...(field?.value || []), value])
                : field.onChange(
                    field.value?.filter((newValue) => newValue !== value)
                  );
            }}
          />
          <ShadLabel htmlFor={`checkbox-${value}`}>{label}</ShadLabel>
        </div>
      )}
    />
  );
};

export const CheckboxGroupField = ({
  name,
  options,
  validation,
}: InputFieldProps & {
  options: { label: React.ReactNode; value: string }[];
}) => {
  return (
    <Controller
      name={name}
      rules={validation}
      render={() => (
        <>
          {options.map((option) => (
            <CheckboxField
              key={option.value}
              label={option.label}
              value={option.value}
              name={name}
            />
          ))}
        </>
      )}
    />
  );
};

export const SwitchField = ({
  label,
  name,
  validation,
  defaultValue,
}: InputFieldProps & {
  label?: React.ReactNode;
}) => {
  return (
    <Controller
      name={name}
      rules={validation}
      defaultValue={defaultValue}
      render={({ field }) => (
        <div className="flex gap-4" key={name}>
          <Switch
            id={`switch-${name}`}
            checked={field.value}
            onCheckedChange={field.onChange}
          />
          {label && <ShadLabel htmlFor={`switch-${name}`}>{label}</ShadLabel>}
        </div>
      )}
    />
  );
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants