redwoodjs / rw-shad

Generate components in your RedwoodJS project
MIT License
5 stars 0 forks source link

Idea: support for wrapped Redwood field components #3

Open Benjamin-Lee opened 1 year ago

Benjamin-Lee commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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>