Гибкий лэйаут для динамических форм с react-jsonschema-form
- воскресенье, 23 февраля 2025 г. в 00:00:06
Библиотека react‑jsonschema‑form (RJSF) предназначена для автоматической генерации форм на основе JSON‑схемы. Вы задаёте схему, а RJSF берёт на себя остальное: отображение полей ввода, валидацию и обработку данных. Это удобный и простой в использовании инструмент, тем не менее, у библиотеки есть определённые ограничения. Одно из них — отсутствие поддержки многоколоночных макетов «из коробки».
В этой статье я покажу, как можно добавить гибкость в структуру формы, используя кастомные шаблоны.
По умолчанию RJSF располагает все поля формы в одну колонку, растягивая их на всю ширину контейнера. Это может быть приемлемо для простых форм, но в реальных проектах зачастую возникает необходимость в более сложной структуре.
Допустим, у нас есть JSON-схема, описывающая простую форму:
{
"title": "Заполните информацию о пользователе",
"type": "object",
"required": ["name", "username"],
"properties": {
"name": { "type": "string", "title": "ФИО" },
"username": { "type": "string", "title": "Логин" },
"email": { "type": "string", "title": "E-mail" },
"telephone": { "type": "string", "title": "Телефон" },
"telegram": { "type": "string", "title": "Telegram" },
"date": { "type": "string", "format": "date", "title": "Дата рождения" },
"bio": { "type": "string", "title": "О себе" },
"city": { "type": "string", "title": "Город" }
}
}
Если использовать её без дополнительных настроек, форма будет выглядеть следующим образом:
Как видно, все поля выстроены в один длинный список, что далеко не всегда удобно.
Для демонстрации я буду использовать RJSF совместно с Ant Design, но предложенный подход можно адаптировать для любой библиотеки компонентов с минимальными изменениями.
Чтобы реализовать многоколоночную структуру, сначала определимся со схемой, которая будет описывать расположение полей:
{
"sections": [ // массив секций
{
"id": "string",
"header": { // заголовок секции
"title": "string",
"align": "string",
"heading_size": "number"
},
"blocks": [ // массив блоков в секции
{
"id": "string",
"fields": { // список полей в блоке
"fieldName": {
"width": "number" // Ширина поля относительно блока. Не обязательный параметр, если не указана будет во всю ширину
}
},
"width": "number" // Ширина блока. Не обязательный параметр, если не указана - 100%/число блоков в секции
}
]
}
]
}
RJSF поддерживает кастомные шаблоны (templates), которые позволяют изменять макет формы под свои нужды. Я буду использовать ObjectFieldTemplate
, который отвечает за рендеринг контейнера для всех полей объекта. Он позволяет переопределять стандартное расположение элементов, задавая кастомную разметку.
Код ObjectFieldTemplate.tsx
import React, { useState, useEffect, useMemo } from 'react';
import { Typography, Col } from 'antd';
import './ObjectFieldTemplate.css';
type PropertyType = {
content: any;
name: string;
};
function ObjectFieldTemplate(props: any) {
// properties нужны нам для вывода в конце формы полей, которые мы могли забыть перечислить в layout
const [properties, setProperties] = useState<Record<string, PropertyType>>(
{}
);
const layout = props.formContext.getLayout();
useEffect(() => {
const obj = (props.properties || []).reduce(
(acc: any, curr: any) => ({
...acc,
[curr.name]: { content: curr.content, name: curr.name },
}),
{}
);
setProperties(obj);
}, [props.properties]);
const gridLayout = useMemo(
() =>
layout
? layout.sections.map((section: any) => {
return (
<div key={section.id}>
{section.header && (
<Typography.Title
level={section.header.heading_size || 4}
style={{
textAlign: section.header.align || 'center',
}}
>
{section.header.title}
</Typography.Title>
)}
<div className="layout__section">
{section.blocks.map((block: any) => {
return (
<div
key={`${section.id}-${block.id}`}
className="layout__block"
style={{
width: `${
block.width ? 100 / (24 / block.width) : 100
}%`,
}}
>
{Object.keys(block.fields).map((el: any) => {
const field = properties[el];
delete properties[el];
return field ? (
<Col
key={field.name}
data-field={field.name}
span={block.fields[el].width || 24}
style={{ padding: '0 8px' }}
>
{field.content}
</Col>
) : null;
})}
</div>
);
})}
</div>
</div>
);
})
: null,
[properties, layout]
);
return (
<div>
{props.title ? (
<Typography.Title level={3}>{props.title}</Typography.Title>
) : null}
{props.description ? (
<Typography.Text>{props.description}</Typography.Text>
) : null}
{gridLayout}
{/* поля, которые могли быть не указаны в layout */}
{props.properties.map((el: any) =>
properties[el.name] ? (
<div key={el.name} style={{ padding: '0 8px' }}>
{el.content}
</div>
) : null
)}
</div>
);
}
export default ObjectFieldTemplate;
Стили для ObjectFieldTemplate
.layout {
display: flex;
flex-wrap: wrap;
}
Вот как будет выглядеть лэйаут для моей формы:
{
"sections": [
{
"id": "section1",
"header": {
"title": "Основная информация",
"align": "center",
"heading_size": 4
},
"blocks": [
{
"id": "block1",
"fields": {
"name": { "width": 16 },
"username": { "width": 8 },
"telegram": { "width": 8 },
"email": { "width": 8 },
"telephone": { "width": 8 }
}
}
]
},
{
"id": "section2",
"header": {
"title": "Общие сведения",
"align": "center"
},
"blocks": [
{
"id": "block2",
"fields": { "date": {}, "city": {} },
"width": 8
},
{
"id": "block3",
"fields": { "bio": {} },
"width": 16
}
]
}
]
}
После применения кастомного шаблона форма выглядит совершенно по-другому:
В данном примере я использую Ant Design, в котором грид-система построена на 24 колонках. Для каждого элемента формы задаётся ширина в рамках 24-колоночной сетки и мы можем легко менять количество колонок для каждого блока, управляя этим параметром.
Используя react-jsonschema-form совместно с кастомными шаблонами, мы можем значительно расширить его возможности. Теперь наша форма больше не ограничена одной колонкой, а её макет можно легко настроить, изменяя всего лишь схему лэйаута.