Form e validazione¶
Il tema include, opzionalmente, uno styling Bootstrap 5 completo per i form
Django: classi form-control/form-select/form-check-input, stato
is-invalid, messaggi di errore come invalid-feedback, e un campo
password con pulsante mostra/nascondi.
Attivazione¶
Django non renderizza i widget dei form tramite le TEMPLATES di progetto:
usa un motore di template separato
(django.forms.renderers.DjangoTemplates), la cui DIRS punta di default
solo alla directory dei template di Django. Per questo lo styling non è
automatico: va attivato esplicitamente nelle settings del progetto
consumer, impostando il renderer del tema:
FORM_RENDERER = "agesci_theme.forms.AgesciFormRenderer"
Vale per tutti i form Django del progetto — non solo per i form di django-allauth (vedi Integrazione con django-allauth), è utile anche a chi non usa affatto allauth.
Cosa cambia visivamente¶
Con AgesciFormRenderer attivo:
Ogni widget di input riceve
form-control(form-selectper i<select>,form-check-inputper checkbox/radio), piùis-invalidquando il campo ha un errore di validazione.{{ form.as_p }}avvolge ogni campo in<div class="mb-3">invece del<p>di default di Django.Gli errori di campo diventano
<div class="invalid-feedback">; gli errori non di campo (form.non_field_errors) diventano<div class="alert alert-danger">.I campi
forms.PasswordInputricevono un pulsante mostra/nascondi con icona SVG inline, dentro un.input-group.
Perché <div class="mb-3"> e non <p>¶
Il default .as_p() di Django mette gli errori dentro un <p> insieme al
campo. Un <div> (compreso .invalid-feedback) non è «phrasing content»:
il parser HTML del browser chiude </p> prima del div, spostandolo
fuori dal genitore e rompendo silenziosamente l’adiacenza richiesta dal CSS
di Bootstrap (.is-invalid ~ .invalid-feedback). Per questo l’override di
django/forms/p.html nel tema usa <div class="mb-3"> al suo posto.
Verificato ispezionando il DOM renderizzato in un browser reale, non
deducibile dal solo sorgente del template.
Perché .invalid-feedback { display: block; }¶
Bootstrap nasconde di default .invalid-feedback finché non è preceduto,
nello stesso genitore, da un fratello .is-invalid (selettore
.is-invalid ~ .invalid-feedback). Il problema: qualunque wrapper attorno
al campo (ad es. il .input-group del toggle password) rende l’errore
non più fratello diretto dell’input, e Bootstrap lo nasconderebbe anche
quando presente.
Il tema risolve il problema diversamente da Bootstrap: il div dell’errore
(django/forms/errors/list/default.html) viene renderizzato SOLO quando il
campo ha davvero un errore — non serve quindi il meccanismo di occultamento
di Bootstrap. La regola, in _forms.scss:
.invalid-feedback {
display: block;
}
Non rimuovere questa regola pensando sia superflua: senza, gli errori spariscono silenziosamente ogni volta che il campo è avvolto in un wrapper.
Precedenza degli override e come estenderli ulteriormente¶
AgesciFormRenderer usa DIRS=[THEME_FORMS_DIR, DJANGO_FORMS_TEMPLATES_DIR]
con APP_DIRS=True. DIRS è sempre controllata prima di APP_DIRS
(comportamento di django.template.engine.Engine): gli override del tema
vincono sempre sui template di default di Django.
Conseguenza pratica: un progetto consumer non può sovrascrivere questi
template mettendo un proprio templates/django/forms/... in un’app
qualsiasi — APP_DIRS, in questo motore isolato, non verrà mai interrogato
prima di DIRS, indipendentemente dalla posizione dell’app in
INSTALLED_APPS.
Per personalizzare ulteriormente, sottoclassare AgesciFormRenderer e
anteporre la propria directory a engine.dirs (letta dinamicamente dal
loader ad ogni render, quindi sicura da modificare dopo
l’inizializzazione):
from pathlib import Path
from django.utils.functional import cached_property
from agesci_theme.forms import AgesciFormRenderer
class ProgettoFormRenderer(AgesciFormRenderer):
@cached_property
def engine(self):
engine = super().engine
engine.dirs.insert(0, Path(__file__).resolve().parent / "templates")
return engine
FORM_RENDERER = "config.forms.ProgettoFormRenderer"
Il componente {% ag_password_field %}¶
Oltre all’override automatico via FORM_RENDERER, il tema espone un
inclusion tag standalone per un campo password con toggle, utilizzabile in
qualsiasi form scritto a mano — anche senza attivare FORM_RENDERER,
o in progetti che non usano affatto Django Forms per quel campo:
{% load agesci_components %}
{% ag_password_field name="password" label="Password" required=True %}
Condivide lo stesso partial del pulsante toggle (icona, comportamento)
dell’override django/forms/widgets/password.html, così un campo
disegnato con questo tag e un campo renderizzato via FORM_RENDERER sono
visivamente identici. Riferimento completo dei parametri in
Template tag → ag_password_field.
Icona del toggle password¶
Il pulsante mostra/nascondi usa un’icona SVG inline, non
{% bs_icon %} di django-bootstrap-icons. Motivo: il widget
django/forms/widgets/password.html è renderizzato dal motore di template
isolato di AgesciFormRenderer, che non può assumere disponibile l’extra
opzionale [icons] (e un {% load bootstrap_icons %} condizionale non è
gestibile in un template). L’SVG inline evita anche una differenza visiva
tra il widget renderizzato via FORM_RENDERER e il tag standalone
{% ag_password_field %}, che condividono lo stesso partial.
Il widget SelectMultiploADiscesa¶
agesci_theme.forms.SelectMultiploADiscesa è un CheckboxSelectMultiple
renderizzato come tendina Bootstrap chiusa (bottone + menu con checkbox),
non come elenco checkbox sempre aperto:
from agesci_theme.forms import SelectMultiploADiscesa
interessi = forms.MultipleChoiceField(
choices=SCELTE,
required=False,
widget=SelectMultiploADiscesa(placeholder="Nessuno"),
)
A differenza degli altri override di questo documento, non richiede
FORM_RENDERER = "agesci_theme.forms.AgesciFormRenderer": i suoi template
vivono in un namespace proprio (agesci_theme/forms/..., non
django/forms/widgets/...), quindi vengono trovati anche dal renderer di
default di Django, che ha già APP_DIRS=True — basta che agesci_theme
sia in INSTALLED_APPS. È un widget opt-in per singolo campo: le altre
CheckboxSelectMultiple del progetto non cambiano comportamento.
Il menu resta aperto durante la selezione di più checkbox
(data-bs-auto-close="outside", Bootstrap 5.2+) e l’etichetta del bottone
si aggiorna via JS (agesci_theme/static/agesci_theme/js/multiselect-dropdown.js,
caricato globalmente da base.html): placeholder se nessuna opzione è
selezionata, l’etichetta della singola opzione se una sola, altrimenti
«N selezionati».
Il tema espone anche un inclusion tag standalone equivalente, per form
scritti a mano senza passare da MultipleChoiceField:
{% load agesci_components %}
{% ag_multiselect_dropdown name="gruppo" label="Gruppo"
choices=lista_gruppi placeholder="Tutti" %}
Widget e tag condividono lo stesso partial di markup
(components/_multiselect_dropdown_menu.html), stessa dualità di
ag_password_field/password.html. Riferimento completo dei parametri in
Template tag → ag_multiselect_dropdown.