Гибкий лэйаут для динамических форм с react-jsonschema-form

от автора

Библиотека 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 совместно с кастомными шаблонами, мы можем значительно расширить его возможности. Теперь наша форма больше не ограничена одной колонкой, а её макет можно легко настроить, изменяя всего лишь схему лэйаута.

Ссылка на GitHub


ссылка на оригинал статьи https://habr.com/ru/articles/884862/