docs: Move docs-folder to root (#816)

* Move docs-folder form src to root

* Avoid mkdocs package / module name clash

* Update location of docs & add Windows compatibility

* Update requirements-docs

* Update generated file to current state
This commit is contained in:
David Linke 2024-12-03 12:32:21 +01:00 committed by GitHub
parent cdc830fca3
commit 594c0689ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 116 additions and 108 deletions

View file

@ -0,0 +1,255 @@
---
title: Authoring component libraries
weight: 7
---
You can publish and share your components for others to use. Here are the steps to do so:
## Writing component libraries
1. Create a Django project with a similar structure:
```txt
project/
|-- myapp/
|-- __init__.py
|-- apps.py
|-- templates/
|-- table/
|-- table.py
|-- table.js
|-- table.css
|-- table.html
|-- menu.py <--- single-file component
|-- templatetags/
|-- __init__.py
|-- mytags.py
```
2. Create custom [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
and [`ComponentRegistry`](django_components.component_registry.ComponentRegistry) instances in `mytags.py`
This will be the entrypoint for using the components inside Django templates.
Remember that Django requires the [`Library`](https://docs.djangoproject.com/en/5.1/howto/custom-template-tags/#how-to-create-custom-template-tags-and-filters)
instance to be accessible under the `register` variable ([See Django docs](https://docs.djangoproject.com/en/dev/howto/custom-template-tags)):
```py
from django.template import Library
from django_components import ComponentRegistry, RegistrySettings
register = library = django.template.Library()
comp_registry = ComponentRegistry(
library=library,
settings=RegistrySettings(
context_behavior="isolated",
tag_formatter="django_components.component_formatter",
),
)
```
As you can see above, this is also the place where we configure how our components should behave,
using the [`settings`](django_components.component_registry.ComponentRegistry.settings) argument.
If omitted, default settings are used.
For library authors, we recommend setting [`context_behavior`](django_components.app_settings.ComponentsSettings.context_behavior)
to [`"isolated"`](django_components.app_settings.ContextBehavior.ISOLATED), so that the state cannot leak into the components,
and so the components' behavior is configured solely through the inputs. This means that the components will be more predictable and easier to debug.
Next, you can decide how will others use your components by setting the
[`tag_formatter`](django_components.app_settings.ComponentsSettings.tag_formatter)
options.
If omitted or set to `"django_components.component_formatter"`,
your components will be used like this:
```django
{% component "table" items=items headers=headers %}
{% endcomponent %}
```
Or you can use `"django_components.component_shorthand_formatter"`
to use components like so:
```django
{% table items=items headers=headers %}
{% endtable %}
```
Or you can define a [custom TagFormatter](#tagformatter).
Either way, these settings will be scoped only to your components. So, in the user code,
there may be components side-by-side that use different formatters:
```django
{% load mytags %}
{# Component from your library "mytags", using the "shorthand" formatter #}
{% table items=items headers=header %}
{% endtable %}
{# User-created components using the default settings #}
{% component "my_comp" title="Abc..." %}
{% endcomponent %}
```
3. Write your components and register them with your instance of [`ComponentRegistry`](../../reference/api#ComponentRegistry)
There's one difference when you are writing components that are to be shared, and that's
that the components must be explicitly registered with your instance of
[`ComponentRegistry`](../../reference/api#ComponentRegistry) from the previous step.
For better user experience, you can also define the types for the args, kwargs, slots and data.
It's also a good idea to have a common prefix for your components, so they can be easily distinguished from users' components. In the example below, we use the prefix `my_` / `My`.
```py
from typing import Dict, NotRequired, Optional, Tuple, TypedDict
from django_components import Component, SlotFunc, register, types
from myapp.templatetags.mytags import comp_registry
# Define the types
class EmptyDict(TypedDict):
pass
type MyMenuArgs = Tuple[int, str]
class MyMenuSlots(TypedDict):
default: NotRequired[Optional[SlotFunc[EmptyDict]]]
class MyMenuProps(TypedDict):
vertical: NotRequired[bool]
klass: NotRequired[str]
style: NotRequired[str]
# Define the component
# NOTE: Don't forget to set the `registry`!
@register("my_menu", registry=comp_registry)
class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots, Any, Any, Any]):
def get_context_data(
self,
*args,
attrs: Optional[Dict] = None,
):
return {
"attrs": attrs,
}
template: types.django_html = """
{# Load django_components template tags #}
{% load component_tags %}
<div {% html_attrs attrs class="my-menu" %}>
<div class="my-menu__content">
{% slot "default" default / %}
</div>
</div>
"""
```
4. Import the components in `apps.py`
Normally, users rely on [autodiscovery](../../concepts/autodiscovery) and [`COMPONENTS.dirs`](../../reference/settings#dirs)
to load the component files.
Since you, as the library author, are not in control of the file system, it is recommended to load the components manually.
We recommend doing this in the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
hook of your `apps.py`:
```py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "myapp"
# This is the code that gets run when user adds myapp
# to Django's INSTALLED_APPS
def ready(self) -> None:
# Import the components that you want to make available
# inside the templates.
from myapp.templates import (
menu,
table,
)
```
Note that you can also include any other startup logic within
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready).
And that's it! The next step is to publish it.
## Publishing component libraries
Once you are ready to share your library, you need to build
a distribution and then publish it to PyPI.
django_components uses the [`build`](https://build.pypa.io/en/stable/) utility to build a distribution:
```bash
python -m build --sdist --wheel --outdir dist/ .
```
And to publish to PyPI, you can use [`twine`](https://docs.djangoproject.com/en/5.1/ref/applications/#django.apps.AppConfig.ready)
([See Python user guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-the-distribution-archives))
```bash
twine upload --repository pypi dist/* -u __token__ -p <PyPI_TOKEN>
```
Notes on publishing:
- If you use components where the HTML / CSS / JS files are separate, you may need to define
[`MANIFEST.in`](https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html)
to include those files with the distribution
([see user guide](https://setuptools.pypa.io/en/latest/userguide/miscellaneous.html)).
## Installing and using component libraries
After the package has been published, all that remains is to install it in other django projects:
1. Install the package:
```bash
pip install myapp django_components
```
2. Add the package to `INSTALLED_APPS`
```py
INSTALLED_APPS = [
...
"django_components",
"myapp",
]
```
3. Optionally add the template tags to the [`builtins`](https://docs.djangoproject.com/en/5.1/topics/templates/#django.template.backends.django.DjangoTemplates),
so you don't have to call `{% load mytags %}` in every template:
```python
TEMPLATES = [
{
...,
'OPTIONS': {
'context_processors': [
...
],
'builtins': [
'myapp.templatetags.mytags',
]
},
},
]
```
4. And, at last, you can use the components in your own project!
```django
{% my_menu title="Abc..." %}
Hello World!
{% endmy_menu %}
```

View file

@ -0,0 +1,151 @@
---
title: Registering components
weight: 4
---
In previous examples you could repeatedly see us using `@register()` to "register"
the components. In this section we dive deeper into what it actually means and how you can
manage (add or remove) components.
As a reminder, we may have a component like this:
```python
from django_components import Component, register
@register("calendar")
class Calendar(Component):
template_name = "template.html"
# This component takes one parameter, a date string to show in the template
def get_context_data(self, date):
return {
"date": date,
}
```
which we then render in the template as:
```django
{% component "calendar" date="1970-01-01" %}
{% endcomponent %}
```
As you can see, `@register` links up the component class
with the `{% component %}` template tag. So when the template tag comes across
a component called `"calendar"`, it can look up it's class and instantiate it.
## What is ComponentRegistry
The `@register` decorator is a shortcut for working with the `ComponentRegistry`.
`ComponentRegistry` manages which components can be used in the template tags.
Each `ComponentRegistry` instance is associated with an instance
of Django's `Library`. And Libraries are inserted into Django template
using the `{% load %}` tags.
The `@register` decorator accepts an optional kwarg `registry`, which specifies, the `ComponentRegistry` to register components into.
If omitted, the default `ComponentRegistry` instance defined in django_components is used.
```py
my_registry = ComponentRegistry()
@register(registry=my_registry)
class MyComponent(Component):
...
```
The default `ComponentRegistry` is associated with the `Library` that
you load when you call `{% load component_tags %}` inside your template, or when you
add `django_components.templatetags.component_tags` to the template builtins.
So when you register or unregister a component to/from a component registry,
then behind the scenes the registry automatically adds/removes the component's
template tags to/from the Library, so you can call the component from within the templates
such as `{% component "my_comp" %}`.
## Working with ComponentRegistry
The default `ComponentRegistry` instance can be imported as:
```py
from django_components import registry
```
You can use the registry to manually add/remove/get components:
```py
from django_components import registry
# Register components
registry.register("button", ButtonComponent)
registry.register("card", CardComponent)
# Get all or single
registry.all() # {"button": ButtonComponent, "card": CardComponent}
registry.get("card") # CardComponent
# Unregister single component
registry.unregister("card")
# Unregister all components
registry.clear()
```
## Registering components to custom ComponentRegistry
If you are writing a component library to be shared with others, you may want to manage your own instance of `ComponentRegistry`
and register components onto a different `Library` instance than the default one.
The `Library` instance can be set at instantiation of `ComponentRegistry`. If omitted,
then the default Library instance from django_components is used.
```py
from django.template import Library
from django_components import ComponentRegistry
my_library = Library(...)
my_registry = ComponentRegistry(library=my_library)
```
When you have defined your own `ComponentRegistry`, you can either register the components
with `my_registry.register()`, or pass the registry to the `@component.register()` decorator
via the `registry` kwarg:
```py
from path.to.my.registry import my_registry
@register("my_component", registry=my_registry)
class MyComponent(Component):
...
```
NOTE: The Library instance can be accessed under `library` attribute of `ComponentRegistry`.
## ComponentRegistry settings
When you are creating an instance of `ComponentRegistry`, you can define the components' behavior within the template.
The registry accepts these settings:
- `context_behavior`
- `tag_formatter`
```py
from django.template import Library
from django_components import ComponentRegistry, RegistrySettings
register = library = django.template.Library()
comp_registry = ComponentRegistry(
library=library,
settings=RegistrySettings(
context_behavior="isolated",
tag_formatter="django_components.component_formatter",
),
)
```
These settings are [the same as the ones you can set for django_components](#available-settings).
In fact, when you set `COMPONENT.tag_formatter` or `COMPONENT.context_behavior`, these are forwarded to the default `ComponentRegistry`.
This makes it possible to have multiple registries with different settings in one projects, and makes sharing of component libraries possible.

View file

@ -0,0 +1,84 @@
---
title: Lifecycle hooks
weight: 3
---
_New in version 0.96_
Component hooks are functions that allow you to intercept the rendering process at specific positions.
## Available hooks
- `on_render_before`
```py
def on_render_before(
self: Component,
context: Context,
template: Template
) -> None:
```
Hook that runs just before the component's template is rendered.
You can use this hook to access or modify the context or the template:
```py
def on_render_before(self, context, template) -> None:
# Insert value into the Context
context["from_on_before"] = ":)"
# Append text into the Template
template.nodelist.append(TextNode("FROM_ON_BEFORE"))
```
- `on_render_after`
```py
def on_render_after(
self: Component,
context: Context,
template: Template,
content: str
) -> None | str | SafeString:
```
Hook that runs just after the component's template was rendered.
It receives the rendered output as the last argument.
You can use this hook to access the context or the template, but modifying
them won't have any effect.
To override the content that gets rendered, you can return a string or SafeString from this hook:
```py
def on_render_after(self, context, template, content):
# Prepend text to the rendered content
return "Chocolate cookie recipe: " + content
```
## Component hooks example
You can use hooks together with [provide / inject](#how-to-use-provide--inject) to create components
that accept a list of items via a slot.
In the example below, each `tab_item` component will be rendered on a separate tab page, but they are all defined in the default slot of the `tabs` component.
[See here for how it was done](https://github.com/EmilStenstrom/django-components/discussions/540)
```django
{% component "tabs" %}
{% component "tab_item" header="Tab 1" %}
<p>
hello from tab 1
</p>
{% component "button" %}
Click me!
{% endcomponent %}
{% endcomponent %}
{% component "tab_item" header="Tab 2" %}
Hello this is tab 2
{% endcomponent %}
{% endcomponent %}
```

View file

@ -0,0 +1,135 @@
---
title: Prop drilling and provide / inject
weight: 2
---
_New in version 0.80_:
Django components supports the provide / inject or ContextProvider pattern with the combination of:
1. `{% provide %}` tag
1. `inject()` method of the `Component` class
## 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.
Normally, you'd use props to send data from a parent component to its children. However, this straightforward method becomes cumbersome and inefficient if the data has to travel through many levels or if several components scattered at different depths all need the same piece of information.
This results in a situation where the intermediate components, which don't need the data for their own functioning, end up having to manage and pass along these props. This clutters the component tree and makes the code verbose and harder to manage.
A neat solution to avoid prop drilling is using the "provide and inject" technique.
With provide / inject, a parent component acts like a data hub for all its descendants. This setup allows any component, no matter how deeply nested it is, to access the required data directly from this centralized provider without having to messily pass props down the chain. This approach significantly cleans up the code and makes it easier to maintain.
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
2. Injecting provided data
For examples of advanced uses of provide / inject, [see this discussion](https://github.com/EmilStenstrom/django-components/pull/506#issuecomment-2132102584).
## Using `{% provide %}` tag
First we use the `{% provide %}` tag to define the data we want to "provide" (make available).
```django
{% provide "my_data" key="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.
`provide` tag name 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.
> 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 %}
> ```
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:
```django
{% provide name=name ... %}
...
{% provide %}
</table>
```
## Using `inject()` method
To "inject" (access) the data defined on the `provide` tag, you can use the `inject()` method inside of `get_context_data()`.
For a component to be able to "inject" some data, the component (`{% component %}` tag) must be nested inside the `{% 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`.
```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
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`.
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)`:
```py
class ChildComponent(Component):
def get_context_data(self):
my_data = self.inject("invalid_key", DEFAULT_DATA)
assert my_data == DEFAUKT_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: `inject()` works strictly only in `get_context_data`. If you try to call it from elsewhere, it will raise an error.
## Full example
```py
@register("child")
class ChildComponent(Component):
template = """
<div> {{ my_data.key }} </div>
<div> {{ my_data.another }} </div>
"""
def get_context_data(self):
my_data = self.inject("my_data", "default")
return {"my_data": my_data}
template_str = """
{% load component_tags %}
{% provide "my_data" key="hi" another=123 %}
{% component "child" / %}
{% endprovide %}
"""
```
renders:
```html
<div>hi</div>
<div>123</div>
```

View file

@ -0,0 +1,227 @@
---
title: Rendering JS / CSS
weight: 1
---
### JS and CSS output locations
If:
1. Your components use JS and CSS via any of:
- [`Component.css`](#TODO)
- [`Component.js`](#TODO)
- [`Component.Media.css`](#TODO)
- [`Component.Media.js`](#TODO)
2. And you use the [`ComponentDependencyMiddleware`](#TODO) middleware
Then, by default, the components' JS and CSS will be automatically inserted into the HTML:
- CSS styles will be inserted at the end of the `<head>`
- JS scripts will be inserted at the end of the `<body>`
If you want to place the dependencies elsewhere in the HTML, you can override
the locations by inserting following Django template tags:
- [`{% component_js_dependencies %}`](#TODO) - Set new location(s) for JS scripts
- [`{% component_css_dependencies %}`](#TODO) - Set new location(s) for CSS styles
So if you have a component with JS and CSS:
```python
from django_components import Component, types
class MyButton(Component):
template: types.django_html = """
<button class="my-button">
Click me!
</button>
"""
js: types.js = """
for (const btnEl of document.querySelectorAll(".my-button")) {
btnEl.addEventListener("click", () => {
console.log("BUTTON CLICKED!");
});
}
"""
css: types.css """
.my-button {
background: green;
}
"""
class Media:
js = ["/extra/script.js"]
css = ["/extra/style.css"]
```
Then the JS from `MyButton.js` and `MyButton.Media.js` will be rendered at the default place,
or in [`{% component_js_dependencies %}`](#TODO).
And the CSS from `MyButton.css` and `MyButton.Media.css` will be rendered at the default place,
or in [`{% component_css_dependencies %}`](#TODO).
And if you don't specify `{% component_dependencies %}` tags, it is the equivalent of:
```django
<!doctype html>
<html>
<head>
<title>MyPage</title>
...
{% component_css_dependencies %}
</head>
<body>
<main>
...
</main>
{% component_js_dependencies %}
</body>
</html>
```
### Setting up the middleware
[`ComponentDependencyMiddleware`](#TODO) is a Django [middleware](https://docs.djangoproject.com/en/5.1/topics/http/middleware/)
designed to manage and inject CSS / JS dependencies of rendered components dynamically.
It ensures that only the necessary stylesheets and scripts are loaded
in your HTML responses, based on the components used in your Django templates.
To set it up, add the middleware to your [`MIDDLEWARE`](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-MIDDLEWARE)
in `settings.py`:
```python
MIDDLEWARE = [
# ... other middleware classes ...
'django_components.middleware.ComponentDependencyMiddleware'
# ... other middleware classes ...
]
```
### `render_dependencies` and rendering JS / CSS without the middleware
For most scenarios, using the [`ComponentDependencyMiddleware`](#TODO) middleware will be just fine.
However, this section is for you if you want to:
- Render HTML that will NOT be sent as a server response
- Insert pre-rendered HTML into another component
- Render HTML fragments (partials)
Every time there is an HTML string that has parts which were rendered using components,
and any of those components has JS / CSS, then this HTML string MUST be processed with [`render_dependencies()`](#TODO).
It is actually [`render_dependencies()`](#TODO) that finds all used components in the HTML string,
and inserts the component's JS and CSS into `{% component_dependencies %}` tags, or at the default locations.
#### Render JS / CSS without the middleware
The truth is that the [`ComponentDependencyMiddleware`](#TODO) middleware just calls [`render_dependencies()`](#TODO),
passing in the HTML content. So if you render a template that contained [`{% component %}`](#TODO) tags,
you MUST pass the result through [`render_dependencies()`](#TODO). And the middleware is just one of the options.
Here is how you can achieve the same, without the middleware, using [`render_dependencies()`](#TODO):
```python
from django.template.base import Template
from django.template.context import Context
from django_component import render_dependencies
template = Template("""
{% load component_tags %}
<!doctype html>
<html>
<head>
<title>MyPage</title>
</head>
<body>
<main>
{% component "my_button" %}
Click me!
{% endcomponent %}
</main>
</body>
</html>
""")
rendered = template.render(Context())
rendered = render_dependencies(rendered)
```
Same applies if you render a template using Django's [`django.shortcuts.render`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render):
```python
from django.shortcuts import render
def my_view(request):
rendered = render(request, "pages/home.html")
rendered = render_dependencies(rendered)
return rendered
```
Alternatively, when you render HTML with [`Component.render()`](#TODO)
or [`Component.render_to_response()`](#TODO),
these, by default, call [`render_dependencies()`](#TODO) for you, so you don't have to:
```python
from django_components import Component
class MyButton(Component):
...
# No need to call `render_dependencies()`
rendered = MyButton.render()
```
#### Inserting pre-rendered HTML into another component
In previous section we've shown that [`render_dependencies()`](#TODO) does NOT need to be called
when you render a component via [`Component.render()`](#TODO).
API of django_components makes it possible to compose components in a "React-like" way,
where we pre-render a piece of HTML and then insert it into a larger structure.
To do this, you must add [`render_dependencies=False`](#TODO) to the nested components:
```python
card_actions = CardActions.render(
kwargs={"editable": editable},
render_dependencies=False,
)
card = Card.render(
slots={"actions": card_actions},
render_dependencies=False,
)
page = MyPage.render(
slots={"card": card},
)
```
Why is `render_dependencies=False` required?
This is a technical limitation of the current implementation.
As mentioned earlier, each time we call [`Component.render()`](#TODO),
we also call [`render_dependencies()`](#TODO).
However, there is a problem here - When we call [`render_dependencies()`](#TODO)
inside [`CardActions.render()`](#TODO),
we extract and REMOVE the info on components' JS and CSS from the HTML. But the template
of `CardActions` contains no `{% component_depedencies %}` tags, and nor `<head>` nor `<body>` HTML tags.
So the component's JS and CSS will NOT be inserted, and will be lost.
To work around this, you must set [`render_dependencies=False`](#TODO) when rendering pieces of HTML
with [`Component.render()`](#TODO) and inserting them into larger structures.
#### Summary
1. Every time you render HTML that contained components, you have to call [`render_dependencies()`](#TODO)
on the rendered output.
2. There are several ways to call [`render_dependencies()`](#TODO):
- Using the [`ComponentDependencyMiddleware`](#TODO) middleware
- Rendering the HTML by calling [`Component.render()`](#TODO) with `render_dependencies=True` (default)
- Rendering the HTML by calling [`Component.render_to_response()`](#TODO) (always renders dependencies)
- Directly passing rendered HTML to [`render_dependencies()`](#TODO)
3. If you pre-render one component to pass it into another, the pre-rendered component must be rendered with
[`render_dependencies=False`](#TODO).

View file

@ -0,0 +1,171 @@
---
title: Tag formatters
weight: 6
---
## Customizing component tags with TagFormatter
_New in version 0.89_
By default, components are rendered using the pair of `{% component %}` / `{% endcomponent %}` template tags:
```django
{% component "button" href="..." disabled %}
Click me!
{% endcomponent %}
{# or #}
{% component "button" href="..." disabled / %}
```
You can change this behaviour in the settings under the [`COMPONENTS.tag_formatter`](#tag-formatter-setting).
For example, if you set the tag formatter to
`django_components.component_shorthand_formatter`
then the components' names will be used as the template tags:
```django
{% button href="..." disabled %}
Click me!
{% endbutton %}
{# or #}
{% button href="..." disabled / %}
```
## Available TagFormatters
django_components provides following predefined TagFormatters:
- **`ComponentFormatter` (`django_components.component_formatter`)**
Default
Uses the `component` and `endcomponent` tags, and the component name is gives as the first positional argument.
Example as block:
```django
{% component "button" href="..." %}
{% fill "content" %}
...
{% endfill %}
{% endcomponent %}
```
Example as inlined tag:
```django
{% component "button" href="..." / %}
```
- **`ShorthandComponentFormatter` (`django_components.component_shorthand_formatter`)**
Uses the component name as start tag, and `end<component_name>`
as an end tag.
Example as block:
```django
{% button href="..." %}
Click me!
{% endbutton %}
```
Example as inlined tag:
```django
{% button href="..." / %}
```
## Writing your own TagFormatter
### Background
First, let's discuss how TagFormatters work, and how components are rendered in django_components.
When you render a component with `{% component %}` (or your own tag), the following happens:
1. `component` must be registered as a Django's template tag
2. Django triggers django_components's tag handler for tag `component`.
3. The tag handler passes the tag contents for pre-processing to `TagFormatter.parse()`.
So if you render this:
```django
{% component "button" href="..." disabled %}
{% endcomponent %}
```
Then `TagFormatter.parse()` will receive a following input:
```py
["component", '"button"', 'href="..."', 'disabled']
```
4. `TagFormatter` extracts the component name and the remaining input.
So, given the above, `TagFormatter.parse()` returns the following:
```py
TagResult(
component_name="button",
tokens=['href="..."', 'disabled']
)
```
5. The tag handler resumes, using the tokens returned from `TagFormatter`.
So, continuing the example, at this point the tag handler practically behaves as if you rendered:
```django
{% component href="..." disabled %}
```
6. Tag handler looks up the component `button`, and passes the args, kwargs, and slots to it.
### TagFormatter
`TagFormatter` handles following parts of the process above:
- Generates start/end tags, given a component. This is what you then call from within your template as `{% component %}`.
- When you `{% component %}`, tag formatter pre-processes the tag contents, so it can link back the custom template tag to the right component.
To do so, subclass from `TagFormatterABC` and implement following method:
- `start_tag`
- `end_tag`
- `parse`
For example, this is the implementation of [`ShorthandComponentFormatter`](#available-tagformatters)
```py
class ShorthandComponentFormatter(TagFormatterABC):
# Given a component name, generate the start template tag
def start_tag(self, name: str) -> str:
return name # e.g. 'button'
# Given a component name, generate the start template tag
def end_tag(self, name: str) -> str:
return f"end{name}" # e.g. 'endbutton'
# Given a tag, e.g.
# `{% button href="..." disabled %}`
#
# The parser receives:
# `['button', 'href="..."', 'disabled']`
def parse(self, tokens: List[str]) -> TagResult:
tokens = [*tokens]
name = tokens.pop(0)
return TagResult(
name, # e.g. 'button'
tokens # e.g. ['href="..."', 'disabled']
)
```
That's it! And once your `TagFormatter` is ready, don't forget to update the settings!

View file

@ -0,0 +1,178 @@
---
title: Typing and validation
weight: 5
---
## Adding type hints with Generics
_New in version 0.92_
The `Component` class optionally accepts type parameters
that allow you to specify the types of args, kwargs, slots, and
data:
```py
class Button(Component[Args, Kwargs, Slots, Data, JsData, CssData]):
...
```
- `Args` - Must be a `Tuple` or `Any`
- `Kwargs` - Must be a `TypedDict` or `Any`
- `Data` - Must be a `TypedDict` or `Any`
- `Slots` - Must be a `TypedDict` or `Any`
Here's a full example:
```py
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
# Positional inputs
Args = Tuple[int, str]
# Kwargs inputs
class Kwargs(TypedDict):
variable: str
another: int
maybe_var: NotRequired[int] # May be ommited
# Data returned from `get_context_data`
class Data(TypedDict):
variable: str
# The data available to the `my_slot` scoped slot
class MySlotData(TypedDict):
value: int
# Slots
class Slots(TypedDict):
# Use SlotFunc for slot functions.
# The generic specifies the `data` dictionary
my_slot: NotRequired[SlotFunc[MySlotData]]
# SlotContent == Union[str, SafeString]
another_slot: SlotContent
class Button(Component[Args, Kwargs, Slots, Data, JsData, CssData]):
def get_context_data(self, variable, another):
return {
"variable": variable,
}
```
When you then call `Component.render` or `Component.render_to_response`, you will get type hints:
```py
Button.render(
# Error: First arg must be `int`, got `float`
args=(1.25, "abc"),
# Error: Key "another" is missing
kwargs={
"variable": "text",
},
)
```
### Usage for Python <3.11
On Python 3.8-3.10, use `typing_extensions`
```py
from typing_extensions import TypedDict, NotRequired
```
Additionally on Python 3.8-3.9, also import `annotations`:
```py
from __future__ import annotations
```
Moreover, on 3.10 and less, you may not be able to use `NotRequired`, and instead you will need to mark either all keys are required, or all keys as optional, using TypeDict's `total` kwarg.
[See PEP-655](https://peps.python.org/pep-0655) for more info.
## Passing additional args or kwargs
You may have a function that supports any number of args or kwargs:
```py
def get_context_data(self, *args, **kwargs):
...
```
This is not supported with the typed components.
As a workaround:
- For `*args`, set a positional argument that accepts a list of values:
```py
# Tuple of one member of list of strings
Args = Tuple[List[str]]
```
- For `*kwargs`, set a keyword argument that accepts a dictionary of values:
```py
class Kwargs(TypedDict):
variable: str
another: int
# Pass any extra keys under `extra`
extra: Dict[str, any]
```
## Handling no args or no kwargs
To declare that a component accepts no Args, Kwargs, etc, you can use `EmptyTuple` and `EmptyDict` types:
```py
from django_components import Component, EmptyDict, EmptyTuple
Args = EmptyTuple
Kwargs = Data = Slots = EmptyDict
class Button(Component[Args, Kwargs, Slots, Data, JsData, CssData]):
...
```
## Runtime input validation with types
_New in version 0.96_
> NOTE: Kwargs, slots, and data validation is supported only for Python >=3.11
In Python 3.11 and later, when you specify the component types, you will get also runtime validation of the inputs you pass to `Component.render` or `Component.render_to_response`.
So, using the example from before, if you ignored the type errors and still ran the following code:
```py
Button.render(
# Error: First arg must be `int`, got `float`
args=(1.25, "abc"),
# Error: Key "another" is missing
kwargs={
"variable": "text",
},
)
```
This would raise a `TypeError`:
```txt
Component 'Button' expected positional argument at index 0 to be <class 'int'>, got 1.25 of type <class 'float'>
```
In case you need to skip these errors, you can either set the faulty member to `Any`, e.g.:
```py
# Changed `int` to `Any`
Args = Tuple[Any, str]
```
Or you can replace `Args` with `Any` altogether, to skip the validation of args:
```py
# Replaced `Args` with `Any`
class Button(Component[Any, Kwargs, Slots, Data, JsData, CssData]):
...
```
Same applies to kwargs, data, and slots.