Deployed 49afdb49 to dev with MkDocs 1.6.1 and mike 2.1.3

This commit is contained in:
github-actions 2025-10-07 22:18:41 +00:00
parent 01f6852b4d
commit 5c55ebeac8
188 changed files with 3598 additions and 863 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,64 @@
# ruff: noqa: S311
import random
from typing import NamedTuple, Optional
from django_components import Component, register, types
DESCRIPTION = "Dynamically render different component versions. Use for A/B testing, phased rollouts, etc."
@register("offer_card_old")
class OfferCardOld(Component):
class Kwargs(NamedTuple):
savings_percent: int
def get_template_data(self, args, kwargs, slots, context):
return {
"savings_percent": kwargs.savings_percent,
}
template: types.django_html = """
<div class="p-4 border rounded-lg bg-gray-100">
<h3 class="text-lg font-bold text-gray-800">
Special Offer!
</h3>
<p class="text-gray-600">
Get {{ savings_percent }}% off on your next purchase.
</p>
</div>
"""
@register("offer_card_new")
class OfferCardNew(OfferCardOld):
template: types.django_html = """
<div class="p-6 border-2 border-dashed border-blue-500 rounded-lg bg-blue-50 text-center">
<h3 class="text-xl font-extrabold text-blue-800 animate-pulse">
FLASH SALE!
</h3>
<p class="text-blue-600">
Exclusive Offer: {{ savings_percent }}% off everything!
</p>
</div>
"""
@register("offer_card")
class OfferCard(Component):
class Kwargs(NamedTuple):
savings_percent: int
use_new_version: Optional[bool] = None
def on_render(self, context, template):
# Pass all kwargs to the child component
kwargs_for_child = self.kwargs._asdict()
use_new = kwargs_for_child.pop("use_new_version")
# If version not specified, choose randomly
if use_new is None:
use_new = random.choice([True, False])
if use_new:
return OfferCardNew.render(context=context, kwargs=kwargs_for_child)
else:
return OfferCardOld.render(context=context, kwargs=kwargs_for_child)

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,63 @@
from django.http import HttpRequest, HttpResponse
from django_components import Component, types
class ABTestingPage(Component):
class Media:
js = ("https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries",)
template: types.django_html = """
{% load component_tags %}
<html>
<head>
<title>A/B Testing Example</title>
</head>
<body class="bg-gray-100 p-8">
<div class="max-w-2xl mx-auto bg-white p-6 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4">
A/B Testing Components
</h1>
<p class="text-gray-600 mb-6">
This example shows how a single component can render different versions
based on a parameter (or a random choice), perfect for A/B testing.
</p>
<div class="mb-8">
<h2 class="text-xl font-semibold mb-2">
Variant A (Old Offer)
</h2>
<p class="text-sm text-gray-500 mb-2">
Rendered with <code>use_new_version=False</code>
</p>
{% component "offer_card" use_new_version=False savings_percent=10 / %}
</div>
<div>
<h2 class="text-xl font-semibold mb-2">
Variant B (New Offer)
</h2>
<p class="text-sm text-gray-500 mb-2">
Rendered with <code>use_new_version=True</code>
</p>
{% component "offer_card" use_new_version=True savings_percent=25 / %}
</div>
<div class="mt-8">
<h2 class="text-xl font-semibold mb-2">
Variant C (Random)
</h2>
<p class="text-sm text-gray-500 mb-2">
Rendered without <code>use_new_version</code>.
Reload the page to see a different version.
</p>
{% component "offer_card" savings_percent=15 / %}
</div>
</div>
</body>
</html>
"""
class View:
def get(self, request: HttpRequest) -> HttpResponse:
return ABTestingPage.render_to_response(request=request)

View file

@ -0,0 +1,58 @@
import pytest
from django.template import Context, Template
from django_components import registry, types
from django_components.testing import djc_test
def _import_components():
from docs.examples.ab_testing.component import OfferCard, OfferCardNew, OfferCardOld # noqa: PLC0415
registry.register("offer_card", OfferCard)
registry.register("offer_card_old", OfferCardOld)
registry.register("offer_card_new", OfferCardNew)
@pytest.mark.django_db
@djc_test
class TestABTesting:
def test_renders_old_version(self):
_import_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "offer_card" use_new_version=False savings_percent=10 / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert "Special Offer!" in rendered
assert "10% off" in rendered
assert "FLASH SALE!" not in rendered
def test_renders_new_version(self):
_import_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "offer_card" use_new_version=True savings_percent=25 / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert "FLASH SALE!" in rendered
assert "25% off" in rendered
assert "Special Offer!" not in rendered
def test_renders_random_version(self):
_import_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "offer_card" savings_percent=15 / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
is_new = "FLASH SALE!" in rendered and "15% off" in rendered
is_old = "Special Offer!" in rendered and "15% off" in rendered
# Check that one and only one of the versions is rendered
assert (is_new and not is_old) or (is_old and not is_new)

View file

@ -0,0 +1,64 @@
from typing import Dict, List, NamedTuple
from django_components import Component, register, types
DESCRIPTION = "Track component errors or success rates to send them to Sentry or other services."
# A mock analytics service
analytics_events: List[Dict] = []
error_rate = {
"error": 0,
"success": 0,
}
@register("api_widget")
class ApiWidget(Component):
class Kwargs(NamedTuple):
simulate_error: bool = False
def get_template_data(self, args, kwargs: Kwargs, slots, context):
if kwargs.simulate_error:
raise ConnectionError("API call failed")
return {"data": "Mock API response data"}
template: types.django_html = """
<div class="p-4 border rounded-lg bg-gray-50">
<h4 class="font-bold text-gray-800">API Widget</h4>
<p class="text-gray-600">Data: {{ data }}</p>
</div>
"""
@register("sentry_error_tracker")
class SentryErrorTracker(Component):
def on_render_after(self, context, template, result, error):
if error:
event = {
"type": "error",
"component": self.registered_name,
"error": error,
}
analytics_events.append(event)
print(f"SENTRY: Captured error in component {self.registered_name}: {error}")
template: types.django_html = """
{% load component_tags %}
{% slot "default" / %}
"""
@register("success_rate_tracker")
class SuccessRateTracker(Component):
def on_render_after(self, context, template, result, error):
# Track error
if error:
error_rate["error"] += 1
# Track success
else:
error_rate["success"] += 1
template: types.django_html = """
{% load component_tags %}
{% slot "default" / %}
"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,117 @@
from django.http import HttpRequest, HttpResponse
from django_components import Component, register, types
from .component import analytics_events, error_rate
class AnalyticsPage(Component):
class Media:
js = ("https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries",)
template: types.django_html = """
{% load component_tags %}
<html>
<head>
<title>Analytics Example</title>
</head>
<body class="bg-gray-100 p-8">
<div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4">
Component Analytics
</h1>
<p class="text-gray-600 mb-6">
Track component errors or success rates to send them
to Sentry or other services.
</p>
{# NOTE: Intentionally hidden so we focus on the events tracking #}
<div style="display: none;">
{% component "template_with_errors" / %}
</div>
{% component "captured_events" / %}
</div>
</body>
</html>
"""
class View:
def get(self, request: HttpRequest) -> HttpResponse:
# Clear events on each page load
analytics_events.clear()
error_rate["error"] = 0
error_rate["success"] = 0
return AnalyticsPage.render_to_response(request=request)
@register("template_with_errors")
class TemplateWithErrors(Component):
template: types.django_html = """
<div class="mb-8">
<h2 class="text-xl font-semibold mb-2">
Sentry Error Tracking
</h2>
<p class="text-sm text-gray-500 mb-2">
This component only logs events when an error occurs.
</p>
{% component "error_fallback" %}
{% component "sentry_error_tracker" %}
{% component "api_widget" simulate_error=True / %}
{% endcomponent %}
{% endcomponent %}
{% component "sentry_error_tracker" %}
{% component "api_widget" simulate_error=False / %}
{% endcomponent %}
</div>
<div>
<h2 class="text-xl font-semibold mb-2">
Success Rate Analytics
</h2>
<p class="text-sm text-gray-500 mb-2">
This component logs both successful and failed renders.
</p>
{% component "error_fallback" %}
{% component "success_rate_tracker" %}
{% component "api_widget" simulate_error=True / %}
{% endcomponent %}
{% endcomponent %}
{% component "success_rate_tracker" %}
{% component "api_widget" simulate_error=False / %}
{% endcomponent %}
</div>
"""
# NOTE: Since this runs after `template_with_errors`,
# the `analytics_events` will be populated.
@register("captured_events")
class CapturedEvents(Component):
def get_template_data(self, args, kwargs, slots, context):
return {"events": analytics_events, "error_rate": error_rate}
template: types.django_html = """
<div class="mt-8 p-4 border rounded-lg bg-gray-50">
<h3 class="text-lg font-semibold mb-2">
Captured Analytics Events
</h3>
<pre class="text-sm text-gray-700 whitespace-pre-wrap">
{% for event in events %}
{{ event }}
{% endfor %}
</pre>
</div>
<div class="mt-8 p-4 border rounded-lg bg-gray-50">
<h3 class="text-lg font-semibold mb-2">
Error Rate
</h3>
<pre class="text-sm text-gray-700 whitespace-pre-wrap">
{{ error_rate }}
</pre>
<p class="text-sm text-gray-500">
{{ error_rate.error }} errors out of {{ error_rate.success }} calls.
</p>
</div>
"""

View file

@ -0,0 +1,71 @@
import pytest
from django.template import Context, Template
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_components():
from docs.examples.analytics.component import ( # noqa: PLC0415
ApiWidget,
SentryErrorTracker,
SuccessRateTracker,
analytics_events,
error_rate,
)
registry.register("api_widget", ApiWidget)
registry.register("sentry_error_tracker", SentryErrorTracker)
registry.register("success_rate_tracker", SuccessRateTracker)
analytics_events.clear()
error_rate["error"] = 0
error_rate["success"] = 0
return analytics_events, error_rate
@pytest.mark.django_db
@djc_test
class TestAnalytics:
def test_sentry_tracker_logs_only_errors(self):
analytics_events, error_rate = _create_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "error_fallback" %}
{% component "sentry_error_tracker" %}
{% component "api_widget" simulate_error=True / %}
{% endcomponent %}
{% endcomponent %}
{% component "sentry_error_tracker" %}
{% component "api_widget" simulate_error=False / %}
{% endcomponent %}
"""
template = Template(template_str)
template.render(Context({}))
assert error_rate["error"] == 0
assert error_rate["success"] == 0
assert len(analytics_events) == 1
assert analytics_events[0]["type"] == "error"
assert analytics_events[0]["component"] == "sentry_error_tracker"
assert analytics_events[0]["error"] is not None
def test_success_rate_tracker_logs_all(self):
analytics_events, error_rate = _create_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "error_fallback" %}
{% component "success_rate_tracker" %}
{% component "api_widget" simulate_error=True / %}
{% endcomponent %}
{% endcomponent %}
{% component "success_rate_tracker" %}
{% component "api_widget" simulate_error=False / %}
{% endcomponent %}
"""
template = Template(template_str)
template.render(Context({}))
assert len(analytics_events) == 0
assert error_rate["error"] == 1
assert error_rate["success"] == 1

View file

@ -0,0 +1,40 @@
# ruff: noqa: S311
import random
from typing import NamedTuple
from django_components import Component, register, types
DESCRIPTION = "A component that catches errors and displays fallback content, similar to React's ErrorBoundary."
@register("weather_widget")
class WeatherWidget(Component):
class Kwargs(NamedTuple):
location: str
simulate_error: bool = False
def get_template_data(self, args, kwargs: Kwargs, slots, context):
if kwargs.simulate_error:
raise OSError(f"Failed to connect to weather service for '{kwargs.location}'.")
return {
"location": kwargs.location,
"temperature": f"{random.randint(10, 30)}°C",
"condition": random.choice(["Sunny", "Cloudy", "Rainy"]),
}
template: types.django_html = """
<div class="bg-white rounded-lg shadow-md p-6">
<h3 class="text-xl font-semibold text-gray-800 mb-2">
Weather in {{ location }}
</h3>
<p class="text-gray-600">
<strong class="font-medium text-gray-700">Temperature:</strong>
{{ temperature }}
</p>
<p class="text-gray-600">
<strong class="font-medium text-gray-700">Condition:</strong>
{{ condition }}
</p>
</div>
"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,68 @@
from django.http import HttpRequest, HttpResponse
from django.utils.safestring import mark_safe
from django_components import Component, types
class ErrorFallbackPage(Component):
class Media:
js = (
mark_safe(
'<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp,container-queries"></script>'
),
)
template: types.django_html = """
{% load component_tags %}
<html>
<head>
<title>ErrorFallback Example</title>
</head>
<body class="bg-gray-100 p-8">
<div class="max-w-2xl mx-auto bg-white p-6 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4">Weather API Widget Example</h1>
<p class="text-gray-600 mb-6">
This example demonstrates using ErrorFallback to handle potential API failures gracefully.
</p>
<div class="mb-8">
<h2 class="text-xl font-semibold mb-2">Case 1: API call is successful</h2>
{% component "error_fallback" %}
{% fill "content" %}
{% component "weather_widget" location="New York" / %}
{% endfill %}
{% fill "fallback" %}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
<strong class="font-bold">Error:</strong>
<span class="block sm:inline">Could not load weather data.</span>
</div>
{% endfill %}
{% endcomponent %}
</div>
<div>
<h2 class="text-xl font-semibold mb-2">Case 2: API call fails</h2>
{% component "error_fallback" %}
{% fill "content" %}
{% component "weather_widget" location="Atlantis" simulate_error=True / %}
{% endfill %}
{% fill "fallback" %}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
<strong class="font-bold">Error:</strong>
<span class="block sm:inline">
Could not load weather data for
<strong>Atlantis</strong>.
The location may not be supported or the service is temporarily down.
</span>
</div>
{% endfill %}
{% endcomponent %}
</div>
</div>
</body>
</html>
""" # noqa: E501
class View:
def get(self, request: HttpRequest) -> HttpResponse:
return ErrorFallbackPage.render_to_response(request=request)

View file

@ -0,0 +1,56 @@
import pytest
from django.template import Context, Template
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_components():
from docs.examples.error_fallback.component import WeatherWidget # noqa: PLC0415
registry.register("weather_widget", WeatherWidget)
@pytest.mark.django_db
@djc_test
class TestExampleWeatherWidget:
def test_renders_successfully(self):
_create_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "error_fallback" %}
{% fill "content" %}
{% component "weather_widget" location="New York" / %}
{% endfill %}
{% fill "fallback" %}
Error!
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert "Weather in New York" in rendered
assert "Error!" not in rendered
def test_renders_fallback_on_error(self):
_create_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "error_fallback" %}
{% fill "content" %}
{% component "weather_widget" location="Atlantis" simulate_error=True / %}
{% endfill %}
{% fill "fallback" %}
<p>Weather service unavailable.</p>
{% endfill %}
{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert "Weather in Atlantis" not in rendered
assert "Weather service unavailable." in rendered

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View file

@ -2,9 +2,13 @@ from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple
from django_components import Component, Slot, register, types 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."""
@register("form")
class Form(Component):
class Kwargs(NamedTuple): class Kwargs(NamedTuple):
editable: bool = True editable: bool = True
method: str = "post" method: str = "post"
@ -109,8 +113,8 @@ def prepare_form_grid(slots: Dict[str, Slot]):
else: else:
# Case: Component user didn't explicitly define how to render the label # Case: Component user didn't explicitly define how to render the label
# We will create the label for the field automatically # We will create the label for the field automatically
label = FormLabel.render( label = FormGridLabel.render(
kwargs=FormLabel.Kwargs(field_name=field_name), kwargs=FormGridLabel.Kwargs(field_name=field_name),
deps_strategy="ignore", deps_strategy="ignore",
) )
@ -122,8 +126,8 @@ def prepare_form_grid(slots: Dict[str, Slot]):
return fields return fields
@register("form_label") @register("form_grid_label")
class FormLabel(Component): class FormGridLabel(Component):
template: types.django_html = """ template: types.django_html = """
<label for="{{ field_name }}" class="font-semibold text-gray-700"> <label for="{{ field_name }}" class="font-semibold text-gray-700">
{{ title }} {{ title }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,7 @@ from django.utils.safestring import mark_safe
from django_components import Component, types from django_components import Component, types
class FormPage(Component): class FormGridPage(Component):
class Media: class Media:
js = ( js = (
# AlpineJS # AlpineJS
@ -16,7 +16,7 @@ class FormPage(Component):
template: types.django_html = """ template: types.django_html = """
<html> <html>
<head> <head>
<title>Form</title> <title>FormGrid</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp,container-queries"></script> <script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp,container-queries"></script>
</head> </head>
<body> <body>
@ -29,7 +29,7 @@ class FormPage(Component):
<h3>Submit form</h3> <h3>Submit form</h3>
</div> </div>
{% component "form" {% component "form_grid"
attrs:class="pb-4 px-4 pt-6 sm:px-6 lg:px-8 flex-auto flex flex-col" 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:style="max-width: 600px;"
attrs:@submit.prevent="onSubmit" attrs:@submit.prevent="onSubmit"
@ -56,7 +56,7 @@ class FormPage(Component):
{# Defined both label and field because label name is different from field name #} {# Defined both label and field because label name is different from field name #}
{% fill "label:description" %} {% fill "label:description" %}
{% component "form_label" field_name="description" title="Marvelous description" / %} {% component "form_grid_label" field_name="description" title="Marvelous description" / %}
{% endfill %} {% endfill %}
{% fill "field:description" %} {% fill "field:description" %}
<textarea <textarea
@ -85,4 +85,4 @@ class FormPage(Component):
class View: class View:
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
return FormPage.render_to_response(request=request) return FormGridPage.render_to_response(request=request)

View file

@ -8,10 +8,10 @@ from django_components.testing import djc_test
# Imported lazily, so we import components only once settings are set # Imported lazily, so we import components only once settings are set
def _create_form_components(): def _create_form_components():
from docs.examples.form.component import Form, FormLabel # noqa: PLC0415 from docs.examples.form_grid.component import FormGrid, FormGridLabel # noqa: PLC0415
registry.register("form", Form) registry.register("form_grid", FormGrid)
registry.register("form_label", FormLabel) registry.register("form_grid_label", FormGridLabel)
@pytest.mark.django_db @pytest.mark.django_db
@ -21,7 +21,7 @@ class TestExampleForm:
_create_form_components() _create_form_components()
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "form" %} {% component "form_grid" %}
{% fill "field:project" %}<input name="project">{% endfill %} {% fill "field:project" %}<input name="project">{% endfill %}
{% fill "field:option" %}<select name="option"></select>{% endfill %} {% fill "field:option" %}<select name="option"></select>{% endfill %}
{% endcomponent %} {% endcomponent %}
@ -53,7 +53,7 @@ class TestExampleForm:
_create_form_components() _create_form_components()
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "form" %} {% component "form_grid" %}
{% fill "label:project" %}<strong>Custom Project Label</strong>{% endfill %} {% fill "label:project" %}<strong>Custom Project Label</strong>{% endfill %}
{% fill "field:project" %}<input name="project">{% endfill %} {% fill "field:project" %}<input name="project">{% endfill %}
{% endcomponent %} {% endcomponent %}
@ -68,7 +68,7 @@ class TestExampleForm:
_create_form_components() _create_form_components()
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "form" %} {% component "form_grid" %}
{% fill "label:project" %}Custom Project Label{% endfill %} {% fill "label:project" %}Custom Project Label{% endfill %}
{% endcomponent %} {% endcomponent %}
""" """
@ -81,7 +81,7 @@ class TestExampleForm:
_create_form_components() _create_form_components()
template_str: types.django_html = """ template_str: types.django_html = """
{% load component_tags %} {% load component_tags %}
{% component "form" %} {% component "form_grid" %}
{% fill "prepend" %}<div>Prepended content</div>{% endfill %} {% fill "prepend" %}<div>Prepended content</div>{% endfill %}
{% fill "field:project" %}<input name="project">{% endfill %} {% fill "field:project" %}<input name="project">{% endfill %}
{% fill "append" %}<div>Appended content</div>{% endfill %} {% fill "append" %}<div>Appended content</div>{% endfill %}

View file

@ -0,0 +1,66 @@
from typing import NamedTuple
from django.http import HttpRequest, HttpResponse
from django_components import Component, get_component_url, register, types
DESCRIPTION = "Handle the entire form submission flow in a single file and without Django's Form class."
@register("thank_you_message")
class ThankYouMessage(Component):
class Kwargs(NamedTuple):
name: str
def get_template_data(self, args, kwargs: Kwargs, slots, context):
return {"name": kwargs.name}
template: types.django_html = """
<div class="p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg mt-4">
<p>Thank you for your submission, {{ name }}!</p>
</div>
"""
@register("contact_form")
class ContactFormComponent(Component):
def get_template_data(self, args, kwargs: NamedTuple, slots, context):
# Send the form data to the HTTP handlers of this component
submit_url = get_component_url(ContactFormComponent)
return {
"submit_url": submit_url,
}
template: types.django_html = """
<form hx-post="{{ submit_url }}" hx-target="#thank-you-container" hx-swap="innerHTML" class="space-y-4">
{% csrf_token %}
<div>
<label for="name" class="block text-sm font-medium text-gray-700">
Name
</label>
<input
type="text"
name="name"
id="name"
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
>
</div>
<div>
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Submit
</button>
</div>
</form>
<div id="thank-you-container"></div>
""" # noqa: E501
class View:
public = True
# Submit handler
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Access the submitted data
name = request.POST.get("name", "stranger")
# Respond with the "thank you" message
return ThankYouMessage.render_to_response(kwargs={"name": name})

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,36 @@
from django.http import HttpRequest, HttpResponse
from django_components import Component, types
class FormSubmissionPage(Component):
class Media:
js = (
"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries",
"https://unpkg.com/htmx.org@2.0.7",
)
template: types.django_html = """
{% load component_tags %}
<html>
<head>
<title>Form Submission Example</title>
</head>
<body class="bg-gray-100 p-8" hx-boost="true">
<div class="max-w-md mx-auto bg-white p-6 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4">
Self-Contained Form Component
</h1>
<p class="text-gray-600 mb-6">
This form's HTML and submission logic are all
handled within a single component file.
</p>
{% component "contact_form" / %}
</div>
</body>
</html>
"""
class View:
def get(self, request: HttpRequest) -> HttpResponse:
return FormSubmissionPage.render_to_response(request=request)

View file

@ -0,0 +1,39 @@
import pytest
from django.template import Context, Template
from django_components import registry, types
from django_components.testing import djc_test
def _import_components():
from docs.examples.form_submission.component import ContactFormComponent, ThankYouMessage # noqa: PLC0415
registry.register("contact_form", ContactFormComponent)
registry.register("thank_you_message", ThankYouMessage)
@pytest.mark.django_db
@djc_test
class TestFormSubmission:
def test_form_renders(self):
_import_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "contact_form" / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert 'hx-post="' in rendered
assert '<div id="thank-you-container" data-djc-id-ca1bc3f=""></div>' in rendered
assert "Thank you" not in rendered
def test_form_submission(self):
_import_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "thank_you_message" name="John Doe" / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert "Thank you for your submission, John Doe!" in rendered
assert '<div id="thank-you-container"></div>' not in rendered

View file

@ -0,0 +1,79 @@
from typing import NamedTuple
from django_components import Component, register, types
DESCRIPTION = "Use HTML fragments (partials) with HTMX, AlpineJS, or plain JS."
@register("simple_fragment")
class SimpleFragment(Component):
"""A simple fragment with JS and CSS."""
class Kwargs(NamedTuple):
type: str
template: types.django_html = """
<div class="frag_simple">
Fragment with JS and CSS (plain).
<span id="frag-text"></span>
</div>
"""
js: types.js = """
document.querySelector('#frag-text').textContent = ' JavaScript has run.';
"""
css: types.css = """
.frag_simple {
background: #f0f8ff;
border: 1px solid #add8e6;
padding: 1rem;
border-radius: 5px;
}
"""
@register("alpine_fragment")
class AlpineFragment(Component):
"""A fragment that defines an AlpineJS component."""
class Kwargs(NamedTuple):
type: str
# The fragment is wrapped in `<template x-if="false">` so that we prevent
# AlpineJS from inserting the HTML right away. Instead, we want to load it
# only once this component's JS has been loaded.
template: types.django_html = """
<template x-if="false" data-name="frag">
<div
class="frag_alpine"
x-data="frag"
x-text="message"
x-init="() => {
document.querySelectorAll('#loader-alpine').forEach((el) => {
el.innerHTML = 'Fragment loaded!';
el.disabled = true;
});
}"
></div>
</template>
"""
js: types.js = """
Alpine.data('frag', () => ({
message: 'Fragment with JS and CSS (AlpineJS).',
}));
document.querySelectorAll('[data-name="frag"]').forEach((el) => {
el.setAttribute('x-if', 'true');
});
"""
css: types.css = """
.frag_alpine {
background: #f0fff0;
border: 1px solid #98fb98;
padding: 1rem;
border-radius: 5px;
}
"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,141 @@
from django.http import HttpRequest, HttpResponse
from django.utils.safestring import mark_safe
from django_components import Component, get_component_url, types
from .component import AlpineFragment, SimpleFragment
class FragmentsPage(Component):
class Media:
js = (
"https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,container-queries",
mark_safe('<script defer src="https://unpkg.com/alpinejs"></script>'),
)
def get_template_data(self, args, kwargs, slots, context):
# Get URLs that points to the FragmentsPageView.get() method
alpine_url = get_component_url(FragmentsPage, query={"type": "alpine"})
js_url = get_component_url(FragmentsPage, query={"type": "js"})
htmx_url = get_component_url(FragmentsPage, query={"type": "htmx"})
return {
"alpine_url": alpine_url,
"js_url": js_url,
"htmx_url": htmx_url,
}
template: types.django_html = """
{% load component_tags %}
<html>
<head>
<title>HTML Fragments Example</title>
<script src="https://unpkg.com/htmx.org@2.0.7/dist/htmx.js"></script>
</head>
<body
class="bg-gray-100 p-8"
data-alpine-url="{{ alpine_url }}"
data-js-url="{{ js_url }}"
hx-boost="true"
>
<div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4">
HTML Fragments
</h1>
<p class="text-gray-600 mb-6">
This example shows how to load HTML fragments
using different client-side techniques.
</p>
<!-- Vanilla JS -->
<div class="mb-8 p-4 border rounded-lg">
<h2 class="text-xl font-semibold mb-2">
Vanilla JS
</h2>
<div id="target-js">Initial content</div>
<button
id="loader-js"
class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Load Fragment
</button>
</div>
<!-- AlpineJS -->
<div
class="mb-8 p-4 border rounded-lg"
x-data="{
htmlVar: '<div id=\\'target-alpine\\'>Initial content</div>',
}"
>
<h2 class="text-xl font-semibold mb-2">
AlpineJS
</h2>
<div x-html="htmlVar"></div>
<button
id="loader-alpine"
@click="() => {
const alpineUrl = document.body.dataset.alpineUrl;
fetch(alpineUrl)
.then(r => r.text())
.then(html => {
htmlVar = html;
})
}"
class="mt-2 px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
>
Load Fragment
</button>
</div>
<!-- HTMX -->
<div class="p-4 border rounded-lg">
<h2 class="text-xl font-semibold mb-2">
HTMX
</h2>
<div id="target-htmx">Initial content</div>
<button
id="loader-htmx"
hx-get="{{ htmx_url }}"
hx-swap="outerHTML"
hx-target="#target-htmx"
class="mt-2 px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700"
>
Load Fragment
</button>
</div>
</div>
<script>
document.querySelector('#loader-js').addEventListener('click', function () {
const jsUrl = document.body.dataset.jsUrl;
fetch(jsUrl)
.then(response => response.text())
.then(html => {
document.querySelector('#target-js').outerHTML = html;
});
});
</script>
</body>
</html>
"""
class View:
public = True
# The same GET endpoint handles rendering either the whole page or a fragment.
# We use the `type` query parameter to determine which one to render.
def get(self, request: HttpRequest) -> HttpResponse:
fragment_type = request.GET.get("type")
if fragment_type:
fragment_cls = AlpineFragment if fragment_type == "alpine" else SimpleFragment
return fragment_cls.render_to_response(
request=request,
deps_strategy="fragment",
kwargs={"type": fragment_type},
)
else:
return FragmentsPage.render_to_response(
request=request,
deps_strategy="fragment",
)

View file

@ -0,0 +1,51 @@
import pytest
from django.template import Context, Template
from django_components import registry, types
from django_components.testing import djc_test
def _import_components():
from docs.examples.fragments.component import AlpineFragment, SimpleFragment # noqa: PLC0415
from docs.examples.fragments.page import FragmentsPage # noqa: PLC0415
registry.register("alpine_fragment", AlpineFragment)
registry.register("simple_fragment", SimpleFragment)
registry.register("fragments_page", FragmentsPage)
@pytest.mark.django_db
@djc_test
class TestFragments:
def test_page_renders(self):
_import_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "fragments_page" / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert "HTML Fragments" in rendered
assert "Vanilla JS" in rendered
assert "AlpineJS" in rendered
assert "HTMX" in rendered
def test_alpine_fragment_view(self):
_import_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "alpine_fragment" type="alpine" / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert 'class="frag_alpine"' in rendered
def test_simple_fragment_view(self):
_import_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "simple_fragment" type="plain" / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
assert "Fragment with JS and CSS (plain)" in rendered

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,34 @@
from typing import NamedTuple
from django_components import Component, register, types
DESCRIPTION = "100 nested components? Not a problem! Handle recursive rendering out of the box."
@register("recursion")
class Recursion(Component):
class Kwargs(NamedTuple):
current_depth: int = 0
def get_template_data(self, args, kwargs: Kwargs, slots, context):
current_depth = kwargs.current_depth
return {
"current_depth": current_depth,
"next_depth": current_depth + 1,
}
template: types.django_html = """
{% load component_tags %}
<div class="py-4 border-l-2 border-gray-300 ml-1">
{% if current_depth < 100 %}
<p class="text-sm text-gray-600">
Recursion depth: {{ current_depth }}
</p>
{% component "recursion" current_depth=next_depth / %}
{% else %}
<p class="text-sm font-semibold text-green-600">
Reached maximum recursion depth!
</p>
{% endif %}
</div>
"""

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,35 @@
from django.http import HttpRequest, HttpResponse
from django.utils.safestring import mark_safe
from django_components import Component, types
class RecursionPage(Component):
class Media:
js = (
mark_safe(
'<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio,line-clamp,container-queries"></script>'
),
)
template: types.django_html = """
{% load component_tags %}
<html>
<head>
<title>Recursion Example</title>
</head>
<body class="bg-gray-100 p-8">
<div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow-md">
<h1 class="text-2xl font-bold mb-4">Recursion</h1>
<p class="text-gray-600 mb-6">
Django components easily handles even deeply nested components.
</p>
{% component "recursion" / %}
</div>
</body>
</html>
"""
class View:
def get(self, request: HttpRequest) -> HttpResponse:
return RecursionPage.render_to_response(request=request)

View file

@ -0,0 +1,29 @@
import pytest
from django.template import Context, Template
from django_components import registry, types
from django_components.testing import djc_test
def _import_components():
from docs.examples.recursion.component import Recursion # noqa: PLC0415
registry.register("recursion", Recursion)
@pytest.mark.django_db
@djc_test
class TestRecursion:
def test_renders_recursively(self):
_import_components()
template_str: types.django_html = """
{% load component_tags %}
{% component "recursion" / %}
"""
template = Template(template_str)
rendered = template.render(Context({}))
# Expect 101 levels of depth (0 to 100)
assert rendered.count("Recursion depth:") == 100
assert "Reached maximum recursion depth!" in rendered

View file

@ -12,6 +12,8 @@ from django.utils.text import slugify
from django_components import Component, register from django_components import Component, register
from django_components import types as t from django_components import types as t
DESCRIPTION = "Dynamic tabs with AlpineJS."
class TabDatum(NamedTuple): class TabDatum(NamedTuple):
"""Datum for an individual tab.""" """Datum for an individual tab."""
@ -230,7 +232,7 @@ class _TablistImpl(Component):
@register("Tablist") @register("Tablist")
class Tablist(Component): class Tablist(Component):
""" """
Tablist role component comprised of nested tab components. Dynamic tabs with [AlpineJS](https://alpinejs.dev/).
After the input is processed, this component delegates to an internal implementation After the input is processed, this component delegates to an internal implementation
component that renders the content. component that renders the content.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more