Skip to content

Form¤

A Form component that automatically generates labels and arranges fields in a grid. It simplifies form creation by handling the layout for you.

Form example

To get started, use the following example to create a simple form with 2 fields - project and option:

{% component "form" %}
  {% fill "field:project" %}
    <input name="project" required>
  {% endfill %}

  {% fill "field:option" %}
    <select name="option" required>
      <option value="1">Option 1</option>
      <option value="2">Option 2</option>
      <option value="3">Option 3</option>
    </select>
  {% endfill %}
{% endcomponent %}

This will render a <form> where fields are defined using field:<field_name> slots.

Labels are automatically generated from the field name. If you want to define a custom label for a field, you can use the label:<field_name> slot.

{% component "form" %}
  {# Custom label for "description" field #}
  {% fill "label:description" %}
    {% component "form_label"
      field_name="description"
      title="Marvelous description"
    / %}
  {% endfill %}

  {% fill "field:description" %}
    <textarea name="description" required></textarea>
  {% endfill %}
{% endcomponent %}

Whether you define custom labels or not, the form will have the following structure:

Form structure

API¤

Form component¤

The Form component is the main container for your form fields. It accepts the following arguments:

  • editable (optional, default True): A boolean that determines if the form is editable.
  • method (optional, default "post"): The HTTP method for the form submission.
  • form_content_attrs (optional): A dictionary of HTML attributes to be added to the form's content container.
  • attrs (optional): A dictionary of HTML attributes to be added to the <form> element itself.

To define the fields, you define a slot for each field.

Slots:

  • field:<field_name>: Use this slot to define a form field. The component will automatically generate a label for it based on <field_name>.
  • label:<field_name>: If you need a custom label for a field, you can define it using this slot.
  • prepend: Content in this slot will be placed at the beginning of the form, before the main fields.
  • append: Content in this slot will be placed at the end of the form, after the main fields. This is a good place for submit buttons.

FormLabel component¤

When Form component automatically generates labels for fields, it uses the FormLabel component.

When you need a custom label for a field, you can use the FormLabel component explicitly in label:<field_name> slots.

The FormLabel component accepts the following arguments:

  • field_name (required): The name of the field that this label is for. This will be used as the for attribute of the label.
  • title (optional): Custom text for the label. If not provided, the component will automatically generate a title from the field_name by replacing underscores and hyphens with spaces and applying title case.

Example:

{% component "form_label"
  field_name="user_name"
  title="Your Name"
/ %}

This will render:

<label for="user_name" class="font-semibold text-gray-700">
  Your Name
</label>

If title is not provided, field_name="user_name" would automatically generate the title "User Name", converting snake_case to "Title Case".

Example¤

To see the component in action, you can set up a view and a URL pattern as shown below.

views.py¤

from django.http import HttpRequest
from django.utils.safestring import mark_safe

from django_components import Component, types


class FormPage(Component):
    class Media:
        js = (
            # AlpineJS
            mark_safe('<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>'),
            # TailwindCSS
            "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4",
        )

    template: types.django_html = """
      <html>
        <head>
          <title>Form</title>
          <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp,container-queries"></script>
        </head>
        <body>
          <div x-data="{
            onSubmit: () => {
              alert('Submitted!');
            }
          }">
            <div class="prose-xl p-6">
              <h3>Submit form</h3>
            </div>

            {% component "form"
              attrs:class="pb-4 px-4 pt-6 sm:px-6 lg:px-8 flex-auto flex flex-col"
              attrs:style="max-width: 600px;"
              attrs:@submit.prevent="onSubmit"
            %}
              {% fill "field:project" %}
                <input
                  name="project"
                  required
                  class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                >
              {% endfill %}

              {% fill "field:option" %}
                <select
                  name="option"
                  required
                  class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:max-w-xs sm:text-sm sm:leading-6"
                >
                  <option value="1">Option 1</option>
                  <option value="2">Option 2</option>
                  <option value="3">Option 3</option>
                </select>
              {% endfill %}

              {# Defined both label and field because label name is different from field name #}
              {% fill "label:description" %}
                {% component "form_label" field_name="description" title="Marvelous description" / %}
              {% endfill %}
              {% fill "field:description" %}
                <textarea
                  name="description"
                  id="description"
                  rows="5"
                  class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                ></textarea>
              {% endfill %}

              {% fill "append" %}
                <div class="flex justify-end items-center gap-x-6 border-t border-gray-900/10 py-4">
                  <button type="submit" class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
                    Submit
                  </button>
                  <button type="button" class="text-sm font-semibold leading-6 text-gray-900">
                    Cancel
                  </button>
                </div>
              {% endfill %}
            {% endcomponent %}
          </div>
        </body>
      </html>
    """  # noqa: E501

    class View:
        def get(self, request: HttpRequest):
            return FormPage.render_to_response(request=request)

urls.py¤

from django.urls import path

from examples.pages.form import FormPage

urlpatterns = [
    path("examples/form", FormPage.as_view(), name="form"),
]

Definition¤

form.py¤

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,
        }

form.html¤

{% load component_tags %}
<form
  {% if submit_href and editable %} action="{{ submit_href }}" {% endif %}
  method="{{ method }}"
  {% html_attrs attrs %}
>
  {% slot "prepend" / %}

  <div {% html_attrs form_content_attrs %}>
    {# Generate a grid of fields and labels out of given slots #}
    <div class="grid grid-cols-[auto,1fr] gap-x-4 gap-y-2 items-center">
      {% for field_name, label in fields %}
        {{ label }}
        {% slot name=field_name / %}
      {% endfor %}
    </div>
  </div>

  {% slot "append" / %}
</form>