Forms and User Input

Built-in Fields and Widgets

Django provides a comprehensive set of form fields and widgets that handle different data types and user interface elements. Understanding these built-in components enables you to create sophisticated forms without custom implementations.

Built-in Fields and Widgets

Django provides a comprehensive set of form fields and widgets that handle different data types and user interface elements. Understanding these built-in components enables you to create sophisticated forms without custom implementations.

Core Form Fields

Text and String Fields

# forms.py - Text field variations
from django import forms

class TextFieldExamplesForm(forms.Form):
    """Comprehensive text field examples"""
    
    # Basic CharField
    name = forms.CharField(
        max_length=100,
        min_length=2,
        label='Full Name',
        help_text='Enter your complete name',
        initial='',
        required=True
    )
    
    # CharField with custom widget
    description = forms.CharField(
        widget=forms.Textarea(attrs={
            'rows': 4,
            'cols': 50,
            'placeholder': 'Enter description...'
        }),
        max_length=500,
        help_text='Maximum 500 characters'
    )
    
    # Slug field for URL-friendly strings
    slug = forms.SlugField(
        max_length=50,
        help_text='URL-friendly version (letters, numbers, hyphens, underscores only)'
    )
    
    # RegexField for pattern matching
    product_code = forms.RegexField(
        regex=r'^[A-Z]{2}\d{4}$',
        max_length=6,
        error_message='Product code must be 2 uppercase letters followed by 4 digits (e.g., AB1234)',
        help_text='Format: AB1234'
    )
    
    # CharField with choices (acts like a select)
    STATUS_CHOICES = [
        ('draft', 'Draft'),
        ('published', 'Published'),
        ('archived', 'Archived'),
    ]
    
    status = forms.CharField(
        max_length=20,
        choices=STATUS_CHOICES,
        initial='draft'
    )
    
    # Password field
    password = forms.CharField(
        widget=forms.PasswordInput(attrs={
            'placeholder': 'Enter password',
            'autocomplete': 'new-password'
        }),
        min_length=8,
        help_text='Minimum 8 characters'
    )
    
    # Hidden field
    csrf_token = forms.CharField(
        widget=forms.HiddenInput(),
        required=False
    )

Numeric Fields

# forms.py - Numeric field types
from django import forms
from decimal import Decimal

class NumericFieldsForm(forms.Form):
    """Examples of numeric form fields"""
    
    # Integer field
    age = forms.IntegerField(
        min_value=0,
        max_value=150,
        help_text='Enter your age in years',
        widget=forms.NumberInput(attrs={
            'placeholder': 'Age',
            'step': '1'
        })
    )
    
    # Float field
    rating = forms.FloatField(
        min_value=0.0,
        max_value=5.0,
        help_text='Rating from 0.0 to 5.0',
        widget=forms.NumberInput(attrs={
            'step': '0.1',
            'placeholder': '0.0'
        })
    )
    
    # Decimal field for precise calculations
    price = forms.DecimalField(
        max_digits=10,
        decimal_places=2,
        min_value=Decimal('0.01'),
        max_value=Decimal('999999.99'),
        help_text='Price in USD',
        widget=forms.NumberInput(attrs={
            'step': '0.01',
            'placeholder': '0.00'
        })
    )
    
    # Integer field with range widget
    priority = forms.IntegerField(
        min_value=1,
        max_value=10,
        initial=5,
        widget=forms.NumberInput(attrs={
            'type': 'range',
            'step': '1'
        }),
        help_text='Priority level (1-10)'
    )
    
    # Percentage field (using DecimalField)
    completion = forms.DecimalField(
        max_digits=5,
        decimal_places=2,
        min_value=Decimal('0.00'),
        max_value=Decimal('100.00'),
        widget=forms.NumberInput(attrs={
            'step': '0.01',
            'placeholder': '0.00',
            'suffix': '%'
        }),
        help_text='Completion percentage'
    )

Date and Time Fields

# forms.py - Date and time fields
from django import forms
from datetime import date, datetime, time

class DateTimeFieldsForm(forms.Form):
    """Date and time field examples"""
    
    # Date field
    birth_date = forms.DateField(
        help_text='Format: YYYY-MM-DD',
        widget=forms.DateInput(attrs={
            'type': 'date',
            'max': date.today().isoformat()
        })
    )
    
    # Time field
    appointment_time = forms.TimeField(
        help_text='Format: HH:MM',
        widget=forms.TimeInput(attrs={
            'type': 'time',
            'step': '900'  # 15-minute intervals
        })
    )
    
    # DateTime field
    event_datetime = forms.DateTimeField(
        help_text='Event date and time',
        widget=forms.DateTimeInput(attrs={
            'type': 'datetime-local'
        })
    )
    
    # Date field with custom widget
    project_deadline = forms.DateField(
        widget=forms.DateInput(attrs={
            'type': 'date',
            'min': date.today().isoformat(),
            'class': 'form-control'
        }),
        help_text='Project completion deadline'
    )
    
    # Duration field (using CharField with custom validation)
    duration = forms.DurationField(
        help_text='Duration in format: DD HH:MM:SS or HH:MM:SS'
    )
    
    # Custom date range validation
    start_date = forms.DateField(
        widget=forms.DateInput(attrs={'type': 'date'})
    )
    
    end_date = forms.DateField(
        widget=forms.DateInput(attrs={'type': 'date'})
    )
    
    def clean(self):
        cleaned_data = super().clean()
        start_date = cleaned_data.get('start_date')
        end_date = cleaned_data.get('end_date')
        
        if start_date and end_date:
            if end_date < start_date:
                raise forms.ValidationError('End date must be after start date.')
        
        return cleaned_data

Choice Fields

# forms.py - Choice field variations
from django import forms

class ChoiceFieldsForm(forms.Form):
    """Examples of choice-based fields"""
    
    # Basic choice field (dropdown)
    CATEGORY_CHOICES = [
        ('', 'Select Category'),
        ('tech', 'Technology'),
        ('health', 'Health & Fitness'),
        ('education', 'Education'),
        ('entertainment', 'Entertainment'),
    ]
    
    category = forms.ChoiceField(
        choices=CATEGORY_CHOICES,
        help_text='Select the most appropriate category'
    )
    
    # Choice field with radio buttons
    PRIORITY_CHOICES = [
        ('low', 'Low Priority'),
        ('medium', 'Medium Priority'),
        ('high', 'High Priority'),
        ('urgent', 'Urgent'),
    ]
    
    priority = forms.ChoiceField(
        choices=PRIORITY_CHOICES,
        widget=forms.RadioSelect,
        initial='medium',
        help_text='Select priority level'
    )
    
    # Multiple choice field with checkboxes
    SKILL_CHOICES = [
        ('python', 'Python'),
        ('javascript', 'JavaScript'),
        ('java', 'Java'),
        ('csharp', 'C#'),
        ('php', 'PHP'),
        ('ruby', 'Ruby'),
    ]
    
    skills = forms.MultipleChoiceField(
        choices=SKILL_CHOICES,
        widget=forms.CheckboxSelectMultiple,
        required=False,
        help_text='Select all applicable skills'
    )
    
    # Multiple choice with select multiple
    languages = forms.MultipleChoiceField(
        choices=[
            ('en', 'English'),
            ('es', 'Spanish'),
            ('fr', 'French'),
            ('de', 'German'),
            ('it', 'Italian'),
            ('pt', 'Portuguese'),
        ],
        widget=forms.SelectMultiple(attrs={
            'size': '4',
            'class': 'form-control'
        }),
        help_text='Hold Ctrl/Cmd to select multiple languages'
    )
    
    # Typed choice field (converts values to specific type)
    difficulty_level = forms.TypedChoiceField(
        choices=[
            (1, 'Beginner'),
            (2, 'Intermediate'),
            (3, 'Advanced'),
            (4, 'Expert'),
        ],
        coerce=int,  # Convert to integer
        widget=forms.RadioSelect,
        help_text='Select your skill level'
    )
    
    # Boolean field variations
    is_active = forms.BooleanField(
        required=False,
        help_text='Check to activate'
    )
    
    terms_accepted = forms.BooleanField(
        help_text='You must accept the terms and conditions',
        error_messages={
            'required': 'You must accept the terms and conditions to proceed.'
        }
    )
    
    # Null boolean field (True/False/None)
    newsletter_subscription = forms.NullBooleanField(
        widget=forms.Select(choices=[
            (None, 'Not specified'),
            (True, 'Yes, subscribe me'),
            (False, 'No, do not subscribe'),
        ]),
        help_text='Newsletter subscription preference'
    )

File and Image Fields

# forms.py - File handling fields
from django import forms
from django.core.validators import FileExtensionValidator

class FileFieldsForm(forms.Form):
    """File and image field examples"""
    
    # Basic file field
    document = forms.FileField(
        help_text='Upload a document (PDF, DOC, DOCX)',
        validators=[
            FileExtensionValidator(
                allowed_extensions=['pdf', 'doc', 'docx'],
                message='Only PDF, DOC, and DOCX files are allowed.'
            )
        ]
    )
    
    # Image field with validation
    profile_picture = forms.ImageField(
        required=False,
        help_text='Upload a profile picture (JPG, PNG, GIF)',
        widget=forms.FileInput(attrs={
            'accept': 'image/*',
            'class': 'form-control'
        })
    )
    
    # Multiple file upload
    attachments = forms.FileField(
        widget=forms.ClearableFileInput(attrs={
            'multiple': True,
            'class': 'form-control'
        }),
        required=False,
        help_text='Upload multiple files'
    )
    
    # File field with size validation
    def clean_document(self):
        document = self.cleaned_data.get('document')
        
        if document:
            # Check file size (5MB limit)
            if document.size > 5 * 1024 * 1024:
                raise forms.ValidationError('File size cannot exceed 5MB.')
            
            # Check file type by content
            if document.content_type not in ['application/pdf', 'application/msword', 
                                          'application/vnd.openxmlformats-officedocument.wordprocessingml.document']:
                raise forms.ValidationError('Invalid file type.')
        
        return document
    
    def clean_profile_picture(self):
        image = self.cleaned_data.get('profile_picture')
        
        if image:
            # Check image dimensions
            if image.width > 1920 or image.height > 1080:
                raise forms.ValidationError('Image dimensions cannot exceed 1920x1080 pixels.')
            
            # Check file size (2MB limit for images)
            if image.size > 2 * 1024 * 1024:
                raise forms.ValidationError('Image size cannot exceed 2MB.')
        
        return image

Widget Customization

Built-in Widget Options

# forms.py - Widget customization examples
from django import forms

class WidgetCustomizationForm(forms.Form):
    """Examples of widget customization"""
    
    # TextInput with custom attributes
    username = forms.CharField(
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Enter username',
            'autocomplete': 'username',
            'maxlength': '30',
            'pattern': '[a-zA-Z0-9_]+',
            'title': 'Username can only contain letters, numbers, and underscores'
        })
    )
    
    # Textarea with custom size and attributes
    message = forms.CharField(
        widget=forms.Textarea(attrs={
            'rows': 5,
            'cols': 40,
            'class': 'form-control',
            'placeholder': 'Enter your message here...',
            'style': 'resize: vertical;',
            'maxlength': '1000'
        })
    )
    
    # Select with custom styling
    country = forms.ChoiceField(
        choices=[
            ('', 'Select Country'),
            ('us', 'United States'),
            ('ca', 'Canada'),
            ('uk', 'United Kingdom'),
            ('au', 'Australia'),
        ],
        widget=forms.Select(attrs={
            'class': 'form-select',
            'data-placeholder': 'Choose a country'
        })
    )
    
    # CheckboxSelectMultiple with custom layout
    interests = forms.MultipleChoiceField(
        choices=[
            ('sports', 'Sports'),
            ('music', 'Music'),
            ('travel', 'Travel'),
            ('technology', 'Technology'),
        ],
        widget=forms.CheckboxSelectMultiple(attrs={
            'class': 'form-check-input'
        }),
        required=False
    )
    
    # RadioSelect with custom styling
    gender = forms.ChoiceField(
        choices=[
            ('M', 'Male'),
            ('F', 'Female'),
            ('O', 'Other'),
        ],
        widget=forms.RadioSelect(attrs={
            'class': 'form-check-input'
        }),
        required=False
    )
    
    # Date input with custom format
    birth_date = forms.DateField(
        widget=forms.DateInput(attrs={
            'type': 'date',
            'class': 'form-control',
            'max': '2023-12-31'
        })
    )
    
    # Password input with strength indicator
    password = forms.CharField(
        widget=forms.PasswordInput(attrs={
            'class': 'form-control',
            'placeholder': 'Enter password',
            'data-strength-meter': 'true',
            'autocomplete': 'new-password'
        })
    )
    
    # Number input with range
    rating = forms.IntegerField(
        widget=forms.NumberInput(attrs={
            'type': 'range',
            'min': '1',
            'max': '10',
            'step': '1',
            'class': 'form-range',
            'oninput': 'this.nextElementSibling.value = this.value'
        })
    )
    
    # Color picker
    theme_color = forms.CharField(
        widget=forms.TextInput(attrs={
            'type': 'color',
            'class': 'form-control form-control-color'
        }),
        initial='#007bff'
    )
    
    # File input with preview
    avatar = forms.ImageField(
        widget=forms.FileInput(attrs={
            'class': 'form-control',
            'accept': 'image/*',
            'onchange': 'previewImage(this)'
        }),
        required=False
    )

Custom Widget Classes

# widgets.py - Custom widget implementations
from django import forms
from django.forms.widgets import Widget
from django.html import format_html
from django.utils.safestring import mark_safe

class RangeSliderWidget(Widget):
    """Custom range slider with value display"""
    
    def __init__(self, attrs=None, min_value=0, max_value=100, step=1):
        self.min_value = min_value
        self.max_value = max_value
        self.step = step
        super().__init__(attrs)
    
    def render(self, name, value, attrs=None, renderer=None):
        if value is None:
            value = self.min_value
        
        final_attrs = self.build_attrs(attrs, {
            'type': 'range',
            'name': name,
            'min': self.min_value,
            'max': self.max_value,
            'step': self.step,
            'value': value,
            'oninput': f'document.getElementById("{name}_display").textContent = this.value'
        })
        
        return format_html(
            '<input{} /><span id="{}_display" class="range-value">{}</span>',
            forms.utils.flatatt(final_attrs),
            name,
            value
        )

class TagInputWidget(Widget):
    """Custom tag input widget"""
    
    def render(self, name, value, attrs=None, renderer=None):
        if value is None:
            value = ''
        
        final_attrs = self.build_attrs(attrs, {
            'type': 'text',
            'name': name,
            'value': value,
            'data-role': 'tagsinput',
            'placeholder': 'Add tags...'
        })
        
        return format_html('<input{} />', forms.utils.flatatt(final_attrs))

class RichTextWidget(Widget):
    """Rich text editor widget"""
    
    def render(self, name, value, attrs=None, renderer=None):
        if value is None:
            value = ''
        
        final_attrs = self.build_attrs(attrs, {
            'name': name,
            'id': f'id_{name}',
            'class': 'rich-text-editor'
        })
        
        html = format_html(
            '<textarea{}>{}</textarea>',
            forms.utils.flatatt(final_attrs),
            value
        )
        
        # Add JavaScript for rich text editor
        js = format_html(
            '<script>initRichTextEditor("id_{}");</script>',
            name
        )
        
        return mark_safe(html + js)

class DateTimePickerWidget(Widget):
    """Custom datetime picker widget"""
    
    def __init__(self, attrs=None, format=None):
        self.format = format or '%Y-%m-%d %H:%M'
        super().__init__(attrs)
    
    def render(self, name, value, attrs=None, renderer=None):
        if value:
            if hasattr(value, 'strftime'):
                value = value.strftime(self.format)
        else:
            value = ''
        
        final_attrs = self.build_attrs(attrs, {
            'type': 'text',
            'name': name,
            'value': value,
            'data-datetime-picker': 'true',
            'placeholder': 'YYYY-MM-DD HH:MM'
        })
        
        return format_html('<input{} />', forms.utils.flatatt(final_attrs))

# forms.py - Using custom widgets
class CustomWidgetForm(forms.Form):
    """Form using custom widgets"""
    
    priority = forms.IntegerField(
        widget=RangeSliderWidget(min_value=1, max_value=10),
        help_text='Select priority level'
    )
    
    tags = forms.CharField(
        widget=TagInputWidget(attrs={'class': 'form-control'}),
        help_text='Enter tags separated by commas',
        required=False
    )
    
    content = forms.CharField(
        widget=RichTextWidget(attrs={'rows': 10}),
        help_text='Enter rich text content'
    )
    
    scheduled_time = forms.DateTimeField(
        widget=DateTimePickerWidget(),
        help_text='Select date and time'
    )

Advanced Field Patterns

Dynamic Field Generation

# forms.py - Dynamic field creation
from django import forms

class DynamicFieldForm(forms.Form):
    """Form with dynamically generated fields"""
    
    def __init__(self, field_configs=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        if field_configs:
            for config in field_configs:
                field_type = config.get('type', 'CharField')
                field_name = config.get('name')
                field_label = config.get('label', field_name.title())
                field_required = config.get('required', True)
                field_help_text = config.get('help_text', '')
                
                # Create field based on type
                if field_type == 'CharField':
                    field = forms.CharField(
                        label=field_label,
                        required=field_required,
                        help_text=field_help_text,
                        max_length=config.get('max_length', 100)
                    )
                
                elif field_type == 'EmailField':
                    field = forms.EmailField(
                        label=field_label,
                        required=field_required,
                        help_text=field_help_text
                    )
                
                elif field_type == 'IntegerField':
                    field = forms.IntegerField(
                        label=field_label,
                        required=field_required,
                        help_text=field_help_text,
                        min_value=config.get('min_value'),
                        max_value=config.get('max_value')
                    )
                
                elif field_type == 'ChoiceField':
                    choices = config.get('choices', [])
                    field = forms.ChoiceField(
                        label=field_label,
                        required=field_required,
                        help_text=field_help_text,
                        choices=choices
                    )
                
                elif field_type == 'BooleanField':
                    field = forms.BooleanField(
                        label=field_label,
                        required=field_required,
                        help_text=field_help_text
                    )
                
                else:
                    # Default to CharField
                    field = forms.CharField(
                        label=field_label,
                        required=field_required,
                        help_text=field_help_text
                    )
                
                # Add field to form
                self.fields[field_name] = field

# Usage example
field_configs = [
    {
        'name': 'first_name',
        'type': 'CharField',
        'label': 'First Name',
        'required': True,
        'max_length': 50
    },
    {
        'name': 'age',
        'type': 'IntegerField',
        'label': 'Age',
        'required': True,
        'min_value': 0,
        'max_value': 150
    },
    {
        'name': 'country',
        'type': 'ChoiceField',
        'label': 'Country',
        'required': True,
        'choices': [('us', 'USA'), ('ca', 'Canada'), ('uk', 'UK')]
    }
]

# Create form with dynamic fields
dynamic_form = DynamicFieldForm(field_configs=field_configs)

Conditional Field Display

# forms.py - Conditional field visibility
from django import forms

class ConditionalFieldForm(forms.Form):
    """Form with conditional field display"""
    
    user_type = forms.ChoiceField(
        choices=[
            ('individual', 'Individual'),
            ('business', 'Business'),
        ],
        widget=forms.RadioSelect(attrs={
            'onchange': 'toggleFields(this.value)'
        })
    )
    
    # Individual fields
    first_name = forms.CharField(
        max_length=50,
        widget=forms.TextInput(attrs={
            'class': 'form-control individual-field'
        }),
        required=False
    )
    
    last_name = forms.CharField(
        max_length=50,
        widget=forms.TextInput(attrs={
            'class': 'form-control individual-field'
        }),
        required=False
    )
    
    # Business fields
    company_name = forms.CharField(
        max_length=100,
        widget=forms.TextInput(attrs={
            'class': 'form-control business-field'
        }),
        required=False
    )
    
    tax_id = forms.CharField(
        max_length=20,
        widget=forms.TextInput(attrs={
            'class': 'form-control business-field'
        }),
        required=False
    )
    
    def clean(self):
        cleaned_data = super().clean()
        user_type = cleaned_data.get('user_type')
        
        # Make fields required based on user type
        if user_type == 'individual':
            if not cleaned_data.get('first_name'):
                self.add_error('first_name', 'First name is required for individuals.')
            if not cleaned_data.get('last_name'):
                self.add_error('last_name', 'Last name is required for individuals.')
        
        elif user_type == 'business':
            if not cleaned_data.get('company_name'):
                self.add_error('company_name', 'Company name is required for businesses.')
            if not cleaned_data.get('tax_id'):
                self.add_error('tax_id', 'Tax ID is required for businesses.')
        
        return cleaned_data

Django's built-in fields and widgets provide a comprehensive foundation for creating sophisticated forms. Understanding the available field types, widget customization options, and advanced patterns enables you to build user-friendly interfaces that handle diverse data types and user interactions while maintaining consistency and accessibility.