docs: self-contained examples (#1436)

This commit is contained in:
Juro Oravec 2025-10-05 09:09:08 +02:00 committed by GitHub
parent 48adaf98f1
commit 9877cf30ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 673 additions and 361 deletions

View file

@ -1,3 +1,47 @@
# Examples
This directory contains example components from the "Examples" documentation section.
This Django app dynamically discovers and registers example components from the documentation (`docs/examples/`).
## How it works
1. **Discovery**: At startup, the app scans `docs/examples/*/` directories for:
- `component.py` - Component definitions with inline templates
- `page.py` - Page views for live demos
2. **Registration**: Found modules are imported as:
- `examples.dynamic.<example_name>.component`
- `examples.dynamic.<example_name>.page`
3. **Components**: Are automatically registered with django-components registry via `@register()` decorators
4. **URLs**: Page views are automatically registered as URL patterns at `examples/<example_name>`
## Structure
Each example in `docs/examples/` follows this structure:
```
docs/examples/form/
├── README.md # Documentation
├── component.py # Component with inline templates
├── page.py # Page view for live demo
├── test.py # Tests
└── images/ # Screenshots/assets
```
## Live examples
All examples are available as live demos:
- **Index page**: [http://localhost:8000/examples/](http://localhost:8000/examples/) - Lists all available examples
- **Individual examples**: `http://localhost:8000/examples/<example_name>`
- [http://localhost:8000/examples/form](http://localhost:8000/examples/form)
- [http://localhost:8000/examples/tabs](http://localhost:8000/examples/tabs)
## Adding new examples
1. Create a new directory in `docs/examples/<example_name>/`
2. Add `component.py`, `page.py`, and other files as seen above.
3. Start the server and open `http://localhost:8000/examples/<example_name>` to see the example.

View file

@ -1,6 +1,17 @@
from django.apps import AppConfig
from .utils import discover_example_modules
# This adds finds all examples defined in `docs/examples/` and for each of them
# adds a URL that renders that example's live demo. These are available under
# `http://localhost:8000/examples/<example_name>`.
#
# Overview of all examples is available under `http://localhost:8000/examples/`.
class ExamplesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "examples"
def ready(self):
# Auto-discover and register example components and pages
discover_example_modules()

View file

@ -1,19 +0,0 @@
<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>

View file

@ -1,126 +0,0 @@
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,
}

View file

@ -1,100 +0,0 @@
/* based on https://codepen.io/brettsmason/pen/zYGEgZP */
[role="tablist"] {
margin: 0 0 -0.1em;
overflow: visible;
}
[role="tab"] {
position: relative;
margin: 0;
padding: 0.3em 0.5em 0.4em;
border: 1px solid hsl(219, 1%, 72%);
border-radius: 0.2em 0.2em 0 0;
box-shadow: 0 0 0.2em hsl(219, 1%, 72%);
overflow: visible;
font-family: inherit;
font-size: inherit;
background: hsl(220, 20%, 94%);
}
[role="tab"]:hover::before,
[role="tab"]:focus::before,
[role="tab"][aria-selected="true"]::before {
position: absolute;
bottom: 100%;
right: -1px;
left: -1px;
border-radius: 0.2em 0.2em 0 0;
border-top: 3px solid LinkText;
content: '';
}
[role="tab"][aria-selected="true"] {
border-radius: 0;
background: hsl(220, 43%, 99%);
outline: 0;
}
[role="tab"][aria-selected="true"]:not(:focus):not(:hover)::before {
border-top: 5px solid SelectedItem;
}
[role="tab"][aria-selected="true"]::after {
position: absolute;
z-index: 3;
bottom: -1px;
right: 0;
left: 0;
height: 0.3em;
background: hsl(220, 43%, 99%);
box-shadow: none;
content: '';
}
[role="tab"]:hover,
[role="tab"]:focus,
[role="tab"]:active {
outline: 0;
border-radius: 0;
color: inherit;
}
[role="tab"]:hover::before,
[role="tab"]:focus::before {
border-color: LinkText;
}
[role="tabpanel"] {
position: relative;
z-index: 2;
padding: 0.5em 0.5em 0.7em;
border: 1px solid hsl(219, 1%, 72%);
border-radius: 0 0.2em 0.2em 0.2em;
box-shadow: 0 0 0.2em hsl(219, 1%, 72%);
background: hsl(220, 43%, 99%);
}
[role="tabpanel"]:focus {
border-color: LinkText;
box-shadow: 0 0 0.2em LinkText;
outline: 0;
}
[role="tabpanel"]:focus::after {
position: absolute;
bottom: 0;
right: -1px;
left: -1px;
border-bottom: 3px solid LinkText;
border-radius: 0 0 0.2em 0.2em;
content: '';
}
[role="tabpanel"] p {
margin: 0;
}
[role="tabpanel"] * + p {
margin-top: 1em;
}

View file

@ -1,43 +0,0 @@
{% load component_tags %}
<div
x-data="{ selectedTab: '{{ selected_tab }}' }"
{% html_attrs
container_attrs
id=id
%}
>
<div
{% html_attrs
tablist_attrs
role="tablist"
aria-label=name
%}>
{% for tab_datum, is_hidden in tab_data %}
<button
:aria-selected="selectedTab === '{{ tab_datum.tab_id }}'"
@click="selectedTab = '{{ tab_datum.tab_id }}'"
{% html_attrs
tab_attrs
id=tab_datum.tab_id
role="tab"
aria-controls=tab_datum.tabpanel_id
disabled=tab_datum.disabled
%}>
{{ tab_datum.header }}
</button>
{% endfor %}
</div>
{% for tab_datum, is_hidden in tab_data %}
<article
:hidden="selectedTab != '{{ tab_datum.tab_id }}'"
{% html_attrs
tabpanel_attrs
hidden=is_hidden
role="tabpanel"
id=tab_datum.tabpanel_id
aria-labelledby=tab_datum.tab_id
%}>
{{ tab_datum.content }}
</article>
{% endfor %}
</div>

View file

@ -1,260 +0,0 @@
"""
Alpine-based tab components: Tablist and Tab.
Based on https://github.com/django-components/django-components/discussions/540
"""
from typing import List, NamedTuple, Optional
from django.utils.safestring import mark_safe
from django.utils.text import slugify
from django_components import Component, register
from django_components import types as t
class TabDatum(NamedTuple):
"""Datum for an individual tab."""
tab_id: str
tabpanel_id: str
header: str
content: str
disabled: bool = False
class TabContext(NamedTuple):
id: str
tab_data: List[TabDatum]
enabled: bool
@register("_tabset")
class _TablistImpl(Component):
"""
Delegated Tablist component.
Refer to `Tablist` API below.
"""
template_file = "tabs.html"
css_file = "tabs.css"
class Media:
js = (
# `mark_safe` is used so the script tag is usd as is, so we can add `defer` flag.
# `defer` is used so that AlpineJS is actually loaded only after all plugins are registered
mark_safe('<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>'),
)
class Kwargs(NamedTuple):
tab_data: List[TabDatum]
id: Optional[str] = None
name: Optional[str] = None
selected_tab: Optional[str] = None
container_attrs: Optional[dict] = None
tablist_attrs: Optional[dict] = None
tab_attrs: Optional[dict] = None
tabpanel_attrs: Optional[dict] = None
def get_template_data(self, args, kwargs: Kwargs, slots, context):
selected_tab = kwargs.selected_tab if kwargs.selected_tab is not None else kwargs.tab_data[0].tab_id
tab_data = [
(tab, tab.tab_id != selected_tab) # (tab, is_hidden)
for tab in kwargs.tab_data
]
return {
"id": kwargs.id,
"name": kwargs.name,
"container_attrs": kwargs.container_attrs,
"tablist_attrs": kwargs.tablist_attrs,
"tab_attrs": kwargs.tab_attrs,
"tabpanel_attrs": kwargs.tabpanel_attrs,
"tab_data": tab_data,
"selected_tab": selected_tab,
}
@register("Tablist")
class Tablist(Component):
"""
Tablist role component comprised of nested tab components.
After the input is processed, this component delegates to an internal implementation
component that renders the content.
`name` identifies the tablist and is used as a WAI-ARIA label
`id`, by default, is a sligified `name`, we could be used to preselect a tab based
on query parameters (TODO)
Example:
```
{% component "Tablist" id="my-tablist" name="My Tabs" %}
{% component Tab header="Tab 1" %}
This is the content of Tab 1
{% endcomponent %}
{% component Tab header="Tab 2" disabled=True %}
This is the content of Tab 2
{% endcomponent %}
{% endcomponent %}
```
"""
template: t.django_html = """
{% load component_tags %}
{% provide "_tab" ...tab_context %}
{% slot "content" default / %}
{% endprovide %}
"""
class Kwargs(NamedTuple):
id: Optional[str] = None
name: str = "Tabs"
selected_tab: Optional[str] = None
container_attrs: Optional[dict] = None
tablist_attrs: Optional[dict] = None
tab_attrs: Optional[dict] = None
tabpanel_attrs: Optional[dict] = None
def get_template_data(self, args, kwargs: Kwargs, slots, context):
self.tablist_id: str = kwargs.id or slugify(kwargs.name)
self.tab_data: List[TabDatum] = []
tab_context = TabContext(
id=self.tablist_id,
tab_data=self.tab_data,
enabled=True,
)
return {
"tab_context": tab_context._asdict(),
}
def on_render_after(self, context, template, result, error) -> Optional[str]:
"""
Render the tab set.
By the time we get here, all child Tab components should have been rendered,
and they should've populated the tablist.
"""
if error or result is None:
return None
kwargs: Tablist.Kwargs = self.kwargs
# Render the TablistImpl component in place of Tablist.
return _TablistImpl.render(
kwargs=_TablistImpl.Kwargs(
# Access variables we've defined in get_template_data
id=self.tablist_id,
tab_data=self.tab_data,
name=kwargs.name,
selected_tab=kwargs.selected_tab,
container_attrs=kwargs.container_attrs,
tablist_attrs=kwargs.tablist_attrs,
tab_attrs=kwargs.tab_attrs,
tabpanel_attrs=kwargs.tabpanel_attrs,
),
deps_strategy="ignore",
)
@register("Tab")
class Tab(Component):
"""
Individual tab, inside the default slot of the `Tablist` component.
Example:
```
{% component "Tablist" id="my-tablist" name="My Tabs" %}
{% component Tab header="Tab 1" %}
This is the content of Tab 1
{% endcomponent %}
{% component Tab header="Tab 2" disabled=True %}
This is the content of Tab 2
{% endcomponent %}
{% endcomponent %}
```
"""
template: t.django_html = """
{% load component_tags %}
{% provide "_tab" ...overriding_tab_context %}
{% slot "content" default / %}
{% endprovide %}
"""
class Kwargs(NamedTuple):
header: str
disabled: bool = False
id: Optional[str] = None
def get_template_data(self, args, kwargs: Kwargs, slots, context):
"""
Access the tab data registered for the parent Tablist component.
This raises if we're not nested inside a Tablist component.
"""
tab_ctx: TabContext = self.inject("_tab")
# We accessed the _tab context, but we're inside ANOTHER Tab
if not tab_ctx.enabled:
raise RuntimeError(
f"Component '{self.name}' was called with no parent Tablist component. "
f"Either wrap '{self.name}' in Tablist component, or check if the "
f"component is not a descendant of another instance of '{self.name}'"
)
if kwargs.id:
slug = kwargs.id
else:
group_slug = slugify(tab_ctx.id)
tab_slug = slugify(kwargs.header)
slug = f"{group_slug}_{tab_slug}"
self.tab_id = f"{slug}_tab"
self.tabpanel_id = f"{slug}_content"
self.parent_tabs: List[TabDatum] = tab_ctx.tab_data
# Prevent Tab's children from accessing the parent Tablist context.
# If we didn't do this, then you could place a Tab inside another Tab,
# ```
# {% component Tablist %}
# {% component Tab header="Tab 1" %}
# {% component Tab header="Tab 2" %}
# This is the content of Tab 2
# {% endcomponent %}
# {% endcomponent %}
# {% endcomponent %}
# ```
overriding_tab_context = TabContext(
id=self.tab_id,
tab_data=[],
enabled=False,
)
return {
"overriding_tab_context": overriding_tab_context._asdict(),
}
# This runs when the Tab component is rendered and the content is returned.
# We add the TabDatum to the parent Tablist component.
def on_render_after(self, context, template, result, error) -> None:
if error or result is None:
return
kwargs: Tab.Kwargs = self.kwargs
self.parent_tabs.append(
TabDatum(
tab_id=self.tab_id,
tabpanel_id=self.tabpanel_id,
header=kwargs.header,
disabled=kwargs.disabled,
content=mark_safe(result.strip()),
),
)

View file

@ -1,88 +0,0 @@
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)

View file

@ -1,39 +0,0 @@
from django.http import HttpRequest
from django_components import Component, types
class TabsPage(Component):
template: types.django_html = """
<html>
<head>
<title>Tabs</title>
</head>
<body>
{% component "Tablist"
id="optional-tablist-id"
name="Bonza tablist"
container_attrs:class="optional-container-attrs"
tablist_attrs:class="optional-tablist-attrs"
tab_attrs:class="optional-tab-attrs"
tabpanel_attrs:class="optional-panel-attrs"
%}
{% component "Tab" id="optional-tab-id" header="I'm a tab!" %}
{% lorem %}
{% endcomponent %}
{% component "Tab" header="I'm also a tab!" %}
<p>{% lorem %}</p>
<p>{% lorem %}</p>
{% endcomponent %}
{% component "Tab" header="I am a gorilla!" %}
<p>{% lorem %}</p>
<p>I wonder if anyone got the Monty Python reference. 🤔</p>
{% endcomponent %}
{% endcomponent %}
</body>
</html>
"""
class View:
def get(self, request: HttpRequest):
return TabsPage.render_to_response(request=request)

View file

@ -1,9 +1,57 @@
import importlib
from django.urls import path
from examples.pages.form import FormPage
from examples.pages.tabs import TabsPage
from django_components.component import Component
urlpatterns = [
path("examples/tabs", TabsPage.as_view(), name="tabs"),
path("examples/form", FormPage.as_view(), name="form"),
]
from .utils import discover_example_modules
from .views import ExamplesIndexPage
# For each example in `docs/examples/*`, register a URL pattern that points to the example's view.
# The example will be available at `http://localhost:8000/examples/<example_name>`.
# The view is the first Component class that we find in example's `page.py` module.
#
# So if we have an example called `form`:
# 1. We look for a module `examples.dynamic.form.page`,
# 2. We find the first Component class in that module (in this case `FormPage`),
# 3. We register a URL pattern that points to that view (in this case `http://localhost:8000/examples/form`).
def get_example_urls():
# First, ensure all example modules are discovered and imported
examples_names = discover_example_modules()
urlpatterns = [
# Index page that lists all examples
path("examples/", ExamplesIndexPage.as_view(), name="examples_index"),
]
for example_name in examples_names:
try:
# Import the page module (should already be loaded by discover_example_modules)
module_name = f"examples.dynamic.{example_name}.page"
module = importlib.import_module(module_name)
# Find the view class (assume it's the first Component class)
view_class = None
for attr_name in dir(module):
attr = getattr(module, attr_name)
if issubclass(attr, Component) and attr_name != "Component":
view_class = attr
break
if not view_class:
raise ValueError(f"No Component class found in {module_name}")
# Make the example availble under localhost:8000/examples/<example_name>
url_pattern = f"examples/{example_name}"
view_name = example_name
urlpatterns.append(path(url_pattern, view_class.as_view(), name=view_name))
print(f"Registered URL: {url_pattern} -> {view_class.__name__}")
except Exception as e: # noqa: BLE001
print(f"Failed to register URL for {example_name}: {e}")
return urlpatterns
urlpatterns = get_example_urls()

View file

@ -0,0 +1,93 @@
import importlib.util
import sys
from pathlib import Path
from typing import List, Set
from django.conf import settings
# Keep track of what we've already discovered to make subsequent calls a noop
_discovered_examples: Set[str] = set()
def discover_example_modules() -> List[str]:
"""
Find and import `component.py` and `page.py` files from example directories
`docs/examples/*/` (e.g. `docs/examples/form/component.py`).
These files will be importable from other modules like:
```python
from examples.dynamic.form.component import Form
# or
from examples.dynamic.form.page import FormPage
```
Components will be also registered with the ComponentRegistry, so they can be used
in the templates via the `{% component %}` tag like:
```django
{% component "form" / %}
```
This function is idempotent - calling it multiple times will not re-import modules.
"""
# Skip if we've already discovered examples
if _discovered_examples:
return list(_discovered_examples)
docs_examples_dir: Path = settings.EXAMPLES_DIR
if not docs_examples_dir.exists():
raise FileNotFoundError(f"Docs examples directory not found: {docs_examples_dir}")
for example_dir in docs_examples_dir.iterdir():
if not example_dir.is_dir():
continue
example_name = example_dir.name
component_file = example_dir / "component.py"
if component_file.exists():
_import_module_file(component_file, example_name, "component")
page_file = example_dir / "page.py"
if page_file.exists():
_import_module_file(page_file, example_name, "page")
# Mark this example as discovered
_discovered_examples.add(example_name)
return list(_discovered_examples)
def _import_module_file(py_file: Path, example_name: str, module_type: str):
"""
Dynamically import a python file as a module.
This file will then be importable from other modules like:
```python
from examples.dynamic.form.component import Form
# or
from examples.dynamic.form.page import FormPage
```
"""
module_name = f"examples.dynamic.{example_name}.{module_type}"
# Skip if module is already imported
if module_name in sys.modules:
return
try:
spec = importlib.util.spec_from_file_location(module_name, py_file)
if not spec or not spec.loader:
raise ValueError(f"Failed to load {module_type} {example_name}/{py_file.name}")
module = importlib.util.module_from_spec(spec)
# Add to sys.modules so the contents can be imported from other modules
# via Python import system.
sys.modules[module_name] = module
spec.loader.exec_module(module)
print(f"Loaded example {module_type}: {example_name}/{py_file.name}")
except Exception as e: # noqa: BLE001
print(f"Failed to load {module_type} {example_name}/{py_file.name}: {e}")

View file

@ -0,0 +1,122 @@
from django.http import HttpRequest
from django.utils.safestring import mark_safe
from django_components import Component, types
from .utils import discover_example_modules
class ExamplesIndexPage(Component):
"""Index page that lists all available examples"""
class Media:
js = (
mark_safe(
'<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp,container-queries"></script>'
),
)
def get_template_data(self, args, kwargs, slots, context):
# Get the list of discovered examples
example_names = discover_example_modules()
# Convert example names to display format
examples = []
for name in sorted(example_names):
# Convert snake_case to PascalCase (e.g. error_fallback -> ErrorFallback)
display_name = "".join(word.capitalize() for word in name.split("_"))
examples.append(
{
"name": name, # Original name for URLs
"display_name": display_name, # PascalCase for display
}
)
return {
"examples": examples,
}
class View:
def get(self, request: HttpRequest):
return ExamplesIndexPage.render_to_response(request=request)
template: types.django_html = """
<html>
<head>
<title>Django Components Examples</title>
</head>
<body class="bg-gray-50">
<div class="max-w-4xl mx-auto py-12 px-6">
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-gray-900 mb-4">
Django Components Examples
</h1>
<p class="text-xl text-gray-600">
Interactive examples showcasing django-components features
</p>
</div>
{% if examples %}
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{% for example in examples %}
<div class="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 flex flex-col">
<div class="p-6 flex flex-col flex-grow">
<h2 class="text-xl font-semibold text-gray-900 mb-2">
{{ example.display_name }}
</h2>
<p class="text-gray-600 mb-4 flex-grow">
{{ example.display_name }} component example
</p>
<a
href="/examples/{{ example.name }}"
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 transition-colors duration-200 self-start"
>
View Example
<svg class="ml-2 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-12">
<div class="text-gray-400 mb-4">
<svg class="mx-auto w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<h3 class="text-lg font-medium text-gray-900 mb-2">No examples found</h3>
<p class="text-gray-600">
No example components were discovered in the docs/examples/ directory.
</p>
</div>
{% endif %}
<div class="mt-12 text-center">
<div class="bg-white rounded-lg shadow-sm p-6">
<h3 class="text-lg font-medium text-gray-900 mb-2">
About these examples
</h3>
<p class="text-gray-600 mb-4">
These examples are dynamically discovered from the <code class="bg-gray-100 px-2 py-1 rounded text-sm">docs/examples/</code> directory.
Each example includes a component definition, live demo page, and tests.
</p>
<a
href="https://github.com/django-components/django-components"
class="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
target="_blank"
rel="noopener noreferrer"
>
View on GitHub
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
</svg>
</a>
</div>
</div>
</div>
</body>
</html>
""" # noqa: E501