← Назад к вопросам
Как создать форму на Vue на основании полученного config с JSON в виде дерева компонентов?
1.3 Junior🔥 121 комментариев
#Vue.js
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Создание динамической формы из JSON конфигурации
Динамическое создание форм на основе JSON конфигурации — это мощная техника, позволяющая генерировать различные формы без изменения кода. Вместо того чтобы писать каждую форму вручную, описываешь структуру в JSON и используешь компоненты для рендеринга. Покажу подход, который работает во фронтенде (примеры на React, но концепция универсальна).
Структура JSON конфигурации
{
"title": "User Registration Form",
"fields": [
{
"id": "name",
"type": "text",
"label": "Full Name",
"required": true,
"validation": {
"minLength": 2,
"maxLength": 50
}
},
{
"id": "email",
"type": "email",
"label": "Email Address",
"required": true,
"validation": {
"pattern": "^[^@]+@[^@]+\\.[^@]+$"
}
},
{
"id": "country",
"type": "select",
"label": "Country",
"required": false,
"options": [
{ "value": "us", "label": "United States" },
{ "value": "uk", "label": "United Kingdom" },
{ "value": "ru", "label": "Russia" }
]
},
{
"id": "agree",
"type": "checkbox",
"label": "I agree to terms",
"required": true
},
{
"id": "message",
"type": "textarea",
"label": "Message",
"required": false,
"rows": 5
},
{
"id": "roles",
"type": "radio",
"label": "Select Role",
"options": [
{ "value": "user", "label": "User" },
{ "value": "admin", "label": "Administrator" }
]
}
],
"submitButton": {
"text": "Register",
"className": "btn-primary"
}
}
Решение 1: Компонент DynamicForm
Создаём универсальный компонент, который рендерит форму на основе конфига:
import React, { useState, useCallback } from 'react';
function DynamicForm({ config, onSubmit }) {
const [formData, setFormData] = useState({});
const [errors, setErrors] = useState({});
const handleChange = useCallback((fieldId, value) => {
setFormData(prev => ({ ...prev, [fieldId]: value }));
// Очистить ошибку при изменении поля
if (errors[fieldId]) {
setErrors(prev => {
const newErrors = { ...prev };
delete newErrors[fieldId];
return newErrors;
});
}
}, [errors]);
const validateField = (field, value) => {
if (field.required && !value) {
return `${field.label} is required`;
}
if (field.validation) {
const { minLength, maxLength, pattern } = field.validation;
if (minLength && value.length < minLength) {
return `${field.label} must be at least ${minLength} characters`;
}
if (maxLength && value.length > maxLength) {
return `${field.label} must be at most ${maxLength} characters`;
}
if (pattern && !new RegExp(pattern).test(value)) {
return `${field.label} format is invalid`;
}
}
return null;
};
const handleSubmit = (e) => {
e.preventDefault();
// Валидация всех полей
const newErrors = {};
config.fields.forEach(field => {
const error = validateField(field, formData[field.id] || '');
if (error) {
newErrors[field.id] = error;
}
});
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
onSubmit(formData);
}
};
return (
<form onSubmit={handleSubmit} className="dynamic-form">
<h1>{config.title}</h1>
{config.fields.map(field => (
<FormField
key={field.id}
field={field}
value={formData[field.id] || ''}
onChange={(value) => handleChange(field.id, value)}
error={errors[field.id]}
/>
))}
<button
type="submit"
className={config.submitButton?.className || 'btn-default'}
>
{config.submitButton?.text || 'Submit'}
</button>
</form>
);
}
Компонент FormField — рендеринг отдельных полей
function FormField({ field, value, onChange, error }) {
const renderInput = () => {
switch (field.type) {
case 'text':
case 'email':
case 'password':
return (
<input
type={field.type}
id={field.id}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={field.placeholder}
className="form-input"
/>
);
case 'textarea':
return (
<textarea
id={field.id}
value={value}
onChange={(e) => onChange(e.target.value)}
rows={field.rows || 3}
className="form-textarea"
/>
);
case 'select':
return (
<select
id={field.id}
value={value}
onChange={(e) => onChange(e.target.value)}
className="form-select"
>
<option value="">-- Select --</option>
{field.options.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
case 'checkbox':
return (
<input
type="checkbox"
id={field.id}
checked={value === true}
onChange={(e) => onChange(e.target.checked)}
className="form-checkbox"
/>
);
case 'radio':
return (
<div className="radio-group">
{field.options.map(option => (
<label key={option.value} className="radio-label">
<input
type="radio"
name={field.id}
value={option.value}
checked={value === option.value}
onChange={(e) => onChange(e.target.value)}
/>
{option.label}
</label>
))}
</div>
);
case 'date':
return (
<input
type="date"
id={field.id}
value={value}
onChange={(e) => onChange(e.target.value)}
className="form-input"
/>
);
default:
return null;
}
};
return (
<div className={`form-group ${error ? 'has-error' : ''}`}>
<label htmlFor={field.id} className="form-label">
{field.label}
{field.required && <span className="required">*</span>}
</label>
{renderInput()}
{error && <div className="error-message">{error}</div>}
</div>
);
}
Решение 2: Рекурсивные секции и вложенные формы
Для более сложных форм со строками и секциями:
function DynamicFormAdvanced({ config, onSubmit }) {
const [formData, setFormData] = useState({});
const renderSection = (section, parentPath = '') => {
if (section.type === 'section') {
return (
<fieldset key={section.id} className="form-section">
{section.title && <legend>{section.title}</legend>}
{section.fields.map(field =>
renderField(field, parentPath)
)}
</fieldset>
);
}
if (section.type === 'array') {
return (
<div key={section.id} className="form-array">
<h3>{section.title}</h3>
{(formData[section.id] || []).map((item, index) => (
<div key={index} className="array-item">
{section.fields.map(field =>
renderField(field, `${section.id}[${index}]`)
)}
</div>
))}
<button
type="button"
onClick={() => {
setFormData(prev => ({
...prev,
[section.id]: [...(prev[section.id] || []), {}]
}));
}}
>
Add Item
</button>
</div>
);
}
};
const renderField = (field, parentPath) => {
const fieldPath = parentPath ? `${parentPath}.${field.id}` : field.id;
return (
<FormField
key={fieldPath}
field={field}
value={getNestedValue(formData, fieldPath)}
onChange={(value) => setNestedValue(formData, fieldPath, value)}
/>
);
};
return (
<form onSubmit={(e) => {
e.preventDefault();
onSubmit(formData);
}}>
{config.sections?.map(section => renderSection(section))}
</form>
);
}
// Утилиты для работы с вложенными объектами
function getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => {
if (key.includes('[')) {
const [arrayKey, index] = key.match(/(\w+)\[(\d+)\]/).slice(1);
return current?.[arrayKey]?.[index];
}
return current?.[key];
}, obj);
}
function setNestedValue(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (key.includes('[')) {
const [arrayKey, index] = key.match(/(\w+)\[(\d+)\]/).slice(1);
if (!current[arrayKey]) current[arrayKey] = [];
if (!current[arrayKey][index]) current[arrayKey][index] = {};
current = current[arrayKey][index];
} else {
if (!current[key]) current[key] = {};
current = current[key];
}
}
const lastKey = keys[keys.length - 1];
current[lastKey] = value;
}
Практический пример: форма с условной видимостью
const formConfig = {
title: "Product Form",
fields: [
{
id: "name",
type: "text",
label: "Product Name",
required: true
},
{
id: "category",
type: "select",
label: "Category",
options: [
{ value: "physical", label: "Physical Product" },
{ value: "digital", label: "Digital Product" }
]
},
{
id: "weight",
type: "text",
label: "Weight (kg)",
required: true,
condition: (formData) => formData.category === 'physical'
},
{
id: "downloadUrl",
type: "text",
label: "Download URL",
required: true,
condition: (formData) => formData.category === 'digital'
}
]
};
function ConditionalDynamicForm({ config, onSubmit }) {
const [formData, setFormData] = useState({});
const visibleFields = config.fields.filter(field => {
if (field.condition) {
return field.condition(formData);
}
return true;
});
return (
<form onSubmit={(e) => {
e.preventDefault();
onSubmit(formData);
}}>
{visibleFields.map(field => (
<FormField
key={field.id}
field={field}
value={formData[field.id] || ''}
onChange={(value) => setFormData(prev => ({
...prev,
[field.id]: value
}))}
/>
))}
<button type="submit">Submit</button>
</form>
);
}
Использование в приложении
function App() {
const [formConfig, setFormConfig] = useState(null);
useEffect(() => {
// Загрузить конфиг с сервера
fetch('/api/form-config')
.then(r => r.json())
.then(setFormConfig);
}, []);
const handleFormSubmit = (data) => {
console.log('Form data:', data);
// Отправить на сервер
fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
};
if (!formConfig) return <div>Loading...</div>;
return <DynamicForm config={formConfig} onSubmit={handleFormSubmit} />;
}
Преимущества динамических форм
- Гибкость: Изменяй конфиг без изменения кода
- Переиспользование: Один компонент для всех форм
- Легче тестировать: JSON конфиг легче проверить
- Backend control: Сервер может контролировать структуру формы
- Масштабируемость: Просто добавляй новые типы полей
Выводы
- Определи JSON схему, описывающую форму
- Создай компонент, рендерящий форму по конфигу
- Поддерживай различные типы полей (text, select, radio, checkbox, etc.)
- Реализуй валидацию на основе конфига
- Используй рекурсию для сложных структур (секции, массивы)
- Кешируй конфиг, если он не меняется часто
- Протестируй компонент на различных конфигурациях