docs: docstrings, fundamentals, and minor changes (#1145)

* docs: docstrings, fundamentals, and minor changes

* refactor: fix tests + linter errors
This commit is contained in:
Juro Oravec 2025-04-24 12:47:04 +02:00 committed by GitHub
parent 89db10a643
commit 59f82307ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 2239 additions and 673 deletions

View file

@ -53,7 +53,7 @@
Kwargs = Empty
```
- The order of arguments in `Component.render_to_response()` has changed
- Arguments in `Component.render_to_response()` have changed
to match that of `Component.render()`.
Please ensure that you pass the parameters as kwargs, not as positional arguments,
@ -61,6 +61,13 @@
The signature changed, moving the `args` and `kwargs` parameters to 2nd and 3rd position.
Next, the `render_dependencies` parameter was added to match `Component.render()`.
Lastly:
- Previously, any extra ARGS and KWARGS were passed to the `response_class`.
- Now, only extra KWARGS will be passed to the `response_class`.
Before:
```py
@ -90,7 +97,6 @@
type: RenderType = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
*response_args: Any,
**response_kwargs: Any,
) -> HttpResponse:
```
@ -124,6 +130,29 @@
return self.render_to_response()
```
- Component name in the `{% component %}` tag can no longer be set as a kwarg.
Instead, the component name MUST be the first POSITIONAL argument only.
Before, it was possible to set the component name as a kwarg
and put it anywhere in the `{% component %}` tag:
```django
{% component rows=rows headers=headers name="my_table" ... / %}
```
Now, the component name MUST be the first POSITIONAL argument:
```django
{% component "my_table" rows=rows headers=headers ... / %}
```
Thus, the `name` kwarg can now be used as a regular input.
```django
{% component "profile" name="John" job="Developer" / %}
```
#### 🚨📢 Deprecation
- `get_context_data()` is now deprecated. Use `get_template_data()` instead.

View file

@ -6,9 +6,9 @@ nav:
- Lifecycle hooks: hooks.md
- Registering components: component_registry.md
- Component caching: component_caching.md
- Typing and validation: typing_and_validation.md
- Component context and scope: component_context_scope.md
- Custom template tags: template_tags.md
- Tag formatters: tag_formatter.md
- Tag formatters: tag_formatters.md
- Extensions: extensions.md
- Testing: testing.md
- Authoring component libraries: authoring_component_libraries.md
- Component libraries: component_libraries.md

View file

@ -1,15 +1,17 @@
# `.nav.yml` is provided by https://lukasgeiter.github.io/mkdocs-awesome-nav
nav:
- Single-file components: single_file_components.md
- Components in Python: components_in_python.md
- Render API: render_api.md
- HTML / JS / CSS files: html_js_css_files.md
- HTML / JS / CSS variables: html_js_css_variables.md
- Secondary JS / CSS files: secondary_js_css_files.md
- Component defaults: component_defaults.md
- Component context and scope: component_context_scope.md
- Render API: render_api.md
- Rendering components: rendering_components.md
- Template tag syntax: template_tag_syntax.md
- Slots: slots.md
- HTML attributes: html_attributes.md
- Defining HTML / JS / CSS files: defining_js_css_html_files.md
- Autodiscovery: autodiscovery.md
- Component views and URLs: component_views_urls.md
- HTTP Request: http_request.md
- Typing and validation: typing_and_validation.md
- Subclassing components: subclassing_components.md
- Autodiscovery: autodiscovery.md

View file

@ -1,13 +1,15 @@
django-components automatically register all components found in the
django-components automatically searches for files containing components in the
[`COMPONENTS.dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.dirs) and
[`COMPONENTS.app_dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.app_dirs)
directories by loading all Python files in these directories.
directories.
### Manually register components
Every component that you want to use in the template with the
[`{% component %}`](../../../reference/template_tags#component)
tag needs to be registered with the [`ComponentRegistry`](../../../reference/api#django_components.ComponentRegistry).
Normally, we use the [`@register`](../../../reference/api#django_components.register) decorator for that:
We use the [`@register`](../../../reference/api#django_components.register) decorator for that:
```python
from django_components import Component, register
@ -44,7 +46,7 @@ However, there's a simpler way!
By default, the Python files found in the
[`COMPONENTS.dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.dirs) and
[`COMPONENTS.app_dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.app_dirs)
are auto-imported in order to register the components.
are auto-imported in order to execute the code that registers the components.
Autodiscovery occurs when Django is loaded, during the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
hook of the `apps.py` file.

View file

@ -10,8 +10,8 @@ no value is provided, or when the value is set to `None` for a particular input.
### Defining defaults
To define defaults for a component, you create a nested `Defaults` class within your
[`Component`](../../../reference/api#django_components.Component) class.
To define defaults for a component, you create a nested [`Defaults`](../../../reference/api#django_components.Component.Defaults)
class within your [`Component`](../../../reference/api#django_components.Component) class.
Each attribute in the `Defaults` class represents a default value for a corresponding input.
```py
@ -33,7 +33,7 @@ class MyTable(Component):
...
```
In this example, `position` is a simple default value, while `selected_items` uses a factory function wrapped in `Default` to ensure a new list is created each time the default is used.
In this example, `position` is a simple default value, while `selected_items` uses a factory function wrapped in [`Default`](../../../reference/api#django_components.Default) to ensure a new list is created each time the default is used.
Now, when we render the component, the defaults will be applied:
@ -65,6 +65,26 @@ and so `selected_items` will be set to `[1, 2, 3]`.
The defaults are aplied only to keyword arguments. They are NOT applied to positional arguments!
!!! warning
When [typing](../advanced/typing_and_validation.md) your components with [`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
you may be inclined to define the defaults in the classes.
```py
class ProfileCard(Component):
class Kwargs(NamedTuple):
show_details: bool = True
```
This is **NOT recommended**, because:
- The defaults will NOT be applied to inputs when using [`self.input`](../../../reference/api/#django_components.Component.input) property.
- The defaults will NOT be applied when a field is given but set to `None`.
Instead, define the defaults in the [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class.
### Default factories
For objects such as lists, dictionaries or other instances, you have to be careful - if you simply set a default value, this instance will be shared across all instances of the component!
@ -78,7 +98,7 @@ class MyTable(Component):
selected_items = [1, 2, 3]
```
To avoid this, you can use a factory function wrapped in `Default`.
To avoid this, you can use a factory function wrapped in [`Default`](../../../reference/api#django_components.Default).
```py
from django_components import Component, Default
@ -104,7 +124,7 @@ class MyTable(Component):
### Accessing defaults
Since the defaults are defined on the component class, you can access the defaults for a component with the `Component.Defaults` property.
Since the defaults are defined on the component class, you can access the defaults for a component with the [`Component.Defaults`](../../../reference/api#django_components.Component.Defaults) property.
So if we have a component like this:

View file

@ -1,134 +0,0 @@
_New in version 0.81_
Components can be rendered outside of Django templates, calling them as regular functions ("React-style").
The component class defines `render` and `render_to_response` class methods. These methods accept positional args, kwargs, and slots, offering the same flexibility as the `{% component %}` tag:
```djc_py
class SimpleComponent(Component):
template = """
{% load component_tags %}
hello: {{ hello }}
foo: {{ foo }}
kwargs: {{ kwargs|safe }}
slot_first: {% slot "first" required / %}
"""
def get_context_data(self, arg1, arg2, **kwargs):
return {
"hello": arg1,
"foo": arg2,
"kwargs": kwargs,
}
rendered = SimpleComponent.render(
args=["world", "bar"],
kwargs={"kw1": "test", "kw2": "ooo"},
slots={"first": "FIRST_SLOT"},
context={"from_context": 98},
)
```
Renders:
```
hello: world
foo: bar
kwargs: {'kw1': 'test', 'kw2': 'ooo'}
slot_first: FIRST_SLOT
```
## Inputs of `render` and `render_to_response`
Both `render` and `render_to_response` accept the same input:
```py
Component.render(
context: Mapping | django.template.Context | None = None,
args: List[Any] | None = None,
kwargs: Dict[str, Any] | None = None,
slots: Dict[str, str | SafeString | SlotInput] | None = None,
escape_slots_content: bool = True
) -> str:
```
- _`args`_ - Positional args for the component. This is the same as calling the component
as `{% component "my_comp" arg1 arg2 ... %}`
- _`kwargs`_ - Keyword args for the component. This is the same as calling the component
as `{% component "my_comp" key1=val1 key2=val2 ... %}`
- _`slots`_ - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
or `Slot`.
- _`escape_slots_content`_ - Whether the content from `slots` should be escaped. `True` by default to prevent XSS attacks. If you disable escaping, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
- _`context`_ - A context (dictionary or Django's Context) within which the component
is rendered. The keys on the context can be accessed from within the template.
- NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
component's args and kwargs.
- _`request`_ - A Django request object. This is used to enable Django template `context_processors` to run,
allowing for template tags like `{% csrf_token %}` and variables like `{{ debug }}`.
- Similar behavior can be achieved with [provide / inject](#how-to-use-provide--inject).
- This is used internally to convert `context` to a RequestContext. It does nothing if `context` is already
a `Context` instance.
### `SlotFunc`
When rendering components with slots in `render` or `render_to_response`, you can pass either a string or a function.
The function has following signature:
```py
def render_func(
context: Context,
data: Dict[str, Any],
slot_ref: SlotRef,
) -> str | SafeString:
return nodelist.render(ctx)
```
- _`context`_ - Django's Context available to the Slot Node.
- _`data`_ - Data passed to the `{% slot %}` tag. See [Scoped Slots](#scoped-slots).
- _`slot_ref`_ - The default slot content. See [Accessing original content of slots](#accessing-original-content-of-slots).
- NOTE: The slot is lazily evaluated. To render the slot, convert it to string with `str(slot_ref)`.
Example:
```py
def footer_slot(ctx, data, slot_ref):
return f"""
SLOT_DATA: {data['abc']}
ORIGINAL: {slot_ref}
"""
MyComponent.render_to_response(
slots={
"footer": footer_slot,
},
)
```
## Response class of `render_to_response`
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is `django.http.HttpResponse`.
If you want to use a different Response class in `render_to_response`, set the `Component.response_class` attribute:
```py
class MyResponse(HttpResponse):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
# Configure response
self.headers = ...
self.status = ...
class SimpleComponent(Component):
response_class = MyResponse
template: types.django_html = "HELLO"
response = SimpleComponent.render_to_response()
assert isinstance(response, MyResponse)
```

View file

@ -0,0 +1,355 @@
## Overview
Each component can have single "primary" HTML, CSS and JS file associated with them.
Each of these can be either defined inline, or in a separate file:
- HTML files are defined using [`Component.template`](../../reference/api.md#django_components.Component.template) or [`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
- CSS files are defined using [`Component.css`](../../reference/api.md#django_components.Component.css) or [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
- JS files are defined using [`Component.js`](../../reference/api.md#django_components.Component.js) or [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
```py
@register("calendar")
class Calendar(Component):
template_file = "calendar.html"
css_file = "calendar.css"
js_file = "calendar.js"
```
or
```djc_py
@register("calendar")
class Calendar(Component):
template = """
<div class="welcome">
Hi there!
</div>
"""
css = """
.welcome {
color: red;
}
"""
js = """
console.log("Hello, world!");
"""
```
These "primary" files will have special behavior. For example, each will receive variables from the component's data methods.
Read more about each file type below:
- [HTML](#html)
- [CSS](#css)
- [JS](#js)
In addition, you can define extra "secondary" CSS / JS files using the nested [`Component.Media`](../../reference/api.md#django_components.Component.Media) class,
by setting [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js) and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
Single component can have many secondary files. There is no special behavior for them.
You can use these for third-party libraries, or for shared CSS / JS files.
Read more about [Secondary JS / CSS files](../secondary_js_css_files).
!!! warning
You **cannot** use both inlined code **and** separate file for a single language type (HTML, CSS, JS).
However, you can freely mix these for different languages:
```djc_py
class MyTable(Component):
template: types.django_html = """
<div class="welcome">
Hi there!
</div>
"""
js_file = "my_table.js"
css_file = "my_table.css"
```
## HTML
Components use Django's template system to define their HTML.
This means that you can use [Django's template syntax](https://docs.djangoproject.com/en/5.2/ref/templates/language/) to define your HTML.
Inside the template, you can access the data returned from the [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) method.
You can define the HTML directly in your Python code using the [`template`](../../reference/api.md#django_components.Component.template) attribute:
```djc_py
class Button(Component):
template = """
<button class="btn">
{% if icon %}
<i class="{{ icon }}"></i>
{% endif %}
{{ text }}
</button>
"""
def get_template_data(self, args, kwargs, slots, context):
return {
"text": kwargs.get("text", "Click me"),
"icon": kwargs.get("icon", None),
}
```
Or you can define the HTML in a separate file and reference it using [`template_file`](../../reference/api.md#django_components.Component.template_file):
```python
class Button(Component):
template_file = "button.html"
def get_template_data(self, args, kwargs, slots, context):
return {
"text": kwargs.get("text", "Click me"),
"icon": kwargs.get("icon", None),
}
```
```django title="button.html"
<button class="btn">
{% if icon %}
<i class="{{ icon }}"></i>
{% endif %}
{{ text }}
</button>
```
### HTML processing
Django Components expects the rendered template to be a valid HTML. This is needed to enable features like [CSS / JS variables](../html_js_css_variables).
Here is how the HTML is post-processed:
1. **Insert component ID**: Each root element in the rendered HTML automatically receives a `data-djc-id-cxxxxxx` attribute containing a unique component instance ID.
```html
<!-- Output HTML -->
<div class="card" data-djc-id-c1a2b3c>
...
</div>
<div class="backdrop" data-djc-id-c1a2b3c>
...
</div>
```
2. **Insert CSS ID**: If the component defines CSS variables through [`get_css_data()`](../../../reference/api/#django_components.Component.get_css_data), the root elements also receive a `data-djc-css-xxxxxx` attribute. This attribute links the element to its specific CSS variables.
```html
<!-- Output HTML -->
<div class="card" data-djc-id-c1a2b3c data-djc-css-d4e5f6>
<!-- Component content -->
</div>
```
3. **Insert JS and CSS**: After the HTML is rendered, Django Components handles inserting JS and CSS dependencies into the page based on the [render type](../rendering_components/#render-types) (document, fragment, or inline).
For example, if your component contains the
[`{% component_js_dependencies %}`](../../reference/template_tags.md#component_js_dependencies)
or
[`{% component_css_dependencies %}`](../../reference/template_tags.md#component_css_dependencies)
tags, or the `<head>` and `<body>` elements, the JS and CSS scripts will be inserted into the HTML.
For more information on how JS and CSS dependencies are rendered, see [Rendering JS / CSS](../../advanced/rendering_js_css).
## JS
The component's JS script is executed in the browser:
- It is executed AFTER the "secondary" JS files from [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js) are loaded.
- The script is only executed once, even if there are multiple instances of the component on the page.
- Component JS scripts are executed in the order how they appeared in the template / HTML (top to bottom).
You can define the JS directly in your Python code using the [`js`](../../reference/api.md#django_components.Component.js) attribute:
```djc_py
class Button(Component):
js = """
console.log("Hello, world!");
"""
def get_js_data(self, args, kwargs, slots, context):
return {
"text": kwargs.get("text", "Click me"),
}
```
Or you can define the JS in a separate file and reference it using [`js_file`](../../reference/api.md#django_components.Component.js_file):
```python
class Button(Component):
js_file = "button.js"
def get_js_data(self, args, kwargs, slots, context):
return {
"text": kwargs.get("text", "Click me"),
}
```
```django title="button.js"
console.log("Hello, world!");
```
## CSS
You can define the CSS directly in your Python code using the [`css`](../../reference/api.md#django_components.Component.css) attribute:
```djc_py
class Button(Component):
css = """
.btn {
width: 100px;
color: var(--color);
}
"""
def get_css_data(self, args, kwargs, slots, context):
return {
"color": kwargs.get("color", "red"),
}
```
Or you can define the CSS in a separate file and reference it using [`css_file`](../../reference/api.md#django_components.Component.css_file):
```python
class Button(Component):
css_file = "button.css"
def get_css_data(self, args, kwargs, slots, context):
return {
"text": kwargs.get("text", "Click me"),
}
```
```django title="button.css"
.btn {
color: red;
}
```
## File paths
Compared to the [secondary JS / CSS files](../secondary_js_css_files), the definition of file paths for the main HTML / JS / CSS files is quite simple - just strings, without any lists, objects, or globs.
However, similar to the secondary JS / CSS files, you can specify the file paths [relative to the component's directory](../secondary_js_css_files/#relative-to-component).
So if you have a directory with following files:
```
[project root]/components/calendar/
├── calendar.html
├── calendar.css
├── calendar.js
└── calendar.py
```
You can define the component like this:
```py title="[project root]/components/calendar/calendar.py"
from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_file = "calendar.html"
css_file = "calendar.css"
js_file = "calendar.js"
```
Assuming that
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
contains path `[project root]/components`, the example above is the same as writing out:
```py title="[project root]/components/calendar/calendar.py"
from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_file = "calendar/template.html"
css_file = "calendar/style.css"
js_file = "calendar/script.js"
```
If the path cannot be resolved relative to the component, django-components will attempt
to resolve the path relative to the component directories, as set in
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
or
[`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs).
Read more about [file path resolution](../secondary_js_css_files/#relative-to-component).
## Access component definition
Component's HTML / CSS / JS is resolved and loaded lazily.
This means that, when you specify any of
[`template_file`](../../reference/api.md#django_components.Component.template_file),
[`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css_file`](../../reference/api.md#django_components.Component.css_file),
or [`Media.js/css`](../../reference/api.md#django_components.Component.Media),
these file paths will be resolved only once you either:
1. Access any of the following attributes on the component:
- [`media`](../../reference/api.md#django_components.Component.media),
[`template`](../../reference/api.md#django_components.Component.template),
[`template_file`](../../reference/api.md#django_components.Component.template_file),
[`js`](../../reference/api.md#django_components.Component.js),
[`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css`](../../reference/api.md#django_components.Component.css),
[`css_file`](../../reference/api.md#django_components.Component.css_file)
2. Render the component.
Once the component's media files have been loaded once, they will remain in-memory
on the Component class:
- HTML from [`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
will be available under [`Component.template`](../../reference/api.md#django_components.Component.template)
- CSS from [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
will be available under [`Component.css`](../../reference/api.md#django_components.Component.css)
- JS from [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
will be available under [`Component.js`](../../reference/api.md#django_components.Component.js)
Thus, whether you define HTML via
[`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
or [`Component.template`](../../reference/api.md#django_components.Component.template),
you can always access the HTML content under [`Component.template`](../../reference/api.md#django_components.Component.template).
And the same applies for JS and CSS.
**Example:**
```py
# When we create Calendar component, the files like `calendar/template.html`
# are not yet loaded!
@register("calendar")
class Calendar(Component):
template_file = "calendar/template.html"
css_file = "calendar/style.css"
js_file = "calendar/script.js"
class Media:
css = "calendar/style1.css"
js = "calendar/script2.js"
# It's only at this moment that django-components reads the files like `calendar/template.html`
print(Calendar.css)
# Output:
# .calendar {
# width: 200px;
# background: pink;
# }
```
!!! warning
**Do NOT modify HTML / CSS / JS after it has been loaded**
django-components assumes that the component's media files like `js_file` or `Media.js/css` are static.
If you need to dynamically change these media files, consider instead defining multiple Components.
Modifying these files AFTER the component has been loaded at best does nothing. However, this is
an untested behavior, which may lead to unexpected errors.

View file

@ -0,0 +1,364 @@
When a component recieves input through [`{% component %}`](../../../reference/template_tags/#component) tag,
or the [`Component.render()`](../../../reference/api/#django_components.Component.render) or [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response) methods, you can define how the input is handled, and what variables will be available to the template, JavaScript and CSS.
## Overview
Django Components offers three key methods for passing variables to different parts of your component:
- [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) - Provides variables to your HTML template
- [`get_js_data()`](../../../reference/api/#django_components.Component.get_js_data) - Provides variables to your JavaScript code
- [`get_css_data()`](../../../reference/api/#django_components.Component.get_css_data) - Provides variables to your CSS styles
These methods let you pre-process inputs before they're used in rendering.
Each method handles the data independently - you can define different data for the template, JS, and CSS.
```python
class ProfileCard(Component):
class Kwargs(NamedTuple):
user_id: int
show_details: bool
class Defaults:
show_details = True
def get_template_data(self, args, kwargs: Kwargs, slots, context):
user = User.objects.get(id=kwargs.user_id)
return {
"user": user,
"show_details": kwargs.show_details,
}
def get_js_data(self, args, kwargs: Kwargs, slots, context):
return {
"user_id": kwargs.user_id,
}
def get_css_data(self, args, kwargs: Kwargs, slots, context):
text_color = "red" if kwargs.show_details else "blue"
return {
"text_color": text_color,
}
```
## Template variables
The [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) method is the primary way to provide variables to your HTML template. It receives the component inputs and returns a dictionary of data that will be available in the template.
If [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) returns `None`, an empty dictionary will be used.
```python
class ProfileCard(Component):
template_file = "profile_card.html"
class Kwargs(NamedTuple):
user_id: int
show_details: bool
def get_template_data(self, args, kwargs: Kwargs, slots, context):
user = User.objects.get(id=kwargs.user_id)
# Process and transform inputs
return {
"user": user,
"show_details": kwargs.show_details,
"user_joined_days": (timezone.now() - user.date_joined).days,
}
```
In your template, you can then use these variables:
```django
<div class="profile-card">
<h2>{{ user.username }}</h2>
{% if show_details %}
<p>Member for {{ user_joined_days }} days</p>
<p>Email: {{ user.email }}</p>
{% endif %}
</div>
```
### Legacy `get_context_data()`
The [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data) method is the legacy way to provide variables to your HTML template. It serves the same purpose as [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) - it receives the component inputs and returns a dictionary of data that will be available in the template.
However, [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data) has a few drawbacks:
- It does NOT receive the `slots` and `context` parameters.
- The `args` and `kwargs` parameters are given as variadic `*args` and `**kwargs` parameters. As such, they cannot be typed.
```python
class ProfileCard(Component):
template_file = "profile_card.html"
def get_context_data(self, user_id, show_details=False, *args, **kwargs):
user = User.objects.get(id=user_id)
return {
"user": user,
"show_details": show_details,
}
```
!!! warning
[`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data)
and [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data)
are mutually exclusive.
If both methods return non-empty dictionaries, an error will be raised.
!!! note
The `get_context_data()` method will be removed in v2.
## Accessing component inputs
The component inputs are available in two ways:
1. **Function arguments (recommended)**
The data methods receive the inputs as parameters, which you can access directly.
```python
class ProfileCard(Component):
def get_template_data(self, args, kwargs, slots, context):
# Access inputs directly as parameters
return {
"user_id": user_id,
"show_details": show_details,
}
```
!!! info
By default, the `args` parameter is a list, while `kwargs` and `slots` are dictionaries.
If you add typing to your component with
[`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
the respective inputs will be given as instances of these classes.
Learn more about [Component typing](../../advanced/typing_and_validation).
2. **`self.input` property**
The data methods receive only the main inputs. There are additional settings that may be passed
to components. If you need to access these, you can do so via the [`self.input`](../../../reference/api/#django_components.Component.input) property.
The `input` property contains all the inputs passed to the component (instance of [`ComponentInput`](../../../reference/api/#django_components.ComponentInput)).
This includes:
- [`input.args`](../../../reference/api/#django_components.ComponentInput.args) - List of positional arguments
- [`input.kwargs`](../../../reference/api/#django_components.ComponentInput.kwargs) - Dictionary of keyword arguments
- [`input.slots`](../../../reference/api/#django_components.ComponentInput.slots) - Dictionary of slots. Values are normalized to [`Slot`](../../../reference/api/#django_components.Slot) instances
- [`input.context`](../../../reference/api/#django_components.ComponentInput.context) - [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) object that should be used to render the component
- [`input.type`](../../../reference/api/#django_components.ComponentInput.type) - The type of the component (document, fragment)
- [`input.render_dependencies`](../../../reference/api/#django_components.ComponentInput.render_dependencies) - Whether to render dependencies (CSS, JS)
For more details, see [Component inputs](../render_api/#component-inputs).
```python
class ProfileCard(Component):
def get_template_data(self, args, kwargs, slots, context):
# Access positional arguments
user_id = self.input.args[0] if self.input.args else None
# Access keyword arguments
show_details = self.input.kwargs.get("show_details", False)
# Render component differently depending on the type
if self.input.type == "fragment":
...
return {
"user_id": user_id,
"show_details": show_details,
}
```
!!! info
Unlike the parameters passed to the data methods, the `args`, `kwargs`, and `slots` in `self.input` property are always lists and dictionaries,
regardless of whether you added typing to your component.
## Default values
You can use [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class to provide default values for your inputs.
These defaults will be applied either when:
- The input is not provided at rendering time
- The input is provided as `None`
When you then access the inputs in your data methods, the default values will be already applied.
Read more about [Component Defaults](./component_defaults.md).
```py
from django_components import Component, Default, register
@register("profile_card")
class ProfileCard(Component):
class Kwargs(NamedTuple):
show_details: bool
class Defaults:
show_details = True
# show_details will be set to True if `None` or missing
def get_template_data(self, args, kwargs: Kwargs, slots, context):
return {
"show_details": kwargs.show_details,
}
...
```
!!! warning
When typing your components with [`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
you may be inclined to define the defaults in the classes.
```py
class ProfileCard(Component):
class Kwargs(NamedTuple):
show_details: bool = True
```
This is **NOT recommended**, because:
- The defaults will NOT be applied to inputs when using [`self.input`](../../../reference/api/#django_components.Component.input) property.
- The defaults will NOT be applied when a field is given but set to `None`.
Instead, define the defaults in the [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class.
## Accessing Render API
All three data methods have access to the Component's [Render API](./render_api.md), which includes:
- [`self.id`](./render_api/#component-id) - The unique ID for the current render call
- [`self.input`](./render_api/#component-inputs) - All the component inputs
- [`self.request`](./render_api/#request-object-and-context-processors) - The request object (if available)
- [`self.context_processors_data`](./render_api/#request-object-and-context-processors) - Data from Django's context processors (if request is available)
- [`self.inject()`](./render_api/#provide-inject) - Inject data into the component
## Type hints
### Typing inputs
You can add type hints for the component inputs to ensure that the component logic is correct.
For this, define the [`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
and then add type hints to the data methods.
This will also validate the inputs at runtime, as the type classes will be instantiated with the inputs.
Read more about [Component typing](../../advanced/typing_and_validation).
```python
from typing import NamedTuple, Optional
from django_components import Component, SlotInput
class Button(Component):
class Args(NamedTuple):
name: str
class Kwargs(NamedTuple):
surname: str
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
my_slot: Optional[SlotInput] = None
footer: SlotInput
# Use the above classes to add type hints to the data method
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
# The parameters are instances of the classes we defined
assert isinstance(args, Button.Args)
assert isinstance(kwargs, Button.Kwargs)
assert isinstance(slots, Button.Slots)
```
!!! note
The data available via [`self.input`](../../../reference/api/#django_components.Component.input) property is NOT typed.
### Typing data
In the same fashion, you can add types and validation for the data that should be RETURNED from each data method.
For this, set the [`TemplateData`](../../../reference/api/#django_components.Component.TemplateData),
[`JsData`](../../../reference/api/#django_components.Component.JsData),
and [`CssData`](../../../reference/api/#django_components.Component.CssData) classes on the component class.
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class.
```python
from typing import NamedTuple
from django_components import Component
class Button(Component):
class TemplateData(NamedTuple):
data1: str
data2: int
class JsData(NamedTuple):
js_data1: str
js_data2: int
class CssData(NamedTuple):
css_data1: str
css_data2: int
def get_template_data(self, args, kwargs, slots, context):
return Button.TemplateData(
data1="...",
data2=123,
)
def get_js_data(self, args, kwargs, slots, context):
return Button.JsData(
js_data1="...",
js_data2=123,
)
def get_css_data(self, args, kwargs, slots, context):
return Button.CssData(
css_data1="...",
css_data2=123,
)
```
## Pass-through kwargs
It's best practice to explicitly define what args and kwargs a component accepts.
However, if you want a looser setup, you can easily write components that accept any number
of kwargs, and pass them all to the template
(similar to [django-cotton](https://github.com/wrabit/django-cotton)).
To do that, simply return the `kwargs` dictionary itself from [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data):
```py
class MyComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
return kwargs
```
You can do the same for [`get_js_data()`](../../../reference/api/#django_components.Component.get_js_data) and [`get_css_data()`](../../../reference/api/#django_components.Component.get_css_data), if needed:
```py
class MyComponent(Component):
def get_js_data(self, args, kwargs, slots, context):
return kwargs
def get_css_data(self, args, kwargs, slots, context):
return kwargs
```

View file

@ -1,14 +1,21 @@
When a component is being rendered, whether with [`Component.render()`](../../../reference/api#django_components.Component.render)
or [`{% component %}`](../../../reference/template_tags#component), a component instance is populated with the current inputs and context. This allows you to access things like component inputs - methods and attributes on the component instance which would otherwise not be available.
or [`{% component %}`](../../../reference/template_tags#component), a component instance is populated with the current inputs and context. This allows you to access things like component inputs.
We refer to these render-only methods and attributes as the "Render API".
We refer to these render-time-only methods and attributes as the "Render API".
Render API is available inside these [`Component`](../../../reference/api#django_components.Component) methods:
- [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
- [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data)
- [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data)
- [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
- [`on_render_before()`](../../../reference/api#django_components.Component.on_render_before)
- [`on_render_after()`](../../../reference/api#django_components.Component.on_render_after)
!!! note
If you try to access the Render API outside of these methods, you will get a `RuntimeError`.
Example:
```python
@ -38,9 +45,15 @@ rendered = Table.render(
)
```
## Accessing Render API
## Overview
If you try to access the Render API outside of a render call, you will get a `RuntimeError`.
The Render API includes:
- [`self.id`](../render_api/#component-id) - The unique ID for the current render call
- [`self.input`](../render_api/#component-inputs) - All the component inputs
- [`self.request`](../render_api/#request-object-and-context-processors) - The request object (if available)
- [`self.context_processors_data`](../render_api/#request-object-and-context-processors) - Data from Django's context processors (if request is available)
- [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
## Component ID
@ -138,3 +151,23 @@ rendered = Table.render(
request=HttpRequest(),
)
```
## Provide / Inject
Components support a provide / inject system as known from Vue or React.
When rendering the component, you can call [`self.inject()`](../../../reference/api/#django_components.Component.inject) with the key of the data you want to inject.
The object returned by [`self.inject()`](../../../reference/api/#django_components.Component.inject)
To provide data to components, use the [`{% provide %}`](../../../reference/template_tags#provide) template tag.
Read more about [Provide / Inject](../advanced/provide_inject.md).
```python
class Table(Component):
def get_context_data(self, *args, **attrs):
# Access provided data
data = self.inject("some_data")
assert data.some_data == "some_data"
```

View file

@ -0,0 +1,649 @@
Your components can be rendered either within your Django templates, or directly in Python code.
## Overview
Django Components provides three main methods to render components:
- [`{% component %}` tag](#component-tag) - Renders the component within your Django templates
- [`Component.render()` method](#render-method) - Renders the component to a string
- [`Component.render_to_response()` method](#render-to-response-method) - Renders the component and wraps it in an HTTP response
## `{% component %}` tag
Use the [`{% component %}`](../../../reference/template_tags#component) tag to render a component within your Django templates.
The [`{% component %}`](../../../reference/template_tags#component) tag takes:
- Component's registered name as the first positional argument,
- Followed by any number of positional and keyword arguments.
```django
{% load component_tags %}
<div>
{% component "button" name="John" job="Developer" / %}
</div>
```
To pass in slots content, you can insert [`{% fill %}`](../../../reference/template_tags#fill) tags,
directly within the [`{% component %}`](../../../reference/template_tags#component) tag to "fill" the slots:
```django
{% component "my_table" rows=rows headers=headers %}
{% fill "pagination" %}
< 1 | 2 | 3 >
{% endfill %}
{% endcomponent %}
```
You can even nest [`{% fill %}`](../../../reference/template_tags#fill) tags within
[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
and other tags:
```django
{% component "my_table" rows=rows headers=headers %}
{% if rows %}
{% fill "pagination" %}
< 1 | 2 | 3 >
{% endfill %}
{% endif %}
{% endcomponent %}
```
!!! info "Omitting the `component` keyword"
If you would like to omit the `component` keyword, and simply refer to your
components by their registered names:
```django
{% button name="John" job="Developer" / %}
```
You can do so by setting the "shorthand" [Tag formatter](../../advanced/tag_formatters) in the settings:
```python
# settings.py
COMPONENTS = {
"tag_formatter": "django_components.component_shorthand_formatter",
}
```
!!! info "Extended template tag syntax"
Unlike regular Django template tags, django-components' tags offer extra features like
defining literal lists and dicts, and more. Read more about [Template tag syntax](../template_tag_syntax).
### Registering components
For a component to be renderable with the [`{% component %}`](../../../reference/template_tags#component) tag, it must be first registered with the [`@register()`](../../../reference/api/#django_components.register) decorator.
For example, if you register a component under the name `"button"`:
```python
from typing import NamedTuple
from django_components import Component, register
@register("button")
class Button(Component):
template_file = "button.html"
class Kwargs(NamedTuple):
name: str
job: str
def get_template_data(self, args, kwargs, slots, context):
...
```
Then you can render this component by using its registered name `"button"` in the template:
```django
{% component "button" name="John" job="Developer" / %}
```
As you can see above, the args and kwargs passed to the [`{% component %}`](../../../reference/template_tags#component) tag correspond
to the component's input.
For more details, read [Registering components](../../advanced/component_registry).
!!! note "Why do I need to register components?"
TL;DR: To be able to share components as libraries, and because components can be registed with multiple registries / libraries.
Django-components allows to [share components across projects](../../advanced/component_libraries).
However, different projects may use different settings. For example, one project may prefer the "long" format:
```django
{% component "button" name="John" job="Developer" / %}
```
While the other may use the "short" format:
```django
{% button name="John" job="Developer" / %}
```
Both approaches are supported simultaneously for backwards compatibility, because django-components
started out with only the "long" format.
To avoid ambiguity, when you use a 3rd party library, it uses the syntax that the author
had configured for it.
So when you are creating a component, django-components need to know which registry the component
belongs to, so it knows which syntax to use.
### Rendering templates
If you have embedded the component in a Django template using the
[`{% component %}`](../../reference/template_tags#component) tag:
```django title="[project root]/templates/my_template.html"
{% load component_tags %}
<div>
{% component "calendar" date="2024-12-13" / %}
</div>
```
You can simply render the template with the Django's API:
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
```python
from django.shortcuts import render
context = {"date": "2024-12-13"}
rendered_template = render(request, "my_template.html", context)
```
- [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render)
```python
from django.template import Template
from django.template.loader import get_template
# Either from a file
template = get_template("my_template.html")
# or inlined
template = Template("""
{% load component_tags %}
<div>
{% component "calendar" date="2024-12-13" / %}
</div>
""")
rendered_template = template.render()
```
### Isolating components
By default, components behave similarly to Django's
[`{% include %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#include),
and the template inside the component has access to the variables defined in the outer template.
You can selectively isolate a component, using the `only` flag, so that the inner template
can access only the data that was explicitly passed to it:
```django
{% component "name" positional_arg keyword_arg=value ... only / %}
```
Alternatively, you can set all components to be isolated by default, by setting
[`context_behavior`](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior)
to `"isolated"` in your settings:
```python
# settings.py
COMPONENTS = {
"context_behavior": "isolated",
}
```
## `render()` method
The [`Component.render()`](../../../reference/api/#django_components.Component.render) method renders a component to a string.
This is the equivalent of calling the [`{% component %}`](../template_tags#component) tag.
```python
from typing import NamedTuple, Optional
from django_components import Component, SlotInput
class Button(Component):
template_file = "button.html"
class Args(NamedTuple):
name: str
class Kwargs(NamedTuple):
surname: str
age: int
class Slots(NamedTuple):
footer: Optional[SlotInput] = None
def get_template_data(self, args, kwargs, slots, context):
...
Button.render(
args=["John"],
kwargs={
"surname": "Doe",
"age": 30,
},
slots={
"footer": "i AM A SLOT",
},
)
```
[`Component.render()`](../../../reference/api/#django_components.Component.render) accepts the following arguments:
- `args` - Positional arguments to pass to the component (as a list or tuple)
- `kwargs` - Keyword arguments to pass to the component (as a dictionary)
- `slots` - Slot content to pass to the component (as a dictionary)
- `context` - Django context for rendering (can be a dictionary or a `Context` object)
- `type` - Type of rendering (default: `"document"`)
- `request` - HTTP request object, used for context processors (optional)
- `escape_slots_content` - Whether to HTML-escape slot content (default: `True`)
- `render_dependencies` - Whether to process JS and CSS dependencies (default: `True`)
All arguments are optional. If not provided, they default to empty values or sensible defaults.
See the API reference for [`Component.render()`](../../../reference/api/#django_components.Component.render)
for more details on the arguments.
## `render_to_response()` method
The [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
method works just like [`Component.render()`](../../../reference/api/#django_components.Component.render),
but wraps the result in an HTTP response.
It accepts all the same arguments as [`Component.render()`](../../../reference/api/#django_components.Component.render).
Any extra arguments are passed to the [`HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse)
constructor.
```python
from typing import NamedTuple, Optional
from django_components import Component, SlotInput
class Button(Component):
template_file = "button.html"
class Args(NamedTuple):
name: str
class Kwargs(NamedTuple):
surname: str
age: int
class Slots(NamedTuple):
footer: Optional[SlotInput] = None
def get_template_data(self, args, kwargs, slots, context):
...
# Render the component to an HttpResponse
response = Button.render_to_response(
args=["John"],
kwargs={
"surname": "Doe",
"age": 30,
},
slots={
"footer": "i AM A SLOT",
},
# Additional response arguments
status=200,
headers={"X-Custom-Header": "Value"},
)
```
This method is particularly useful in view functions, as you can return the result of the component directly:
```python
def profile_view(request, user_id):
return Button.render_to_response(
kwargs={
"surname": "Doe",
"age": 30,
},
request=request,
)
```
### Custom response classes
By default, [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
returns a standard Django [`HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse).
You can customize this by setting the [`response_class`](../../../reference/api/#django_components.Component.response_class)
attribute on your component:
```python
from django.http import HttpResponse
from django_components import Component
class MyHttpResponse(HttpResponse):
...
class MyComponent(Component):
response_class = MyHttpResponse
response = MyComponent.render_to_response()
assert isinstance(response, MyHttpResponse)
```
## Render types
The rendered HTML may be used in different contexts (browser, email, etc).
If your components use JS and CSS scripts, you need to handle them differently.
[`render()`](../../../reference/api/#django_components.Component.render) and [`render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
accept a `type` parameter, which controls this behavior.
The `type` parameter is set at the root of a component render tree, which is why it is not available for
the [`{% component %}`](../../../reference/template_tags#component) tag.
!!! info
The `type` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
Learn more about [Rendering JS / CSS](../../advanced/rendering_js_css).
There are three render types:
### `document`
`type="document"` is the default. Use this if you are rendering a whole page, or if no other option suits better.
```python
html = Button.render(type="document")
```
When you render a component tree with the `"document"` type, it is expected that:
- The HTML will be rendered at page load.
- The HTML will be inserted into a page / browser where JS can be executed.
With this setting, the JS and CSS is set up to avoid any delays for end users:
- Components' primary JS and CSS scripts ([`Component.js`](../../../reference/api/#django_components.Component.js)
and [`Component.css`](../../../reference/api/#django_components.Component.css)) are inlined into the rendered HTML.
```html
<script>
console.log("Hello from Button!");
</script>
<style>
.button {
background-color: blue;
}
</style>
```
- Components' secondary JS and CSS scripts ([`Component.Media`](../../../reference/api/#django_components.Component.Media))
are inserted into the rendered HTML as links.
```html
<link rel="stylesheet" href="https://example.com/styles.css" />
<script src="https://example.com/script.js"></script>
```
- A JS script is injected to manage component dependencies, enabling lazy loading of JS and CSS
for HTML fragments.
!!! info
This render type is required for fragments to work properly, as it sets up the dependency manager that fragments rely on.
!!! note "How the dependency manager works"
The dependency manager is a JS script that keeps track of all the JS and CSS dependencies that have already been loaded.
When a fragment is inserted into the page, it will also insert a JSON `<script>` tag with fragment metadata.
The dependency manager will pick up on that, and check which scripts the fragment needs.
It will then fetch only the scripts that haven't been loaded yet.
### `fragment`
`type="fragment"` is used when rendering a piece of HTML that will be inserted into a page
that has already been rendered with the `"document"` type:
```python
fragment = MyComponent.render(type="fragment")
```
The HTML of fragments is very lightweight because it doesn't include the JS and CSS scripts
of the rendered components.
With fragments, even if a component has JS and CSS, you can insert the same component into a page
hundreds of times, and the JS and CSS will only ever be loaded once.
The fragment type:
- Does not include the dependency manager script (assumes it's already loaded)
- Does not inline JS or CSS directly in the HTML
- Includes a special JSON `<script>` tag that tells the dependency manager what JS and CSS to load
- The dependency manager will fetch only scripts that haven't been loaded yet
This is intended for dynamic content that's loaded after the initial page load, such as with [HTMX](https://htmx.org/) or similar.
## Passing context
The [`render()`](../../../reference/api/#django_components.Component.render) and [`render_to_response()`](../../../reference/api/#django_components.Component.render_to_response) methods accept an optional `context` argument.
This sets the context within which the component is rendered.
When a component is rendered within a template with the [`{% component %}`](../../../reference/template_tags#component)
tag, this will be automatically set to the
[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
instance that is used for rendering the template.
When you call [`Component.render()`](../../../reference/api/#django_components.Component.render) directly from Python,
there is no context object, so you can ignore this input most of the time.
Instead, use `args`, `kwargs`, and `slots` to pass data to the component.
However, you can pass
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
to the `context` argument, so that the component will gain access to the request object and will use
[context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
Read more on [Working with HTTP requests](../http_request).
```py
Button.render(
context=RequestContext(request),
)
```
For advanced use cases, you can use `context` argument to "pre-render" the component in Python, and then
pass the rendered output as plain string to the template. With this, the inner component is rendered as if
it was within the template with [`{% component %}`](../../../reference/template_tags#component).
```py
class Button(Component):
def render(self, context, template):
# Pass `context` to Icon component so it is rendered
# as if nested within Button.
icon = Icon.render(
context=context,
args=["icon-name"],
render_dependencies=False,
)
# Update context with icon
with context.update({"icon": icon}):
return template.render(context)
```
!!! warning
Whether the variables defined in `context` are actually available in the template depends on the
[context behavior mode](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior):
- In `"django"` context behavior mode, the template will have access to the keys of this context.
- In `"isolated"` context behavior mode, the template will NOT have access to this context,
and data MUST be passed via component's args and kwargs.
Therefore, it's **strongly recommended** to not rely on defining variables on the context object,
but instead passing them through as `args` and `kwargs`
❌ Don't do this:
```python
html = ProfileCard.render(
context={"name": "John"},
)
```
✅ Do this:
```python
html = ProfileCard.render(
kwargs={"name": "John"},
)
```
## Typing render methods
Neither [`Component.render()`](../../../reference/api/#django_components.Component.render)
nor [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
are typed, due to limitations of Python's type system.
To add type hints, you can wrap the inputs
in component's [`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes.
Read more on [Typing and validation](../../advanced/typing_and_validation).
```python
from typing import NamedTuple, Optional
from django_components import Component, Slot, SlotInput
# Define the component with the types
class Button(Component):
class Args(NamedTuple):
name: str
class Kwargs(NamedTuple):
surname: str
age: int
class Slots(NamedTuple):
my_slot: Optional[SlotInput] = None
footer: SlotInput
# Add type hints to the render call
Button.render(
args=Button.Args(
name="John",
),
kwargs=Button.Kwargs(
surname="Doe",
age=30,
),
slots=Button.Slots(
footer=Slot(lambda *a, **kwa: "Click me!"),
),
)
```
## Nesting components
You can nest components by rendering one component and using its output as input to another:
```python
from django.utils.safestring import mark_safe
# Render the inner component
inner_html = InnerComponent.render(
kwargs={"some_data": "value"},
render_dependencies=False, # Important for nesting!
)
# Use inner component's output in the outer component
outer_html = OuterComponent.render(
kwargs={
"content": mark_safe(inner_html), # Mark as safe to prevent escaping
},
)
```
The key here is setting `render_dependencies=False` for the inner component. This prevents duplicate dependencies when the outer component is rendered.
When `render_dependencies=False`:
- No JS or CSS dependencies will be added to the output HTML
- The component's content is rendered as-is
- The outer component will take care of including all needed dependencies
Read more about [Rendering JS / CSS](../../advanced/rendering_js_css).
## Dynamic components
Django components defines a special "dynamic" component ([`DynamicComponent`](../../../reference/components#django_components.components.dynamic.DynamicComponent)).
Normally, you have to hard-code the component name in the template:
```django
{% component "button" / %}
```
The dynamic component allows you to dynamically render any component based on the `is` kwarg. This is similar
to [Vue's dynamic components](https://vuejs.org/guide/essentials/component-basics#dynamic-components) (`<component :is>`).
```django
{% component "dynamic" is=table_comp data=table_data headers=table_headers %}
{% fill "pagination" %}
{% component "pagination" / %}
{% endfill %}
{% endcomponent %}
```
The args, kwargs, and slot fills are all passed down to the underlying component.
As with other components, the dynamic component can be rendered from Python:
```py
from django_components import DynamicComponent
DynamicComponent.render(
kwargs={
"is": table_comp,
"data": table_data,
"headers": table_headers,
},
slots={
"pagination": PaginationComponent.render(
render_dependencies=False,
),
},
)
```
### Dynamic component name
By default, the dynamic component is registered under the name `"dynamic"`. In case of a conflict,
you can set the
[`COMPONENTS.dynamic_component_name`](../../../reference/settings#django_components.app_settings.ComponentsSettings.dynamic_component_name)
setting to change the name used for the dynamic components.
```py
# settings.py
COMPONENTS = ComponentsSettings(
dynamic_component_name="my_dynamic",
)
```
After which you will be able to use the dynamic component with the new name:
```django
{% component "my_dynamic" is=table_comp data=table_data headers=table_headers %}
{% fill "pagination" %}
{% component "pagination" / %}
{% endfill %}
{% endcomponent %}
```

View file

@ -1,38 +1,30 @@
As you could have seen in [the tutorial](../../getting_started/adding_js_and_css.md), there's multiple ways how you can associate
HTML / JS / CSS with a component:
## Overview
- You can set [`Component.template`](../../reference/api.md#django_components.Component.template),
[`Component.css`](../../reference/api.md#django_components.Component.css) and
[`Component.js`](../../reference/api.md#django_components.Component.js) to define the main HTML / CSS / JS for a component
as inlined code.
- You can set [`Component.template_file`](../../reference/api.md#django_components.Component.template_file),
[`Component.css_file`](../../reference/api.md#django_components.Component.css_file) and
[`Component.js_file`](../../reference/api.md#django_components.Component.js_file) to define the main HTML / CSS / JS
for a component in separate files.
- You can link additional CSS / JS files using
[`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js)
and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
Each component can define extra or "secondary" CSS / JS files using the nested [`Component.Media`](../../reference/api.md#django_components.Component.Media) class,
by setting [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js) and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
!!! warning
The [main HTML / JS / CSS files](../html_js_css_files) are limited to 1 per component. This is not the case for the secondary files,
where components can have many of them.
You **cannot** use both inlined code **and** separate file for a single language type:
There is also no special behavior or post-processing for these secondary files, they are loaded as is.
- You can only either set `Component.template` or `Component.template_file`
- You can only either set `Component.css` or `Component.css_file`
- You can only either set `Component.js` or `Component.js_file`
You can use these for third-party libraries, or for shared CSS / JS files.
However, you can freely mix these for different languages:
These must be set as paths, URLs, or [custom objects](#paths-as-objects).
```djc_py
class MyTable(Component):
template: types.django_html = """
<div class="welcome">
Hi there!
</div>
"""
js_file = "my_table.js"
css_file = "my_table.css"
```
```py
@register("calendar")
class Calendar(Component):
class Media:
js = [
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js",
"calendar/script.js",
]
css = [
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css",
"calendar/style.css",
]
```
!!! note
@ -42,7 +34,233 @@ HTML / JS / CSS with a component:
- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.0/howto/static-files/)
## Defining file paths relative to component
## `Media` class
<!-- TODO: This section deserves to be expanded with more examples,
so it's easier to follow. Right now it assumes that the read
is familiar with Django's Media class, as we describe our Media class
in constrast to it.
Instead we should go over all features / behaviours of the `Media` class.
We should also make `Media` class into a separate extension,
and then have a separate page on "Secondary JS / CSS files".
-->
Use the `Media` class to define secondary JS / CSS files for a component.
This `Media` class behaves similarly to
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition):
- **Static paths** - Paths are handled as static file paths, and are resolved to URLs with Django's
[`{% static %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#static) template tag.
- **URLs** - A path that starts with `http`, `https`, or `/` is considered a URL. URLs are NOT resolved with [`{% static %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#static).
- **HTML tags** - Both static paths and URLs are rendered to `<script>` and `<link>` HTML tags with
`media_class.render_js()` and `media_class.render_css()`.
- **Bypass formatting** - A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
- **Inheritance** - You can set [`extend`](../../../reference/api#django_components.ComponentMediaInput.extend) to configure
whether to inherit JS / CSS from parent components. See [Media inheritance](#media-inheritance).
However, there's a few differences from Django's Media class:
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list,
or (CSS-only) a dictonary (See [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
4. If you set [`Media.extend`](../../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
it should be a list of [`Component`](../../../reference/api/#django_components.Component) classes.
```py
from components.layout import LayoutComp
class MyTable(Component):
class Media:
js = [
"path/to/script.js",
"path/to/*.js", # Or as a glob
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
]
css = {
"all": [
"path/to/style.css",
"path/to/*.css", # Or as a glob
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # TailwindCSS
],
"print": ["path/to/style2.css"],
}
# Reuse JS / CSS from LayoutComp
extend = [
LayoutComp,
]
```
### CSS media types
You can define which stylesheets will be associated with which
[CSS media types](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#targeting_media_types). You do so by defining CSS files as a dictionary.
See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.0/topics/forms/media/#css).
Again, you can set either a single file or a list of files per media type:
```py
class MyComponent(Component):
class Media:
css = {
"all": "path/to/style1.css",
"print": ["path/to/style2.css", "path/to/style3.css"],
}
```
Which will render the following HTML:
```html
<link href="/static/path/to/style1.css" media="all" rel="stylesheet">
<link href="/static/path/to/style2.css" media="print" rel="stylesheet">
<link href="/static/path/to/style3.css" media="print" rel="stylesheet">
```
!!! note
When you define CSS as a string or a list, the `all` media type is implied.
So these two examples are the same:
```py
class MyComponent(Component):
class Media:
css = "path/to/style1.css"
```
```py
class MyComponent(Component):
class Media:
css = {
"all": ["path/to/style1.css"],
}
```
### Media inheritance
By default, the media files are inherited from the parent component.
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
js = ["script.js"]
print(MyComponent.media._js) # ["parent.js", "script.js"]
```
You can set the component NOT to inherit from the parent component by setting the [`extend`](../../reference/api.md#django_components.ComponentMediaInput.extend) attribute to `False`:
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
extend = False # Don't inherit parent media
js = ["script.js"]
print(MyComponent.media._js) # ["script.js"]
```
Alternatively, you can specify which components to inherit from. In such case, the media files are inherited ONLY from the specified components, and NOT from the original parent components:
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
# Only inherit from these, ignoring the files from the parent
extend = [OtherComponent1, OtherComponent2]
js = ["script.js"]
print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
```
!!! info
The `extend` behaves consistently with
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend),
with one exception:
- When you set `extend` to a list, the list is expected to contain Component classes (or other classes that have a nested `Media` class).
### Accessing Media files
To access the files that you defined under [`Component.Media`](../../../reference/api#django_components.Component.Media),
use [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
This is consistent behavior with
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition).
```py
class MyComponent(Component):
class Media:
js = "path/to/script.js"
css = "path/to/style.css"
print(MyComponent.media)
# Output:
# <script src="/static/path/to/script.js"></script>
# <link href="/static/path/to/style.css" media="all" rel="stylesheet">
```
When working with component media files, it is important to understand the difference:
- `Component.Media`
- Is the "raw" media definition, or the input, which holds only the component's **own** media definition
- This class is NOT instantiated, it merely holds the JS / CSS files.
- `Component.media`
- Returns all resolved media files, **including** those inherited from parent components
- Is an instance of [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class ChildComponent(ParentComponent):
class Media:
js = ["child.js"]
# Access only this component's media
print(ChildComponent.Media.js) # ["child.js"]
# Access all inherited media
print(ChildComponent.media._js) # ["parent.js", "child.js"]
```
!!! note
You should **not** manually modify `Component.media` or `Component.Media` after the component has been resolved, as this may lead to unexpected behavior.
If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media),
you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
([See example](#rendering-paths)).
## File paths
Unlike the [main HTML / JS / CSS files](../html_js_css_files), the path definition for the secondary files are quite ergonomic.
### Relative to component
As seen in the [getting started example](../../getting_started/your_first_component.md), to associate HTML / JS / CSS
files with a component, you can set them as
@ -117,93 +335,30 @@ class Calendar(Component):
NOTE: In case of ambiguity, the preference goes to resolving the files relative to the component's directory.
## Defining additional JS and CSS files
### Globs
Each component can have only a single template, and single main JS and CSS. However, you can define additional JS or CSS
using the nested [`Component.Media` class](../../../reference/api#django_components.Component.Media).
Components can have many secondary files. To simplify their declaration, you can use globs.
This `Media` class behaves similarly to
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition):
Globs MUST be relative to the component's directory.
- Paths are generally handled as static file paths, and resolved URLs are rendered to HTML with
`media_class.render_js()` or `media_class.render_css()`.
- A path that starts with `http`, `https`, or `/` is considered a URL, skipping the static file resolution.
This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`.
- A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
- You can set [`extend`](../../../reference/api#django_components.ComponentMediaInput.extend) to configure
whether to inherit JS / CSS from parent components. See
[Controlling Media Inheritance](../../fundamentals/defining_js_css_html_files/#controlling-media-inheritance).
```py title="[project root]/components/calendar/calendar.py"
from django_components import Component, register
However, there's a few differences from Django's Media class:
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list,
or (CSS-only) a dictonary (See [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
4. If you set [`Media.extend`](../../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
it should be a list of [`Component`](../../../reference/api/#django_components.Component) classes.
```py
class MyTable(Component):
@register("calendar")
class Calendar(Component):
class Media:
js = [
"path/to/script.js",
"path/to/*.js", # Or as a glob
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
"path/to/*.js",
"another/path/*.js",
]
css = {
"all": [
"path/to/style.css",
"path/to/*.css", # Or as a glob
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # TailwindCSS
],
"print": ["path/to/style2.css"],
}
css = "*.css"
```
## Configuring CSS Media Types
How this works is that django-components will detect that the path is a glob, and will try to resolve all files matching the glob pattern relative to the component's directory.
You can define which stylesheets will be associated with which
[CSS Media types](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#targeting_media_types). You do so by defining CSS files as a dictionary.
After that, the file paths are handled the same way as if you defined them explicitly.
See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.0/topics/forms/media/#css).
Again, you can set either a single file or a list of files per media type:
```py
class MyComponent(Component):
class Media:
css = {
"all": "path/to/style1.css",
"print": ["path/to/style2.css", "path/to/style3.css"],
}
```
!!! note
When you define CSS as a string or a list, the `all` media type is implied.
So these two examples are the same:
```py
class MyComponent(Component):
class Media:
css = "path/to/style1.css"
```
```py
class MyComponent(Component):
class Media:
css = {
"all": ["path/to/style1.css"],
}
```
## Supported types for file paths
### Supported types
File paths can be any of:
@ -213,7 +368,7 @@ File paths can be any of:
- `SafeData` (`__html__` method)
- `Callable` that returns any of the above, evaluated at class creation (`__new__`)
See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath).
To help with typing the union, use [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath).
```py
from pathlib import Path
@ -238,18 +393,18 @@ class SimpleComponent(Component):
]
```
## Paths as objects
### Paths as objects
In the example [above](#supported-types-for-file-paths), you can see that when we used Django's
In the example [above](#supported-types), you can see that when we used Django's
[`mark_safe()`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.mark_safe)
to mark a string as a [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
we had to define the full `<script>`/`<link>` tag.
we had to define the URL / path as an HTML `<script>`/`<link>` elements.
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.0/topics/forms/media/#paths-as-objects)
feature, where "safe" strings are taken as is, and accessed only at render time.
feature, where "safe" strings are taken as is, and are accessed only at render time.
Because of that, the paths defined as "safe" strings are NEVER resolved, neither relative to component's directory,
nor relative to [`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs).
nor relative to [`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs). It is assumed that you will define the full `<script>`/`<link>` tag with the correct URL / path.
"Safe" strings can be used to lazily resolve a path, or to customize the `<script>` or `<link>` tag for individual paths:
@ -257,10 +412,12 @@ In the example below, we make use of "safe" strings to add `type="module"` to th
In this case, we implemented a "safe" string by defining a `__html__` method.
```py
# Path object
class ModuleJsPath:
def __init__(self, static_path: str) -> None:
self.static_path = static_path
# Lazily resolve the path
def __html__(self):
full_path = static(self.static_path)
return format_html(
@ -271,11 +428,6 @@ class ModuleJsPath:
class Calendar(Component):
template_file = "calendar/template.html"
def get_context_data(self, date):
return {
"date": date,
}
class Media:
css = "calendar/style1.css"
js = [
@ -286,13 +438,17 @@ class Calendar(Component):
]
```
## Customize how paths are rendered into HTML tags
### Rendering paths
Sometimes you may need to change how all CSS `<link>` or JS `<script>` tags are rendered for a given component. You can achieve this by providing your own subclass of [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media) to component's `media_class` attribute.
As part of the rendering process, the secondary JS / CSS files are resolved and rendered into `<link>` and `<script>` HTML tags, so they can be inserted into the render.
Normally, the JS and CSS paths are passed to `Media` class, which decides how the paths are resolved and how the `<link>` and `<script>` tags are constructed.
In the [Paths as objects](#paths-as-objects) section, we saw that we can use that to selectively change
how the HTML tags are constructed.
To change how the tags are constructed, you can override the [`Media.render_js` and `Media.render_css` methods](https://github.com/django/django/blob/fa7848146738a9fe1d415ee4808664e54739eeb7/django/forms/widgets.py#L102):
However, if you need to change how ALL CSS and JS files are rendered for a given component,
you can provide your own subclass of [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media) to the [`Component.media_class`](../../reference/api.md#django_components.Component.media_class) attribute.
To change how the tags are constructed, you can override the [`Media.render_js()` and `Media.render_css()` methods](https://github.com/django/django/blob/fa7848146738a9fe1d415ee4808664e54739eeb7/django/forms/widgets.py#L102):
```py
from django.forms.widgets import Media
@ -326,189 +482,3 @@ class Calendar(Component):
# Override the behavior of Media class
media_class = MyMedia
```
## Accessing component's HTML / JS / CSS
Component's HTML / CSS / JS is resolved and loaded lazily.
This means that, when you specify any of
[`template_file`](../../reference/api.md#django_components.Component.template_file),
[`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css_file`](../../reference/api.md#django_components.Component.css_file),
or [`Media.js/css`](../../reference/api.md#django_components.Component.Media),
these file paths will be resolved only once you either:
1. Access any of the following attributes on the component:
- [`media`](../../reference/api.md#django_components.Component.media),
[`template`](../../reference/api.md#django_components.Component.template),
[`template_file`](../../reference/api.md#django_components.Component.template_file),
[`js`](../../reference/api.md#django_components.Component.js),
[`js_file`](../../reference/api.md#django_components.Component.js_file),
[`css`](../../reference/api.md#django_components.Component.css),
[`css_file`](../../reference/api.md#django_components.Component.css_file)
2. Render the component.
Once the component's media files have been loaded once, they will remain in-memory
on the Component class:
- HTML from [`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
will be available under [`Component.template`](../../reference/api.md#django_components.Component.template)
- CSS from [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
will be available under [`Component.css`](../../reference/api.md#django_components.Component.css)
- JS from [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
will be available under [`Component.js`](../../reference/api.md#django_components.Component.js)
Thus, whether you define HTML via
[`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
or [`Component.template`](../../reference/api.md#django_components.Component.template),
you can always access the HTML content under [`Component.template`](../../reference/api.md#django_components.Component.template).
And the same applies for JS and CSS.
**Example:**
```py
# When we create Calendar component, the files like `calendar/template.html`
# are not yet loaded!
@register("calendar")
class Calendar(Component):
template_file = "calendar/template.html"
css_file = "calendar/style.css"
js_file = "calendar/script.js"
class Media:
css = "calendar/style1.css"
js = "calendar/script2.js"
# It's only at this moment that django-components reads the files like `calendar/template.html`
print(Calendar.css)
# Output:
# .calendar {
# width: 200px;
# background: pink;
# }
```
!!! warning
**Do NOT modify HTML / CSS / JS after it has been loaded**
django-components assumes that the component's media files like `js_file` or `Media.js/css` are static.
If you need to dynamically change these media files, consider instead defining multiple Components.
Modifying these files AFTER the component has been loaded at best does nothing. However, this is
an untested behavior, which may lead to unexpected errors.
## Accessing component's Media files
To access the files that you defined under [`Component.Media`](../../../reference/api#django_components.Component.Media),
use [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
This is consistent behavior with
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition).
```py
class MyComponent(Component):
class Media:
js = "path/to/script.js"
css = "path/to/style.css"
print(MyComponent.media)
# Output:
# <script src="/static/path/to/script.js"></script>
# <link href="/static/path/to/style.css" media="all" rel="stylesheet">
```
### `Component.Media` vs `Component.media`
When working with component media files, there are a few important concepts to understand:
- `Component.Media`
- Is the "raw" media definition, or the input, which holds only the component's **own** media definition
- This class is NOT instantiated, it merely holds the JS / CSS files.
- `Component.media`
- Returns all resolved media files, **including** those inherited from parent components
- Is an instance of [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class ChildComponent(ParentComponent):
class Media:
js = ["child.js"]
# Access only this component's media
print(ChildComponent.Media.js) # ["child.js"]
# Access all inherited media
print(ChildComponent.media._js) # ["parent.js", "child.js"]
```
!!! note
You should **not** manually modify `Component.media` or `Component.Media` after the component has been resolved, as this may lead to unexpected behavior.
If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media),
you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
([See example](#customize-how-paths-are-rendered-into-html-tags)).
## Controlling Media Inheritance
By default, the media files are inherited from the parent component.
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
js = ["script.js"]
print(MyComponent.media._js) # ["parent.js", "script.js"]
```
You can set the component NOT to inherit from the parent component by setting the [`extend`](../../reference/api.md#django_components.ComponentMediaInput.extend) attribute to `False`:
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
extend = False # Don't inherit parent media
js = ["script.js"]
print(MyComponent.media._js) # ["script.js"]
```
Alternatively, you can specify which components to inherit from. In such case, the media files are inherited ONLY from the specified components, and NOT from the original parent components:
```python
class ParentComponent(Component):
class Media:
js = ["parent.js"]
class MyComponent(ParentComponent):
class Media:
# Only inherit from these, ignoring the files from the parent
extend = [OtherComponent1, OtherComponent2]
js = ["script.js"]
print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
```
!!! info
The `extend` behaves consistently with
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend),
with one exception:
- When you set `extend` to a list, the list is expected to contain Component classes (or other classes that have a nested `Media` class).

View file

@ -4,7 +4,7 @@ In such cases, you can extract shared behavior into a standalone component class
When subclassing a component, there's a couple of things to keep in mind:
### Template, JS, and CSS Inheritance
### Template, JS, and CSS inheritance
When it comes to the pairs:
@ -52,13 +52,13 @@ class CustomCard(BaseCard):
"""
```
### Media Class Inheritance
### Media inheritance
The [`Component.Media`](../../reference/api.md#django_components.Component.Media) nested class follows Django's media inheritance rules:
- If both parent and child define a `Media` class, the child's media will automatically include both its own and the parent's JS and CSS files.
- This behavior can be configured using the [`extend`](../../reference/api.md#django_components.Component.Media.extend) attribute in the Media class, similar to Django's forms.
Read more on this in [Controlling Media Inheritance](./defining_js_css_html_files.md#controlling-media-inheritance).
Read more on this in [Media inheritance](./secondary_js_css_files/#media-inheritance).
For example:
@ -83,7 +83,7 @@ class SimpleModal(BaseModal):
js = ["simple_modal.js"] # Only this JS will be included
```
### Regular Python Inheritance
### Regular Python inheritance
All other attributes and methods (including the [`Component.View`](../../reference/api.md#django_components.ComponentView) class and its methods) follow standard Python inheritance rules.

View file

@ -96,7 +96,7 @@ class Button(Component):
class Kwargs(NamedTuple):
surname: str
age: int
maybe_var: Optional[int] = None # May be ommited
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
# Use `SlotInput` to allow slots to be given as `Slot` instance,
@ -128,7 +128,7 @@ Button.render(
age=30,
),
slots=Button.Slots(
footer=Button.Slot(lambda ctx, slot_data, slot_ref: slot_data.value + 1),
footer=Slot(lambda *a, **kwa: "Click me!"),
),
)
```
@ -447,6 +447,75 @@ class Button(Component):
Slots = Empty
```
## Subclassing
Subclassing components with types is simple.
Since each type class is a separate class attribute, you can just override them in the Component subclass.
In the example below, `ButtonExtra` inherits `Kwargs` from `Button`, but overrides the `Args` class.
```py
from django_components import Component, Empty
class Button(Component):
class Args(NamedTuple):
size: int
class Kwargs(NamedTuple):
color: str
class ButtonExtra(Button):
class Args(NamedTuple):
name: str
size: int
# Stil works the same way!
ButtonExtra.render(
args=ButtonExtra.Args(name="John", size=30),
kwargs=ButtonExtra.Kwargs(color="red"),
)
```
The only difference is when it comes to type hints to the data methods like
[`get_template_data()`](../../../reference/api#django_components.Component.get_template_data).
When you define the nested classes like `Args` and `Kwargs` directly on the class, you
can reference them just by their class name (`Args` and `Kwargs`).
But when you have a Component subclass, and it uses `Args` or `Kwargs` from the parent,
you will have to reference the type as a [forward reference](https://peps.python.org/pep-0563/#forward-references), including the full name of the component
(`Button.Args` and `Button.Kwargs`).
Compare the following:
```py
class Button(Component):
class Args(NamedTuple):
size: int
class Kwargs(NamedTuple):
color: str
# Both `Args` and `Kwargs` are defined on the class
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
pass
class ButtonExtra(Button):
class Args(NamedTuple):
name: str
size: int
# `Args` is defined on the subclass, `Kwargs` is defined on the parent
def get_template_data(self, args: Args, kwargs: "ButtonExtra.Kwargs", slots, context):
pass
class ButtonSame(Button):
# Both `Args` and `Kwargs` are defined on the parent
def get_template_data(self, args: "ButtonSame.Args", kwargs: "ButtonSame.Kwargs", slots, context):
pass
```
## Runtime type validation
When you add types to your component, and implement

View file

@ -11,7 +11,6 @@ from typing import (
Dict,
Generator,
List,
Literal,
Mapping,
NamedTuple,
Optional,
@ -582,6 +581,8 @@ class Component(metaclass=ComponentMeta):
This method has access to the [Render API](../../concepts/fundamentals/render_api).
Read more about [Template variables](../../concepts/fundamentals/html_js_css_variables).
**Example:**
```py
@ -601,6 +602,8 @@ class Component(metaclass=ComponentMeta):
`get_context_data()` and [`get_template_data()`](../api#django_components.Component.get_template_data)
are mutually exclusive.
If both methods return non-empty dictionaries, an error will be raised.
"""
return None
@ -610,6 +613,8 @@ class Component(metaclass=ComponentMeta):
This method has access to the [Render API](../../concepts/fundamentals/render_api).
Read more about [Template variables](../../concepts/fundamentals/html_js_css_variables).
**Example:**
```py
@ -730,6 +735,8 @@ class Component(metaclass=ComponentMeta):
`get_template_data()` and [`get_context_data()`](../api#django_components.Component.get_context_data)
are mutually exclusive.
If both methods return non-empty dictionaries, an error will be raised.
"""
return None
@ -873,6 +880,8 @@ class Component(metaclass=ComponentMeta):
The data returned from this method will be serialized to JSON.
Read more about [JavaScript variables](../../concepts/fundamentals/html_js_css_variables).
**Example:**
```py
@ -1143,6 +1152,8 @@ class Component(metaclass=ComponentMeta):
The data returned from this method will be serialized to string.
Read more about [CSS variables](../../concepts/fundamentals/html_js_css_variables).
**Example:**
```py
@ -1150,7 +1161,6 @@ class Component(metaclass=ComponentMeta):
def get_css_data(self, args, kwargs, slots, context):
return {
"color": kwargs["color"],
"id": self.id,
}
css = '''
@ -1226,7 +1236,6 @@ class Component(metaclass=ComponentMeta):
return {
"color": args.color,
"size": kwargs.size,
"id": self.id,
}
```
@ -1405,7 +1414,7 @@ class Component(metaclass=ComponentMeta):
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
- You can set [`extend`](../api#django_components.ComponentMediaInput.extend) to configure
whether to inherit JS / CSS from parent components. See
[Controlling Media Inheritance](../../concepts/fundamentals/defining_js_css_html_files/#controlling-media-inheritance).
[Media inheritance](../../concepts/fundamentals/secondary_js_css_files/#media-inheritance).
However, there's a few differences from Django's Media class:
@ -1435,7 +1444,31 @@ class Component(metaclass=ComponentMeta):
""" # noqa: E501
response_class = HttpResponse
"""This allows to configure what class is used to generate response from `render_to_response`"""
"""
This attribute configures what class is used to generate response from
[`Component.render_to_response()`](../api/#django_components.Component.render_to_response).
The response class should accept a string as the first argument.
Defaults to
[`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#httpresponse-objects).
**Example:**
```py
from django.http import HttpResponse
from django_components import Component
class MyHttpResponse(HttpResponse):
...
class MyComponent(Component):
response_class = MyHttpResponse
response = MyComponent.render_to_response()
assert isinstance(response, MyHttpResponse)
```
"""
# #####################################
# PUBLIC API - HOOKS (Configurable by users)
@ -1580,8 +1613,6 @@ class Component(metaclass=ComponentMeta):
self.outer_context: Optional[Context] = outer_context
self.registry = registry or registry_
self._metadata_stack: Deque[MetadataItem] = deque()
# None == uninitialized, False == No types, Tuple == types
self._types: Optional[Union[Tuple[Any, Any, Any, Any, Any, Any], Literal[False]]] = None
extensions._init_component_instance(self)
@ -1769,13 +1800,18 @@ class Component(metaclass=ComponentMeta):
return this data from
[`get_context_data()`](../api#django_components.Component.get_context_data).
In regular Django templates, you need to use `RequestContext` to apply context processors.
In regular Django templates, you need to use
[`RequestContext`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext)
to apply context processors.
In Components, the context processors are applied to components either when:
- The component is rendered with `RequestContext` (Regular Django behavior)
- The component is rendered with a regular `Context` (or none), but the `request` kwarg
of [`Component.render()`](../api#django_components.Component.render) is set.
- The component is rendered with
[`RequestContext`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext)
(Regular Django behavior)
- The component is rendered with a regular
[`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context) (or none),
but the `request` kwarg of [`Component.render()`](../api#django_components.Component.render) is set.
- The component is nested in another component that matches any of these conditions.
See
@ -1786,7 +1822,7 @@ class Component(metaclass=ComponentMeta):
Raises `RuntimeError` if accessed outside of rendering execution.
NOTE: This object is generated dynamically, so changes to it are not persisted.
NOTE: This dictionary is generated dynamically, so any changes to it will not be persisted.
**Example:**
@ -1873,25 +1909,26 @@ class Component(metaclass=ComponentMeta):
def inject(self, key: str, default: Optional[Any] = None) -> Any:
"""
Use this method to retrieve the data that was passed to a `{% provide %}` tag
Use this method to retrieve the data that was passed to a [`{% provide %}`](../template_tags#provide) tag
with the corresponding key.
To retrieve the data, `inject()` must be called inside a component that's
inside the `{% provide %}` tag.
inside the [`{% provide %}`](../template_tags#provide) tag.
You may also pass a default that will be used if the `provide` tag with given
key was NOT found.
You may also pass a default that will be used if the [`{% provide %}`](../template_tags#provide) tag
with given key was NOT found.
This method mut be used inside the `get_context_data()` method and raises
an error if called elsewhere.
This method is part of the [Render API](../../concepts/fundamentals/render_api), and
raises an error if called from outside the rendering execution.
Example:
Read more about [Provide / Inject](../../concepts/advanced/provide_inject).
**Example:**
Given this template:
```django
{% provide "provider" hello="world" %}
{% component "my_comp" %}
{% endcomponent %}
{% provide "my_provide" message="hello" %}
{% component "my_comp" / %}
{% endprovide %}
```
@ -1901,18 +1938,20 @@ class Component(metaclass=ComponentMeta):
@register("my_comp")
class MyComp(Component):
template = "hi {{ data.hello }}!"
def get_context_data(self):
data = self.inject("provider")
return {"data": data}
template = "hi {{ message }}!"
def get_template_data(self, args, kwargs, slots, context):
data = self.inject("my_provide")
message = data.message
return {"message": message}
```
This renders into:
```
hi world!
hi hello!
```
As the `{{ data.hello }}` is taken from the "provider".
As the `{{ message }}` is taken from the "my_provide" provider.
"""
if not len(self._metadata_stack):
raise RuntimeError(
@ -1960,61 +1999,60 @@ class Component(metaclass=ComponentMeta):
slots: Optional[Any] = None,
escape_slots_content: bool = True,
type: RenderType = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
*response_args: Any,
**response_kwargs: Any,
) -> HttpResponse:
"""
Render the component and wrap the content in the response class.
Render the component and wrap the content in an HTTP response class.
The response class is taken from `Component.response_class`. Defaults to `django.http.HttpResponse`.
`render_to_response()` takes the same inputs as
[`Component.render()`](../api/#django_components.Component.render).
See that method for more information.
This is the interface for the `django.views.View` class which allows us to
use components as Django views with `component.as_view()`.
After the component is rendered, the HTTP response class is instantiated with the rendered content.
Inputs:
Any additional kwargs are passed to the response class.
- `args` - Positional args for the component. This is the same as calling the component
as `{% component "my_comp" arg1 arg2 ... %}`
- `kwargs` - Kwargs for the component. This is the same as calling the component
as `{% component "my_comp" key1=val1 key2=val2 ... %}`
- `slots` - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
or render function.
- `escape_slots_content` - Whether the content from `slots` should be escaped.
- `context` - A context (dictionary or Django's Context) within which the component
is rendered. The keys on the context can be accessed from within the template.
- NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
component's args and kwargs.
- `type` - Configure how to handle JS and CSS dependencies.
- `"document"` (default) - JS dependencies are inserted into `{% component_js_dependencies %}`,
or to the end of the `<body>` tag. CSS dependencies are inserted into
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
- `"fragment"` - `{% component_js_dependencies %}` and `{% component_css_dependencies %}`,
are ignored, and a script that loads the JS and CSS dependencies is appended to the HTML.
- `request` - The request object. This is only required when needing to use RequestContext,
e.g. to enable template `context_processors`.
**Example:**
Any additional args and kwargs are passed to the `response_class`.
Example:
```py
MyComponent.render_to_response(
args=[1, "two", {}],
```python
Button.render_to_response(
args=["John"],
kwargs={
"key": 123,
"surname": "Doe",
"age": 30,
},
slots={
"header": 'STATIC TEXT HERE',
"footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
"footer": "i AM A SLOT",
},
escape_slots_content=False,
# HttpResponse input
# HttpResponse kwargs
status=201,
headers={...},
)
# HttpResponse(content=..., status=201, headers=...)
```
**Custom response class:**
You can set a custom response class on the component via
[`Component.response_class`](../api/#django_components.Component.response_class).
Defaults to
[`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#httpresponse-objects).
```python
from django.http import HttpResponse
from django_components import Component
class MyHttpResponse(HttpResponse):
...
class MyComponent(Component):
response_class = MyHttpResponse
response = MyComponent.render_to_response()
assert isinstance(response, MyHttpResponse)
```
"""
content = cls.render(
args=args,
@ -2023,10 +2061,10 @@ class Component(metaclass=ComponentMeta):
slots=slots,
escape_slots_content=escape_slots_content,
type=type,
render_dependencies=True,
render_dependencies=render_dependencies,
request=request,
)
return cls.response_class(content, *response_args, **response_kwargs)
return cls.response_class(content, **response_kwargs)
@classmethod
def render(
@ -2041,46 +2079,212 @@ class Component(metaclass=ComponentMeta):
request: Optional[HttpRequest] = None,
) -> str:
"""
Render the component into a string.
Render the component into a string. This is the equivalent of calling
the [`{% component %}`](../template_tags#component) tag.
Inputs:
- `args` - Positional args for the component. This is the same as calling the component
as `{% component "my_comp" arg1 arg2 ... %}`
- `kwargs` - Kwargs for the component. This is the same as calling the component
as `{% component "my_comp" key1=val1 key2=val2 ... %}`
- `slots` - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
or render function.
- `escape_slots_content` - Whether the content from `slots` should be escaped.
- `context` - A context (dictionary or Django's Context) within which the component
is rendered. The keys on the context can be accessed from within the template.
- NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
component's args and kwargs.
- `type` - Configure how to handle JS and CSS dependencies.
- `"document"` (default) - JS dependencies are inserted into `{% component_js_dependencies %}`,
or to the end of the `<body>` tag. CSS dependencies are inserted into
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
- `"fragment"` - `{% component_js_dependencies %}` and `{% component_css_dependencies %}`,
are ignored, and a script that loads the JS and CSS dependencies is appended to the HTML.
- `render_dependencies` - Set this to `False` if you want to insert the resulting HTML into another component.
- `request` - The request object. This is only required when needing to use RequestContext,
e.g. to enable template `context_processors`.
Example:
```py
MyComponent.render(
args=[1, "two", {}],
```python
Button.render(
args=["John"],
kwargs={
"key": 123,
"surname": "Doe",
"age": 30,
},
slots={
"header": 'STATIC TEXT HERE',
"footer": lambda ctx, slot_kwargs, slot_ref: f'CTX: {ctx['hello']} SLOT_DATA: {slot_kwargs['abc']}',
"footer": "i AM A SLOT",
},
escape_slots_content=False,
)
```
"""
**Inputs:**
- `args` - Optional. A list of positional args for the component. This is the same as calling the component
as:
```django
{% component "button" arg1 arg2 ... %}
```
- `kwargs` - Optional. A dictionary of keyword arguments for the component. This is the same as calling
the component as:
```django
{% component "button" key1=val1 key2=val2 ... %}
```
- `slots` - Optional. A dictionary of slot fills. This is the same as passing [`{% fill %}`](../template_tags#fill)
tags to the component.
```django
{% component "button" %}
{% fill "content" %}
Click me!
{% endfill %}
{% endcomponent %}
```
Dictionary keys are the slot names. Dictionary values are the slot fills.
Slot fills can be strings, render functions, or [`Slot`](../api/#django_components.Slot) instances:
```python
Button.render(
slots={
"content": "Click me!"
"content2": lambda *a, **kwa: "Click me!",
"content3": Slot(lambda *a, **kwa: "Click me!"),
},
)
```
- `context` - Optional. Plain dictionary or Django's
[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context).
The context within which the component is rendered.
When a component is rendered within a template with the [`{% component %}`](../template_tags#component)
tag, this will be set to the
[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
instance that is used for rendering the template.
When you call `Component.render()` directly from Python, you can ignore this input most of the time.
Instead use `args`, `kwargs`, and `slots` to pass data to the component.
You can pass
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
to the `context` argument, so that the component will gain access to the request object and will use
[context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
Read more on [Working with HTTP requests](../../concepts/fundamentals/http_request).
```py
Button.render(
context=RequestContext(request),
)
```
For advanced use cases, you can use `context` argument to "pre-render" the component in Python, and then
pass the rendered output as plain string to the template. With this, the inner component is rendered as if
it was within the template with [`{% component %}`](../template_tags#component).
```py
class Button(Component):
def render(self, context, template):
# Pass `context` to Icon component so it is rendered
# as if nested within Button.
icon = Icon.render(
context=context,
args=["icon-name"],
render_dependencies=False,
)
# Update context with icon
with context.update({"icon": icon}):
return template.render(context)
```
Whether the variables defined in `context` are available to the template depends on the
[context behavior mode](../settings#django_components.app_settings.ComponentsSettings.context_behavior):
- In `"django"` context behavior mode, the template will have access to the keys of this context.
- In `"isolated"` context behavior mode, the template will NOT have access to this context,
and data MUST be passed via component's args and kwargs.
- `type` - Optional. Configure how to handle JS and CSS dependencies. Read more about
[Render types](../../concepts/fundamentals/rendering_components#render-types).
Options:
- `"document"` (default) - Use this if you are rendering a whole page, or if no other option suits better.
If it is possible to insert JS and/or CSS into the rendered HTML, then:
- JS and CSS from [`Component.js`](../api/#django_components.Component.js)
and [`Component.css`](../api/#django_components.Component.css) are inlined into the rendered HTML.
- JS and CSS from [`Component.Media`](../api/#django_components.Component.Media) are inserted
into the rendered HTML only as links.
- Extra JS script to manage component dependencies is inserted into the HTML.
- `"fragment"` - Use this if you plan to insert this HTML into a page that was rendered as `"document"`.
- No JS / CSS is inserted. Instead, a JSON `<script>` is inserted. This JSON
tells the dependency manager to load the component's JS and CSS dependencies.
- No extra scripts are inserted.
- `"inline"` - Use this for non-browser use cases like emails, or when you don't want to use
django-component's dependency manager.
This is the same as `"document"`, except no extra scripts are inserted:
- JS and CSS from [`Component.js`](../api/#django_components.Component.js)
and [`Component.css`](../api/#django_components.Component.css) are inlined into the rendered HTML.
- JS and CSS from [`Component.Media`](../api/#django_components.Component.Media) are inserted
into the rendered HTML only as links.
- No extra scripts are inserted.
- `request` - Optional. HTTPRequest object. Pass a request object directly to the component to apply
[context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context.update).
Read more about [Working with HTTP requests](../../concepts/fundamentals/http_request).
- `escape_slots_content` - Optional. Whether the content from `slots` should be escaped with Django's
[`escape`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#std-templatefilter-escape).
Defaults to `True`.
- `render_dependencies` - Optional. Whether the output should be processed to finalize JS and CSS dependencies.
Defaults to `True`.
Set this to `False` if you want to insert the resulting HTML into another component:
```py
html = Button.render(
render_dependencies=False,
)
# Insert the resulting HTML into another component
MyOtherComponent.render(
content=html,
)
```
**Type hints:**
`Component.render()` is NOT typed. To add type hints, you can wrap the inputs
in component's [`Args`](../api/#django_components.Component.Args),
[`Kwargs`](../api/#django_components.Component.Kwargs),
and [`Slots`](../api/#django_components.Component.Slots) classes.
Read more on [Typing and validation](../../concepts/advanced/typing_and_validation).
```python
from typing import NamedTuple, Optional
from django_components import Component, Slot, SlotInput
# Define the component with the types
class Button(Component):
class Args(NamedTuple):
name: str
class Kwargs(NamedTuple):
surname: str
age: int
class Slots(NamedTuple):
my_slot: Optional[SlotInput] = None
footer: SlotInput
# Add type hints to the render call
Button.render(
args=Button.Args(
name="John",
),
kwargs=Button.Kwargs(
surname="Doe",
age=30,
),
slots=Button.Slots(
footer=Slot(lambda *a, **kwa: "Click me!"),
),
)
```
""" # noqa: 501
# This method may be called as class method or as instance method.
# If called as class method, create a new instance.
if isinstance(cls, Component):
@ -2587,84 +2791,48 @@ class ComponentNode(BaseNode):
[`@register()`](./api.md#django_components.register)
decorator.
**Args:**
The `{% component %}` tag takes:
- `name` (str, required): Registered name of the component to render
- All other args and kwargs are defined based on the component itself.
If you defined a component `"my_table"`
```python
from django_component import Component, register
@register("my_table")
class MyTable(Component):
template = \"\"\"
<table>
<thead>
{% for header in headers %}
<th>{{ header }}</th>
{% endfor %}
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
<tbody>
</table>
\"\"\"
def get_context_data(self, rows: List, headers: List):
return {
"rows": rows,
"headers": headers,
}
```
Then you can render this component by referring to `MyTable` via its
registered name `"my_table"`:
- Component's registered name as the first positional argument,
- Followed by any number of positional and keyword arguments.
```django
{% component "my_table" rows=rows headers=headers ... / %}
{% load component_tags %}
<div>
{% component "button" name="John" job="Developer" / %}
</div>
```
### Component input
The component name must be a string literal.
Positional and keyword arguments can be literals or template variables.
The component name must be a single- or double-quotes string and must
be either:
- The first positional argument after `component`:
```django
{% component "my_table" rows=rows headers=headers ... / %}
```
- Passed as kwarg `name`:
```django
{% component rows=rows headers=headers name="my_table" ... / %}
```
### Inserting into slots
### Inserting slot fills
If the component defined any [slots](../concepts/fundamentals/slots.md), you can
pass in the content to be placed inside those slots by inserting [`{% fill %}`](#fill) tags,
directly within the `{% component %}` tag:
"fill" these slots by placing the [`{% fill %}`](#fill) tags within the `{% component %}` tag:
```django
{% component "my_table" rows=rows headers=headers ... / %}
{% component "my_table" rows=rows headers=headers %}
{% fill "pagination" %}
< 1 | 2 | 3 >
{% endfill %}
{% endcomponent %}
```
You can even nest [`{% fill %}`](#fill) tags within
[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
and other tags:
```django
{% component "my_table" rows=rows headers=headers %}
{% if rows %}
{% fill "pagination" %}
< 1 | 2 | 3 >
{% endfill %}
{% endif %}
{% endcomponent %}
```
### Isolating components
By default, components behave similarly to Django's
@ -2677,6 +2845,36 @@ class ComponentNode(BaseNode):
```django
{% component "name" positional_arg keyword_arg=value ... only %}
```
Alternatively, you can set all components to be isolated by default, by setting
[`context_behavior`](../settings#django_components.app_settings.ComponentsSettings.context_behavior)
to `"isolated"` in your settings:
```python
# settings.py
COMPONENTS = {
"context_behavior": "isolated",
}
```
### Omitting the `component` keyword
If you would like to omit the `component` keyword, and simply refer to your
components by their registered names:
```django
{% button name="John" job="Developer" / %}
```
You can do so by setting the "shorthand" [Tag formatter](../../concepts/advanced/tag_formatters)
in the settings:
```python
# settings.py
COMPONENTS = {
"tag_formatter": "django_components.component_shorthand_formatter",
}
```
"""
tag = "component"

View file

@ -197,7 +197,7 @@ class ComponentMediaInput(Protocol):
- If `False`, the component does not inherit the media files from the parent component.
- If a list of components classes, the component inherits the media files ONLY from these specified components.
Read more in [Controlling Media Inheritance](../concepts/fundamentals/defining_js_css_html_files.md#controlling-media-inheritance) section.
Read more in [Media inheritance](../../concepts/fundamentals/secondary_js_css_files/#media-inheritance) section.
**Example:**

View file

@ -65,7 +65,7 @@ class DynamicComponent(Component):
)
```
# Use cases
## Use cases
Dynamic components are suitable if you are writing something like a form component. You may design
it such that users give you a list of input types, and you render components depending on the input types.
@ -73,7 +73,7 @@ class DynamicComponent(Component):
While you could handle this with a series of if / else statements, that's not an extensible approach.
Instead, you can use the dynamic component in place of normal components.
# Component name
## Component name
By default, the dynamic component is registered under the name `"dynamic"`. In case of a conflict,
you can set the

View file

@ -237,27 +237,11 @@ class ComponentFormatter(TagFormatterABC):
if not args:
raise TemplateSyntaxError(f"{self.__class__.__name__}: Component tag did not receive tag name")
# If the first arg is a kwarg, not a positional arg, then look for the "name" kwarg
# for component name.
# If the first arg is a kwarg, then clearly the component name is not set.
if "=" in args[0]:
comp_name = None
final_args = []
for kwarg in args:
if not kwarg.startswith("name="):
final_args.append(kwarg)
continue
if comp_name:
raise TemplateSyntaxError(
f"ComponentFormatter: 'name' kwarg for component '{comp_name}'" " was defined more than once."
)
# NOTE: We intentionally do NOT add to `final_args` here
# because we want to remove the the `name=` kwarg from args list
comp_name = kwarg[5:]
else:
comp_name = args.pop(0)
final_args = args
if not comp_name:
raise TemplateSyntaxError("Component name must be a non-empty quoted string, e.g. 'my_comp'")
@ -268,7 +252,7 @@ class ComponentFormatter(TagFormatterABC):
# Remove the quotes
comp_name = comp_name[1:-1]
return TagResult(comp_name, final_args)
return TagResult(comp_name, args)
class ShorthandComponentFormatter(TagFormatterABC):

View file

@ -859,13 +859,13 @@ class TestMediaRelativePath:
{% load component_tags %}
<div>
<h1>Parent content</h1>
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}
{% component "variable_display" shadowing_variable='override' new_variable='unique_val' %}
{% endcomponent %}
</div>
<div>
{% slot 'content' %}
<h2>Slot content</h2>
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
{% component "variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
{% endcomponent %}
{% endslot %}
</div>
@ -922,7 +922,7 @@ class TestMediaRelativePath:
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
{% component name='relative_file_component' variable=variable / %}
{% component 'relative_file_component' variable=variable / %}
"""
template = Template(template_str)
rendered = render_dependencies(template.render(Context({"variable": "test"})))
@ -969,7 +969,7 @@ class TestMediaRelativePath:
{% component_css_dependencies %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component name='relative_file_component' variable='hello' %}
{% component 'relative_file_component' variable='hello' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}

View file

@ -613,3 +613,28 @@ class TestComponentTyping:
Button.render(
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"),
)
def test_subclass_overrides_parent_type(self):
class Button(Component):
template = "Hello"
class Args(NamedTuple):
size: int
class Kwargs(NamedTuple):
color: str
class ButtonExtra(Button):
class Args(NamedTuple):
name: str
size: int
def get_template_data(self, args: Args, kwargs: "ButtonExtra.Kwargs", slots, context):
assert isinstance(args, ButtonExtra.Args)
assert isinstance(kwargs, ButtonExtra.Kwargs)
assert ButtonExtra.Kwargs is Button.Kwargs
ButtonExtra.render(
args=ButtonExtra.Args(name="John", size=30),
kwargs=ButtonExtra.Kwargs(color="red"),
)

View file

@ -76,13 +76,13 @@ class TestContext:
{% load component_tags %}
<div>
<h1>Parent content</h1>
{% component name="variable_display" shadowing_variable='override' new_variable='unique_val' %}
{% component "variable_display" shadowing_variable='override' new_variable='unique_val' %}
{% endcomponent %}
</div>
<div>
{% slot 'content' %}
<h2>Slot content</h2>
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
{% component "variable_display" shadowing_variable='slot_default_override' new_variable='slot_default_unique' %}
{% endcomponent %}
{% endslot %}
</div>
@ -118,7 +118,7 @@ class TestContext:
template_str: types.django_html = """
{% load component_tags %}
{% component name='parent_component' %}{% endcomponent %}
{% component 'parent_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context())
@ -138,7 +138,7 @@ class TestContext:
{% load component_tags %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
@ -159,7 +159,7 @@ class TestContext:
{% load component_tags %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
@ -179,7 +179,7 @@ class TestContext:
template_str: types.django_html = """
{% load component_tags %}
{% component name='parent_component' %}{% endcomponent %}
{% component 'parent_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
@ -199,7 +199,7 @@ class TestContext:
{% load component_tags %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component name='variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
@ -219,13 +219,13 @@ class TestParentArgs:
{% load component_tags %}
<div>
<h1>Parent content</h1>
{% component name="variable_display" shadowing_variable=inner_parent_value new_variable='unique_val' %}
{% component "variable_display" shadowing_variable=inner_parent_value new_variable='unique_val' %}
{% endcomponent %}
</div>
<div>
{% slot 'content' %}
<h2>Slot content</h2>
{% component name="variable_display" shadowing_variable='slot_default_override' new_variable=inner_parent_value %}
{% component "variable_display" shadowing_variable='slot_default_override' new_variable=inner_parent_value %}
{% endcomponent %}
{% endslot %}
</div>
@ -300,7 +300,7 @@ class TestParentArgs:
{% load component_tags %}
{% component 'parent_with_args' parent_value='passed_in' %}
{% fill 'content' %}
{% component name='variable_display' shadowing_variable='value_from_slot' new_variable=inner_parent_value %}
{% component 'variable_display' shadowing_variable='value_from_slot' new_variable=inner_parent_value %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
@ -331,7 +331,7 @@ class TestContextCalledOnce:
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component name='incrementer' %}{% endcomponent %}
{% component 'incrementer' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context()).strip().replace("\n", "")
@ -345,7 +345,7 @@ class TestContextCalledOnce:
registry.register(name="incrementer", component=IncrementerComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component name='incrementer' value='2' %}{% endcomponent %}
{% component 'incrementer' value='2' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context()).strip()

View file

@ -57,7 +57,7 @@ class TestComponentTemplateTag:
simple_tag_template: types.django_html = """
{% load component_tags %}
{% component name="test" variable="variable" %}{% endcomponent %}
{% component "test" variable="variable" %}{% endcomponent %}
"""
template = Template(simple_tag_template)
@ -70,7 +70,7 @@ class TestComponentTemplateTag:
simple_tag_template: types.django_html = """
{% load component_tags %}
{% component name="test" variable="variable" /%}
{% component "test" variable="variable" /%}
"""
template = Template(simple_tag_template)
@ -83,7 +83,7 @@ class TestComponentTemplateTag:
simple_tag_template: types.django_html = """
{% load component_tags %}
{% component name="test" variable="variable" %}{% endcomponent %}
{% component "test" variable="variable" %}{% endcomponent %}
"""
template = Template(simple_tag_template)
@ -126,7 +126,7 @@ class TestComponentTemplateTag:
simple_tag_template: types.django_html = """
{% load component_tags %}
{% component name="test" variable="variable" variable2="hej" %}{% endcomponent %}
{% component "test" variable="variable" variable2="hej" %}{% endcomponent %}
"""
template = Template(simple_tag_template)