← Назад к вопросам

Как создать форму на 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.)
  • Реализуй валидацию на основе конфига
  • Используй рекурсию для сложных структур (секции, массивы)
  • Кешируй конфиг, если он не меняется часто
  • Протестируй компонент на различных конфигурациях