import { Callout } from 'nextra/components'
import { FileTree } from 'nextra-theme-docs'
# Компоненты, полиморфные компоненты
## Button
### Подготовка
- Создайте папку _src/components/ui_
- Создайте папку _button_ в _src/components/ui_ со следующей структурой:
### Дизайн и варианты
После рассмотрения дизайна стало понятно, что у нас будет несколько вариантов кнопок, а именно:

- Стандартная, она же основная, она же `primary`
- Стандартная, она же основная, она же `primary` с иконкой
- Второстепенная, она же `secondary`
- Второстепенная, она же `secondary` с иконкой
- Кнопка шириной в 100%, она же fullWidth, может быть как `primary`, так и `secondary`
### Варианты реализации
Какие варианты реализации у нас есть?
1. Создать один компонент, который будет принимать все возможные пропсы и в зависимости от них будет
рендериться тот или иной вариант кнопки
2. Создать отдельный компонент для каждого варианта кнопки
Мы отдадим предпочтение **первому варианту**, так как он более гибкий и позволит нам легко добавлять
новые варианты кнопок в будущем.
### Props
Опишем пропсы, которые будет принимать наш компонент:
```tsx filename="button.tsx"
import { ComponentPropsWithoutRef } from 'react'
export type ButtonProps = {
variant?: 'primary' | 'secondary'
fullWidth?: boolean
} & ComponentPropsWithoutRef<'button'>
```
`ComponentPropsWithoutRef<'button'>` - это пропсы, которые принимает стандартный html-тег button, мы
их расширяем своими пропсами.
### Реализация
Создадим сам компонент:
```tsx filename="button.tsx"
import s from './button.module.scss'
export const Button = ({ className, fullWidth, variant = 'primary', ...rest }: ButtonProps) => {
return (
)
}
```
Для проверки добавим стили в _button.module.scss_:
```scss filename="button.module.scss"
.button {
all: unset;
cursor: pointer;
box-sizing: border-box;
color: inherit;
font-family: inherit;
&:focus-visible {
outline: 2px solid var(--color-info-500);
}
}
.primary {
background-color: var(--color-primary-500);
}
.secondary {
background-color: var(--color-dark-300);
}
.fullWidth {
width: 100%;
}
```
Стили согласно дизайну напишите сами
- В файл _index.ts_ добавим экспорт компонента:
```ts filename="src/components/ui/button/index.ts"
export * from './button'
```
- Добавим stories для компонента:
```tsx filename="button.stories.ts"
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './'
const meta = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
options: ['primary', 'secondary'],
control: { type: 'radio' },
},
},
} satisfies Meta
export default meta
type Story = StoryObj
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Primary Button',
disabled: false,
},
}
export const Secondary: Story = {
args: {
variant: 'secondary',
children: 'Secondary Button',
disabled: false,
},
}
export const FullWidth: Story = {
args: {
variant: 'primary',
children: 'Full Width Primary Button',
disabled: false,
fullWidth: true,
},
}
```
Вот что должно получиться:

## Полиморфные компоненты
### Теория и реализация
Дизайнеры могут внезапно решить, что кнопка должна выглядеть как ссылка, а ссылка как кнопка, но при
этом мы должны рендерить правильные тэги в DOM, для удобства пользователей. Как быть в таком случае?
Для этого нам нужно будет передать нужный тег через пропс:
```tsx filename="button.tsx" showLineNumbers {6,15,19} /Component/4
import { ComponentPropsWithoutRef } from 'react'
import s from './button.module.scss'
export type ButtonProps = {
as: any
variant?: 'primary' | 'secondary'
fullWidth?: boolean
} & ComponentPropsWithoutRef<'button'>
export const Button = ({
variant = 'primary',
fullWidth,
className,
as: Component = 'button',
...rest
}: ButtonProps) => {
return (
)
}
```
Добавим в stories пример использования:
```tsx filename="button.stories.ts"
export const AsLink: Story = {
args: {
variant: 'primary',
children: 'Link that looks like a button',
as: 'a',
},
}
```
И проверим что всё работает как надо:

Как видите, в DOM мы получили тег `a` с нужными пропсами.
### Типизация
В примере выше мы использовали `as: any`, но это не очень хорошо, так как мы теряем типизацию.
Давайте попробуем это исправить.
Добавим generic параметр в наш компонент и в пропсы:
```tsx filename="button.tsx" showLineNumbers {1,5,6,7,11,13,14}
import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
import s from './button.module.scss'
export type ButtonProps = {
as?: T
children: ReactNode
variant?: 'primary' | 'secondary'
fullWidth?: boolean
className?: string
} & ComponentPropsWithoutRef
export const Button = (props: ButtonProps) => {
const { variant = 'primary', fullWidth, className, as: Component = 'button', ...rest } = props
return (
)
}
```
Попробуем отрисовать две кнопки в App.tsx, где одна кнопка будет с тегом `button`, а другая с тегом
`a`:

Типизация работает правильно, потому что в первом случае мы отрисовываем тэг `a`, у которого есть
проп `href`, а во втором случае мы отрисовываем тэг `button`, у которого его нет, поэтому мы и видим
ошибку.
Обратите внимание, что типизацию мы тестировали в App.tsx, а не в stories, потому что storybook
все еще иногда ошибается с TypeScript.
## Коммитим изменения
```bash filename="Terminal"
git add . && git commit -m "add polymorphic button"
```