import { Callout } from 'nextra/components'
import { FileTree } from 'nextra-theme-docs'

# Компоненты, полиморфные компоненты

## Button

### Подготовка

- Создайте папку _src/components/ui_

- Создайте папку _button_ в _src/components/ui_ со следующей структурой:

<FileTree>
  <FileTree.Folder name={'src'} defaultOpen>
    <FileTree.Folder name={'components'} defaultOpen>
      <FileTree.Folder name={'ui'} defaultOpen>
        <FileTree.Folder name={'button'} defaultOpen>
          <FileTree.File name={'button.tsx'} />
          <FileTree.File name={'button.module.scss'} />
          <FileTree.File name={'button.stories.ts'} />
          <FileTree.File name={'index.ts'} />
        </FileTree.Folder>
        <FileTree.File name={'index.ts'} />
      </FileTree.Folder>
      <FileTree.File name={'index.ts'} />
    </FileTree.Folder>

  </FileTree.Folder>
</FileTree>

### Дизайн и варианты

После рассмотрения дизайна стало понятно, что у нас будет несколько вариантов кнопок, а именно:

![Button](./images/Button.png)

- Стандартная, она же основная, она же `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
      className={`${s.button} ${s[variant]} ${fullWidth ? s.fullWidth : ''} ${className}`}
      {...rest}
    />
  )
}
```

Для проверки добавим стили в _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%;
}
```

<Callout type="info">Стили согласно дизайну напишите сами</Callout>

- В файл _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<typeof Button>

export default meta
type Story = StoryObj<typeof meta>

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,
  },
}
```

Вот что должно получиться:

![storybook-button](./images/storybook-button.png)

## Полиморфные компоненты

### Теория и реализация

Дизайнеры могут внезапно решить, что кнопка должна выглядеть как ссылка, а ссылка как кнопка, но при
этом мы должны рендерить правильные тэги в 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 (
    <Component
      className={`${s.button} ${s[variant]} ${fullWidth ? s.fullWidth : ''} ${className}`}
      {...rest}
    />
  )
}
```

Добавим в stories пример использования:

```tsx filename="button.stories.ts"
export const AsLink: Story = {
  args: {
    variant: 'primary',
    children: 'Link that looks like a button',
    as: 'a',
  },
}
```

И проверим что всё работает как надо:

![storybook-button-as-link](./images/storybook-button-as-link.png)

Как видите, в 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<T extends ElementType = 'button'> = {
  as?: T
  children: ReactNode
  variant?: 'primary' | 'secondary'
  fullWidth?: boolean
  className?: string
} & ComponentPropsWithoutRef<T>

export const Button = <T extends ElementType = 'button'>(props: ButtonProps<T>) => {
  const { variant = 'primary', fullWidth, className, as: Component = 'button', ...rest } = props

  return (
    <Component className={`${s[variant]} ${fullWidth ? s.fullWidth : ''} ${className}`} {...rest} />
  )
}
```

Попробуем отрисовать две кнопки в App.tsx, где одна кнопка будет с тегом `button`, а другая с тегом
`a`:

![button-as-link-error.png](./images/button-as-link-error.png)

Типизация работает правильно, потому что в первом случае мы отрисовываем тэг `a`, у которого есть
проп `href`, а во втором случае мы отрисовываем тэг `button`, у которого его нет, поэтому мы и видим
ошибку.

<Callout type={'default'}>
  Обратите внимание, что типизацию мы тестировали в App.tsx, а не в stories, потому что storybook
  все еще иногда ошибается с TypeScript.
</Callout>

## Коммитим изменения

```bash filename="Terminal"
git add . && git commit -m "add polymorphic button"
```
