Storybook: документация API и составной пример формы
Компоненты готовы как учебные исходники, но потребителю ещё нужны правила применения. Теперь включим подробное описание API в Autodocs и соберём ProfileEditor из кнопки, поля и сообщения. Составной пример покажет, где заканчивается библиотека и начинается приложение. Он использует локальный таймер вместо HTTP, поэтому не является готовым серверным сохранением организации.
Документация компонента должна объяснять назначение, входы, состояния и ограничения. Таблица автоматически найденных пропсов полезна, но не сообщает, когда выбрать submit, зачем передавать pending или как связать ошибку с вводом. Эти решения находятся в тексте и примерах. Если потребитель читает только TypeScript-тип, он может собрать допустимый по компилятору, но неудобный пользовательский маршрут.
Описание в CSF-метаданных
Для кнопки в parameters.docs.description.component добавлено короткое объяснение: действие, исходный type, явная отправка формы и граница занятости. Тег autodocs сохраняется. Storybook выбранного профиля получает информацию из meta и может сформировать документационный экран при отдельном будущем запуске. Автор не собирал этот экран и не утверждает, что все описания уже были отрисованы в браузере.
Полный файл src/ProfileEditor.tsx для этого состояния:
import { useState, type FormEvent } from 'react';
import { Button } from './Button';
import { TextField } from './TextField';
import { Notice } from './Notice';
export function ProfileEditor() {
const [name,setName]=useState('Контур'); const [error,setError]=useState(''); const [pending,setPending]=useState(false); const [saved,setSaved]=useState(false);
async function submit(event:FormEvent<HTMLFormElement>) { event.preventDefault(); if(pending)return; setSaved(false); const valid=name.trim().length>=2&&name.trim().length<=80; setError(valid?'':'Введите от 2 до 80 символов'); if(!valid)return; setPending(true); try{await new Promise<void>(resolve=>setTimeout(resolve,800));setSaved(true);}finally{setPending(false);} }
return <form onSubmit={submit} noValidate><TextField label="Название организации" value={name} onChange={event=>{setName(event.target.value);setSaved(false);}} error={error} disabled={pending} required/><Button type="submit" pending={pending}>Сохранить организацию</Button>{saved&&<Notice kind="status" title="Изменения сохранены"/>}</form>;
}
Полный файл src/ProfileEditor.stories.tsx для этого состояния:
import type { Meta, StoryObj } from '@storybook/react-vite';
import { ProfileEditor } from './ProfileEditor';
const meta={ title:'Nabor/Examples/ProfileEditor', component:ProfileEditor, tags:['autodocs'], parameters:{docs:{description:{component:'Учебная форма использует задержку вместо HTTP. Компоненты не выполняют сетевые операции.'}}} } satisfies Meta<typeof ProfileEditor>;
export default meta;
export const Editable:StoryObj<typeof meta>={};
Полный файл src/Button.stories.tsx для этого состояния:
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';
const meta = {
title: 'Nabor/Button', component: Button, tags: ['autodocs'],
parameters: { docs: { description: { component: 'Кнопка выполняет действие. По умолчанию type=button; для отправки формы задайте type=submit. pending запрещает повторный клик, но не заменяет идемпотентность сервера.' } } },
args: { children: 'Сохранить', variant: 'primary', size: 'regular' },
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'danger'] },
size: { control: 'radio', options: ['regular', 'compact'] },
children: { control: 'text' }, onClick: { control: false },
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {};
export const Secondary: Story = { args: { variant: 'secondary' } };
export const Danger: Story = { args: { variant: 'danger', children: 'Удалить' } };
export const Disabled: Story = { args: { disabled: true } };
export const LongLabel: Story = { args: { children: 'Сохранить изменения в настройках организации' }, decorators: [(Story) => <div style={{ maxWidth: 220 }}><Story /></div>] };
export const Pending: Story = { args: { pending: true } };
Общее правило USAGE.md различает кнопку действия и ссылку перехода. Для поля требуется видимый label, для dialog — название и управляемое открытие, для Notice — осмысленный выбор статуса или ошибки. Это небольшие правила использования, которые не выражаются целиком типами. Они дают библиотеке педагогическую основу: читатель может объяснить назначение элемента и заметить неподходящий сценарий до подключения к большой странице.
Описание ограничений особенно полезно. Например, pending уменьшает повторные пользовательские клики, но не реализует серверную идемпотентность. Это не предупреждение на всякий случай, а конкретная граница уже введённого механизма. Если её скрыть, потребитель может перенести локальную демонстрацию на создание заказа и получить дубликаты при сетевом повторе. Хорошая документация предотвращает такое неверное расширение примера.
Состояния формы организации
ProfileEditor хранит name, error, pending и saved. Каждое значение отвечает на свой вопрос. Имя представляет ввод, ошибка — результат правила текущего действия, занятость — выполняемую операцию, saved — подтверждённое завершение демонстрации. Если объединить всё в один неясный флаг valid, станет непонятно, почему кнопка отключена и к какой версии текста относится сообщение. Разделение состояний следует пользовательскому договору.
При изменении имени флаг saved сбрасывается. Прежнее сообщение о сохранении не должно описывать новый несохранённый текст. Это маленькая, но важная причинная связь. Ошибка в этой учебной форме обновляется при следующей попытке отправки; мы не скрываем её мгновенно после каждого символа. Так выбран явный режим validation, который позже можно поменять отдельным решением, например на проверку после blur или на постепенное исправление.
Обработчик формы получает FormEvent и вызывает preventDefault, поскольку дальнейший цикл реализован локально. Атрибут noValidate отключает автоматическую браузерную процедуру, чтобы наш текстовый контракт был виден в одном месте. Но компонент поля сохраняет required как семантический признак. Это не универсальная рекомендация отказаться от нативных ограничений: настоящая форма может выбрать другой договор. Важно не смешать две системы сообщений непреднамеренно.
Одно правило и один ожидаемый результат
Название после trim должно иметь длину от 2 до 80 символов в смысле выбранной JavaScript-операции. Пример не претендует на полноценную международную модель подсчёта графем. Для обычной учебной строки он показывает структуру validation. Если правило не выполнено, появляется конкретный текст у поля и операция не начинается. Если выполнено, кнопка и поле получают занятость, затем возникает Notice о завершении.
Поле отключено во время ожидания, чтобы демонстрационный результат относился к отправленной версии текста. В реальном приложении можно разрешить дальнейшее редактирование, но тогда надо хранить версию отправки и решать, как сопоставлять ответ с текущим вводом. Наш простой выбор делает временную модель обозримой. Это ограничение названо в объяснении, а не скрыто за общей фразой «форма сохраняет данные».
Где будет настоящий HTTP
Внутри ProfileEditor используется задержка Promise на 800 миллисекунд. Она не проверяет токен пользователя, не записывает базу и не подтверждает бизнес-операцию. При интеграции в клиент приложение заменяет этот участок собственным сервисным вызовом и обработкой ошибок. Базовые Button, TextField и Notice не получают скрытые URL. Библиотека остаётся независимой от маршрутов API и способов аутентификации.
Для сетевой ошибки потребуется отдельное состояние общей ошибки и понятное сообщение, сохраняющее name. Для конфликтующего изменения — правило согласования версий. Для долгой операции — политика отмены или продолжения при уходе с экрана. Эти результаты не появятся автоматически из локального таймера. Составная история полезна именно тем, что показывает место будущего расширения без ложного утверждения о законченном серверном процессе.
Документация, которую можно поддерживать
Текст API и исходный пример должны изменяться вместе. Если тип кнопки по умолчанию поменяется, надо пересмотреть объяснение и формы-потребители. Если Notice начнёт автоматически исчезать, это затронет договор времени сообщения. Документация является частью совместимости, потому что задаёт ожидания автора приложения. Случайное расхождение между примером и реализацией приводит к ошибкам даже тогда, когда обе части отдельно выглядят разумно.
Теперь мастерская показывает отдельные элементы и их составной маршрут. Читатель видит, какие состояния принадлежат форме, а какие параметры получают компоненты. Следующий завершающий урок определит публичные экспорты, CSS-вход и версию пакета. Мы подготовим структуру локальной библиотеки, сохранив статус неопубликованного учебного исходника и не выполняя её сборку или выпуск.
Пример и поддерживаемый компонент
ProfileEditor входит в мастерскую, но не становится публичной универсальной формой. Его правила названы в тексте: организация, локальная задержка, длина строки и отключение редактирования. Клиент может изучить композицию и применить механизм в другой задаче, сохранив собственный сервисный слой. Если сделать демонстрацию скрытой частью пакета, эти учебные предположения превратятся в неожиданные продуктовые ограничения.
Также различайте пример использования и буквальную рекомендацию копировать все строки. Например, noValidate выбран ради одного понятного контракта ошибки. Продукт с нативной validation может сохранить собственный порядок. Документация должна объяснить причину, чтобы автор клиента мог сделать осознанный выбор. Такой подход сохраняет преподавательскую ценность даже тогда, когда приложение решает похожую задачу иначе.
Оглавление серии, общая подборка и полный архив исходников. Для этого урока выберите lesson-11.
Первичные источники: Storybook CSF, React Vite integration, Аргументы историй, Темы и globals, Декораторы, Accessibility addon, Автоматическая документация, React useId, HTML dialog.
Редакционный статус: локальный черновик. Учебные команды, приложение и проверки не запускались; описанные результаты ожидаемые. Профиль версий закреплён, разрешённый lockfile не создавался.