Skip to content

Commit

Permalink
Merge pull request #44 from igor-tech/QUIZ-29
Browse files Browse the repository at this point in the history
Add Controlled Components
  • Loading branch information
igor-tech committed May 15, 2024
2 parents 5efac5d + 5705b8c commit 521ec64
Show file tree
Hide file tree
Showing 18 changed files with 318 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/assets/icons/stories/icons-button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Typography } from '@/components/ui/typography'
const meta = {
component: IconButton,
tags: ['autodocs'],
title: 'Components/Icons/Icons Button',
title: 'Icons/Icons Buttons',
} satisfies Meta<typeof IconButton>

export default meta
Expand Down
2 changes: 1 addition & 1 deletion src/assets/icons/stories/icons.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import styles from './icons.module.scss'
const meta = {
component: IconButton,
tags: ['autodocs'],
title: 'Components/Icons/All icons',
title: 'Icons/All icons',
} satisfies Meta<typeof IconButton>

export default meta
Expand Down
2 changes: 2 additions & 0 deletions src/common/constants/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './path'
export * from './zod-schemes'
11 changes: 11 additions & 0 deletions src/common/constants/zod-schemes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from 'zod'

const email = z.string({ required_error: 'Email is required' }).email({ message: 'Invalid email' })
const password = z
.string({ required_error: 'Password is required' })
.min(3, { message: 'Password must be at least 3 characters' })
const rememberMe = z.boolean().default(false).optional()

const loginSchema = z.object({ email, password, rememberMe })

export type LoginFormValues = z.infer<typeof loginSchema>
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from '@storybook/react'

import { CSSProperties } from 'react'
import { useForm } from 'react-hook-form'

import { ControlledCheckbox } from '@/components/controlled/controlled-checkbox/controlled-checkbox'
import { Button } from '@/components/ui/button'
import { Typography } from '@/components/ui/typography'

type FormValues = Partial<Record<'check1' | 'check2' | 'check3', boolean>>

const meta = {
component: ControlledCheckbox,
tags: ['autodocs'],
title: 'Components/Controlled/Checkbox',
} satisfies Meta<typeof ControlledCheckbox>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
name: '',
},
render: () => {
const { control, handleSubmit } = useForm<FormValues>()

const onSubmit = (data: FormValues) => {
alert(JSON.stringify(data))
}

const style: CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: '20px',
}

return (
<form onSubmit={handleSubmit(onSubmit)} style={style}>
<Typography variant={'h1'}>Form with controlled checkbox</Typography>
<div style={style}>
<ControlledCheckbox control={control} label={'Checkbox 1'} name={'check1'} />
<ControlledCheckbox control={control} label={'Checkbox 2'} name={'check2'} />
<ControlledCheckbox control={control} label={'Checkbox 3'} name={'check3'} />
</div>
<Button type={'submit'}>Submit</Button>
</form>
)
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FieldValues, UseControllerProps, useController } from 'react-hook-form'

import { Checkbox, CheckboxProps } from '@/components/ui/checkbox'

export type ControlledCheckboxProps<TFieldValues extends FieldValues> = Omit<
CheckboxProps,
'id' | 'value'
> &
UseControllerProps<TFieldValues>

export const ControlledCheckbox = <TFieldValues extends FieldValues>({
control,
defaultValue,
name,
rules,
shouldUnregister,
...checkboxProps
}: ControlledCheckboxProps<TFieldValues>) => {
const {
field: { onChange, value },
} = useController({
control,
defaultValue,
name,
rules,
shouldUnregister,
})

return <Checkbox checked={value} id={name} onCheckedChange={onChange} {...checkboxProps} />
}
1 change: 1 addition & 0 deletions src/components/controlled/controlled-checkbox/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './controlled-checkbox'
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from '@storybook/react'

import { CSSProperties } from 'react'
import { useForm } from 'react-hook-form'

import { ControlledRadioGroup } from '@/components/controlled/controlled-radio-group/controlled-radio-group'
import { Button } from '@/components/ui/button'
import { OptionType } from '@/components/ui/radio-group'
import { Typography } from '@/components/ui/typography'

type FormValues = Partial<Record<'value', OptionType>>

const meta = {
component: ControlledRadioGroup,
tags: ['autodocs'],
title: 'Components/Controlled/Radio Group',
} satisfies Meta<typeof ControlledRadioGroup>

export default meta
type Story = StoryObj<typeof meta>

const options: OptionType[] = [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' },
{ label: 'Option 3', value: '3' },
]

export const Default: Story = {
args: {
name: '',
options: options,
},
render: () => {
const { control, handleSubmit } = useForm<FormValues>()

const onSubmit = (data: FormValues) => {
alert(JSON.stringify(data))
}

const style: CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: '20px',
}

return (
<form onSubmit={handleSubmit(onSubmit)} style={style}>
<Typography variant={'h1'}>Form with controlled radio group</Typography>
<ControlledRadioGroup
control={control}
defaultValue={'1'}
name={'value'}
options={options}
/>
<Button type={'submit'}>Submit</Button>
</form>
)
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FieldValues, UseControllerProps, useController } from 'react-hook-form'

import { RadioGroup, RadioGroupProps } from '@/components/ui/radio-group'

export type ControlledRadioGroupProps<TFieldValues extends FieldValues> = Omit<
RadioGroupProps,
'id' | 'onChange' | 'value'
> &
UseControllerProps<TFieldValues>

export const ControlledRadioGroup = <TFieldValues extends FieldValues>({
control,
name,
...props
}: ControlledRadioGroupProps<TFieldValues>) => {
const {
field: { onChange, ...filed },
fieldState: { error },
} = useController({
control,
name,
})

return (
<RadioGroup
{...props}
{...filed}
errorMessage={error?.message}
id={name}
onValueChange={onChange}
/>
)
}
1 change: 1 addition & 0 deletions src/components/controlled/controlled-radio-group/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './controlled-radio-group'
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { Meta, StoryObj } from '@storybook/react'

import { CSSProperties } from 'react'
import { useForm } from 'react-hook-form'

import { ControlledTextField } from '@/components/controlled/controlled-text-field/controlled-text-field'
import { Button } from '@/components/ui/button'
import { Typography } from '@/components/ui/typography'

type FormValues = Partial<Record<'email' | 'password', string>>

const meta = {
component: ControlledTextField,
tags: ['autodocs'],
title: 'Components/Controlled/Text field',
} satisfies Meta<typeof ControlledTextField>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
name: '',
},
render: () => {
const { control, handleSubmit } = useForm<FormValues>()

const onSubmit = (data: FormValues) => {
alert(JSON.stringify(data))
}

const style: CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: '20px',
}

return (
<form onSubmit={handleSubmit(onSubmit)} style={style}>
<Typography variant={'h1'}>Form with controlled Text field</Typography>
<ControlledTextField
control={control}
label={'Email'}
name={'email'}
placeholder={'email@example.com'}
/>

<ControlledTextField
control={control}
label={'Password'}
name={'password'}
placeholder={'Password'}
type={'password'}
/>

<Button type={'submit'}>Submit</Button>
</form>
)
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FieldValues, UseControllerProps, useController } from 'react-hook-form'

import { TextField, TextFieldProps } from '@/components/ui/textfield/TextField'

export type ControlledTextFieldProps<TFieldValues extends FieldValues> = Omit<
TextFieldProps,
'id' | 'onChange' | 'value'
> &
UseControllerProps<TFieldValues>

export const ControlledTextField = <TFieldValues extends FieldValues>({
control,
name,
...props
}: ControlledTextFieldProps<TFieldValues>) => {
const {
field: { ...field },
fieldState: { error },
} = useController({ control, name })

return <TextField {...props} {...field} error={error?.message} id={name} />
}
1 change: 1 addition & 0 deletions src/components/controlled/controlled-text-field/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './controlled-text-field'
2 changes: 1 addition & 1 deletion src/components/ui/checkbox/checkbox.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const Controlled: Story = {

return (
<>
<Checkbox {...args} checked={checked} onChange={() => setChecked(!checked)} />
<Checkbox {...args} checked={checked} onCheckedChange={() => setChecked(!checked)} />
</>
)
},
Expand Down
22 changes: 5 additions & 17 deletions src/components/ui/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ForwardedRef, forwardRef } from 'react'
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react'

import { Check } from '@/assets/icons/components/check'
import { Typography } from '@/components/ui/typography'
Expand All @@ -8,17 +8,11 @@ import { clsx } from 'clsx'
import styles from './checkbox.module.scss'

export type CheckboxProps = {
checked?: boolean
className?: string
disabled?: boolean
id?: string
label?: string
onChange?: (checked: boolean) => void
required?: boolean
}
} & ComponentPropsWithoutRef<typeof CheckboxRadix.Root>

export const Checkbox = forwardRef((props: CheckboxProps, ref: ForwardedRef<HTMLButtonElement>) => {
const { checked, className, disabled, label, onChange, ...rest } = props
export const Checkbox = forwardRef<ElementRef<'button'>, CheckboxProps>((props, ref) => {
const { checked, className, disabled, label, ...rest } = props

const classNames = {
checkbox: styles.checkbox,
Expand All @@ -27,13 +21,7 @@ export const Checkbox = forwardRef((props: CheckboxProps, ref: ForwardedRef<HTML

return (
<Typography as={'label'} className={classNames.root}>
<CheckboxRadix.Root
className={classNames.checkbox}
disabled={disabled}
onCheckedChange={onChange}
{...rest}
ref={ref}
>
<CheckboxRadix.Root className={classNames.checkbox} disabled={disabled} {...rest} ref={ref}>
<div className={styles.frame}></div>
{checked && (
<CheckboxRadix.Indicator className={styles.indicator} forceMount>
Expand Down
Loading

0 comments on commit 521ec64

Please sign in to comment.