#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
- L'utente cambia la select
- Il JavaScript di Drupal intercetta il
change - Valuta le condizioni dichiarate in
#states - Aggiunge o toglie
display: nonesui 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
-
File sorgente:
core/misc/states.js