Files
flashcards-docs/pages/lesson-1/chapter-3.ru.mdx

277 lines
8.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
```