drupal_react_form

Arquitectura, flujo de datos y componentes del módulo

Drupal 11 PHP 8.3 React 18 + TypeScript Vite
1 Visión General
PHP / DRUPAL JSON CONTRACT REACT / BROWSER Form API (PHP class) FormSerializer #type → type, #states → states… ReactFormController GET /api/react-form/{form_id} DrupalFormResponse success: true form_id: "…" elements: {"{"} key: type, title, states… {"}"} index.tsx detecta [data-react-form] → routea DrupalForm.tsx fetch, state, submit ElementRenderer + Components elementTypeMap lookup + statesEngine JSON fetch() POST /submit → JSON {success, errors}
2 Flujo de carga del formulario (GET)
1
Twig renderiza el mount point
El template Drupal incluye un <div data-react-form data-form-id="…"> y adjunta la librería react-form-app.
2
index.tsx — detección y routing
querySelectorAll('[data-react-form]') itera los mounts. Según el patrón del data-form-id elige qué componente montar.
3
DrupalForm — fetch definición
GET /api/react-form/{form_id}?_format=json
Si hay data-form-definition en el HTML, usa el JSON inline y no hace fetch.
4
ReactFormController → FormSerializer
FormBuilderInterface::getForm($class) construye el array de Form API. FormSerializer lo transforma en JSON (#typetype, #statesstates, etc.) ordenado por weight.
5
DrupalForm — inicializa estado
Guarda definition en state. Recorre elements para construir values con defaultValue de cada campo.
6
Render: ElementRenderer por campo
Object.entries(definition)DrupalElementRendererstatesEngineelementTypeMap[type] → componente React.
Twig Template data-react-form + library attach index.tsx Drupal.x.Form.Y → DrupalForm user:42 → UserEditForm | dynamic:x → … DrupalForm.tsx fetch() → useState(definition, values, errors) GET API ReactFormController → FormSerializer.serialize($class) → FormBuilderInterface::getForm() JSON {"{ success, form_id, elements }"} elements: Record<key, DrupalElement> DrupalElementRenderer statesEngine → elementTypeMap → <TextField /> | <SelectField /> | … DOM renderizado
3 Routing del entry point (index.tsx)
data-form-id="…" user:\d+ ? UserEditForm uid prop → /api/react-form/user/{uid} no dynamic:x ? en REGISTRY Componente dedicado DYNAMIC_REGISTRY[id] no no DrupalForm genérico /api/react-form/{id} o /api/react-form/dynamic/{id}
Cómo elegir el patrón: 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.
4 States Engine — visibilidad y comportamiento dinámico

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ónEvalúa como true si…
valuecampo === valor exacto
checkedcampo es true / 1
uncheckedcampo es falsy o vacío
emptycampo es null / ""
filledcampo tiene cualquier valor
patterncampo matchea regex
formValues estado global del formulario evaluateStates(element.states, values) statesEngine.ts EvaluatedStates visible: boolean required: boolean disabled: boolean expanded: boolean checked: boolean visible ? no return null render componente
5 Flujo de envío (POST)
1
Usuario envía el formulario
handleSubmit en DrupalForm.tsx captura el evento, llama e.preventDefault() y activa submitting: true.
2
Obtiene CSRF token
GET /session/token → string plano. Se envía como header X-CSRF-Token. Drupal valida con _csrf_request_header_token: TRUE en routing.yml.
3
POST al endpoint de submit
POST /api/react-form/{form_id}/submit?_format=json
Body: JSON.stringify(values) con el estado actual de todos los campos.
4
Controlador PHP procesa la respuesta
Retorna {"{ success: true, messages }"} o {"{ success: false, errors: {field: msg} }"}.
5
React actualiza UI
Si success: muestra mensaje de éxito, llama onSubmitSuccess?.(data).
Si errors: setErrors(data.errors) → los campos muestran sus mensajes de error.
6 Componentes disponibles (elementTypeMap)
Texto / Input
Selección
Archivos
Acción
Layout
Otro
textfield
email
password
password_confirm
textarea
number
tel
url
color
range
search
date
datetime
datelist
select
checkbox
checkboxes
radio
radios
file
managed_file
submit
button
fieldset
details
container
item
hidden
entity_autocomplete
Tipo desconocido: Si el JSON devuelve un type que no está en el mapa, DrupalElementRenderer monta un FallbackField (input text) y loguea un warning en consola. Nunca rompe el formulario.
7 Endpoints API
MétodoURLDescripció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
8 Cómo usar el módulo

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>
URLs de demo:
/drupal-react-form/demo
/drupal-react-form/user/{uid}/edit
9 FormSerializer — traducción PHP → JSON
Drupal Form API Array (PHP) #type: 'textfield' #title: t('Nombre') #required: TRUE #default_value: 'Andres' #states: ['visible' => ...] #weight: 10 FormSerializer serializeElement() + serializeStates() DrupalElement (JSON / TypeScript) type: "textfield" title: "Nombre" required: true defaultValue: "Andres" states: {"{ visible: [{field,condition}] }"} weight: 10 #key → key (quita el #) null / vacíos → omitidos (array_filter)
Reglas del serializer: Los keys que empiezan con # 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.
10 Troubleshooting conocido
SíntomaCausaFix
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
drupal_react_form · Drupal 11 + React 18 + Vite · generado 2026-05-16