mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 14:28:18 +00:00
docs: docstrings, fundamentals, and minor changes (#1145)
* docs: docstrings, fundamentals, and minor changes * refactor: fix tests + linter errors
This commit is contained in:
parent
89db10a643
commit
59f82307ae
24 changed files with 2239 additions and 673 deletions
33
CHANGELOG.md
33
CHANGELOG.md
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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)
|
||||
```
|
355
docs/concepts/fundamentals/html_js_css_files.md
Normal file
355
docs/concepts/fundamentals/html_js_css_files.md
Normal 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.
|
364
docs/concepts/fundamentals/html_js_css_variables.md
Normal file
364
docs/concepts/fundamentals/html_js_css_variables.md
Normal 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
|
||||
```
|
|
@ -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"
|
||||
```
|
||||
|
|
649
docs/concepts/fundamentals/rendering_components.md
Normal file
649
docs/concepts/fundamentals/rendering_components.md
Normal 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 %}
|
||||
```
|
|
@ -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).
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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:**
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue