refactor: cleanup docs, add docs on Render API, allow get_context_data return None (#1110)

* refactor: cleanup docs, add docs on Render API, allow get_context_data return None

* refactor: fix linter and tests
This commit is contained in:
Juro Oravec 2025-04-09 15:06:14 +02:00 committed by GitHub
parent 9ede779fa3
commit 613dfea379
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 604 additions and 226 deletions

View file

@ -61,6 +61,8 @@
- `@djc_test` can now be called without first calling `django.setup()`, in which case it does it for you.
- Expose `ComponentInput` class, which is a typing for `Component.input`.
#### Deprecation
- Currently, view request handlers such as `get()` and `post()` methods can be defined
@ -83,6 +85,10 @@
In v1, these methods should be defined only on the `Component.View` class instead.
#### Refactor
- `Component.get_context_data()` can now omit a return statement or return `None`.
## 🚨📢 v0.136
#### 🚨📢 BREAKING CHANGES

210
README.md
View file

@ -133,9 +133,11 @@ class Calendar(Component):
### Composition with slots
- Render components inside templates with `{% component %}` tag.
- Compose them with `{% slot %}` and `{% fill %}` tags.
- Vue-like slot system, including scoped slots.
- Render components inside templates with
[`{% component %}`](https://django-components.github.io/django-components/latest/reference/template_tags#component) tag.
- Compose them with [`{% slot %}`](https://django-components.github.io/django-components/latest/reference/template_tags#slot)
and [`{% fill %}`](https://django-components.github.io/django-components/latest/reference/template_tags#fill) tags.
- Vue-like slot system, including [scoped slots](https://django-components.github.io/django-components/latest/concepts/fundamentals/slots/#scoped-slots).
```django
{% component "Layout"
@ -169,14 +171,17 @@ class Calendar(Component):
### Extended template tags
`django-components` extends Django's template tags syntax with:
`django-components` is designed for flexibility, making working with templates a breeze.
- Literal lists and dictionaries in template tags
- Self-closing tags `{% mytag / %}`
- Multi-line template tags
- Spread operator `...` to dynamically pass args or kwargs into the template tag
- Nested template tags like `"{{ first_name }} {{ last_name }}"`
- Flat definition of dictionary keys `attr:key=val`
It extends Django's template tags syntax with:
<!-- TODO - Document literal lists and dictionaries -->
- Literal lists and dictionaries in the template
- [Self-closing tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#self-closing-tags) `{% mytag / %}`
- [Multi-line template tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#multiline-tags)
- [Spread operator](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#spread-operator) `...` to dynamically pass args or kwargs into the template tag
- [Template tags inside literal strings](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#template-tags-inside-literal-strings) like `"{{ first_name }} {{ last_name }}"`
- [Pass dictonaries by their key-value pairs](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#pass-dictonary-by-its-key-value-pairs) `attr:key=val`
```django
{% component "table"
@ -201,13 +206,70 @@ class Calendar(Component):
/ %}
```
You too can define template tags with these features by using
[`@template_tag()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.template_tag)
or [`BaseNode`](https://django-components.github.io/django-components/latest/reference/api/#django_components.BaseNode).
Read more on [Custom template tags](https://django-components.github.io/django-components/latest/concepts/advanced/template_tags/).
### Full programmatic access
When you render a component, you can access everything about the component:
- Component input: [args, kwargs, slots and context](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-inputs)
- Component's template, CSS and JS
- Django's [context processors](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#request-object-and-context-processors)
- Unique [render ID](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-id)
```python
class Table(Component):
js_file = "table.js"
css_file = "table.css"
template = """
<div class="table">
<span>{{ variable }}</span>
</div>
"""
def get_context_data(self, var1, var2, variable, another, **attrs):
# Access component's ID
assert self.id == "djc1A2b3c"
# Access component's inputs and slots
assert self.input.args == (123, "str")
assert self.input.kwargs == {"variable": "test", "another": 1}
footer_slot = self.input.slots["footer"]
some_var = self.input.context["some_var"]
# Access the request object and Django's context processors, if available
assert self.request.GET == {"query": "something"}
assert self.context_processors_data['user'].username == "admin"
return {
"variable": variable,
}
# Access component's HTML / JS / CSS
Table.template
Table.js
Table.css
# Render the component
rendered = Table.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"footer": "MY_FOOTER"},
)
```
### Granular HTML attributes
Use the [`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) template tag to render HTML attributes.
It supports:
- Defining attributes as dictionaries
- Defining attributes as keyword arguments
- Defining attributes as whole dictionaries or keyword arguments
- Merging attributes from multiple sources
- Boolean attributes
- Appending attributes
@ -224,13 +286,19 @@ It supports:
>
```
`{% html_attrs %}` offers a Vue-like granular control over `class` and `style` HTML attributes,
[`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) offers a Vue-like granular control for
[`class`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-class-attributes)
and [`style`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-style-attributes)
HTML attributes,
where you can use a dictionary to manage each class name or style property separately.
```django
{% html_attrs
class="foo bar"
class={"baz": True, "foo": False}
class={
"baz": True,
"foo": False,
}
class="extra"
%}
```
@ -238,7 +306,11 @@ where you can use a dictionary to manage each class name or style property separ
```django
{% html_attrs
style="text-align: center; background-color: blue;"
style={"background-color": "green", "color": None, "width": False}
style={
"background-color": "green",
"color": None,
"width": False,
}
style="position: absolute; height: 12px;"
%}
```
@ -249,11 +321,11 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
`django-components` makes integration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as HTML fragments:
- Components's JS and CSS is loaded automatically when the fragment is inserted into the DOM.
- Components's JS and CSS files are loaded automatically when the fragment is inserted into the DOM.
- Expose components as views with `get`, `post`, `put`, `patch`, `delete` methods
- Components can be [exposed as Django Views](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/) with `get()`, `post()`, `put()`, `patch()`, `delete()` methods
- Automatically create an endpoint for the component with `Component.Url.public`
- Automatically create an endpoint for a component with [`Component.Url.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
```py
# components/calendar/calendar.py
@ -261,13 +333,15 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
class Calendar(Component):
template_file = "calendar.html"
# Register Component with `urlpatterns`
class Url:
public = True
# Define handlers
class View:
def get(self, request, *args, **kwargs):
page = request.GET.get("page", 1)
return Calendar.render_to_response(
return self.component.render_to_response(
request=request,
kwargs={
"page": page,
@ -286,13 +360,46 @@ url = get_component_url(Calendar)
path("calendar/", Calendar.as_view())
```
### Type hints
### Provide / Inject
Opt-in to type hints by defining types for component's args, kwargs, slots, and more:
`django-components` supports the provide / inject pattern, similarly to React's [Context Providers](https://react.dev/learn/passing-data-deeply-with-context) or Vue's [provide / inject](https://vuejs.org/guide/components/provide-inject):
- Use the [`{% provide %}`](https://django-components.github.io/django-components/latest/reference/template_tags/#provide) tag to provide data to the component tree
- Use the [`Component.inject()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.inject) method to inject data into the component
Read more about [Provide / Inject](https://django-components.github.io/django-components/latest/concepts/advanced/provide_inject).
```django
<body>
{% provide "theme" variant="light" %}
{% component "header" / %}
{% endprovide %}
</body>
```
```djc_py
@register("header")
class Header(Component):
template = "..."
def get_context_data(self, *args, **kwargs):
theme = self.inject("theme").variant
return {
"theme": theme,
}
```
### Static type hints
Components API is fully typed, and supports [static type hints](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/).
To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
```py
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
from django_components import Component
ButtonArgs = Tuple[int, str]
class ButtonKwargs(TypedDict):
@ -318,7 +425,10 @@ class Button(ButtonType):
return {} # Error: Key "variable" is missing
```
When you then call `Button.render()` or `Button.render_to_response()`, you will get type hints:
When you then call
[`Button.render()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render) or
[`Button.render_to_response()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render_to_response),
you will get type hints:
```py
Button.render(
@ -333,12 +443,13 @@ Button.render(
### Extensions
Django-components functionality can be extended with "extensions". Extensions allow for powerful customization and integrations. They can:
Django-components functionality can be extended with [Extensions](https://django-components.github.io/django-components/latest/concepts/advanced/extensions/).
Extensions allow for powerful customization and integrations. They can:
- Tap into lifecycle events, such as when a component is created, deleted, or registered.
- Add new attributes and methods to the components under an extension-specific nested class.
- Add custom CLI commands.
- Add custom URLs.
- Tap into lifecycle events, such as when a component is created, deleted, or registered
- Add new attributes and methods to the components
- Add custom CLI commands
- Add custom URLs
Some of the extensions include:
@ -353,17 +464,35 @@ Some of the planned extensions include:
- Storybook integration
- Component-level benchmarking with asv
### Caching
- [Components can be cached](https://django-components.github.io/django-components/latest/concepts/advanced/component_caching/) using Django's cache framework.
- Caching rules can be configured on a per-component basis.
- Components are cached based on their input. Or you can write custom caching logic.
```py
from django_components import Component
class MyComponent(Component):
class Cache:
enabled = True
ttl = 60 * 60 * 24 # 1 day
def hash(self, *args, **kwargs):
return hash(f"{json.dumps(args)}:{json.dumps(kwargs)}")
```
### Simple testing
- Write tests for components with `@djc_test` decorator.
- Write tests for components with [`@djc_test`](https://django-components.github.io/django-components/latest/concepts/advanced/testing/) decorator.
- The decorator manages global state, ensuring that tests don't leak.
- If using `pytest`, the decorator allows you to parametrize Django or Components settings.
- The decorator also serves as a stand-in for Django's `@override_settings`.
- The decorator also serves as a stand-in for Django's [`@override_settings`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.override_settings).
```python
from djc_test import djc_test
from django_components.testing import djc_test
from components.my_component import MyTable
from components.my_table import MyTable
@djc_test
def test_my_table():
@ -375,21 +504,6 @@ def test_my_table():
assert rendered == "<table>My table</table>"
```
### Caching
- Components can be cached using Django's cache framework.
- Components are cached based on their input. Or you can write custom caching logic.
- Caching rules can be configured on a per-component basis.
```py
from django_components import Component
class MyComponent(Component):
class Cache:
enabled = True
ttl = 60 * 60 * 24 # 1 day
```
### Debugging features
- **Visual component inspection**: Highlight components and slots directly in your browser.
@ -413,10 +527,6 @@ class MyComponent(Component):
{% endcalendar %}
```
### Other features
- Vue-like provide / inject system
## Documentation
[Read the full documentation here](https://django-components.github.io/django-components/latest/).

View file

@ -6,6 +6,10 @@ Django-components functionality can be extended with "extensions". Extensions al
- Add new attributes and methods to the components under an extension-specific nested class.
- Define custom commands that can be executed via the Django management command interface.
## Live examples
- [djc-ext-pydantic](https://github.com/django-components/djc-ext-pydantic)
## Setting up extensions
Extensions are configured in the Django settings under [`COMPONENTS.extensions`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions).

View file

@ -1,11 +1,13 @@
_New in version 0.80_:
Django components supports the provide / inject or ContextProvider pattern with the combination of:
`django-components` supports the provide / inject pattern, similarly to React's [Context Providers](https://react.dev/learn/passing-data-deeply-with-context) or Vue's [provide / inject](https://vuejs.org/guide/components/provide-inject).
1. `{% provide %}` tag
1. `inject()` method of the `Component` class
This is achieved with the combination of:
## What is "prop drilling"?
- [`{% provide %}`](../../../reference/template_tags/#provide) tag
- [`Component.inject()`](../../../reference/api/#django_components.Component.inject) method
## What is "prop drilling"
Prop drilling refers to a scenario in UI development where you need to pass data through many layers of a component tree to reach the nested components that actually need the data.
@ -19,8 +21,6 @@ With provide / inject, a parent component acts like a data hub for all its desce
This feature is inspired by Vue's [Provide / Inject](https://vuejs.org/guide/components/provide-inject) and React's [Context / useContext](https://react.dev/learn/passing-data-deeply-with-context).
## How to use provide / inject
As the name suggest, using provide / inject consists of 2 steps
1. Providing data
@ -28,77 +28,86 @@ As the name suggest, using provide / inject consists of 2 steps
For examples of advanced uses of provide / inject, [see this discussion](https://github.com/django-components/django-components/pull/506#issuecomment-2132102584).
## Using `{% provide %}` tag
## Providing data
First we use the `{% provide %}` tag to define the data we want to "provide" (make available).
First we use the [`{% provide %}`](../../../reference/template_tags/#provide) tag to define the data we want to "provide" (make available).
```django
{% provide "my_data" key="hi" another=123 %}
{% provide "my_data" hello="hi" another=123 %}
{% component "child" / %} <--- Can access "my_data"
{% endprovide %}
{% component "child" / %} <--- Cannot access "my_data"
```
Notice that the `provide` tag REQUIRES a name as a first argument. This is the _key_ by which we can then access the data passed to this tag.
The first argument to the [`{% provide %}`](../../../reference/template_tags/#provide) tag is the _key_ by which we can later access the data passed to this tag. The key in this case is `"my_data"`.
`provide` tag name must resolve to a valid identifier (AKA a valid Python variable name).
The key must resolve to a valid identifier (AKA a valid Python variable name).
Once you've set the name, you define the data you want to "provide" by passing it as keyword arguments. This is similar to how you pass data to the `{% with %}` tag.
Next you define the data you want to "provide" by passing them as keyword arguments. This is similar to how you pass data to the [`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#with) tag or the [`{% slot %}`](../../../reference/template_tags/#slot) tag.
> NOTE: Kwargs passed to `{% provide %}` are NOT added to the context.
> In the example below, the `{{ key }}` won't render anything:
>
> ```django
> {% provide "my_data" key="hi" another=123 %}
> {{ key }}
> {% endprovide %}
> ```
!!! note
Similarly to [slots and fills](#dynamic-slots-and-fills), also provide's name argument can be set dynamically via a variable, a template expression, or a spread operator:
Kwargs passed to `{% provide %}` are NOT added to the context.
In the example below, the `{{ hello }}` won't render anything:
```django
{% provide "my_data" hello="hi" another=123 %}
{{ hello }}
{% endprovide %}
```
Similarly to [slots and fills](../../fundamentals/slots/#dynamic-slots-and-fills), also provide's name argument can be set dynamically via a variable, a template expression, or a spread operator:
```django
{% provide name=name ... %}
...
{% provide %}
</table>
{% with my_name="my_name" %}
{% provide name=my_name ... %}
...
{% endprovide %}
{% endwith %}
```
## Using `inject()` method
## Injecting data
To "inject" (access) the data defined on the `provide` tag, you can use the `inject()` method inside of `get_context_data()`.
To "inject" (access) the data defined on the [`{% provide %}`](../../../reference/template_tags/#provide) tag,
you can use the [`Component.inject()`](../../../reference/api/#django_components.Component.inject) method from within any other component methods.
For a component to be able to "inject" some data, the component (`{% component %}` tag) must be nested inside the `{% provide %}` tag.
For a component to be able to "inject" some data, the component ([`{% component %}`](../../../reference/template_tags/#component) tag) must be nested inside the [`{% provide %}`](../../../reference/template_tags/#provide) tag.
In the example from previous section, we've defined two kwargs: `key="hi" another=123`. That means that if we now inject `"my_data"`, we get an object with 2 attributes - `key` and `another`.
In the example from previous section, we've defined two kwargs: `hello="hi" another=123`. That means that if we now inject `"my_data"`, we get an object with 2 attributes - `hello` and `another`.
```py
class ChildComponent(Component):
def get_context_data(self):
my_data = self.inject("my_data")
print(my_data.key) # hi
print(my_data.another) # 123
print(my_data.hello) # hi
print(my_data.another) # 123
return {}
```
First argument to `inject` is the _key_ (or _name_) of the provided data. This
must match the string that you used in the `provide` tag. If no provider
with given key is found, `inject` raises a `KeyError`.
First argument to [`Component.inject()`](../../../reference/api/#django_components.Component.inject) is the _key_ (or _name_) of the provided data. This
must match the string that you used in the [`{% provide %}`](../../../reference/template_tags/#provide) tag.
To avoid the error, you can pass a second argument to `inject` to which will act as a default value, similar to `dict.get(key, default)`:
If no provider with given key is found, [`inject()`](../../../reference/api/#django_components.Component.inject) raises a `KeyError`.
To avoid the error, you can pass a second argument to [`inject()`](../../../reference/api/#django_components.Component.inject). This will act as a default value similar to `dict.get(key, default)`:
```py
class ChildComponent(Component):
def get_context_data(self):
my_data = self.inject("invalid_key", DEFAULT_DATA)
assert my_data == DEFAUKT_DATA
assert my_data == DEFAULT_DATA
return {}
```
The instance returned from `inject()` is a subclass of `NamedTuple`, so the instance is immutable. This ensures that the data returned from `inject` will always
have all the keys that were passed to the `provide` tag.
!!! note
> NOTE: `inject()` works strictly only in `get_context_data`. If you try to call it from elsewhere, it will raise an error.
The instance returned from [`inject()`](../../../reference/api/#django_components.Component.inject) is immutable (subclass of [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)). This ensures that the data returned from [`inject()`](../../../reference/api/#django_components.Component.inject) will always
have all the keys that were passed to the [`{% provide %}`](../../../reference/template_tags/#provide) tag.
!!! warning
[`inject()`](../../../reference/api/#django_components.Component.inject) works strictly only during render execution. If you try to call `inject()` from outside, it will raise an error.
## Full example
@ -106,7 +115,7 @@ have all the keys that were passed to the `provide` tag.
@register("child")
class ChildComponent(Component):
template = """
<div> {{ my_data.key }} </div>
<div> {{ my_data.hello }} </div>
<div> {{ my_data.another }} </div>
"""
@ -116,7 +125,7 @@ class ChildComponent(Component):
template_str = """
{% load component_tags %}
{% provide "my_data" key="hi" another=123 %}
{% provide "my_data" hello="hi" another=123 %}
{% component "child" / %}
{% endprovide %}
"""

View file

@ -2,7 +2,7 @@
nav:
- Single-file components: single_file_components.md
- Components in Python: components_in_python.md
- Accessing component inputs: access_component_input.md
- Render API: render_api.md
- Component defaults: component_defaults.md
- Component context and scope: component_context_scope.md
- Template tag syntax: template_tag_syntax.md

View file

@ -1,34 +0,0 @@
When you call `Component.render` or `Component.render_to_response`, the inputs to these methods can be accessed from within the instance under `self.input`.
This means that you can use `self.input` inside:
- `get_context_data`
- `get_template_name`
- `get_template`
- `on_render_before`
- `on_render_after`
`self.input` is only defined during the execution of `Component.render`, and raises a `RuntimeError` when called outside of this context.
`self.input` has the same fields as the input to `Component.render`:
```python
class TestComponent(Component):
def get_context_data(self, var1, var2, variable, another, **attrs):
assert self.input.args == (123, "str")
assert self.input.kwargs == {"variable": "test", "another": 1}
assert self.input.slots == {"my_slot": "MY_SLOT"}
assert isinstance(self.input.context, Context)
return {
"variable": variable,
}
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"my_slot": "MY_SLOT"},
)
```
NOTE: The slots in `self.input.slots` are normalized to slot functions.

View file

@ -1,6 +1,13 @@
Every component that you want to use in the template with the [`{% component %}`](django_components.templateags.component_tags)
tag needs to be registered with the [`ComponentRegistry`](django_components.component_registry.ComponentRegistry).
Normally, we use the [`@register`](django_components.component_registry.register) decorator for that:
django-components automatically register all components 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)
directories by loading all Python files in these 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:
```python
from django_components import Component, register
@ -12,6 +19,8 @@ class Calendar(Component):
But for the component to be registered, the code needs to be executed - and for that, the file needs to be imported as a module.
This is the "discovery" part of the process.
One way to do that is by importing all your components in `apps.py`:
```python
@ -30,23 +39,28 @@ class MyAppConfig(AppConfig):
However, there's a simpler way!
By default, the Python files in the [`COMPONENTS.dirs`](django_components.app_settings.ComponentsSettings.dirs) directories (and app-level [`[app]/components/`](django_components.app_settings.ComponentsSettings.app_dirs)) are auto-imported in order to auto-register the components.
### Autodiscovery
Autodiscovery occurs when Django is loaded, during the [`AppConfig.ready`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
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.
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.
If you are using autodiscovery, keep a few points in mind:
- Avoid defining any logic on the module-level inside the `components` dir, that you would not want to run anyway.
- Components inside the auto-imported files still need to be registered with [`@register`](django_components.component_registry.register)
- Avoid defining any logic on the module-level inside the components directories, that you would not want to run anyway.
- Components inside the auto-imported files still need to be registered with [`@register`](../../../reference/api#django_components.register)
- Auto-imported component files must be valid Python modules, they must use suffix `.py`, and module name should follow [PEP-8](https://peps.python.org/pep-0008/#package-and-module-names).
- Subdirectories and files starting with an underscore `_` (except `__init__.py`) are ignored.
Autodiscovery can be disabled in the [settings](django_components.app_settings.ComponentsSettings.autodiscovery).
Autodiscovery can be disabled in the settings with [`autodiscover=False`](../../../reference/settings#django_components.app_settings.ComponentsSettings.autodiscover).
### Manually trigger autodiscovery
Autodiscovery can be also triggered manually, using the [`autodiscover`](django_components.autodiscovery.autodiscover) function. This is useful if you want to run autodiscovery at a custom point of the lifecycle:
Autodiscovery can be also triggered manually, using the [`autodiscover()`](../../../reference/api#django_components.autodiscover) function. This is useful if you want to run autodiscovery at a custom point of the lifecycle:
```python
from django_components import autodiscover

View file

@ -17,9 +17,7 @@ django-components has a suite of features that help you write and manage views a
- In addition, [`Component`](../../../reference/api#django_components.Component) has a [`render_to_response()`](../../../reference/api#django_components.Component.render_to_response) method that renders the component template based on the provided input and returns an `HttpResponse` object.
## Component as view example
### Define handlers
## Define handlers
Here's an example of a calendar component defined as a view. Simply define a `View` class with your custom `get()` method to handle GET requests:
@ -83,7 +81,7 @@ class Calendar(Component):
This is deprecated from v0.137 onwards, and will be removed in v1.0.
### Register the URLs manually
## Register URLs manually
To register the component as a route / endpoint in Django, add an entry to your
[`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/).
@ -102,7 +100,7 @@ urlpatterns = [
internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
instance as one of the arguments.
### Register the URLs automatically
## Register URLs automatically
If you don't care about the exact URL of the component, you can let django-components manage the URLs for you by setting the [`Component.Url.public`](../../../reference/api#django_components.ComponentUrl.public) attribute to `True`:

View file

@ -329,7 +329,7 @@ Renders:
<div class="my-class extra-class"></div>
```
### Merging `style` Attributes
### Merging `style` attributes
The `style` attribute can be specified as a string of style properties as usual.
@ -364,8 +364,8 @@ If you want granular control over individual style properties, you can use a dic
If a style property is specified multiple times, the last value is used.
- If the last time the property is set is `False`, the property is removed.
- Properties set to `None` are ignored.
- If the last non-`None` instance of the property is set to `False`, the property is removed.
**Example:**

View file

@ -0,0 +1,121 @@
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.
We refer to these render-only methods and attributes as the "Render API".
Render API is available inside these [`Component`](../../../reference/api#django_components.Component) methods:
- [`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)
Example:
```python
class Table(Component):
def get_context_data(self, *args, **attrs):
# Access component's ID
assert self.id == "djc1A2b3c"
# Access component's inputs, slots and context
assert self.input.args == (123, "str")
assert self.input.kwargs == {"variable": "test", "another": 1}
footer_slot = self.input.slots["footer"]
some_var = self.input.context["some_var"]
# Access the request object and Django's context processors, if available
assert self.request.GET == {"query": "something"}
assert self.context_processors_data['user'].username == "admin"
return {
"variable": variable,
}
rendered = Table.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"footer": "MY_SLOT"},
)
```
## Accessing Render API
If you try to access the Render API outside of a render call, you will get a `RuntimeError`.
## Component ID
Component ID (or render ID) is a unique identifier for the current render call.
That means that if you call [`Component.render()`](../../../reference/api#django_components.Component.render)
multiple times, the ID will be different for each call.
It is available as [`self.id`](../../../reference/api#django_components.Component.id).
## Component inputs
All the component inputs are captured and available as [`self.input`](../../../reference/api/#django_components.Component.input).
[`self.input`](../../../reference/api/#django_components.Component.input) ([`ComponentInput`](../../../reference/api/#django_components.ComponentInput)) has the mostly the same fields as the input to [`Component.render()`](../../../reference/api/#django_components.Component.render). This includes:
- `args` - List of positional arguments
- `kwargs` - Dictionary of keyword arguments
- `slots` - Dictionary of slots. Values are normalized to [`Slot`](../../../reference/api/#django_components.Slot) instances
- `context` - [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) object that should be used to render the component
- And other kwargs passed to [`Component.render()`](../../../reference/api/#django_components.Component.render) like `type` and `render_dependencies`
Thus, use can use [`self.input.args`](../../../reference/api/#django_components.ComponentInput.args)
and [`self.input.kwargs`](../../../reference/api/#django_components.ComponentInput.kwargs)
to access the positional and keyword arguments passed to [`Component.render()`](../../../reference/api/#django_components.Component.render).
```python
def get_context_data(self, *args, **kwargs):
# Access component's inputs, slots and context
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
footer_slot = self.input.slots["footer"]
some_var = self.input.context["some_var"]
return {}
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"footer": "MY_SLOT"},
)
```
## Request object and context processors
If the component was either:
- Given a [`request`](../../../reference/api/#django_components.Component.render) kwarg
- Rendered with [`RenderContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
- Nested in another component for which any of these conditions is true
Then the request object will be available in [`self.request`](../../../reference/api/#django_components.Component.request).
If the request object is available, you will also be able to access the [`context processors`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#configuring-an-engine) data in [`self.context_processors_data`](../../../reference/api/#django_components.Component.context_processors_data).
This is a dictionary with the context processors data.
If the request object is not available, then [`self.context_processors_data`](../../../reference/api/#django_components.Component.context_processors_data) will be an empty dictionary.
```python
from django.http import HttpRequest
class Table(Component):
def get_context_data(self, *args, **attrs):
# Access the request object and Django's context processors
assert self.request.GET == {"query": "something"}
assert self.context_processors_data['user'].username == "admin"
return {}
rendered = Table.render(
request=HttpRequest(),
)
```
!!! warning
The [`self.context_processors_data`](../../../reference/api/#django_components.Component.context_processors_data) object is generated dynamically, so changes to it are not persisted.

View file

@ -85,7 +85,7 @@ Other than that, you can use spread operators multiple times, and even put keywo
In a case of conflicts, the values added later (right-most) overwrite previous values.
## Use template tags inside component inputs
## Template tags inside literal strings
_New in version 0.93_
@ -119,26 +119,22 @@ Instead, template tags in django_components (`{% component %}`, `{% slot %}`, `{
/ %}
```
In the example above:
In the example above, the component receives:
- Component `test` receives a positional argument with value `"As positional arg "`. The comment is omitted.
- Kwarg `title` is passed as a string, e.g. `John Doe`
- Kwarg `id` is passed as `int`, e.g. `15`
- Kwarg `readonly` is passed as `bool`, e.g. `False`
- Kwarg `author` is passed as a string, e.g. `John Wick ` (Comment omitted)
- Positional argument `"As positional arg "` (Comment omitted)
- `title` - passed as `str`, e.g. `John Doe`
- `id` - passed as `int`, e.g. `15`
- `readonly` - passed as `bool`, e.g. `False`
- `author` - passed as `str`, e.g. `John Wick ` (Comment omitted)
This is inspired by [django-cotton](https://github.com/wrabit/django-cotton#template-expressions-in-attributes).
### Passing data as string vs original values
Sometimes you may want to use the template tags to transform
or generate the data that is then passed to the component.
In the example above, the kwarg `id` was passed as an integer, NOT a string.
The data doesn't necessarily have to be strings. In the example above, the kwarg `id` was passed as an integer, NOT a string.
Although the string literals for components inputs are treated as regular Django templates, there is one special case:
When the string literal contains only a single template tag, with no extra text, then the value is passed as the original type instead of a string.
When the string literal contains only a single template tag, with no extra text (and no extra whitespace),
then the value is passed as the original type instead of a string.
Here, `page` is an integer:

View file

@ -123,9 +123,11 @@ class Calendar(Component):
### Composition with slots
- Render components inside templates with `{% component %}` tag.
- Compose them with `{% slot %}` and `{% fill %}` tags.
- Vue-like slot system, including scoped slots.
- Render components inside templates with
[`{% component %}`](https://django-components.github.io/django-components/latest/reference/template_tags#component) tag.
- Compose them with [`{% slot %}`](https://django-components.github.io/django-components/latest/reference/template_tags#slot)
and [`{% fill %}`](https://django-components.github.io/django-components/latest/reference/template_tags#fill) tags.
- Vue-like slot system, including [scoped slots](https://django-components.github.io/django-components/latest/concepts/fundamentals/slots/#scoped-slots).
```htmldjango
{% component "Layout"
@ -159,14 +161,17 @@ class Calendar(Component):
### Extended template tags
`django-components` extends Django's template tags syntax with:
`django-components` is designed for flexibility, making working with templates a breeze.
- Literal lists and dictionaries in template tags
- Self-closing tags `{% mytag / %}`
- Multi-line template tags
- Spread operator `...` to dynamically pass args or kwargs into the template tag
- Nested template tags like `"{{ first_name }} {{ last_name }}"`
- Flat definition of dictionary keys `attr:key=val`
It extends Django's template tags syntax with:
<!-- TODO - Document literal lists and dictionaries -->
- Literal lists and dictionaries in the template
- [Self-closing tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#self-closing-tags) `{% mytag / %}`
- [Multi-line template tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#multiline-tags)
- [Spread operator](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#spread-operator) `...` to dynamically pass args or kwargs into the template tag
- [Template tags inside literal strings](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#template-tags-inside-literal-strings) like `"{{ first_name }} {{ last_name }}"`
- [Pass dictonaries by their key-value pairs](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#pass-dictonary-by-its-key-value-pairs) `attr:key=val`
```htmldjango
{% component "table"
@ -191,13 +196,70 @@ class Calendar(Component):
/ %}
```
You too can define template tags with these features by using
[`@template_tag()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.template_tag)
or [`BaseNode`](https://django-components.github.io/django-components/latest/reference/api/#django_components.BaseNode).
Read more on [Custom template tags](https://django-components.github.io/django-components/latest/concepts/advanced/template_tags/).
### Full programmatic access
When you render a component, you can access everything about the component:
- Component input: [args, kwargs, slots and context](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-inputs)
- Component's template, CSS and JS
- Django's [context processors](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#request-object-and-context-processors)
- Unique [render ID](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-id)
```python
class Table(Component):
js_file = "table.js"
css_file = "table.css"
template = """
<div class="table">
<span>{{ variable }}</span>
</div>
"""
def get_context_data(self, var1, var2, variable, another, **attrs):
# Access component's ID
assert self.id == "djc1A2b3c"
# Access component's inputs and slots
assert self.input.args == (123, "str")
assert self.input.kwargs == {"variable": "test", "another": 1}
footer_slot = self.input.slots["footer"]
some_var = self.input.context["some_var"]
# Access the request object and Django's context processors, if available
assert self.request.GET == {"query": "something"}
assert self.context_processors_data['user'].username == "admin"
return {
"variable": variable,
}
# Access component's HTML / JS / CSS
Table.template
Table.js
Table.css
# Render the component
rendered = Table.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={"footer": "MY_FOOTER"},
)
```
### Granular HTML attributes
Use the [`{% html_attrs %}`](../../concepts/fundamentals/html_attributes/) template tag to render HTML attributes.
Use the [`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) template tag to render HTML attributes.
It supports:
- Defining attributes as dictionaries
- Defining attributes as keyword arguments
- Defining attributes as whole dictionaries or keyword arguments
- Merging attributes from multiple sources
- Boolean attributes
- Appending attributes
@ -214,13 +276,19 @@ It supports:
>
```
`{% html_attrs %}` offers a Vue-like granular control over `class` and `style` HTML attributes,
[`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) offers a Vue-like granular control for
[`class`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-class-attributes)
and [`style`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-style-attributes)
HTML attributes,
where you can use a dictionary to manage each class name or style property separately.
```django
{% html_attrs
class="foo bar"
class={"baz": True, "foo": False}
class={
"baz": True,
"foo": False,
}
class="extra"
%}
```
@ -228,22 +296,26 @@ where you can use a dictionary to manage each class name or style property separ
```django
{% html_attrs
style="text-align: center; background-color: blue;"
style={"background-color": "green", "color": None, "width": False}
style={
"background-color": "green",
"color": None,
"width": False,
}
style="position: absolute; height: 12px;"
%}
```
Read more about [HTML attributes](../../concepts/fundamentals/html_attributes/).
Read more about [HTML attributes](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/).
### HTML fragment support
`django-components` makes integration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as HTML fragments:
- Components's JS and CSS is loaded automatically when the fragment is inserted into the DOM.
- Components's JS and CSS files are loaded automatically when the fragment is inserted into the DOM.
- Expose components as views with `get`, `post`, `put`, `patch`, `delete` methods
- Components can be [exposed as Django Views](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/) with `get()`, `post()`, `put()`, `patch()`, `delete()` methods
- Automatically create an endpoint for the component with `Component.Url.public`
- Automatically create an endpoint for a component with [`Component.Url.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
```py
# components/calendar/calendar.py
@ -251,9 +323,11 @@ Read more about [HTML attributes](../../concepts/fundamentals/html_attributes/).
class Calendar(Component):
template_file = "calendar.html"
# Register Component with `urlpatterns`
class Url:
public = True
# Define handlers
class View:
def get(self, request, *args, **kwargs):
page = request.GET.get("page", 1)
@ -261,7 +335,7 @@ class Calendar(Component):
request=request,
kwargs={
"page": page,
}
},
)
def get_context_data(self, page):
@ -276,13 +350,46 @@ url = get_component_url(Calendar)
path("calendar/", Calendar.as_view())
```
### Type hints
### Provide / Inject
Opt-in to type hints by defining types for component's args, kwargs, slots, and more:
`django-components` supports the provide / inject pattern, similarly to React's [Context Providers](https://react.dev/learn/passing-data-deeply-with-context) or Vue's [provide / inject](https://vuejs.org/guide/components/provide-inject):
- Use the [`{% provide %}`](https://django-components.github.io/django-components/latest/reference/template_tags/#provide) tag to provide data to the component tree
- Use the [`Component.inject()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.inject) method to inject data into the component
Read more about [Provide / Inject](https://django-components.github.io/django-components/latest/concepts/advanced/provide_inject).
```django
<body>
{% provide "theme" variant="light" %}
{% component "header" / %}
{% endprovide %}
</body>
```
```djc_py
@register("header")
class Header(Component):
template = "..."
def get_context_data(self, *args, **kwargs):
theme = self.inject("theme").variant
return {
"theme": theme,
}
```
### Static type hints
Components API is fully typed, and supports [static type hints](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/).
To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
```py
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
from django_components import Component
ButtonArgs = Tuple[int, str]
class ButtonKwargs(TypedDict):
@ -308,7 +415,10 @@ class Button(ButtonType):
return {} # Error: Key "variable" is missing
```
When you then call `Button.render()` or `Button.render_to_response()`, you will get type hints:
When you then call
[`Button.render()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render) or
[`Button.render_to_response()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render_to_response),
you will get type hints:
```py
Button.render(
@ -323,12 +433,13 @@ Button.render(
### Extensions
Django-components functionality can be extended with "extensions". Extensions allow for powerful customization and integrations. They can:
Django-components functionality can be extended with [Extensions](https://django-components.github.io/django-components/latest/concepts/advanced/extensions/).
Extensions allow for powerful customization and integrations. They can:
- Tap into lifecycle events, such as when a component is created, deleted, or registered.
- Add new attributes and methods to the components under an extension-specific nested class.
- Add custom CLI commands.
- Add custom URLs.
- Tap into lifecycle events, such as when a component is created, deleted, or registered
- Add new attributes and methods to the components
- Add custom CLI commands
- Add custom URLs
Some of the extensions include:
@ -343,17 +454,35 @@ Some of the planned extensions include:
- Storybook integration
- Component-level benchmarking with asv
### Caching
- [Components can be cached](https://django-components.github.io/django-components/latest/concepts/advanced/component_caching/) using Django's cache framework.
- Caching rules can be configured on a per-component basis.
- Components are cached based on their input. Or you can write custom caching logic.
```py
from django_components import Component
class MyComponent(Component):
class Cache:
enabled = True
ttl = 60 * 60 * 24 # 1 day
def hash(self, *args, **kwargs):
return hash(f"{json.dumps(args)}:{json.dumps(kwargs)}")
```
### Simple testing
- Write tests for components with `@djc_test` decorator.
- Write tests for components with [`@djc_test`](https://django-components.github.io/django-components/latest/concepts/advanced/testing/) decorator.
- The decorator manages global state, ensuring that tests don't leak.
- If using `pytest`, the decorator allows you to parametrize Django or Components settings.
- The decorator also serves as a stand-in for Django's `@override_settings`.
- The decorator also serves as a stand-in for Django's [`@override_settings`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.override_settings).
```python
from djc_test import djc_test
from django_components.testing import djc_test
from components.my_component import MyTable
from components.my_table import MyTable
@djc_test
def test_my_table():
@ -365,21 +494,6 @@ def test_my_table():
assert rendered == "<table>My table</table>"
```
### Caching
- Components can be cached using Django's cache framework.
- Components are cached based on their input. Or you can write custom caching logic.
- Caching rules can be configured on a per-component basis.
```py
from django_components import Component
class MyComponent(Component):
class Cache:
enabled = True
ttl = 60 * 60 * 24 # 1 day
```
### Debugging features
- **Visual component inspection**: Highlight components and slots directly in your browser.
@ -403,10 +517,6 @@ class MyComponent(Component):
{% endcalendar %}
```
### Other features
- Vue-like provide / inject system
## Performance
Our aim is to be at least as fast as Django templates.

View file

@ -16,7 +16,13 @@ from django_components.util.command import (
CommandSubcommand,
ComponentCommand,
)
from django_components.component import Component, ComponentVars, all_components, get_component_by_class_id
from django_components.component import (
Component,
ComponentInput,
ComponentVars,
all_components,
get_component_by_class_id,
)
from django_components.component_media import ComponentMediaInput, ComponentMediaInputPath
from django_components.component_registry import (
AlreadyRegistered,
@ -84,6 +90,7 @@ __all__ = [
"ComponentExtension",
"ComponentFileEntry",
"ComponentFormatter",
"ComponentInput",
"ComponentMediaInput",
"ComponentMediaInputPath",
"ComponentRegistry",

View file

@ -86,7 +86,7 @@ from django_components.util.context import gen_context_processors_data, snapshot
from django_components.util.django_monkeypatch import is_template_cls_patched
from django_components.util.exception import component_error_message
from django_components.util.logger import trace_component_msg
from django_components.util.misc import gen_id, get_import_path, hash_comp_cls
from django_components.util.misc import default, gen_id, get_import_path, hash_comp_cls
from django_components.util.template_tag import TagAttr
from django_components.util.weakref import cached_ref
@ -176,7 +176,22 @@ def get_component_by_class_id(comp_cls_id: str) -> Type["Component"]:
@dataclass(frozen=True)
class RenderInput(Generic[ArgsType, KwargsType, SlotsType]):
class ComponentInput(Generic[ArgsType, KwargsType, SlotsType]):
"""
Object holding the inputs that were passed to [`Component.render()`](../api#django_components.Component.render)
or the [`{% component %}`](../template_tags#component) template tag.
This object is available only during render under [`Component.input`](../api#django_components.Component.input).
Read more about the [Render API](../../concepts/fundamentals/render_api).
This class can be typed as:
```py
input: ComponentInput[ArgsType, KwargsType, SlotsType]
```
"""
context: Context
args: ArgsType
kwargs: KwargsType
@ -188,7 +203,7 @@ class RenderInput(Generic[ArgsType, KwargsType, SlotsType]):
@dataclass()
class MetadataItem(Generic[ArgsType, KwargsType, SlotsType]):
render_id: str
input: RenderInput[ArgsType, KwargsType, SlotsType]
input: ComponentInput[ArgsType, KwargsType, SlotsType]
is_filled: Optional[SlotIsFilled]
request: Optional[HttpRequest]
@ -380,8 +395,8 @@ class Component(
"""
return None
def get_context_data(self, *args: Any, **kwargs: Any) -> DataType:
return cast(DataType, {})
def get_context_data(self, *args: Any, **kwargs: Any) -> Optional[DataType]:
return None
js: Optional[str] = None
"""
@ -706,12 +721,12 @@ class Component(
Raises `RuntimeError` if accessed outside of rendering execution.
A single render ID has a chance of collision 1 in 3.3M. However, due to birthday paradox, the chance of
collision increases when approaching ~1,000 render IDs.
A single render ID has a chance of collision 1 in 57 billion. However, due to birthday paradox,
the chance of collision increases to 1% when approaching ~33K render IDs.
**Thus, there is a soft-cap of 1,000 components rendered on a single page.**
Thus, there is currently a soft-cap of ~30K components rendered on a single page.
If you need to more than that, please open an issue on GitHub.
If you need to expand this limit, please open an issue on GitHub.
**Example:**
@ -734,7 +749,7 @@ class Component(
return ctx.render_id
@property
def input(self) -> RenderInput[ArgsType, KwargsType, SlotsType]:
def input(self) -> ComponentInput[ArgsType, KwargsType, SlotsType]:
"""
Input holds the data (like arg, kwargs, slots) that were passed to
the current execution of the `render` method.
@ -819,12 +834,14 @@ class Component(
return this data from
[`get_context_data()`](../api#django_components.Component.get_context_data).
Unlike regular Django templates, the context processors are applied to components either when:
In regular Django templates, you need to use `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 nested in another component that matches one of the two conditions above.
- The component is nested in another component that matches any of these conditions.
See
[`Component.request`](../api#django_components.Component.request)
@ -834,6 +851,8 @@ class Component(
Raises `RuntimeError` if accessed outside of rendering execution.
NOTE: This object is generated dynamically, so changes to it are not persisted.
**Example:**
```py
@ -1213,7 +1232,7 @@ class Component(
render_id = gen_id()
metadata = MetadataItem(
render_id=render_id,
input=RenderInput(
input=ComponentInput(
context=context,
slots=slots,
args=args,
@ -1300,10 +1319,16 @@ class Component(
# Allow to access component input and metadata like component ID from within these hook
with self._with_metadata(metadata):
context_processors_data = self.context_processors_data
context_data = self.get_context_data(*args, **kwargs)
# TODO - enable JS and CSS vars - EXPOSE AND DOCUMENT AND MAKE NON-NULL
js_data = self.get_js_data(*args, **kwargs) if hasattr(self, "get_js_data") else {} # type: ignore
css_data = self.get_css_data(*args, **kwargs) if hasattr(self, "get_css_data") else {} # type: ignore
context_data = default(self.get_context_data(*args, **kwargs), {})
# TODO - enable JS and CSS vars - Remove `hasattr()` checks, expose, and document
if hasattr(self, "get_js_data"):
js_data = default(self.get_js_data(*args, **kwargs), {}) # type: ignore
else:
js_data = {}
if hasattr(self, "get_css_data"):
css_data = default(self.get_css_data(*args, **kwargs), {}) # type: ignore
else:
css_data = {}
extensions.on_component_data(
OnComponentDataContext(

View file

@ -362,6 +362,18 @@ class TestComponent:
assert get_component_by_class_id(SimpleComponent.class_id) == SimpleComponent
def test_get_component_by_id_raises_on_missing_component(self):
with pytest.raises(KeyError):
get_component_by_class_id("nonexistent")
def test_get_context_data_returns_none(self):
class SimpleComponent(Component):
template = "Hello"
def get_context_data(self):
return None
assert SimpleComponent.render() == "Hello"
@djc_test
class TestComponentRender: