mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 06:18:17 +00:00
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:
parent
cdc830fca3
commit
594c0689ba
68 changed files with 116 additions and 108 deletions
255
docs/concepts/advanced/authoring_component_libraries.md
Normal file
255
docs/concepts/advanced/authoring_component_libraries.md
Normal 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 %}
|
||||
```
|
151
docs/concepts/advanced/component_registry.md
Normal file
151
docs/concepts/advanced/component_registry.md
Normal 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.
|
84
docs/concepts/advanced/hooks.md
Normal file
84
docs/concepts/advanced/hooks.md
Normal 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 %}
|
||||
```
|
135
docs/concepts/advanced/provide_inject.md
Normal file
135
docs/concepts/advanced/provide_inject.md
Normal 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>
|
||||
```
|
227
docs/concepts/advanced/rendering_js_css.md
Normal file
227
docs/concepts/advanced/rendering_js_css.md
Normal 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).
|
171
docs/concepts/advanced/tag_formatter.md
Normal file
171
docs/concepts/advanced/tag_formatter.md
Normal 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!
|
178
docs/concepts/advanced/typing_and_validation.md
Normal file
178
docs/concepts/advanced/typing_and_validation.md
Normal 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.
|
Loading…
Add table
Add a link
Reference in a new issue