Skip to content

Commit 0c7cff4

Browse files
committed
chore(composite-component): add russian translation
1 parent b383040 commit 0c7cff4

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
---
2+
title: 'Создание составного компонента'
3+
tocTitle: 'Составной компонент'
4+
description: 'Создание составного компонента из простых компонентов'
5+
commit: '73d7821'
6+
---
7+
8+
В прошлой главе мы создали наш первый компонент; в этой главе мы расширим полученные знания, чтобы создать компонент `TaskList` - список задач. Давайте объединим компоненты вместе и посмотрим, что произойдет, когда мы добавим больше сложности.
9+
10+
## Компонент TaskList
11+
12+
`Taskbox` выделяет закреплённые задачи, размещая их над обычными задачами. Это даёт два варианта отображения компонента `TaskList`, для которых нужно создавать истории: обычный и с закреплёнными элементами.
13+
14+
![обычные and закреплённые задачи](/intro-to-storybook/tasklist-states-1.png)
15+
16+
Поскольку данные для `Task` могут быть отправлены асинхронно, нам **также** необходимо состояние загрузки для рендеринга в отсутствие соединения. Кроме того, нам нужно пустое состояние, когда нет задач.
17+
18+
![состояния когда нет задач и когда задачи загружаются](/intro-to-storybook/tasklist-states-2.png)
19+
20+
## Начинаем подготовку
21+
22+
Составной компонент мало чем отличается от простых компонентов, которые он содержит. Создайте компонент `TaskList` и сопроводительный файл историй: `src/components/TaskList.js` и `src/components/TaskList.stories.js`.
23+
24+
Начните с грубой реализации `TaskList`. Вам нужно будет импортировать компонент `Task` и передать атрибуты и действия в качестве входных данных.
25+
26+
```js:title=src/components/TaskList.js
27+
import React from 'react';
28+
29+
import Task from './Task';
30+
31+
export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
32+
const events = {
33+
onPinTask,
34+
onArchiveTask,
35+
};
36+
37+
if (loading) {
38+
return <div className="list-items">loading</div>;
39+
}
40+
41+
if (tasks.length === 0) {
42+
return <div className="list-items">empty</div>;
43+
}
44+
45+
return (
46+
<div className="list-items">
47+
{tasks.map(task => (
48+
<Task key={task.id} task={task} {...events} />
49+
))}
50+
</div>
51+
);
52+
}
53+
```
54+
55+
Затем создайте тестовые состояния `TaskList` в файле истории.
56+
57+
```js:title=src/components/TaskList.stories.js
58+
import React from 'react';
59+
60+
import TaskList from './TaskList';
61+
import * as TaskStories from './Task.stories';
62+
63+
export default {
64+
component: TaskList,
65+
title: 'TaskList',
66+
decorators: [story => <div style={{ padding: '3rem' }}>{story()}</div>],
67+
};
68+
69+
const Template = args => <TaskList {...args} />;
70+
71+
export const Default = Template.bind({});
72+
Default.args = {
73+
// Shaping the stories through args composition.
74+
// The data was inherited from the Default story in Task.stories.js.
75+
tasks: [
76+
{ ...TaskStories.Default.args.task, id: '1', title: 'Task 1' },
77+
{ ...TaskStories.Default.args.task, id: '2', title: 'Task 2' },
78+
{ ...TaskStories.Default.args.task, id: '3', title: 'Task 3' },
79+
{ ...TaskStories.Default.args.task, id: '4', title: 'Task 4' },
80+
{ ...TaskStories.Default.args.task, id: '5', title: 'Task 5' },
81+
{ ...TaskStories.Default.args.task, id: '6', title: 'Task 6' },
82+
],
83+
};
84+
85+
export const WithPinnedTasks = Template.bind({});
86+
WithPinnedTasks.args = {
87+
// Shaping the stories through args composition.
88+
// Inherited data coming from the Default story.
89+
tasks: [
90+
...Default.args.tasks.slice(0, 5),
91+
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
92+
],
93+
};
94+
95+
export const Loading = Template.bind({});
96+
Loading.args = {
97+
tasks: [],
98+
loading: true,
99+
};
100+
101+
export const Empty = Template.bind({});
102+
Empty.args = {
103+
// Shaping the stories through args composition.
104+
// Inherited data coming from the Loading story.
105+
...Loading.args,
106+
loading: false,
107+
};
108+
```
109+
110+
<div class="aside">
111+
💡 <a href="https://storybook.js.org/docs/react/writing-stories/decorators"><b>Декораторы</b></a> это способ создания произвольных обёрток для историй. В данном случае мы используем декоратор для того, чтобы добавить <code>padding</code> вокруг отрисованного компонента. Их также можно использовать для обёртывания историй в "провайдеры" - т.е. библиотечные компоненты, которые устанавливают контекст React.
112+
</div>
113+
114+
Импортировав `TaskStories`, мы смогли [составить](https://storybook.js.org/docs/react/writing-stories/args#args-composition) аргументы (сокращенно args) в наших историях с минимальными усилиями. Таким образом, данные и действия (замоканные коллбеки), ожидаемые обоими компонентами, сохраняются.
115+
116+
Теперь давайте проверим Storybook на наличие новых историй для `TaskList`.
117+
118+
<video autoPlay muted playsInline loop>
119+
<source
120+
src="/intro-to-storybook/inprogress-tasklist-states-6-0.mp4"
121+
type="video/mp4"
122+
/>
123+
</video>
124+
125+
## Реализуем состояния
126+
127+
Наш компонент все ещё сырой, но теперь у нас есть представление о том, над чем нужно работать. Вы можете подумать, что обёртка `.list-items` слишком упрощена. Вы правы - в большинстве случаев мы не стали бы создавать новый компонент только для того, чтобы добавить обёртку. Но **реальная сложность** компонента `TaskList` проявляется в граничных случаях `withPinnedTasks`, `load` и `empty`.
128+
129+
```js:title=src/components/TaskList.js
130+
import React from 'react';
131+
132+
import Task from './Task';
133+
134+
export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
135+
const events = {
136+
onPinTask,
137+
onArchiveTask,
138+
};
139+
const LoadingRow = (
140+
<div className="loading-item">
141+
<span className="glow-checkbox" />
142+
<span className="glow-text">
143+
<span>Loading</span> <span>cool</span> <span>state</span>
144+
</span>
145+
</div>
146+
);
147+
if (loading) {
148+
return (
149+
<div className="list-items" data-testid="loading" key={"loading"}>
150+
{LoadingRow}
151+
{LoadingRow}
152+
{LoadingRow}
153+
{LoadingRow}
154+
{LoadingRow}
155+
{LoadingRow}
156+
</div>
157+
);
158+
}
159+
if (tasks.length === 0) {
160+
return (
161+
<div className="list-items" key={"empty"} data-testid="empty">
162+
<div className="wrapper-message">
163+
<span className="icon-check" />
164+
<p className="title-message">You have no tasks</p>
165+
<p className="subtitle-message">Sit back and relax</p>
166+
</div>
167+
</div>
168+
);
169+
}
170+
171+
const tasksInOrder = [
172+
...tasks.filter((t) => t.state === "TASK_PINNED"),
173+
...tasks.filter((t) => t.state !== "TASK_PINNED"),
174+
];
175+
return (
176+
<div className="list-items">
177+
{tasksInOrder.map((task) => (
178+
<Task key={task.id} task={task} {...events} />
179+
))}
180+
</div>
181+
);
182+
}
183+
```
184+
185+
Результат добавленной разметки следующий:
186+
187+
<video autoPlay muted playsInline loop>
188+
<source
189+
src="/intro-to-storybook/finished-tasklist-states-6-0.mp4"
190+
type="video/mp4"
191+
/>
192+
</video>
193+
194+
Обратите внимание на положение закреплённого элемента в списке. Мы хотим, чтобы закреплённый элемент отображался в верхней части списка, чтобы сделать его приоритетным для наших пользователей.
195+
196+
## Требования к данным и свойствам
197+
198+
По мере роста компонента растут и входные требования. Определите требования к свойствам `TaskList`. Поскольку `Task` является дочерним компонентом, убедитесь, что данные имеют соответствующую структуру для его рендеринга. Чтобы сэкономить время и головную боль, повторно используйте `propTypes`, которые вы определили в `Task` ранее.
199+
200+
```diff:title=src/components/TaskList.js
201+
import React from 'react';
202+
+ import PropTypes from 'prop-types';
203+
204+
import Task from './Task';
205+
206+
export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
207+
...
208+
}
209+
210+
+ TaskList.propTypes = {
211+
+ /** Checks if it's in loading state */
212+
+ loading: PropTypes.bool,
213+
+ /** The list of tasks */
214+
+ tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
215+
+ /** Event to change the task to pinned */
216+
+ onPinTask: PropTypes.func,
217+
+ /** Event to change the task to archived */
218+
+ onArchiveTask: PropTypes.func,
219+
+ };
220+
+ TaskList.defaultProps = {
221+
+ loading: false,
222+
+ };
223+
```
224+
225+
<div class="aside">
226+
💡 Не забудьте зафиксировать свои изменения с помощью git!
227+
</div>

0 commit comments

Comments
 (0)