Как я могу создать большую динамическую форму в React + TypeScript + MUI, используя повторно используемые компонентыJavascript

Форум по Javascript
Ответить
Anonymous
 Как я могу создать большую динамическую форму в React + TypeScript + MUI, используя повторно используемые компоненты

Сообщение Anonymous »

Я создаю большую форму в React + TypeScript, используя Material UI (MUI). Форма имеет множество полей (более 100), сгруппированных в разделы, и каждое поле может быть разного типа: текст, дата, дата-время, раскрывающийся список/выбор, флажок и т. д.

✅ Сообщение в блоге: Создание огромных форм в React с помощью повторно используемых блоков (Config-Driven + MUI)
Большие формы болезненно, когда каждое поле написано от руки: повторяющийся , повторяющаяся логика проверки и множество шаблонных обработчиков изменений.
Масштабируемый подход заключается в создании форм, управляемых конфигурацией:
  • Вы описываете каждое поле в конфигурации: имя, метка, тип, требуемые, параметры, проверки, и т. д.
  • Общий InputMapper отображает правильный компонент пользовательского интерфейса.
  • Общий механизм проверки проверяет изменения и/или отправку.
  • Форма становится легко расширяемой: добавьте новую запись конфигурации поля вместо написания нового JSX.
В этом посте показана удобная для производства структура с:
✅ текстом, textarea, число

✅ дата, datetime (MUI X Pickers + Dayjs)

✅ выберите (раскрывающийся список), множественный выбор

✅ флажок

✅ Проверка: требуется, шаблон, длина, числовой диапазон, мин/макс дата и пользовательская проверка

✅ Разделы для огромных форм

✅ Многоразовый компонент FormRenderer

1) Установите зависимости

Код: Выделить всё

npm i @mui/material @emotion/react @emotion/styled
npm i @mui/x-date-pickers dayjs
Если вы уже используете MUI, вероятно, у вас уже установлен Material + Emotion.


2) Рекомендуемая структура папок

Код: Выделить всё

src/
form/
types.ts
validation.ts
useFormState.ts
FormRenderer.tsx
InputMapper.tsx
fields/
TextInput.tsx
DateInput.tsx
SelectInput.tsx
CheckboxInput.tsx
pages/
HugeFormExamplePage.tsx
✅ Код

Код: Выделить всё

src/form/types.ts
Это определяет конфигурации строго типизированных полей.

Код: Выделить всё

import { Dayjs } from "dayjs";

export type FieldType =
| "text"
| "textarea"
| "number"
| "date"
| "datetime"
| "select"
| "multiselect"
| "checkbox";

export type Option = { label: string; value: string };

export type CustomValidator = (
value: any,
values: TValues,
field: FieldConfig
) => string | undefined;

export interface ValidationRules {
required?: boolean;
requiredMessage?: string;

pattern?: RegExp;
patternMessage?: string;

minLength?: number;
maxLength?: number;

min?: number;
max?: number;

// For date/datetime types
minDate?: Dayjs;
maxDate?: Dayjs;

validate?: CustomValidator;
}

interface BaseField {
name: string;
label: string;
type: FieldType;
helperText?: string;
disabled?: boolean;
defaultValue?: any;
validation?: ValidationRules;
}

export interface TextFieldConfig extends BaseField {
type: "text" | "textarea" | "number";
placeholder?: string;
}

export interface DateFieldConfig extends BaseField {
type: "date" | "datetime";
}

export interface SelectFieldConfig extends BaseField {
type: "select" | "multiselect";
options: Option[];
}

export interface CheckboxFieldConfig extends BaseField {
type: "checkbox";
}

export type FieldConfig =
| TextFieldConfig
| DateFieldConfig
| SelectFieldConfig
| CheckboxFieldConfig;

export interface SectionConfig {
title: string;
description?: string;
fields: FieldConfig[];
}

Код: Выделить всё

src/form/validation.ts
Единое место для последовательной проверки полей.

Код: Выделить всё

import dayjs from "dayjs";
import { FieldConfig } from "./types";

export function validateField(
value: any,
field: FieldConfig,
values: TValues
): string {
const rules = field.validation;

// 1) Required
const required = rules?.required ?? false;

if (required) {
const isEmpty =
value === null ||
value === undefined ||
value === "" ||
(Array.isArray(value) && value.length === 0) ||
(field.type === "checkbox" &&  value !== true);

if (isEmpty) return rules?.requiredMessage ?? `${field.label} is required`;
}

// 2) Pattern (usually for text)
if (rules?.pattern && typeof value === "string" && value) {
if (!rules.pattern.test(value)) {
return rules.patternMessage ?? `${field.label} format is invalid`;
}
}

// 3) Length (string)
if (typeof value === "string" && value) {
if (rules?.minLength && value.length < rules.minLength) {
return `${field.label} must be at least ${rules.minLength} characters`;
}
if (rules?.maxLength && value.length > rules.maxLength) {
return `${field.label} must be at most ${rules.maxLength} characters`;
}
}

// 4) Numeric range
if (field.type === "number" && value !== null && value !== undefined && value !== "") {
const num = Number(value);
if (Number.isNaN(num)) return `${field.label} must be a number`;
if (rules?.min !== undefined && num < rules.min) {
return `${field.label} must be >= ${rules.min}`;
}
if (rules?.max !== undefined && num > rules.max) {
return `${field.label} must be  s.fields);
}

function buildInitialValues(fields: FieldConfig[]) {
const values: Record = {};
for (const f of fields) {
if (f.defaultValue !== undefined) {
values[f.name] = f.defaultValue;
continue;
}

// sensible defaults by type
switch (f.type) {
case "text":
case "textarea":
case "number":
case "select":
values[f.name] = "";
break;
case "multiselect":
values[f.name] = [];
break;
case "checkbox":
values[f.name] = false;
break;
case "date":
case "datetime":
values[f.name] = null; // or dayjs() if you want current date
break;
default:
values[f.name] = "";
}
}
return values;
}

export function useFormState(sections: SectionConfig[]) {
const fields = useMemo(() => flattenFields(sections), [sections]);

const [values, setValues] = useState(() =>
buildInitialValues(fields)
);
const [errors, setErrors] = useState({});

const setFieldValue = (name: string, value: any) => {
setValues((prev) => {
const next = { ...prev, [name]: value };

const field = fields.find((f) =>  f.name === name);
if (field) {
const err = validateField(value, field, next);
setErrors((prevErr) => ({ ...prevErr, [name]: err }));
}
return next;
});
};

const validateForm = () => {
const nextErrors = validateAll(fields, values);
setErrors(nextErrors);
return nextErrors;
};

const reset = () => {
setValues(buildInitialValues(fields));
setErrors({});
};

return { fields, values, errors, setFieldValue, validateForm, reset, setValues, setErrors };
}
Примечание: импорт dayjs включен на тот случай, если вам позже понадобятся значения по умолчанию, такие как dayjs(), для определенных полей даты.

Код: Выделить всё

src/form/fields/TextInput.tsx

Код: Выделить всё

import React from "react";
import { TextField } from "@mui/material";
import { TextFieldConfig } from "../types";

type Props = {
field: TextFieldConfig;
value: any;
error?: string;
onChange: (name: string, value: any) => void;
};

export default function TextInput({ field, value, error, onChange }: Props) {
const isTextArea = field.type === "textarea";

return (
 onChange(field.name, e.target.value)}
fullWidth
disabled={field.disabled}
placeholder={field.placeholder}
multiline={isTextArea}
minRows={isTextArea ? 3 : undefined}
type={field.type === "number" ? "number" : "text"}
error={!!error}
helperText={error || field.helperText}
/>
);
}

Код: Выделить всё

src/form/fields/DateInput.tsx
При этом используется MUI X DatePicker / DateTimePicker и аккуратно пересылается error/helperText.

Код: Выделить всё

import React from "react";
import { DatePicker, DateTimePicker } from "@mui/x-date-pickers";
import { Dayjs } from "dayjs";
import { DateFieldConfig } from "../types";

type Props = {
field: DateFieldConfig;
value: Dayjs | null;
error?: string;
onChange: (name: string, value: Dayjs | null) => void;
};

export default function DateInput({ field, value, error, onChange }: Props) {
const Picker = field.type === "datetime" ? DateTimePicker : DatePicker;

return (
 onChange(field.name, d)}
disabled={field.disabled}
slotProps={{
textField: {
fullWidth: true,
name: field.name,
error: !!error,
helperText: error || field.helperText,
},
}}
/>
);
}
✅ Этот подход slotProps является наиболее стабильным с современным MUI X.

Код: Выделить всё

src/form/fields/SelectInput.tsx
Поддерживает как одиночный, так и множественный выбор.

Код: Выделить всё

import React from "react";
import {
Checkbox,
ListItemText,
MenuItem,
TextField,
} from "@mui/material";
import { SelectFieldConfig } from "../types";

type Props = {
field: SelectFieldConfig;
value: any;
error?: string;
onChange: (name: string, value: any) => void;
};

export default function SelectInput({ field, value, error, onChange }: Props) {
const multiple = field.type === "multiselect";

return (
 onChange(field.name, e.target.value)}
disabled={field.disabled}
error={!!error}
helperText={error || field.helperText}
SelectProps={{
multiple,
renderValue: multiple
? (selected) =>
(selected as string[])
.map((v) => field.options.find((o) => o.value === v)?.label ?? v)
.join(", ")
: undefined,
}}
>
{field.options.map((opt) =>  (

{multiple ? (


[*]

) : (
opt.label
)}

))}

);
}

Код: Выделить всё

src/form/fields/CheckboxInput.tsx

Код: Выделить всё

import React from "react";
import { Checkbox, FormControlLabel, FormHelperText } from "@mui/material";
import { CheckboxFieldConfig } from "../types";

type Props = {
field: CheckboxFieldConfig;
value: boolean;
error?: string;
onChange: (name: string, value: boolean) => void;
};

export default function CheckboxInput({ field, value, error, onChange }: Props) {
return (


}
label={field.label}
/>

{error || field.helperText}


);
}

Код: Выделить всё

src/form/InputMapper.tsx
Суть повторного использования: выберите, какой компонент отображать в зависимости от типа поля.

Код: Выделить всё

import React from "react";
import { FieldConfig } from "./types";
import TextInput from "./fields/TextInput";
import DateInput from "./fields/DateInput";
import SelectInput from "./fields/SelectInput";
import CheckboxInput from "./fields/CheckboxInput";
import { Dayjs } from "dayjs";

type Props = {
field: FieldConfig;
value: any;
error?: string;
onChange: (name: string, value: any) => void;
};

export default function InputMapper({ field, value, error, onChange }: Props) {
switch (field.type) {
case "text":
case "textarea":
case "number":
return (

);

case "date":
case "datetime":
return (

);

case "select":
case "multiselect":
return (

);

case "checkbox":
return (

);

default:
return (

);
}
}
✅ Обратите внимание, что мы явно поддерживаем «datetime» (в вашем предыдущем коде этот регистр отсутствовал).

Код: Выделить всё

src/form/FormRenderer.tsx
Это отображает полную форму по разделам и полям.

Код: Выделить всё

import React from "react";
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
import InputMapper from "./InputMapper";
import { SectionConfig } from "./types";
import { useFormState } from "./useFormState";

type Props = {
sections: SectionConfig[];
onSubmit: (values: Record) => void;
};

export default function FormRenderer({ sections, onSubmit }: Props) {
const { values, errors, setFieldValue, validateForm, reset } =
useFormState(sections);

const handleSubmit = (e: React.FormEvent) =>  {
e.preventDefault();
const nextErrors = validateForm();
const hasErrors = Object.values(nextErrors).some(Boolean);
if (!hasErrors) onSubmit(values);
};

return (


{sections.map((section) => (

{section.title}
{section.description ? (

{section.description}

) : null}




{section.fields.map((field) => (

))}


))}



Reset


Submit




);
}

Код: Выделить всё

src/pages/HugeFormExamplePage.tsx
В этом примере показано, как определить поля формы исключительно через конфигурацию.

Код: Выделить всё

import React from "react";
import { Container, Paper, Typography } from "@mui/material";
import FormRenderer from "../form/FormRenderer";
import { SectionConfig } from "../form/types";
import dayjs from "dayjs";

// IMPORTANT: MUI X Date Pickers require LocalizationProvider
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";

const sections: SectionConfig[] = [
{
title: "Personal Info",
description: "Basic info collected for profile creation.",
fields: [
{
name: "fullName",
label: "Full Name",
type: "text",
validation: { required: true, minLength: 2 },
},
{
name: "bio",
label: "Bio",
type: "textarea",
helperText: "Write a short description (optional)",
validation: { maxLength: 240 },
},
{
name: "age",
label: "Age",
type: "number",
validation: { min: 1, max: 120 },
},
],
},
{
title: "Dates",
fields: [
{
name: "startDate",
label: "Start Date",
type: "date",
validation: {
required: true,
minDate: dayjs().subtract(1, "year"),
maxDate: dayjs().add(1, "year"),
},
},
{
name: "appointmentAt",
label: "Appointment (Date & Time)",
type: "datetime",
validation: { required: true },
},
],
},
{
title: "Preferences",
fields: [
{
name: "role",
label: "Role",
type: "select",
options: [
{ label: "Developer", value: "dev" },
{ label: "QA Engineer", value: "qa" },
{ label: "Manager", value: "mgr" },
],
validation: { required: true },
},
{
name: "skills",
label: "Skills",
type: "multiselect",
options: [
{ label: "React", value: "react" },
{ label: "TypeScript", value: "ts" },
{ label: "Azure", value: "azure"  },
],
validation: {
validate: (value) =>
Array.isArray(value) && value.length > 0
? undefined
: "Please select at least one skill",
},
},
{
name: "termsAccepted",
label: "I accept the terms & conditions",
type: "checkbox",
validation: { required: true, requiredMessage: "You must accept terms" },
},
],
},
];

export default function HugeFormExamplePage() {
const handleSubmit = (values: Record) => {
// Typically: API call here
console.log("Form Submit:", values);
};

return (




Config-driven Huge Form (React + MUI)






);
}
✔Как это масштабируется до «огромных» форм
А) Добавить новое поле? Просто добавьте конфигурацию
Никакого нового JSX. Никакого нового местного штата. Никаких повторяющихся обработчиков.
Пример: добавьте поле с проверкой шаблона:

Код: Выделить всё

{
name: "assetId",
label: "Asset ID",
type: "text",
validation: {
required: true,
pattern: /^[A-Z0-9]+$/,
patternMessage: "Only uppercase letters and numbers allowed",
},
}
Б) Добавить новый тип поля? Создайте один компонент + добавьте его в картограф
Пример: Хотите тип радио?
  • Создайте RadioInput.tsx
  • Обновите объединение FieldType и FieldConfig
  • Добавьте регистр «радио»: return
C) Проверка остается централизованной
Вместо разрозненных проверок повсюду, validateField() обеспечивает согласованность.
D) Отрисовка на основе разделов обеспечивает управляемость пользовательского интерфейса
Огромные формы остаются читабельными:
  • «Технические атрибуты»
  • »PI Атрибуты»
  • «Атрибуты именной таблички»
  • и т. д.


Подробнее здесь: https://stackoverflow.com/questions/798 ... -a-reusabl
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Javascript»