Django Form Components
Intorduction
Previously, after submitting a form, custom validation rules had to be defined manually on both the frontend and backend. This led to repetitive work and was not concise. Django provides a built-in form component that encapsulates form validation, known as the Form component.
Main features of the Form component:
- Generate renderable HTML tags
- Validate user-submitted data
- Return validation error messages to the frontend
- Preserve previously entered data
Basic Validation Using Form Component
Template File (direct copy)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="/static/bootstrap3/css/bootstrap.min.css" />
<script src="/static/bootstrap3/js/bootstrap.min.js"></script>
</head>
<body>
<form action="" method="post">
<h1>User Login</h1>
<div>
<label for="user">Username</label>
<p><input type="text" name="name" id="user"></p>
</div>
<div>
<label for="pwd">Password</label>
<p><input type="password" name="pwd" id="pwd"></p>
</div>
<div>
<label for="email">Email</label>
<p><input type="text" name="email" id="email"></p>
</div>
<input type="submit">
</form>
</body>
</html>
View File (view.py)
from django import forms
from django.shortcuts import render, HttpResponse
class UserForm(forms.Form):
"""Define login form validation rules"""
name = forms.CharField(max_length=8)
pwd = forms.CharField(max_length=8, min_length=3)
email = forms.EmailField()
def login(request):
if request.method == "GET":
return render(request, "login.html")
else:
name = request.POST.get("name")
pwd = request.POST.get("pwd")
email = request.POST.get("email")
# Validation
# The keys in the dict must match the attribute names of UserForm
form_obj = UserForm({'name': name, 'pwd': pwd, "email":email})
# Alternatively: form_obj = UserForm(request.POST)
if form_obj.is_valid():
prompt = "Login successful"
else:
prompt = "Login failed, invalid format"
return HttpResponse(prompt)
Methods and Properties of Form Component
# Example:
from django import forms
class RegForm(forms.Form):
name = forms.CharField(max_length=6)
pwd = forms.CharField(max_length=8, min_length=3)
email = forms.EmailField()
# Passing values:
res = RegForm({'name': 'abcdef', 'pwd': '12', 'email': '123'})
# Requirements:
# 1. The keys in the dict must match the attribute names of RegForm
# 2. Must be a dictionary
# Check if validation passed
res.is_valid() # Returns True or False
# Get error messages
res.errors
# Different versions may have different formats,
# but they can be considered as a dict of lists; access via dict.get()
res.errors.get('pwd')
# Returns: <ul class="errorlist"><li>Ensure this value has at least 3 characters (it has 2).</li></ul>
# Add custom error message
res.add_error('name', 'Must be uppercase')
print(res.errors.get('name'))
# Returns: <ul class="errorlist"><li>Must be uppercase</li></ul>
# Get cleaned data (only after calling errors or is_valid())
res.cleaned_data
# Returns: {'name': 'sun'}
# Get all data
res.data
# Extra fields are ignored, missing fields cause validation failure
Frontend Rendering with Form Component
The backend must pass the form object to the front end.
form_obj = UserForm()
return render(request, "reg.html", {"form_obj": form_obj})
Three ways to render in the template:
<!-- First way: as paragraphs -->
{{ form_obj.as_p }}
<!-- as unordered list -->
{{ form_obj.as_ul }}
<!-- Second way: manual rendering -->
<form>
<p><label>{{ form_obj.name.label }}</label>{{ form_obj.name }}</p>
<p><label>{{ form_obj.pwd.label }}</label>{{ form_obj.pwd }}</p>
</form>
<!-- Set label via parameter label="Username: " -->
<!-- Third way: iterate over fields -->
<form>
{% for field in form_obj %}
<p>
<label>{{ field.label }}</label>
{{ field }}
</p>
{% endfor %}
</form>
<!-- Add novalidate to disable browser validation -->
<form novalidate>
Form Validation and Error Message Rendering
Backend Implementation
from django import forms
from django.forms import widgets
class UserForm(forms.Form):
name = forms.CharField(
max_length=8,
error_messages={
"max_length": "Username maximum length is 8 characters",
"required": "Username is required",
}
)
pwd = forms.CharField(
max_length=8,
min_length=3,
error_messages={
"max_length": "Password maximum length is 8 characters",
"min_length": "Password minimum length is 3 characters",
"required": "Password is required",
},
widget=widgets.PasswordInput(attrs={"class": "form-control"}),
)
email = forms.EmailField(
error_messages={
"required": "Email is required",
"invalid": "Invalid email format",
}
)
def login(request):
if request.method == "GET":
form_obj = UserForm()
return render(request, "login.html", {"form_obj": form_obj})
else:
form_obj = UserForm(request.POST)
if form_obj.is_valid():
return HttpResponse("OK")
else:
return render(request, "login.html", {"form_obj": form_obj})
Front end Template
<body>
<div>
<form method="post" action="" novalidate>
{% for field in form_obj %}
<p>
<label>{{ field.label }}</label>
{{ field }}{{ field.errors.0 }}
</p>
{% endfor %}
<input type="submit" value="Submit">
</form>
</div>
</body>
Hook Functions (Custom Validation Rules)
Local Hook (per field)
from django.core.exceptions import ValidationError
def clean_name(self):
name = self.cleaned_data.get("name")
if "aaa" in name:
# Add error via add_error
self.add_error("name", "Cannot contain 'aaa'")
# Alternatively
# raise ValidationError("Cannot contain 'aaa'")
# Must return the cleaned value
return name
Global Hook (form-wide)
def clean(self):
pwd_value = self.cleaned_data.get("pwd")
re_pwd_value = self.cleaned_data.get("re_pwd")
if pwd_value != re_pwd_value:
# Add error
self.add_error("re_pwd", "Passwords do not match")
# Or raise ValidationError with dict
# raise ValidationError({"re_pwd": "Passwords do not match"})
# Return cleaned data (optional)
return self.cleaned_data
Important notes:
- In local hooks, using
raiseprevents the field from appearing incleaned_data, whileadd_errordoes not. ValidationErrorcan take a string, list, or dict. For local hooks, use string or list. For global hooks, dict is recommended (oradd_error).
Common Parameters in Form Fields
Field Parameters (inherited by all field types)
| Parameter | Description |
|---|---|
required |
required=False allows empty value |
widget |
Sets the HTML input type |
initial |
initial="value" sets default value |
label |
label="Name" sets label text |
label_suffix |
label_suffix=">>" adds suffix to label |
error_messages |
Custom error messages dict |
help_text |
Help text displayed next to field |
validators=[] |
Custom regex validators |
localize=False |
Localization support |
disabled=False |
If True, field is read-only |
# validators example:
from django.core.validators import RegexValidator
name = forms.CharField(
min_length=2,
validators=[RegexValidator(regex='^\d+$', message="Must be digits")],
error_messages={
'max_length': 'Must be less than 3 characters',
'min_length': 'Must be more than 2 characters',
},
)
Widget Usage for Radio, Checkbox, Select
Radio Buttons (Single Choice)
from django import forms
from django.forms import widgets
# Approach 1:
gender = forms.ChoiceField(
choices=((1, "Male"), (2, "Female"), (3, "Other")),
widget=widgets.RadioSelect()
)
# Approach 2:
gender = forms.IntegerField(
widget=widgets.RadioSelect(
choices=((1, "Male"), (2, "Female"), (3, "Other")),
)
)
Generated HTML:
<div>
<label for="id_gender_0">Gender:</label>
<ul id="id_gender" class="radio">
<li><label for="id_gender_0"><input type="radio" name="gender" value="1" class="radio" required="" id="id_gender_0"> Male</label></li>
<li><label for="id_gender_1"><input type="radio" name="gender" value="2" class="radio" required="" id="id_gender_1"> Female</label></li>
<li><label for="id_gender_2"><input type="radio" name="gender" value="3" class="radio" required="" id="id_gender_2"> Other</label></li>
</ul>
</div>
Checkbox (Single)
keep = forms.BooleanField(
label="Remember password",
initial=True,
widget=forms.widgets.CheckboxInput()
)
Generated HTML:
<p>
<label for="id_keep">Remember password:</label>
<input type="checkbox" name="keep" id="id_keep" checked>
</p>
Checkbox (Multiple)
hobby = forms.MultipleChoiceField(
choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
widget=forms.widgets.CheckboxSelectMultiple()
)
# Or
hobby = forms.CharField(
widget=forms.widgets.CheckboxSelectMultiple(
choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
)
)
Generated HTML:
<div>
<label>Hobby:</label>
<ul id="id_hobby">
<li><label for="id_hobby_0"><input type="checkbox" name="hobby" value="1" id="id_hobby_0"> Basketball</label></li>
<li><label for="id_hobby_1"><input type="checkbox" name="hobby" value="2" id="id_hobby_1"> Football</label></li>
<li><label for="id_hobby_2"><input type="checkbox" name="hobby" value="3" id="id_hobby_2"> Ping Pong</label></li>
</ul>
</div>
Select (Single)
hobby = forms.ChoiceField(
choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
widget=forms.widgets.Select()
)
# Or
hobby = forms.CharField(
widget=forms.widgets.Select(
choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
)
)
Generated HTML:
<div>
<label for="id_hobby">Hobby:</label>
<select name="hobby" id="id_hobby">
<option value="1">Basketball</option>
<option value="2">Football</option>
<option value="3">Ping Pong</option>
</select>
</div>
Select (Multiple)
hobby = forms.MultipleChoiceField(
choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
widget=forms.widgets.SelectMultiple()
)
# Or
hobby = forms.CharField(
widget=forms.widgets.SelectMultiple(
choices=((1, "Basketball"), (2, "Football"), (3, "Ping Pong")),
)
)
Dynamic Choices from Database
To update choices in real time, override __init__:
from django import forms
from django.forms import widgets
from . import models
class RegisterForm(forms.Form):
hobby = forms.MultipleChoiceField(
widget=forms.widgets.SelectMultiple()
)
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.fields['hobby'].choices = models.Hobby.objects.values_list("id", "name")
Built-in Field Types (Similar to Model Fields)
Field
required=True, # Whether empty is allowed
widget=None, # HTML plugin
label=None, # Label text or content
initial=None, # Initial value
help_text='', # Help text
error_messages=None, # Error messages dict
validators=[], # Custom validators
localize=False, # Localization support
disabled=False, # Read-only
label_suffix=None # Label suffix
CharField(Field)
max_length=None, # Maximum length
min_length=None, # Minimum length
strip=True # Remove whitespace
IntegerField(Field)
max_value=None, # Maximum value
min_value=None, # Minimum value
FloatField(IntegerField)
...
DecimalField(IntegerField)
max_value=None,
min_value=None,
max_digits=None, # Total digits
decimal_places=None, # Decimal places
BaseTemporalField(Field)
input_formats=None # Date/time formats
DateField(BaseTemporalField) # Format: 2015-09-01
TimeField(BaseTemporalField) # Format: 11:12
DateTimeField(BaseTemporalField) # Format: 2015-09-01 11:12
DurationField(Field) # Time interval: %d %H:%M:%S.%f
RegexField(CharField)
regex, # Custom regex
max_length=None,
min_length=None,
error_message=None, # Use error_messages dict for 'invalid'
EmailField(CharField)
...
FileField(Field)
allow_empty_file=False # Whether empty file allowed
ImageField(FileField)
... # Requires Pillow: pip install Pillow
# Note: enctype="multipart/form-data" in form, and request.FILES in view
URLField(Field)
...
BooleanField(Field)
...
NullBooleanField(BooleanField)
...
ChoiceField(Field)
choices=(), # Options: ((0,'Shanghai'),(1,'Beijing'))
required=True,
widget=None, # Default select
label=None,
initial=None,
help_text='',
ModelChoiceField(ChoiceField)
queryset, # Query database
empty_label="---------",
to_field_name=None, # Field for value
limit_choices_to=None # Additional filtering for ModelForm
ModelMultipleChoiceField(ModelChoiceField)
...
TypedChoiceField(ChoiceField)
coerce = lambda val: val # Convert selected value
empty_value= '' # Default for empty
MultipleChoiceField(ChoiceField)
...
TypedMultipleChoiceField(MultipleChoiceField)
coerce = lambda val: val
empty_value= ''
ComboField(Field)
fields=() # Combine validations: e.g., fields=[CharField(max_length=20), EmailField()]
MultiValueField(Field)
# Abstract class; use with MultiWidget
SplitDateTimeField(MultiValueField)
input_date_formats=None, # Date formats list
input_time_formats=None # Time formats list
FilePathField(ChoiceField) # File path selection
path, # Folder path
match=None, # Regex match
recursive=False, # Include subfolders
allow_files=True,
allow_folders=False,
required=True,
widget=None,
label=None,
initial=None,
help_text='',
GenericIPAddressField
protocol='both', # both, ipv4, ipv6
unpack_ipv4=False # Unpack IPv4 address
SlugField(CharField) # Letters, digits, underscore, hyphen
UUIDField(CharField) # UUID type