mirror of
https://github.com/django-components/django-components.git
synced 2025-11-23 07:45:21 +00:00
docs: Add "scenarios" code examples (#1445)
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
This commit is contained in:
parent
71c489df8e
commit
49afdb49d6
49 changed files with 1550 additions and 260 deletions
125
docs/examples/form_grid/README.md
Normal file
125
docs/examples/form_grid/README.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# FormGrid
|
||||
|
||||
A `FormGrid` component that automatically generates labels and arranges fields in a grid. It simplifies form creation by handling the layout for you.
|
||||
|
||||
To get started, use the following example to create a simple form with 2 fields - `project` and `option`.
|
||||
|
||||
```django
|
||||
{% component "form_grid" %}
|
||||
{% 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.
|
||||
|
||||
```django
|
||||
{% component "form_grid" %}
|
||||
{# Custom label for "description" field #}
|
||||
{% fill "label:description" %}
|
||||
{% component "form_grid_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:
|
||||
|
||||

|
||||
|
||||
## API
|
||||
|
||||
### `FormGrid` component
|
||||
|
||||
The `FormGrid` 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.
|
||||
|
||||
### `FormGridLabel` component
|
||||
|
||||
When `FormGrid` component automatically generates labels for fields, it uses the `FormGridLabel` component.
|
||||
|
||||
When you need a custom label for a field, you can use the `FormGridLabel` component explicitly in `label:<field_name>` slots.
|
||||
|
||||
The `FormGridLabel` 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:**
|
||||
|
||||
```django
|
||||
{% component "form_grid_label"
|
||||
field_name="user_name"
|
||||
title="Your Name"
|
||||
/ %}
|
||||
```
|
||||
|
||||
This will render:
|
||||
|
||||
```html
|
||||
<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".
|
||||
|
||||
## Definition
|
||||
|
||||
```djc_py
|
||||
--8<-- "docs/examples/form_grid/component.py"
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
To see the component in action, you can set up a view and a URL pattern as shown below.
|
||||
|
||||
### `views.py`
|
||||
|
||||
```djc_py
|
||||
--8<-- "docs/examples/form_grid/page.py"
|
||||
```
|
||||
|
||||
### `urls.py`
|
||||
|
||||
```python
|
||||
from django.urls import path
|
||||
|
||||
from examples.pages.form_grid import FormGridPage
|
||||
|
||||
urlpatterns = [
|
||||
path("examples/form_grid", FormGridPage.as_view(), name="form_grid"),
|
||||
]
|
||||
```
|
||||
150
docs/examples/form_grid/component.py
Normal file
150
docs/examples/form_grid/component.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple
|
||||
|
||||
from django_components import Component, Slot, register, types
|
||||
|
||||
DESCRIPTION = "Form that automatically arranges fields in a grid and generates labels."
|
||||
|
||||
|
||||
@register("form_grid")
|
||||
class FormGrid(Component):
|
||||
"""Form that automatically arranges fields in a grid and generates labels."""
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
<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>
|
||||
"""
|
||||
|
||||
|
||||
# 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 = FormGridLabel.render(
|
||||
kwargs=FormGridLabel.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_grid_label")
|
||||
class FormGridLabel(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,
|
||||
}
|
||||
BIN
docs/examples/form_grid/images/form.png
Normal file
BIN
docs/examples/form_grid/images/form.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/examples/form_grid/images/form_structure.png
Normal file
BIN
docs/examples/form_grid/images/form_structure.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
88
docs/examples/form_grid/page.py
Normal file
88
docs/examples/form_grid/page.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
from django.http import HttpRequest
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from django_components import Component, types
|
||||
|
||||
|
||||
class FormGridPage(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>FormGrid</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_grid"
|
||||
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_grid_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 FormGridPage.render_to_response(request=request)
|
||||
96
docs/examples/form_grid/test_example_form_grid.py
Normal file
96
docs/examples/form_grid/test_example_form_grid.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import pytest
|
||||
from django.template import Context, Template
|
||||
from pytest_django.asserts import assertHTMLEqual
|
||||
|
||||
from django_components import registry, types
|
||||
from django_components.testing import djc_test
|
||||
|
||||
|
||||
# Imported lazily, so we import components only once settings are set
|
||||
def _create_form_components():
|
||||
from docs.examples.form_grid.component import FormGrid, FormGridLabel # noqa: PLC0415
|
||||
|
||||
registry.register("form_grid", FormGrid)
|
||||
registry.register("form_grid_label", FormGridLabel)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@djc_test
|
||||
class TestExampleForm:
|
||||
def test_render_simple_form(self):
|
||||
_create_form_components()
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "form_grid" %}
|
||||
{% fill "field:project" %}<input name="project">{% endfill %}
|
||||
{% fill "field:option" %}<select name="option"></select>{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<form method="post" data-djc-id-ca1bc41>
|
||||
<div>
|
||||
<div class="grid grid-cols-[auto,1fr] gap-x-4 gap-y-2 items-center">
|
||||
<label for="project" class="font-semibold text-gray-700" data-djc-id-ca1bc42>
|
||||
Project
|
||||
</label>
|
||||
<input name="project">
|
||||
<label for="option" class="font-semibold text-gray-700" data-djc-id-ca1bc43>
|
||||
Option
|
||||
</label>
|
||||
<select name="option"></select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
""",
|
||||
)
|
||||
|
||||
def test_custom_label(self):
|
||||
_create_form_components()
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "form_grid" %}
|
||||
{% fill "label:project" %}<strong>Custom Project Label</strong>{% endfill %}
|
||||
{% fill "field:project" %}<input name="project">{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
assert "<strong>Custom Project Label</strong>" in rendered
|
||||
assert '<label for="project"' not in rendered
|
||||
|
||||
def test_unused_label_raises_error(self):
|
||||
_create_form_components()
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "form_grid" %}
|
||||
{% fill "label:project" %}Custom Project Label{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
with pytest.raises(ValueError, match=r"Unused labels: {'label:project'}"):
|
||||
template.render(Context({}))
|
||||
|
||||
def test_prepend_append_slots(self):
|
||||
_create_form_components()
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
{% component "form_grid" %}
|
||||
{% fill "prepend" %}<div>Prepended content</div>{% endfill %}
|
||||
{% fill "field:project" %}<input name="project">{% endfill %}
|
||||
{% fill "append" %}<div>Appended content</div>{% endfill %}
|
||||
{% endcomponent %}
|
||||
"""
|
||||
template = Template(template_str)
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
assert "<div>Prepended content</div>" in rendered
|
||||
assert "<div>Appended content</div>" in rendered
|
||||
assert rendered.find("Prepended content") < rendered.find("project")
|
||||
assert rendered.find("Appended content") > rendered.find("project")
|
||||
Loading…
Add table
Add a link
Reference in a new issue