django-components/sampleproject/examples/components/form/form.py
2025-09-29 15:58:47 +02:00

126 lines
3.6 KiB
Python

from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple
from django_components import Component, Slot, register, types
@register("form")
class Form(Component):
template_file = "form.html"
class Kwargs(NamedTuple):
editable: bool = True
method: str = "post"
form_content_attrs: Optional[dict] = None
attrs: Optional[dict] = None
def get_template_data(self, args, kwargs: Kwargs, slots: Dict[str, Slot], context):
fields = prepare_form_grid(slots)
return {
"form_content_attrs": kwargs.form_content_attrs,
"method": kwargs.method,
"editable": kwargs.editable,
"attrs": kwargs.attrs,
"fields": fields,
}
# Users of this component can define form fields as slots.
#
# For example:
# ```django
# {% component "form" %}
# {% fill "field:field_1" / %}
# <textarea name="field_1" />
# {% endfill %}
# {% fill "field:field_2" / %}
# <select name="field_2">
# <option value="1">Option 1</option>
# <option value="2">Option 2</option>
# </select>
# {% endfill %}
# {% endcomponent %}
# ```
#
# The above will automatically generate labels for the fields,
# and the form will be aligned with a grid.
#
# To explicitly define a label, use `label:<field_name>` slot name.
#
# For example:
# ```django
# {% component "form" %}
# {% fill "label:field_1" / %}
# <label for="field_1">Label 1</label>
# {% endfill %}
# {% fill "field:field_1" / %}
# <textarea name="field_1" />
# {% endfill %}
# {% endcomponent %}
# ```
def prepare_form_grid(slots: Dict[str, Slot]):
used_labels: Set[str] = set()
unused_labels: Set[str] = set()
fields: List[Tuple[str, str]] = []
for slot_name in slots:
# Case: Label slot
is_label = slot_name.startswith("label:")
if is_label and slot_name not in used_labels:
unused_labels.add(slot_name)
continue
# Case: non-field, non-label slot
is_field = slot_name.startswith("field:")
if not is_field:
continue
# Case: Field slot
field_name = slot_name.split(":", 1)[1]
label_slot_name = f"label:{field_name}"
label = None
if label_slot_name in slots:
# Case: Component user explicitly defined how to render the label
label_slot: Slot[Any] = slots[label_slot_name]
label = label_slot()
unused_labels.discard(label_slot_name)
used_labels.add(slot_name)
else:
# Case: Component user didn't explicitly define how to render the label
# We will create the label for the field automatically
label = FormLabel.render(
kwargs=FormLabel.Kwargs(field_name=field_name),
deps_strategy="ignore",
)
fields.append((slot_name, label))
if unused_labels:
raise ValueError(f"Unused labels: {unused_labels}")
return fields
@register("form_label")
class FormLabel(Component):
template: types.django_html = """
<label for="{{ field_name }}" class="font-semibold text-gray-700">
{{ title }}
</label>
"""
class Kwargs(NamedTuple):
field_name: str
title: Optional[str] = None
def get_template_data(self, args, kwargs: Kwargs, slots, context):
if kwargs.title:
title = kwargs.title
else:
title = kwargs.field_name.replace("_", " ").replace("-", " ").title()
return {
"field_name": kwargs.field_name,
"title": title,
}