Advanced Form Patterns
Learn advanced form patterns including dynamic forms, form sets, and custom form widgets for complex applications.
75 min•By Priygop Team•Last updated: Feb 2026
Dynamic Forms and Form Sets
Django provides powerful tools for creating dynamic forms, form sets, and custom form widgets. These patterns are essential for building complex, interactive web applications.
Form Sets and Dynamic Forms
Example
# Form sets for multiple objects
from django.forms import formset_factory, BaseFormSet
from django.core.exceptions import ValidationError
class BasePostFormSet(BaseFormSet):
def clean(self):
"""Custom validation for the entire form set"""
super().clean()
# Check if at least one form has data
if not any(form.cleaned_data for form in self.forms):
raise ValidationError("At least one post must be provided.")
# Check for duplicate titles
titles = []
for form in self.forms:
if form.cleaned_data:
title = form.cleaned_data.get('title')
if title in titles:
raise ValidationError("Duplicate titles are not allowed.")
titles.append(title)
# Create form set
PostFormSet = formset_factory(
PostForm,
formset=BasePostFormSet,
extra=3, # Show 3 empty forms
max_num=10, # Maximum 10 forms
can_delete=True # Allow deletion
)
# Dynamic form creation
class DynamicFormBuilder:
"""Build forms dynamically based on configuration"""
def __init__(self):
self.field_types = {
'text': forms.CharField,
'textarea': forms.CharField,
'email': forms.EmailField,
'number': forms.IntegerField,
'choice': forms.ChoiceField,
}
def build_form(self, field_configs):
"""Build a form class from field configurations"""
form_fields = {}
for field_config in field_configs:
field_name = field_config['name']
field_type = field_config['type']
field_options = field_config.get('options', {})
if field_type in self.field_types:
field_class = self.field_types[field_type]
# Handle special cases
if field_type == 'textarea':
field_options['widget'] = forms.Textarea
elif field_type == 'choice':
field_options['choices'] = field_config.get('choices', [])
form_fields[field_name] = field_class(**field_options)
return type('DynamicForm', (forms.Form,), form_fields)
# Custom form widgets
class RichTextWidget(forms.Textarea):
"""Custom widget for rich text editing"""
class Media:
css = {
'all': ('css/rich-text-editor.css',)
}
js = ('js/rich-text-editor.js',)
def __init__(self, attrs=None):
default_attrs = {
'class': 'rich-text-editor',
'data-editor': 'true'
}
if attrs:
default_attrs.update(attrs)
super().__init__(default_attrs)
class ColorPickerWidget(forms.TextInput):
"""Custom widget for color picking"""
class Media:
css = {
'all': ('css/color-picker.css',)
}
js = ('js/color-picker.js',)
def __init__(self, attrs=None):
default_attrs = {
'type': 'color',
'class': 'color-picker'
}
if attrs:
default_attrs.update(attrs)
super().__init__(default_attrs)
# Advanced form validation
class AdvancedPostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content', 'category', 'tags']
def clean(self):
cleaned_data = super().clean()
title = cleaned_data.get('title')
content = cleaned_data.get('content')
# Cross-field validation
if title and content:
# Check for duplicate content
if Post.objects.filter(title__iexact=title).exists():
raise ValidationError("A post with this title already exists.")
# Check content quality
word_count = len(content.split())
if word_count < 50:
raise ValidationError("Content must be at least 50 words long.")
# Check for spam keywords
spam_keywords = ['buy now', 'click here', 'free money']
content_lower = content.lower()
for keyword in spam_keywords:
if keyword in content_lower:
raise ValidationError("Content contains prohibited keywords.")
return cleaned_data
def clean_title(self):
title = self.cleaned_data.get('title')
if title:
# Check title length
if len(title) < 10:
raise ValidationError("Title must be at least 10 characters long.")
# Check for inappropriate words
inappropriate_words = ['spam', 'scam', 'fake']
if any(word in title.lower() for word in inappropriate_words):
raise ValidationError("Title contains inappropriate words.")
return title
# Form processing with AJAX
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
@csrf_exempt
def ajax_form_processing(request):
"""Process forms via AJAX"""
if request.method == 'POST':
try:
data = json.loads(request.body)
form = AdvancedPostForm(data)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
form.save_m2m()
return JsonResponse({
'success': True,
'message': 'Post created successfully!',
'post_id': post.id
})
else:
return JsonResponse({
'success': False,
'errors': form.errors
})
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'message': 'Invalid JSON data'
})
return JsonResponse({
'success': False,
'message': 'Invalid request method'
})
# Usage in views
def create_multiple_posts(request):
"""Create multiple posts using form sets"""
if request.method == 'POST':
formset = PostFormSet(request.POST)
if formset.is_valid():
for form in formset:
if form.cleaned_data:
post = form.save(commit=False)
post.author = request.user
post.save()
messages.success(request, 'Posts created successfully!')
return redirect('post_list')
else:
formset = PostFormSet()
return render(request, 'blog/create_multiple_posts.html', {
'formset': formset
})Practice Exercise: Multi-Step Form Wizard
Example
# Multi-Step Form Wizard
from django import forms
from django.shortcuts import render, redirect
from django.contrib import messages
from django.core.cache import cache
import uuid
class Step1Form(forms.Form):
"""First step: Basic information"""
title = forms.CharField(max_length=200)
category = forms.ChoiceField(choices=[
('tech', 'Technology'),
('lifestyle', 'Lifestyle'),
('travel', 'Travel')
])
class Step2Form(forms.Form):
"""Second step: Content"""
content = forms.CharField(widget=forms.Textarea)
excerpt = forms.CharField(widget=forms.Textarea, required=False)
class Step3Form(forms.Form):
"""Third step: Publishing options"""
status = forms.ChoiceField(choices=[
('draft', 'Draft'),
('published', 'Published')
])
featured = forms.BooleanField(required=False)
class FormWizard:
"""Multi-step form wizard for creating posts"""
def __init__(self, request):
self.request = request
self.session_key = f"wizard_{request.user.id if request.user.is_authenticated else 'anonymous'}"
self.steps = [Step1Form, Step2Form, Step3Form]
self.current_step = self.get_current_step()
def get_current_step(self):
"""Get the current step from session"""
return self.request.session.get(f"{self.session_key}_step", 0)
def set_current_step(self, step):
"""Set the current step in session"""
self.request.session[f"{self.session_key}_step"] = step
def get_form_data(self):
"""Get all form data from session"""
return self.request.session.get(f"{self.session_key}_data", {})
def set_form_data(self, data):
"""Set form data in session"""
self.request.session[f"{self.session_key}_data"] = data
def get_form(self, step=None):
"""Get the form for the current or specified step"""
if step is None:
step = self.current_step
form_class = self.steps[step]
initial_data = self.get_form_data().get(f"step_{step}", {})
return form_class(initial=initial_data)
def process_step(self, form):
"""Process the current step and save data"""
if form.is_valid():
step_data = self.get_form_data()
step_data[f"step_{self.current_step}"] = form.cleaned_data
self.set_form_data(step_data)
return True
return False
def next_step(self):
"""Move to the next step"""
if self.current_step < len(self.steps) - 1:
self.set_current_step(self.current_step + 1)
return True
return False
def previous_step(self):
"""Move to the previous step"""
if self.current_step > 0:
self.set_current_step(self.current_step - 1)
return True
return False
def is_complete(self):
"""Check if all steps are complete"""
form_data = self.get_form_data()
return len(form_data) == len(self.steps)
def get_final_data(self):
"""Get all form data combined"""
if not self.is_complete():
return None
final_data = {}
form_data = self.get_form_data()
for step in range(len(self.steps)):
step_data = form_data.get(f"step_{step}", {})
final_data.update(step_data)
return final_data
def clear_session(self):
"""Clear wizard session data"""
keys_to_delete = [
f"{self.session_key}_step",
f"{self.session_key}_data"
]
for key in keys_to_delete:
if key in self.request.session:
del self.request.session[key]
# Wizard view
def post_wizard(request):
"""Multi-step form wizard for creating posts"""
wizard = FormWizard(request)
if request.method == 'POST':
if 'next' in request.POST:
# Process current step and move to next
form = wizard.get_form()
if wizard.process_step(form):
if wizard.next_step():
return redirect('post_wizard')
else:
# All steps complete, create post
final_data = wizard.get_final_data()
if final_data:
post = Post.objects.create(
author=request.user,
**final_data
)
wizard.clear_session()
messages.success(request, 'Post created successfully!')
return redirect('post_detail', pk=post.pk)
elif 'previous' in request.POST:
# Move to previous step
wizard.previous_step()
return redirect('post_wizard')
elif 'save_draft' in request.POST:
# Save current progress
form = wizard.get_form()
if wizard.process_step(form):
messages.info(request, 'Progress saved. You can continue later.')
return redirect('post_wizard')
# Get current form
form = wizard.get_form()
# Get progress
progress = (wizard.current_step + 1) / len(wizard.steps) * 100
context = {
'form': form,
'current_step': wizard.current_step + 1,
'total_steps': len(wizard.steps),
'progress': progress,
'can_go_back': wizard.current_step > 0,
'can_go_next': wizard.current_step < len(wizard.steps) - 1,
'is_last_step': wizard.current_step == len(wizard.steps) - 1,
}
return render(request, 'blog/post_wizard.html', context)
# Template for the wizard
"""
{% extends 'base.html' %}
{% block content %}
<div class="wizard-container">
<div class="wizard-progress">
<div class="progress-bar" style="width: {{ progress }}%"></div>
<span class="progress-text">Step {{ current_step }} of {{ total_steps }}</span>
</div>
<form method="post">
{% csrf_token %}
<div class="form-step">
{{ form.as_p }}
</div>
<div class="wizard-actions">
{% if can_go_back %}
<button type="submit" name="previous" class="btn btn-secondary">Previous</button>
{% endif %}
{% if not is_last_step %}
<button type="submit" name="next" class="btn btn-primary">Next</button>
{% else %}
<button type="submit" name="next" class="btn btn-success">Create Post</button>
{% endif %}
<button type="submit" name="save_draft" class="btn btn-info">Save Draft</button>
</div>
</form>
</div>
{% endblock %}
"""Try It Yourself — Forms & Authentication
Try It Yourself — Forms & AuthenticationHTML
HTML Editor
✓ ValidTab = 2 spaces
HTML|32 lines|1680 chars|✓ Valid syntax
UTF-8