Django forms lack built-in support for common reactive behaviors:
- Conditional field visibility - show/hide fields based on other field values
- Dynamic requirements - make fields required based on conditions
- Computed fields - calculate values from other fields
- Cascading choices - filter dropdown options based on other selections
- Cross-field validation - validate fields in relation to each other
Currently, developers must:
- Write JavaScript for frontend reactivity
- Duplicate logic in Python for backend validation
- Keep both in sync manually
Backend is the single source of truth. All reactive rules are defined in Python on the form class. The frontend (Datastar) reflects these rules - it makes the backend rules visible to the user in real-time.
JavaScript is always enabled. We don’t maintain fallback code for disabled JavaScript.
Simplicity over flexibility. Datastar is our frontend framework. We can add adapters for HTMX+Alpine later if needed, but we don’t over-engineer now.
@startuml
skinparam componentStyle rectangle
package "rg.forms" {
[ReactiveForm] --> [ReactiveFields]
[ReactiveFields] --> [Validation]
[ReactiveForm] --> [TemplateRenderer]
[TemplateRenderer] --> [DatastarAttrs]
}
package "Django" {
[Form] <|-- [ReactiveForm]
[Field] <|-- [ReactiveFields]
}
package "Frontend" {
[DatastarAttrs] --> [Datastar]
[Datastar] --> [Browser]
}
note right of ReactiveForm
Single source of truth
All rules defined here
end note
note right of DatastarAttrs
Auto-generated from
Python field attributes
end note
@endumlExtends django.forms.Form. All reactive behavior is defined via field attributes.
from rg.forms import ReactiveForm, ReactiveCharField, ReactiveChoiceField
class OrderForm(ReactiveForm):
order_type = ReactiveChoiceField(
choices=[('standard', 'Standard'), ('urgent', 'Urgent')],
)
# visible_when: Datastar expression, also evaluated on backend
priority = ReactiveChoiceField(
choices=[('normal', 'Normal'), ('high', 'High')],
visible_when="$order_type == 'urgent'",
required=False,
)
# required_when: Field becomes required when condition is true
notes = ReactiveCharField(
required_when="$order_type == 'urgent'",
)
# computed: Value calculated from other fields
total = ReactiveDecimalField(
computed="$quantity * $unit_price",
)| Attribute | Type | Description |
|---|---|---|
| visible_when | str | Datastar expression. Field hidden when false. |
| required_when | str | Datastar expression. Field required when true. |
| computed | str | Datastar expression. Field value is calculated. |
| choices_url | str | URL to fetch choices from (for cascading). |
| depends_on | list | Field names that trigger choices refresh. |
Expressions use Datastar syntax with $fieldname for signal references:
visible_when="$order_type == 'urgent'"
visible_when="$order_type == 'urgent' || $order_type == 'express'"
visible_when="$quantity > 10"
computed="$quantity * $unit_price"The same expressions are:
- Rendered as Datastar attributes (
data-show,data-bind) - Evaluated on backend during validation (converted to Python)
ReactiveForm automatically:
- Skips hidden fields - if
visible_whenevaluates to false, field is not validated - Enforces dynamic requirements - if
required_whenevaluates to true, field must have value - Recomputes computed fields - ignores submitted value, recalculates from source fields
def view(request):
form = OrderForm(request.POST)
if form.is_valid():
# Hidden fields excluded from cleaned_data
# Computed fields have server-calculated values
# Dynamic requirements enforced
process(form.cleaned_data){% load reactive_forms %}
{# Use single quotes - JSON uses double quotes internally #}
<form data-signals='{% reactive_signals form %}'>
<div {% reactive_wrapper_attrs form.priority %}>
<input {% reactive_input_attrs form.priority %}>
</div>
{% required_indicator form.notes %}
</form>Works with both FBV and CBV. No special view classes needed.
def order_view(request):
if request.method == 'POST':
form = OrderForm(request.POST)
if form.is_valid():
save_order(form.cleaned_data)
return redirect('success')
else:
form = OrderForm()
return render(request, 'order.html', {'form': form})- [X] Package structure (PEP 420 namespace)
- [X] ReactiveForm base class
- [X] Reactive field classes
- [X] Field attributes (visible_when, required_when, computed, depends_on)
- [X] Signal generation (get_signals_json)
- [X] Template tags
- [X] Testsite with examples
Backend evaluation of expressions.
- [X] Expression parser (safe subset)
- [X] Python evaluator
- [X] Skip validation for hidden fields (visible_when=false)
- [X] Enforce required_when
- [X] Recompute computed fields
- [X] Tests
Additional reactive field attributes.
- [X] disabled_when - conditionally disable fields
- [X] read_only_when - conditionally make fields read-only
- [X] help_text_when - dynamic help text based on conditions
- [X] placeholder_when - dynamic placeholder text
- [X] min_when, max_when - dynamic min/max bounds
- [X] Template updates for data-attr:disabled, data-attr:readonly
- [X] Example in testsite
Server-side form re-rendering for dependent fields (declarative approach).
- [X] choices_from - callable or queryset for dynamic choices
- [X] depends_on - field names that trigger re-population
- [X] value_field, label_field, label_template - choice rendering options
- [X] empty_choice, empty_choice_no_parent - placeholder options
- [X] Datastar data-on:change to POST form for re-rendering (partial update)
- [X] View pattern: detect “repopulate” vs “submit” requests
- [X] Validate that selected value is valid for current parent
- [X] Example: category -> products cascading form
Organize fields into logical sections.
- [X] FieldGroup class with label, description, css_class
- [X] visible_when on entire groups
- [X] Meta.field_groups configuration
- [X] render_field_group template tag
- [X] Example in testsite
- [X] {% render_reactive_form %} tag
- [X] {% render_reactive_field %} tag
- [X] {% render_field_group %} tag
- [X] Bulma skin (default)
- [ ] Bootstrap 5 skin
- [X] Error display
- [ ] README with examples
- [ ] API documentation
- [ ] Comparison with SPA approaches
We use Datastar. If other frameworks needed later, we can add adapters.
All rules defined in Python. Frontend reflects rules, doesn’t define them.
JavaScript required. No fallback for disabled JS.
Safe subset only: field references, literals, basic operators. No function calls.
Works with FBV and CBV. No special mixins required.
src/rg/forms/
├── __init__.py
├── apps.py
├── forms.py # ReactiveForm
├── fields.py # Reactive field classes
├── expressions.py # Expression parser & evaluator [Phase 2]
├── templatetags/
│ └── reactive_forms.py
└── templates/
└── rg_forms/
├── field.html
└── form.html
Datastar is v1.0-RC (not stable 1.0 yet) with a small community compared to HTMX (~25K stars) or Alpine (~29K stars). If Datastar API changes significantly or project becomes unmaintained, rg.forms is tightly coupled.
Mitigation:
- Abstract expression syntax where possible
- Document migration path to HTMX + Alpine if needed
- Monitor Datastar project health and community growth
- Expression evaluator is already backend-only (not Datastar-dependent)
Field attributes have grown significantly: visible_when, required_when, computed, disabled_when, read_only_when, help_text_when, placeholder_when, min_when, max_when, depends_on, choices_from…
More will likely be needed as real-world usage reveals gaps:
error_message_when- dynamic error messagescss_class_when- conditional stylingchoices_when- conditional choice filtering- FieldGroup: nested groups, collapsible sections, ordering
Mitigation:
- Semantic versioning with clear deprecation policy
- Deprecation warnings before breaking changes
- Collect real-world usage feedback before stabilizing API
- Consider
attrs_whendict pattern instead of individual*_whenattributes
Current parser supports only basic operations. Users will hit limits:
| Missing Feature | Example |
|---|---|
| Functions | len($items), $date.year, contains($text, 'x') |
| Ternary | $a ? $b : $c |
| Array access | $items[0], $items.length |
| String ops | startsWith, endsWith, includes |
| Nested fields | $address.city |
Mitigation:
- Document supported expression subset clearly
- Provide escape hatch: custom
clean_*methods for complex logic - Prioritize most-requested features for expression parser
- Consider allowing Python lambdas for complex computed fields
Common Django patterns not yet supported:
ReactiveModelForm- most common use case in real apps- Formsets / InlineFormsets - editing multiple related objects
- FileField with upload progress and preview
- Integration with crispy-forms, django-widget-tweaks
Mitigation:
- Prioritize
ReactiveModelFormfor next phase - Document workarounds for formsets
- Provide custom widget documentation
All validation is server-side. Users expect instant feedback without round-trip for simple rules like “required”, “min length”, “email format”.
Mitigation:
- HTML5 validation attributes are already rendered (required, min, max, minlength, maxlength)
- Document that HTML5 provides instant feedback for common cases
- Consider optional client-side validation layer for complex rules
Typos in expressions ($ordertpye instead of $order_type) fail silently, defaulting to =True=/visible. This makes debugging difficult.
def _evaluate_expression(self, expression: str) -> bool | None:
try:
return evaluate_expression(expression, self._get_form_data())
except ExpressionError:
return None # Silently defaults to True/visibleMitigation:
- Add
DEBUGmode that raises on expression errors - Add form validation at definition time (check field names exist)
- Log expression errors with field context
- Consider
check_expressions()method for testing
Expression strings like "$order_type = ‘urgent’”= have no autocomplete, type checking, or refactoring support. Easy to make typos.
Mitigation:
- Document expression syntax clearly with examples
- Consider future TypedDict or dataclass approach
- Provide
check_expressions()for CI/testing - IDE plugin is out of scope but could be community contribution
Only Bulma templates implemented. Bootstrap 5 and other frameworks not yet supported.
Mitigation:
- Bootstrap 5 skin planned for Phase 5
- Template override mechanism already works
- Document custom template creation
- Server round-trip for every cascading dropdown change
- No built-in debouncing for rapid input
- Large forms with many expressions have parsing overhead
Mitigation:
- Datastar handles debouncing via
data-on:change__debounce_500ms - Expression parsing is fast (simple tokenizer + recursive descent)
- Document performance best practices
- Consider caching parsed ASTs if profiling shows need
Cannot do “check username availability” or similar async checks without custom code.
Mitigation:
- Document pattern for async validation with Datastar
- Out of scope for core library - keep it simple
- Custom
clean_*methods can call external services
Based on risk analysis, these features may be needed:
- [ ]
ReactiveModelForm- model-backed reactive forms - [ ] Expression expansion: ternary, string functions, nested fields
- [ ]
check_expressions()method for testing/CI - [ ] DEBUG mode for expression errors
- [ ] Async validation pattern documentation
- [ ] HTMX adapter (if Datastar risk materializes)