refactor: fix bug with complex slots and "django" mode + add docs on debugging with AI agents (#956)

This commit is contained in:
Juro Oravec 2025-02-11 10:57:37 +01:00 committed by GitHub
parent be4d744d64
commit eb3f72ee0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 601 additions and 148 deletions

262
README.md
View file

@ -11,17 +11,24 @@
>
> Report any broken links links in [#922](https://github.com/django-components/django-components/issues/922).
Django-components is a package that introduces component-based architecture to Django's server-side rendering. It aims to combine Django's templating system with the modularity seen in modern frontend frameworks.
`django-components` combines Django's templating system with the modularity seen
in modern frontend frameworks like Vue or React.
With `django-components` you can support Django projects small and large without leaving the Django ecosystem.
## Quickstart
A component in django-components can be as simple as a Django template and Python code to declare the component:
```htmldjango title="calendar.html"
```django
{# components/calendar/calendar.html #}
<div class="calendar">
Today's date is <span>{{ date }}</span>
</div>
```
```py title="calendar.py"
```py
# components/calendar/calendar.html
from django_components import Component
class Calendar(Component):
@ -30,43 +37,72 @@ class Calendar(Component):
Or a combination of Django template, Python, CSS, and Javascript:
```htmldjango title="calendar.html"
```django
{# components/calendar/calendar.html #}
<div class="calendar">
Today's date is <span>{{ date }}</span>
</div>
```
```css title="calendar.css"
```css
/* components/calendar/calendar.css */
.calendar {
width: 200px;
background: pink;
}
```
```js title="calendar.js"
document.querySelector(".calendar").onclick = function () {
```js
/* components/calendar/calendar.js */
document.querySelector(".calendar").onclick = () => {
alert("Clicked calendar!");
};
```
```py title="calendar.py"
```py
# components/calendar/calendar.py
from django_components import Component
class Calendar(Component):
template_file = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"
def get_context_data(self, date):
return {"date": date}
```
Alternatively, you can "inline" HTML, JS, and CSS right into the component class:
Use the component like this:
```py
```django
{% component "calendar" date="2024-11-06" %}{% endcomponent %}
```
And this is what gets rendered:
```html
<div class="calendar-component">
Today's date is <span>2024-11-06</span>
</div>
```
## Features
### Modern and modular UI
- Create self-contained, reusable UI elements.
- Each component can include its own HTML, CSS, and JS, or additional third-party JS and CSS.
- HTML, CSS, and JS can be defined on the component class, or loaded from files.
```python
from django_components import Component
@register("calendar")
class Calendar(Component):
template = """
<div class="calendar">
Today's date is <span>{{ date }}</span>
Today's date is
<span>{{ date }}</span>
</div>
"""
@ -78,65 +114,203 @@ class Calendar(Component):
"""
js = """
document.querySelector(".calendar").onclick = function () {
document.querySelector(".calendar")
.addEventListener("click", () => {
alert("Clicked calendar!");
};
});
"""
# Additional JS and CSS
class Media:
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2.1.1/dist/htmx.min.js"]
css = ["bootstrap/dist/css/bootstrap.min.css"]
# Variables available in the template
def get_context_data(self, date):
return {
"date": date
}
```
## Features
### Composition with slots
1. 🧩 **Reusability:** Allows creation of self-contained, reusable UI elements.
2. 📦 **Encapsulation:** Each component can include its own HTML, CSS, and JavaScript.
3. 🚀 **Server-side rendering:** Components render on the server, improving initial load times and SEO.
4. 🐍 **Django integration:** Works within the Django ecosystem, using familiar concepts like template tags.
5. ⚡ **Asynchronous loading:** Components can render independently opening up for integration with JS frameworks like HTMX or AlpineJS.
- Render components inside templates with `{% component %}` tag.
- Compose them with `{% slot %}` and `{% fill %}` tags.
- Vue-like slot system, including scoped slots.
Potential benefits:
```django
{% component "Layout"
bookmarks=bookmarks
breadcrumbs=breadcrumbs
%}
{% fill "header" %}
<div class="flex justify-between gap-x-12">
<div class="prose">
<h3>{{ project.name }}</h3>
</div>
<div class="font-semibold text-gray-500">
{{ project.start_date }} - {{ project.end_date }}
</div>
</div>
{% endfill %}
- 🔄 Reduced code duplication
- 🛠️ Improved maintainability through modular design
- 🧠 Easier management of complex UIs
- 🤝 Enhanced collaboration between frontend and backend developers
{# Access data passed to `{% slot %}` with `data` #}
{% fill "tabs" data="tabs_data" %}
{% component "TabItem" header="Project Info" %}
{% component "ProjectInfo"
project=project
project_tags=project_tags
attrs:class="py-5"
attrs:width=tabs_data.width
/ %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
```
Django-components can be particularly useful for larger Django projects that require a more structured approach to UI development, without necessitating a shift to a separate frontend framework.
### Extended template tags
## Quickstart
`django-components` extends Django's template tags syntax with:
django-components lets you create reusable blocks of code needed to generate the front end code you need for a modern app.
- Literal lists and dictionaries in template tags
- Self-closing tags `{% mytag / %}`
- Multi-line template tags
- Spread operator `...` to dynamically pass args or kwargs into the template tag
- Nested template tags like `"{{ first_name }} {{ last_name }}"`
- Flat definition of dictionary keys `attr:key=val`
Define a component in `components/calendar/calendar.py` like this:
```django
{% component "table"
...default_attrs
title="Friend list for {{ user.name }}"
headers=["Name", "Age", "Email"]
data=[
{
"name": "John"|upper,
"age": 30|add:1,
"email": "john@example.com",
"hobbies": ["reading"],
},
{
"name": "Jane"|upper,
"age": 25|add:1,
"email": "jane@example.com",
"hobbies": ["reading", "coding"],
},
],
attrs:class="py-4 ma-2 border-2 border-gray-300 rounded-md"
/ %}
```
```python
### HTML fragment support
`django-components` makes intergration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as HTML fragments:
- Components's JS and CSS is loaded automatically when the fragment is inserted into the DOM.
- Expose components as views with `get`, `post`, `put`, `patch`, `delete` methods
```py
# components/calendar/calendar.py
@register("calendar")
class Calendar(Component):
template_file = "template.html"
template_file = "calendar.html"
def get_context_data(self, date):
return {"date": date}
def get(self, request, *args, **kwargs):
page = request.GET.get("page", 1)
return self.render_to_response(
kwargs={
"page": page,
}
)
def get_context_data(self, page):
return {
"page": page,
}
# urls.py
path("calendar/", Calendar.as_view()),
```
With this `template.html` file:
### Type hints
```htmldjango
<div class="calendar-component">Today's date is <span>{{ date }}</span></div>
Opt-in to type hints by defining types for component's args, kwargs, slots, and more:
```py
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
ButtonArgs = Tuple[int, str]
class ButtonKwargs(TypedDict):
variable: str
another: int
maybe_var: NotRequired[int] # May be omitted
class ButtonData(TypedDict):
variable: str
class ButtonSlots(TypedDict):
my_slot: NotRequired[SlotFunc]
another_slot: SlotContent
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
class Button(ButtonType):
def get_context_data(self, *args, **kwargs):
self.input.args[0] # int
self.input.kwargs["variable"] # str
self.input.slots["my_slot"] # SlotFunc[MySlotData]
return {} # Error: Key "variable" is missing
```
Use the component like this:
When you then call `Button.render()` or `Button.render_to_response()`, you will get type hints:
```htmldjango
{% component "calendar" date="2024-11-06" %}{% endcomponent %}
```py
Button.render(
# Error: First arg must be `int`, got `float`
args=(1.25, "abc"),
# Error: Key "another" is missing
kwargs={
"variable": "text",
},
)
```
And this is what gets rendered:
### Debugging features
```html
<div class="calendar-component">Today's date is <span>2024-11-06</span></div>
```
- **Visual component inspection**: Highlight components and slots directly in your browser.
- **Detailed tracing logs to supply AI-agents with context**: The logs include component and slot names and IDs, and their position in the tree.
### <table><td>[Read the full documentation](https://django-components.github.io/django-components/latest/)</td></table>
<div style="text-align: center;">
<img src="https://github.com/django-components/django-components/blob/master/docs/images/debug-highlight-slots.png?raw=true" alt="Component debugging visualization showing slot highlighting" width="500" style="margin: auto;">
</div>
... or jump right into the code, [check out the example project](https://github.com/django-components/django-components/tree/master/sampleproject))
### Sharing components
- Install and use third-party components from PyPI
- Or publish your own "component registry"
- Highly customizable - Choose how the components are called in the template, and more:
```django
{% component "calendar" date="2024-11-06" %}
{% endcomponent %}
{% calendar date="2024-11-06" %}
{% endcalendar %}
```
### Other features
- Vue-like provide / inject system
- Format HTML attributes with `{% html_attrs %}`
## Documentation
[Read the full documentation here](https://django-components.github.io/django-components/latest/).
... or jump right into the code, [check out the example project](https://github.com/django-components/django-components/tree/master/sampleproject).
## Release notes

View file

@ -143,3 +143,82 @@ might print:
'left_panel': <Slot component_name='layout' slot_name='left_panel'>,
}
```
## Agentic debugging
All the features above make django-components to work really well with coding AI agents
like Github Copilot or CursorAI.
To debug component rendering with LLMs, you want to provide the LLM with:
1. The components source code
2. The rendered output
3. As much additional context as possible
Your codebase already contains the components source code, but not the latter two.
### Providing rendered output
To provide the LLM with the rendered output, you can simply export the rendered output to a file.
```python
rendered = ProjectPage.render(...)
with open("result.html", "w") as f:
f.write(rendered)
```
If you're using `render_to_response`, access the output from the `HttpResponse` object:
```python
response = ProjectPage.render_to_response(...)
with open("result.html", "wb") as f:
f.write(response.content)
```
### Providing contextual logs
Next, we provide the agent with info on HOW we got the result that we have. We do so
by providing the agent with the trace-level logs.
In your `settings.py`, configure the trace-level logs to be written to the `django_components.log` file:
```python
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"file": {
"class": "logging.FileHandler",
"filename": "django_components.log",
"mode": "w", # Overwrite the file each time
},
},
"loggers": {
"django_components": {
"level": 5,
"handlers": ["file"],
},
},
}
```
### Prompting the agent
Now, you can prompt the agent and include the trace log and the rendered output to guide the agent with debugging.
> I have a django-components (DJC) project. DJC is like if Vue or React component-based web development but made for Django ecosystem.
>
> In the view `project_view`, I am rendering the `ProjectPage` component. However, the output is not as expected.
> The output is missing the tabs.
>
> You have access to the full log trace in `django_components.log`.
>
> You can also see the rendered output in `result.html`.
>
> With this information, help me debug the issue.
>
> First, tell me what kind of info you would be looking for in the logs, and why (how it relates to understanding the cause of the bug).
>
> Then tell me if that info was there, and what the implications are.
>
> Finally, tell me what you would do to fix the issue.

View file

@ -7,18 +7,22 @@ weight: 1
[![PyPI - Version](https://img.shields.io/pypi/v/django-components)](https://pypi.org/project/django-components/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/django-components)](https://pypi.org/project/django-components/) [![PyPI - License](https://img.shields.io/pypi/l/django-components)](https://github.com/django-components/django-components/blob/master/LICENSE/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/django-components)](https://pypistats.org/packages/django-components) [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/django-components/django-components/tests.yml)](https://github.com/django-components/django-components/actions/workflows/tests.yml)
django-components introduces component-based architecture to Django's server-side rendering.
It combines Django's templating system with the modularity seen in modern frontend frameworks like Vue or React.
`django-components` combines Django's templating system with the modularity seen
in modern frontend frameworks like Vue or React.
With `django-components` you can support Django projects small and large without leaving the Django ecosystem.
## Quickstart
A component in django-components can be as simple as a Django template and Python code to declare the component:
```htmldjango title="calendar.html"
```htmldjango title="components/calendar/calendar.html"
<div class="calendar">
Today's date is <span>{{ date }}</span>
</div>
```
```py title="calendar.py"
```py title="components/calendar/calendar.py"
from django_components import Component
class Calendar(Component):
@ -27,98 +31,37 @@ class Calendar(Component):
Or a combination of Django template, Python, CSS, and Javascript:
```htmldjango title="calendar.html"
```htmldjango title="components/calendar/calendar.html"
<div class="calendar">
Today's date is <span>{{ date }}</span>
</div>
```
```css title="calendar.css"
```css title="components/calendar/calendar.css"
.calendar {
width: 200px;
background: pink;
}
```
```js title="calendar.js"
```js title="components/calendar/calendar.js"
document.querySelector(".calendar").onclick = function () {
alert("Clicked calendar!");
};
```
```py title="calendar.py"
```py title="components/calendar/calendar.py"
from django_components import Component
class Calendar(Component):
template_file = "calendar.html"
js_file = "calendar.js"
css_file = "calendar.css"
```
Alternatively, you can "inline" HTML, JS, and CSS right into the component class:
```py
from django_components import Component
class Calendar(Component):
template = """
<div class="calendar">
Today's date is <span>{{ date }}</span>
</div>
"""
css = """
.calendar {
width: 200px;
background: pink;
}
"""
js = """
document.querySelector(".calendar").onclick = function () {
alert("Clicked calendar!");
};
"""
```
## Features
1. 🧩 **Reusability:** Allows creation of self-contained, reusable UI elements.
2. 📦 **Encapsulation:** Each component can include its own HTML, CSS, and JavaScript.
3. 🚀 **Server-side rendering:** Components render on the server, improving initial load times and SEO.
4. 🐍 **Django integration:** Works within the Django ecosystem, using familiar concepts like template tags.
5. ⚡ **Asynchronous loading:** Components can render independently opening up for integration with JS frameworks like HTMX or AlpineJS.
Potential benefits:
- 🔄 Reduced code duplication
- 🛠️ Improved maintainability through modular design
- 🧠 Easier management of complex UIs
- 🤝 Enhanced collaboration between frontend and backend developers
Django-components can be particularly useful for larger Django projects that require a more structured approach to UI development, without necessitating a shift to a separate frontend framework.
## Quickstart
django-components lets you create reusable blocks of code needed to generate the front end code you need for a modern app.
Define a component in `components/calendar/calendar.py` like this:
```python
@register("calendar")
class Calendar(Component):
template_file = "template.html"
def get_context_data(self, date):
return {"date": date}
```
With this `template.html` file:
```htmldjango
<div class="calendar-component">Today's date is <span>{{ date }}</span></div>
```
Use the component like this:
```htmldjango
@ -128,13 +71,235 @@ Use the component like this:
And this is what gets rendered:
```html
<div class="calendar-component">Today's date is <span>2024-11-06</span></div>
<div class="calendar-component">
Today's date is <span>2024-11-06</span>
</div>
```
Read on to learn about all the exciting details and configuration possibilities!
(If you instead prefer to jump right into the code, [check out the example project](https://github.com/django-components/django-components/tree/master/sampleproject))
## Features
### Modern and modular UI
- Create self-contained, reusable UI elements.
- Each component can include its own HTML, CSS, and JS, or additional third-party JS and CSS.
- HTML, CSS, and JS can be defined on the component class, or loaded from files.
```python
from django_components import Component
@register("calendar")
class Calendar(Component):
template = """
<div class="calendar">
Today's date is
<span>{{ date }}</span>
</div>
"""
css = """
.calendar {
width: 200px;
background: pink;
}
"""
js = """
document.querySelector(".calendar")
.addEventListener("click", () => {
alert("Clicked calendar!");
});
"""
# Additional JS and CSS
class Media:
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2.1.1/dist/htmx.min.js"]
css = ["bootstrap/dist/css/bootstrap.min.css"]
# Variables available in the template
def get_context_data(self, date):
return {
"date": date
}
```
### Composition with slots
- Render components inside templates with `{% component %}` tag.
- Compose them with `{% slot %}` and `{% fill %}` tags.
- Vue-like slot system, including scoped slots.
```htmldjango
{% component "Layout"
bookmarks=bookmarks
breadcrumbs=breadcrumbs
%}
{% fill "header" %}
<div class="flex justify-between gap-x-12">
<div class="prose">
<h3>{{ project.name }}</h3>
</div>
<div class="font-semibold text-gray-500">
{{ project.start_date }} - {{ project.end_date }}
</div>
</div>
{% endfill %}
{# Access data passed to `{% slot %}` with `data` #}
{% fill "tabs" data="tabs_data" %}
{% component "TabItem" header="Project Info" %}
{% component "ProjectInfo"
project=project
project_tags=project_tags
attrs:class="py-5"
attrs:width=tabs_data.width
/ %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
```
### Extended template tags
`django-components` extends Django's template tags syntax with:
- Literal lists and dictionaries in template tags
- Self-closing tags `{% mytag / %}`
- Multi-line template tags
- Spread operator `...` to dynamically pass args or kwargs into the template tag
- Nested template tags like `"{{ first_name }} {{ last_name }}"`
- Flat definition of dictionary keys `attr:key=val`
```htmldjango
{% component "table"
...default_attrs
title="Friend list for {{ user.name }}"
headers=["Name", "Age", "Email"]
data=[
{
"name": "John"|upper,
"age": 30|add:1,
"email": "john@example.com",
"hobbies": ["reading"],
},
{
"name": "Jane"|upper,
"age": 25|add:1,
"email": "jane@example.com",
"hobbies": ["reading", "coding"],
},
],
attrs:class="py-4 ma-2 border-2 border-gray-300 rounded-md"
/ %}
```
### HTML fragment support
`django-components` makes intergration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as HTML fragments:
- Components's JS and CSS is loaded automatically when the fragment is inserted into the DOM
- Expose components as views with `get`, `post`, `put`, `patch`, `delete` methods
```py
# components/calendar/calendar.py
@register("calendar")
class Calendar(Component):
template_file = "calendar.html"
def get(self, request, *args, **kwargs):
page = request.GET.get("page", 1)
return self.render_to_response(
kwargs={
"page": page,
}
)
def get_context_data(self, page):
return {
"page": page,
}
# urls.py
path("calendar/", Calendar.as_view()),
```
### Type hints
Opt-in to type hints by defining types for component's args, kwargs, slots, and more:
```py
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
ButtonArgs = Tuple[int, str]
class ButtonKwargs(TypedDict):
variable: str
another: int
maybe_var: NotRequired[int] # May be omitted
class ButtonData(TypedDict):
variable: str
class ButtonSlots(TypedDict):
my_slot: NotRequired[SlotFunc]
another_slot: SlotContent
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
class Button(ButtonType):
def get_context_data(self, *args, **kwargs):
self.input.args[0] # int
self.input.kwargs["variable"] # str
self.input.slots["my_slot"] # SlotFunc[MySlotData]
return {} # Error: Key "variable" is missing
```
When you then call `Button.render()` or `Button.render_to_response()`, you will get type hints:
```py
Button.render(
# Error: First arg must be `int`, got `float`
args=(1.25, "abc"),
# Error: Key "another" is missing
kwargs={
"variable": "text",
},
)
```
### Debugging features
- **Visual component inspection**: Highlight components and slots directly in your browser.
- **Detailed tracing logs to supply AI-agents with context**: The logs include component and slot names and IDs, and their position in the tree.
<div style="text-align: center;">
<img src="https://github.com/django-components/django-components/blob/master/docs/images/debug-highlight-slots.png?raw=true" alt="Component debugging visualization showing slot highlighting" width="500" style="margin: auto;">
</div>
### Sharing components
- Install and use third-party components from PyPI
- Or publish your own "component registry"
- Highly customizable - Choose how the components are called in the template (and more):
```htmldjango
{% component "calendar" date="2024-11-06" %}
{% endcomponent %}
{% calendar date="2024-11-06" %}
{% endcalendar %}
```
### Other features
- Vue-like provide / inject system
- Format HTML attributes with `{% html_attrs %}`
## Release notes
Read the [Release Notes](../release_notes.md)

View file

@ -1045,7 +1045,7 @@ class Component(
component_id=render_id,
slot_name=None,
component_path=component_path,
extra=f"Received {len(args)} args, {len(kwargs)} kwargs, {len(slots)} slots",
extra=f"Received {len(args)} args, {len(kwargs)} kwargs, {len(slots)} slots, Available slots: {slots}",
)
# Register the component to provide

View file

@ -330,6 +330,7 @@ class SlotNode(BaseNode):
slot_name=slot_name,
component_path=component_path,
slot_fills=slot_fills,
extra=f"Available fills: {slot_fills}",
)
# Check for errors
@ -395,6 +396,7 @@ class SlotNode(BaseNode):
if (
component_ctx.registry.settings.context_behavior == ContextBehavior.DJANGO
and component_ctx.outer_context is None
and (slot_name not in component_ctx.fills)
):
# When we have nested components with fills, the context layers are added in
# the following order:
@ -418,20 +420,53 @@ class SlotNode(BaseNode):
#
# In the Context, the components are identified by their ID, NOT by their name, as in the example above.
# So the path is more like this:
# ax3c89 -> hui3q2 -> kok92a -> a1b2c3 -> kok92a -> hui3q2 -> d4e5f6 -> hui3q2
# a1b2c3 -> ax3c89 -> hui3q2 -> kok92a -> a1b2c3 -> kok92a -> hui3q2 -> d4e5f6 -> hui3q2
#
# We're at the right-most `hui3q2`, and we want to find `ax3c89`.
# To achieve that, we first find the left-most `hui3q2`, and then find the `ax3c89`
# in the list of dicts before it.
# We're at the right-most `hui3q2` (index 8), and we want to find `ax3c89` (index 1).
# To achieve that, we first find the left-most `hui3q2` (index 2), and then find the `ax3c89`
# in the list of dicts before it (index 1).
curr_index = get_index(
context.dicts, lambda d: _COMPONENT_CONTEXT_KEY in d and d[_COMPONENT_CONTEXT_KEY] == component_id
)
parent_index = get_last_index(context.dicts[:curr_index], lambda d: _COMPONENT_CONTEXT_KEY in d)
# NOTE: There's an edge case when our component `hui3q2` appears at the start of the stack:
# hui3q2 -> ax3c89 -> ... -> hui3q2
#
# Looking left finds nothing. In this case, look for the first component layer to the right.
if parent_index is None and curr_index + 1 < len(context.dicts):
parent_index = get_index(
context.dicts[curr_index + 1 :], lambda d: _COMPONENT_CONTEXT_KEY in d # noqa: E203
)
if parent_index is not None:
parent_index = parent_index + curr_index + 1
trace_component_msg(
"SLOT_PARENT_INDEX",
component_name=component_ctx.component_name,
component_id=component_ctx.component_id,
slot_name=name,
component_path=component_ctx.component_path,
extra=(
f"Parent index: {parent_index}, Current index: {curr_index}, "
f"Context stack: {[d.get(_COMPONENT_CONTEXT_KEY) for d in context.dicts]}"
),
)
if parent_index is not None:
ctx_id_with_fills = context.dicts[parent_index][_COMPONENT_CONTEXT_KEY]
ctx_with_fills = component_context_cache[ctx_id_with_fills]
slot_fills = ctx_with_fills.fills
# Add trace message when slot_fills are overwritten
trace_component_msg(
"SLOT_FILLS_OVERWRITTEN",
component_name=component_name,
component_id=component_id,
slot_name=slot_name,
component_path=component_path,
extra=f"Slot fills overwritten in django mode. New fills: {slot_fills}",
)
if fill_name in slot_fills:
slot_fill_fn = slot_fills[fill_name]
slot_fill = SlotFill(