Torna al blog

Drupal #states - Campi Condizionali nei Form Senza JavaScript

#states è una proprietà della Form API di Drupal che permette di controllare lo stato di un elemento del form - visibile/nascosto, abilitato/disabilitato, obbligatorio/opzionale - in base al valore di un altro elemento.

Il tutto senza scrivere una riga di JavaScript e senza fare chiamate al server.

Drupal traduce la dichiarazione #states in JavaScript automaticamente tramite il file core/misc/states.js. Tu dichiari cosa deve succedere. Drupal si occupa del come.

Sintassi base

$form['campo_target']['#states'] = [
  'stato' => [
    'selettore CSS' => ['condizione' => 'valore'],
  ],
];

I tre pezzi della dichiarazione:

  • stato - l'effetto da applicare: visible, invisible, enabled, disabled, required, optional, checked, unchecked, expanded, collapsed

  • selettore CSS - identifica l'elemento trigger (quello che l'utente cambia)

  • condizione → valore - il tipo di confronto: value, checked, empty, filled

Esempio reale: form evento con modalità di partecipazione

Immagina un content type evento con un campo select che determina la modalità di partecipazione: In presenza, Online o Ibrido.

In base alla scelta, devono comparire campi diversi: l'indirizzo della sede per gli eventi in presenza, il link streaming per quelli online.

Il campo trigger

field_event_type (list_string)
├── "presenza"  → In presenza
├── "online"    → Online
└── "ibrido"    → Ibrido

Nel DOM, Drupal renderizza il campo così:

<select name="field_event_type">
  <option value="">- Seleziona -</option>
  <option value="presenza">In presenza</option>
  <option value="online">Online</option>
  <option value="ibrido">Ibrido</option>
</select>

I campi target

// Mostra "Indirizzo sede" solo quando la select vale "presenza"
$form['field_venue_address']['#states'] = [
  'visible' => [
    ':input[name="field_event_type"]' => ['value' => 'presenza'],
  ],
];

// Mostra "Link streaming" solo quando la select vale "online"
$form['field_streaming_link']['#states'] = [
  'visible' => [
    ':input[name="field_event_type"]' => ['value' => 'online'],
  ],
];

E per la modalità ibrida? Servono entrambi i campi.

Qui entra in gioco la condizione OR:

// "Indirizzo sede" visibile per "presenza" OPPURE "ibrido"
$form['field_venue_address']['#states'] = [
  'visible' => [
    [':input[name="field_event_type"]' => ['value' => 'presenza']],
    [':input[name="field_event_type"]' => ['value' => 'ibrido']],
  ],
];

// "Link streaming" visibile per "online" OPPURE "ibrido"
$form['field_streaming_link']['#states'] = [
  'visible' => [
    [':input[name="field_event_type"]' => ['value' => 'online']],
    [':input[name="field_event_type"]' => ['value' => 'ibrido']],
  ],
];

Cosa succede nel browser

  1. L'utente cambia la select
  2. Il JavaScript di Drupal intercetta il change
  3. Valuta le condizioni dichiarate in #states
  4. Aggiunge o toglie display: none sui container target

Zero roundtrip al server. Zero AJAX. Tutto istantaneo.

Risultato

Valore della select field_venue_address field_streaming_link
- Seleziona - nascosto nascosto
In presenza visibile nascosto
Online nascosto visibile
Ibrido visibile visibile

Il selettore :input

:input è un pseudo-selettore jQuery (non CSS standard) che matcha tutti gli elementi interattivi: <input>, <textarea>, <select>, <button>.

Drupal lo usa come convenzione nei #states:

// Select
':input[name="field_event_type"]'

// Checkbox
':input[name="field_published"]'

// Textfield
':input[name="field_email"]'

// Radio button specifico
':input[name="field_type"][value="premium"]'

Come trovare il name corretto

Il name deve corrispondere esattamente all'attributo name nell'HTML renderizzato.

Per i campi Drupal il formato cambia in base al widget:

Tipo widget name nel DOM
options_select (cardinality 1) field_name
string_textfield field_name[0][value]
options_buttons (radio) field_name
options_buttons (checkbox) field_name[value]
entity_reference_autocomplete field_name[0][target_id]

Consiglio pratico: in caso di dubbio, ispeziona il DOM con DevTools e cerca l'attributo name dell'elemento HTML. È l'unica fonte di verità affidabile.

Tutti gli stati disponibili

Stato Effetto
visible / invisible Mostra o nasconde l'elemento (display: none)
enabled / disabled Abilita o disabilita l'input
required / optional Rende il campo obbligatorio o facoltativo
checked / unchecked Spunta o deseleziona una checkbox
expanded / collapsed Espande o collassa un elemento <details>

Condizioni multiple

AND - tutte le condizioni devono essere vere

Più selettori nello stesso array:

$form['campo']['#states'] = [
  'visible' => [
    ':input[name="tipo"]' => ['value' => 'premium'],
    ':input[name="attivo"]' => ['checked' => TRUE],
  ],
];

Il campo è visibile solo se tipo = premium e attivo è spuntato.

OR - almeno una condizione vera

Array multipli separati da virgola (come nell'esempio dell'evento ibrido visto sopra):

$form['campo']['#states'] = [
  'visible' => [
    [':input[name="tipo"]' => ['value' => 'premium']],
    [':input[name="tipo"]' => ['value' => 'enterprise']],
  ],
];

Il campo è visibile se tipo = premium oppure tipo = enterprise.

Tipi di condizione

// Valore specifico
['value' => 'premium']

// Checkbox spuntata
['checked' => TRUE]

// Campo non vuoto
['filled' => TRUE]

// Campo vuoto
['empty' => TRUE]

Attenzione: #states è solo frontend

#states controlla solo la visibilità nel browser.

Lato server i campi nascosti:

  • Sono comunque presenti nel form
  • I loro valori vengono inviati nel POST
  • Drupal li processa normalmente durante il submit

Se un campo nascosto non deve essere salvato, va gestito nel submit handler:

function mymodule_form_submit($form, $form_state) {
  $type = $form_state->getValue('field_event_type');
  if ($type === 'online') {
    // Pulisci l'indirizzo sede se l'evento è solo online
    $node->set('field_venue_address', NULL);
  }
  if ($type === 'presenza') {
    // Pulisci il link streaming se l'evento è solo in presenza
    $node->set('field_streaming_link', NULL);
  }
}

Questo è il punto che genera più bug. Ti aspetti che un campo nascosto non venga salvato, ma Drupal lo salva comunque.

La pulizia è sempre responsabilità tua.

Quando usare #states e quando no

Scenario Soluzione
Mostrare/nascondere campi in base a una select #states
Caricare opzioni dinamiche da database AJAX callback (#ajax)
Logica complessa con più dipendenze incrociate JavaScript custom
Nascondere campi a certi ruoli utente #access => FALSE (lato server)

Limitazioni note

1. Field group dinamici

Se aggiungi un elemento con #group nel form_alter, #states potrebbe non funzionare.

Il motivo: field_group sposta l'elemento nel DOM dopo che #states ha fatto il binding JavaScript. Il selettore punta a un nodo che non è più nella posizione originale.

2. Submit button dentro container nascosti

Quando un bottone submit è dentro un container nascosto via #states, Drupal potrebbe non riconoscerlo come triggering element ed eseguire il submit handler di default del form.

Workaround: usare RedirectResponse + exit invece di $form_state->setRedirectUrl().

3. Validazione dei campi nascosti

I campi nascosti via #states vengono comunque validati lato server.

Se un campo è required nel field config ma nascosto via #states, la validazione fallisce quando l'utente fa submit con il campo nascosto. Usa #limit_validation_errors sui bottoni per bypassare la validazione quando serve.

Riferimenti