Custom widgets give full control over rendering, from HTML5 input types to complex multi-value inputs.
Key Concepts
- input_type: Sets HTML type on TextInput subclasses.
- template_name: Django template for widget HTML.
- MultiWidget: Combines sub-widgets.
- decompress/compress: Split and combine values.
Real World Context
A star-rating widget renders clickable stars. An address widget splits street/city/state/ZIP into separate inputs.
Deep Dive
Custom Widgets
Create custom widgets for special input types or enhanced functionality.
Simple Custom Widget
pythonfrom django import forms class ColorPickerWidget(forms.TextInput): """Widget for HTML5 color picker.""" input_type = 'color' def __init__(self, attrs=None): default_attrs = {'class': 'color-picker'} if attrs: default_attrs.update(attrs) super().__init__(attrs=default_attrs) # Usage class ThemeForm(forms.Form): primary_color = forms.CharField( widget=ColorPickerWidget, initial='#007bff' )
Widget with Custom Template
pythonclass StarRatingWidget(forms.Widget): """Star rating input widget.""" template_name = 'widgets/star_rating.html' def __init__(self, max_stars=5, attrs=None): self.max_stars = max_stars super().__init__(attrs) def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) context['widget'].update({ 'max_stars': self.max_stars, 'range': range(1, self.max_stars + 1), }) return context def format_value(self, value): return int(value) if value else 0
html<!-- templates/widgets/star_rating.html --> <div class="star-rating" data-name="{{ widget.name }}"> {% for i in widget.range %} <label> <input type="radio" name="{{ widget.name }}" value="{{ i }}" {% if i == widget.value %}checked{% endif %}> <span class="star">★</span> </label> {% endfor %} </div>
Multi-Value Widget
pythonclass AddressWidget(forms.MultiWidget): """Composite widget for address input.""" def __init__(self, attrs=None): widgets = [ forms.TextInput(attrs={'placeholder': 'Street'}), forms.TextInput(attrs={'placeholder': 'City'}), forms.TextInput(attrs={'placeholder': 'State'}), forms.TextInput(attrs={'placeholder': 'ZIP'}), ] super().__init__(widgets, attrs) def decompress(self, value): """Split a single value into list for each widget.""" if value: return value.split('|') return ['', '', '', ''] class AddressField(forms.MultiValueField): """Field that uses AddressWidget.""" def __init__(self, **kwargs): fields = [ forms.CharField(max_length=100), forms.CharField(max_length=50), forms.CharField(max_length=50), forms.CharField(max_length=10), ] super().__init__(fields, widget=AddressWidget, **kwargs) def compress(self, data_list): """Combine values from all widgets.""" if data_list: return '|'.join(data_list) return '' # Usage class OrderForm(forms.Form): shipping_address = AddressField(required=True)
Widget with JavaScript
pythonclass AutocompleteWidget(forms.TextInput): """Text input with autocomplete functionality.""" template_name = 'widgets/autocomplete.html' def __init__(self, data_url, attrs=None): self.data_url = data_url super().__init__(attrs) def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) context['widget']['data_url'] = self.data_url return context class Media: css = { 'all': ('css/autocomplete.css',) } js = ('js/autocomplete.js',)
html<!-- templates/widgets/autocomplete.html --> <div class="autocomplete-wrapper"> <input type="{{ widget.type }}" name="{{ widget.name }}" {% if widget.value != None %}value="{{ widget.value }}"{% endif %} {% include "django/forms/widgets/attrs.html" %} data-autocomplete-url="{{ widget.data_url }}"> <div class="autocomplete-results"></div> </div>
Using Widget Media
html<!-- In template --> <head> {{ form.media }} <!-- Includes CSS and JS for all widgets --> <!-- Or separately --> {{ form.media.css }} </head> <body> {{ form }} {{ form.media.js }} </body>
Common Pitfalls
- Missing decompress() -- Existing values show empty.
- File pointer not reset -- Call seek(0) after reading.
- Hardcoded HTML -- Use template_name instead.
Best Practices
- Extend closest built-in -- Just change input_type.
- Use Media class -- Auto-include CSS/JS.
- Test edge cases -- None, empty strings.
Summary
- Override input_type for HTML5 types.
- Use template_name for complex widgets.
- MultiWidget pairs with MultiValueField.
- Implement decompress/compress.
- Declare CSS/JS in Media class.