Arquitectura, flujo de datos y componentes del módulo
<div data-react-form data-form-id="…"> y adjunta la librería react-form-app.querySelectorAll('[data-react-form]') itera los mounts. Según el patrón del data-form-id elige qué componente montar.GET /api/react-form/{form_id}?_format=jsondata-form-definition en el HTML, usa el JSON inline y no hace fetch.FormBuilderInterface::getForm($class) construye el array de Form API. FormSerializer lo transforma en JSON (#type→type, #states→states, etc.) ordenado por weight.definition en state. Recorre elements para construir values con defaultValue de cada campo.Object.entries(definition) → DrupalElementRenderer → statesEngine → elementTypeMap[type] → componente React.Drupal.mi_modulo.Form.MiForm para cualquier Form API class, user:42 para edición de usuario, dynamic:mi_form para formularios creados desde el admin de Drupal sin escribir PHP.
El statesEngine replica #states de Drupal Form API en React. Se evalúa en cada render, por lo que cualquier cambio en formValues actualiza instantáneamente la visibilidad, requerimiento y estado de cada campo.
// Ejemplo: campo visible sólo si checkbox está marcado states: { visible: [ { field: "acepta_terminos", condition: "checked" } ], required: [ { field: "acepta_terminos", condition: "checked" } ] }
| Condición | Evalúa como true si… |
|---|---|
value | campo === valor exacto |
checked | campo es true / 1 |
unchecked | campo es falsy o vacío |
empty | campo es null / "" |
filled | campo tiene cualquier valor |
pattern | campo matchea regex |
handleSubmit en DrupalForm.tsx captura el evento, llama e.preventDefault() y activa submitting: true.GET /session/token → string plano. Se envía como header X-CSRF-Token. Drupal valida con _csrf_request_header_token: TRUE en routing.yml.POST /api/react-form/{form_id}/submit?_format=jsonJSON.stringify(values) con el estado actual de todos los campos.{"{ success: true, messages }"} o {"{ success: false, errors: {field: msg} }"}.success: muestra mensaje de éxito, llama onSubmitSuccess?.(data).errors: setErrors(data.errors) → los campos muestran sus mensajes de error.type que no está en el mapa, DrupalElementRenderer monta un FallbackField (input text) y loguea un warning en consola. Nunca rompe el formulario.
| Método | URL | Descripción |
|---|---|---|
| GET | /api/react-form/{form_id}?_format=json |
Serializa cualquier Form API class a JSON |
| POST | /api/react-form/{form_id}/submit?_format=json |
Recibe valores del formulario |
| GET | /api/react-form/dynamic/{form_id}?_format=json |
Formulario creado desde el admin Drupal |
| POST | /api/react-form/dynamic/{form_id}/submit?_format=json |
Submit de formulario dinámico |
| GET | /api/react-form/user/{uid}?_format=json |
Definición del formulario de perfil de usuario |
| POST | /api/react-form/user/{uid}/submit?_format=json |
Guarda cambios de perfil |
| POST | /api/react-form/user/{uid}/upload/picture |
Sube foto de perfil |
| GET | /session/token |
CSRF token (Drupal core) — requerido para POST |
Opción A — Twig template
Adjuntá la librería e incluí el div con el data-form-id correcto:
{# Template Twig #} <div data-react-form data-form-id="Drupal.mi_modulo.Form.MiForm" ></div> {{ attach_library('drupal_react_form/react-form-app') }}
Opción B — JSON inline
Para evitar el fetch, pasá la definición serializada directamente en el HTML:
<div data-react-form data-form-id="mi_form" data-form-definition='{{ definition|json_encode }}' ></div>
Opción C — Formulario dinámico (sin PHP)
Creado desde /admin/drupal-react-form/forms:
<div data-react-form data-form-id="dynamic:mi_form_id" ></div>
Opción D — Perfil de usuario
<div data-react-form data-form-id="user:{{ user.id }}" ></div>
/drupal-react-form/demo/drupal-react-form/user/{uid}/edit
# o _ son ignorados. Sólo se serializa si el elemento tiene #type. Elementos de fieldset, details y container incluyen sus hijos en el campo children. El resultado se ordena por weight antes de devolver.
| Síntoma | Causa | Fix |
|---|---|---|
| Página en blanco | process.env.NODE_ENV no definido en vite.config |
Agregar define: {'{"process.env.NODE_ENV": JSON.stringify("production")'} |
| HTTP 400 en la API | Browser convierte %5C → / en URLs |
Usar puntos en data-form-id, no backslashes |
| HTTP 403 "Invalid CSRF token" | Validación manual con seed incorrecto | Usar _csrf_request_header_token: 'TRUE' en routing.yml |
| HTTP 500 user_roles() | Función global no disponible | Usar $this->entityTypeManager()->getStorage('user_role') |
| PHP sigue con error tras fix | OPcache tiene versión vieja | lando drush ev "opcache_reset();" + lando drush cr |
| TypeError: private readonly en rebuild | DependencySerializationTrait incompatible |
Cambiar a private sin readonly |