Ответ
Для динамической генерации форм используется подход, при котором бэкенд предоставляет машиночитаемую схему данных (например, в формате JSON Schema), а фронтенд использует эту схему для построения пользовательского интерфейса.
Это позволяет иметь единый источник правды о структуре данных и правилах валидации, избегая дублирования логики на клиенте и сервере.
Пошаговый процесс
- Бэкенд: Определение модели и эндпоинта для схемы. На бэкенде создается модель данных. Современные фреймворки позволяют легко генерировать из нее JSON-схему.
- Фронтенд: Запрос схемы. При загрузке страницы фронтенд-приложение отправляет GET-запрос на специальный эндпоинт, чтобы получить эту схему.
- Фронтенд: Рендеринг формы. Получив JSON, фронтенд-код итерируется по свойствам схемы и динамически создает соответствующие поля формы (
input,select,checkboxи т.д.), включая типы данных, обязательность и другие ограничения.
Пример: FastAPI (бэкенд) и JavaScript (фронтенд)
1. Бэкенд на FastAPI с Pydantic
Pydantic идеально подходит для этой задачи, так как автоматически генерирует JSON Schema из Python-классов.
from fastapi import FastAPI
from pydantic import BaseModel, Field, EmailStr
app = FastAPI()
# Описываем модель данных с валидацией и метаданными
class UserProfile(BaseModel):
username: str = Field(..., min_length=3, max_length=50, title="Имя пользователя")
email: EmailStr = Field(..., title="Электронная почта")
age: int | None = Field(None, gt=18, title="Возраст", description="Полных лет")
# Эндпоинт, который возвращает JSON-схему модели
@app.get("/api/v1/form-schema/user-profile")
async def get_user_profile_schema():
# .model_json_schema() генерирует стандартную JSON Schema
return UserProfile.model_json_schema()
2. Фронтенд на чистом JavaScript
Фронтенд запрашивает схему и строит на ее основе HTML-форму.
async function renderForm(containerId, schemaUrl) {
const response = await fetch(schemaUrl);
const schema = await response.json();
const form = document.createElement('form');
const container = document.getElementById(containerId);
for (const [fieldName, fieldProps] of Object.entries(schema.properties)) {
const label = document.createElement('label');
label.textContent = fieldProps.title || fieldName;
const input = document.createElement('input');
input.name = fieldName;
input.type = (fieldProps.type === 'integer') ? 'number' : 'text'; // Простое определение типа
if (schema.required && schema.required.includes(fieldName)) {
input.required = true;
}
form.appendChild(label);
form.appendChild(input);
form.appendChild(document.createElement('br'));
}
const submitButton = document.createElement('button');
submitButton.type = 'submit';
submitButton.textContent = 'Отправить';
form.appendChild(submitButton);
container.appendChild(form);
}
// Вызов функции для рендеринга
renderForm('form-container', '/api/v1/form-schema/user-profile');
Этот подход значительно упрощает поддержку и развитие сложных форм, так как все изменения в модели данных автоматически отражаются в интерфейсе.