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

@ -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: