mirror of
https://github.com/django-components/django-components.git
synced 2025-08-05 06:48:00 +00:00
Compare commits
226 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
09e96d92d6 | ||
![]() |
df8c6ab1a4 | ||
![]() |
10e1a25b26 | ||
![]() |
9e280f2e61 | ||
![]() |
de1ebcd071 | ||
![]() |
740a8c5f1c | ||
![]() |
7a260f7e47 | ||
![]() |
6772736f4c | ||
![]() |
8246f908bf | ||
![]() |
5d7e235725 | ||
![]() |
2a44fbec27 | ||
![]() |
f4347dd9d9 | ||
![]() |
65373ec3e8 | ||
![]() |
81c0d419b4 | ||
![]() |
672811b8b4 | ||
![]() |
f8c8292441 | ||
![]() |
8cba8702a3 | ||
![]() |
fb4b654433 | ||
![]() |
5f8ec71358 | ||
![]() |
f15f1e967a | ||
![]() |
b7b0d250c4 | ||
![]() |
0c7e17d8cf | ||
![]() |
514b8206b5 | ||
![]() |
bcc7bac48d | ||
![]() |
39e9ee0dc3 | ||
![]() |
df7f94cb2d | ||
![]() |
9a4f835201 | ||
![]() |
59e5fb3b38 | ||
![]() |
c692b7a310 | ||
![]() |
007009a480 | ||
![]() |
f8e2721244 | ||
![]() |
77fd149346 | ||
![]() |
055dd07694 | ||
![]() |
e7883d0b54 | ||
![]() |
64322a7b84 | ||
![]() |
b021b54ad4 | ||
![]() |
212336c99a | ||
![]() |
8ceb143fb3 | ||
![]() |
15669a2fb1 | ||
![]() |
10a9e555c0 | ||
![]() |
4d9c814e32 | ||
![]() |
68089daefb | ||
![]() |
b0da3b166d | ||
![]() |
5e34ec28cf | ||
![]() |
b0ef1f2a4c | ||
![]() |
14a8358b08 | ||
![]() |
b09c83b128 | ||
![]() |
6e86851894 | ||
![]() |
b9e8b8e569 | ||
![]() |
1db72b6e96 | ||
![]() |
cd1643ad23 | ||
![]() |
6d754acb99 | ||
![]() |
50d71b6187 | ||
![]() |
bdfe966919 | ||
![]() |
d537ba1ee5 | ||
![]() |
06c89cf9e8 | ||
![]() |
3fc96daa9b | ||
![]() |
458e1894db | ||
![]() |
2350a6b6c4 | ||
![]() |
291b97cfd1 | ||
![]() |
6a8fc27bad | ||
![]() |
328b55ff29 | ||
![]() |
136a7a591c | ||
![]() |
4d056b2c49 | ||
![]() |
8209614fc2 | ||
![]() |
7bece01024 | ||
![]() |
49f0c05bf6 | ||
![]() |
831db00598 | ||
![]() |
09bcf8dbcc | ||
![]() |
efe5eb0ba5 | ||
![]() |
ace29db9ac | ||
![]() |
04737412d9 | ||
![]() |
b2e053118b | ||
![]() |
7b24b86f4a | ||
![]() |
468abaf92f | ||
![]() |
593c66db7f | ||
![]() |
04f79a6e6b | ||
![]() |
eceebb9696 | ||
![]() |
46e524e37d | ||
![]() |
abc6be343e | ||
![]() |
87eb1f8479 | ||
![]() |
c5a4a81852 | ||
![]() |
09cb8714cc | ||
![]() |
8677ee7941 | ||
![]() |
fa9ae9892f | ||
![]() |
bb129aefab | ||
![]() |
7df8019544 | ||
![]() |
42eb6f93ae | ||
![]() |
dfb90c4005 | ||
![]() |
26845fe60a | ||
![]() |
e8eb296897 | ||
![]() |
6aec2d23dc | ||
![]() |
7884705869 | ||
![]() |
d7fee43dd9 | ||
![]() |
55b1c8bc62 | ||
![]() |
bae0f28813 | ||
![]() |
2e08af9a13 | ||
![]() |
046569e16d | ||
![]() |
6ff2d78a2f | ||
![]() |
223fc2c68c | ||
![]() |
e054a68715 | ||
![]() |
d514694788 | ||
![]() |
f069255b64 | ||
![]() |
7a49a7806c | ||
![]() |
0718b4cac6 | ||
![]() |
4f15ad8360 | ||
![]() |
49ad23b21d | ||
![]() |
71db46d5ca | ||
![]() |
79c42da2f9 | ||
![]() |
b6b574d875 | ||
![]() |
0d05ef4cb2 | ||
![]() |
53d80684bb | ||
![]() |
2eb9f5d43d | ||
![]() |
1bb7c3fdfe | ||
![]() |
f80457e964 | ||
![]() |
a96f5fdd2f | ||
![]() |
b83eaefcd0 | ||
![]() |
9dda6f8c16 | ||
![]() |
5fb0c9032b | ||
![]() |
e5126b04ef | ||
![]() |
77178190ed | ||
![]() |
d7e6d5a961 | ||
![]() |
406828c51a | ||
![]() |
fcc8d71dca | ||
![]() |
ccf02fa316 | ||
![]() |
5f4fbe76e5 | ||
![]() |
661413d4a9 | ||
![]() |
e64cd197c1 | ||
![]() |
ccd8e26956 | ||
![]() |
6aa2a39fe9 | ||
![]() |
26078d5340 | ||
![]() |
2dacac1f43 | ||
![]() |
6253042e9e | ||
![]() |
1049c08324 | ||
![]() |
ca4e7f7f8b | ||
![]() |
b13f859d7b | ||
![]() |
330578a2c7 | ||
![]() |
d4d834256a | ||
![]() |
28b61c1609 | ||
![]() |
c69980493d | ||
![]() |
bf7a204e92 | ||
![]() |
e74e1241ac | ||
![]() |
76a888aa11 | ||
![]() |
bb9e8e0bad | ||
![]() |
95e5df9d3c | ||
![]() |
c5c7b7a52c | ||
![]() |
3eb148754a | ||
![]() |
b8dbf89612 | ||
![]() |
d33a1b87aa | ||
![]() |
456e011f9b | ||
![]() |
6a452b285e | ||
![]() |
7b15ea9ea8 | ||
![]() |
59f82307ae | ||
![]() |
89db10a643 | ||
![]() |
5704bc86a1 | ||
![]() |
a5a93e9bbb | ||
![]() |
a368786267 | ||
![]() |
dbe8dc074d | ||
![]() |
2b69980845 | ||
![]() |
519529d4e4 | ||
![]() |
c8002d241a | ||
![]() |
b949e4a6ec | ||
![]() |
b49002b545 | ||
![]() |
912d8e8074 | ||
![]() |
4c90948606 | ||
![]() |
e0b718c314 | ||
![]() |
eed15d32ab | ||
![]() |
68ede4f662 | ||
![]() |
bd1a4fd65d | ||
![]() |
56846c5fc4 | ||
![]() |
8cf42dd20f | ||
![]() |
2905c6980a | ||
![]() |
0fad34f271 | ||
![]() |
6e7e5cd162 | ||
![]() |
a69c2329b0 | ||
![]() |
c650e7f3a5 | ||
![]() |
fc026cbd99 | ||
![]() |
06cad2ec64 | ||
![]() |
61528ef0ad | ||
![]() |
3148557e91 | ||
![]() |
ad402fc619 | ||
![]() |
d37291a3b6 | ||
![]() |
5b01d6c3c6 | ||
![]() |
07f747d705 | ||
![]() |
cc249022c4 | ||
![]() |
1319a95627 | ||
![]() |
613dfea379 | ||
![]() |
9ede779fa3 | ||
![]() |
b6994e9ad3 | ||
![]() |
ef15117459 | ||
![]() |
625c1a4735 | ||
![]() |
c8f9b35f2e | ||
![]() |
077ba59ce1 | ||
![]() |
0417e369f7 | ||
![]() |
2213f43c89 | ||
![]() |
1074ebe2f1 | ||
![]() |
225f45d0b8 | ||
![]() |
ec5cc278a4 | ||
![]() |
e0e9501dca | ||
![]() |
fd12bb47ab | ||
![]() |
bd703ead4b | ||
![]() |
084b6c7f34 | ||
![]() |
dfb675343e | ||
![]() |
2fedd9d0e2 | ||
![]() |
db324e291f | ||
![]() |
1dfec8fc6d | ||
![]() |
9f9b1f7232 | ||
![]() |
bb5de86b69 | ||
![]() |
a49f5e51dd | ||
![]() |
3555411f1e | ||
![]() |
0ed46e4d30 | ||
![]() |
637e143538 | ||
![]() |
fdd29baa65 | ||
![]() |
2499126d1f | ||
![]() |
7e74831599 | ||
![]() |
5e263ec143 | ||
![]() |
ce140ae2c2 | ||
![]() |
57d035c67d | ||
![]() |
1b8122312e | ||
![]() |
e184834e93 | ||
![]() |
d523a97fcc | ||
![]() |
61a70e7705 | ||
![]() |
e35c4e24fb | ||
![]() |
53adc9e18a | ||
![]() |
184c3e4c6c | ||
![]() |
22dac99e4e |
258 changed files with 23881 additions and 7509 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
32
.devcontainer/devcontainer.json
Normal file
32
.devcontainer/devcontainer.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.0/containers/python-3
|
||||
{
|
||||
// Uncomment to run Python 3.13 or other specific version
|
||||
// "image": "mcr.microsoft.com/devcontainers/python:3.13-bullseye",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
||||
"python.linting.enabled": true
|
||||
},
|
||||
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.vscode-python-envs",
|
||||
"jurooravec.python-inline-source-2"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
//"postCreateCommand": ""
|
||||
}
|
6
.github/workflows/docs.yml
vendored
6
.github/workflows/docs.yml
vendored
|
@ -30,7 +30,7 @@ jobs:
|
|||
# Authenticate with git with the Github App that has permission
|
||||
# to push to master, in order to push benchmark results.
|
||||
# See https://stackoverflow.com/a/79142962/9788634
|
||||
- uses: actions/create-github-app-token@v1
|
||||
- uses: actions/create-github-app-token@v2
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.RELEASE_BOT_APP_ID }}
|
||||
|
@ -56,7 +56,9 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel
|
||||
python -m pip install -q hatch pre-commit asv
|
||||
# NOTE: pin virtualenv to <20.31 until asv fixes it.
|
||||
# See https://github.com/airspeed-velocity/asv/issues/1484
|
||||
python -m pip install -q hatch pre-commit asv virtualenv==20.30
|
||||
hatch --version
|
||||
|
||||
###########################################
|
||||
|
|
4
.github/workflows/pr-benchmark-generate.yml
vendored
4
.github/workflows/pr-benchmark-generate.yml
vendored
|
@ -39,7 +39,9 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install asv
|
||||
# NOTE: pin virtualenv to <20.31 until asv fixes it.
|
||||
# See https://github.com/airspeed-velocity/asv/issues/1484
|
||||
pip install asv virtualenv==20.30
|
||||
|
||||
- name: Run benchmarks
|
||||
run: |
|
||||
|
|
6
.github/workflows/tests.yml
vendored
6
.github/workflows/tests.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
|
||||
os: [ubuntu-20.04, windows-latest]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
# Configure git to handle long paths
|
||||
|
@ -45,7 +45,7 @@ jobs:
|
|||
|
||||
# Verify that docs build
|
||||
test_docs:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.13']
|
||||
|
@ -70,7 +70,7 @@ jobs:
|
|||
|
||||
# Verify that the sample project works
|
||||
test_sampleproject:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.13']
|
||||
|
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
# Project-specific files
|
||||
sampleproject/staticfiles/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
@ -43,6 +46,7 @@ htmlcov/
|
|||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
@ -76,7 +80,11 @@ poetry.lock
|
|||
site
|
||||
.direnv/
|
||||
.envrc
|
||||
.mypy_cache/
|
||||
|
||||
# JS, NPM Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Cursor
|
||||
.cursorrules
|
1584
CHANGELOG.md
1584
CHANGELOG.md
File diff suppressed because it is too large
Load diff
304
README.md
304
README.md
|
@ -23,9 +23,10 @@ A component in django-components can be as simple as a Django template and Pytho
|
|||
```
|
||||
|
||||
```py
|
||||
# components/calendar/calendar.html
|
||||
from django_components import Component
|
||||
# components/calendar/calendar.py
|
||||
from django_components import Component, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
```
|
||||
|
@ -56,15 +57,16 @@ document.querySelector(".calendar").onclick = () => {
|
|||
|
||||
```py
|
||||
# components/calendar/calendar.py
|
||||
from django_components import Component
|
||||
from django_components import Component, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
js_file = "calendar.js"
|
||||
css_file = "calendar.css"
|
||||
|
||||
def get_context_data(self, date):
|
||||
return {"date": date}
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"date": kwargs["date"]}
|
||||
```
|
||||
|
||||
Use the component like this:
|
||||
|
@ -121,21 +123,23 @@ class Calendar(Component):
|
|||
|
||||
# Additional JS and CSS
|
||||
class Media:
|
||||
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2.1.1/dist/htmx.min.js"]
|
||||
js = ["https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"]
|
||||
css = ["bootstrap/dist/css/bootstrap.min.css"]
|
||||
|
||||
# Variables available in the template
|
||||
def get_context_data(self, date):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": date
|
||||
"date": kwargs["date"]
|
||||
}
|
||||
```
|
||||
|
||||
### Composition with slots
|
||||
|
||||
- Render components inside templates with `{% component %}` tag.
|
||||
- Compose them with `{% slot %}` and `{% fill %}` tags.
|
||||
- Vue-like slot system, including scoped slots.
|
||||
- Render components inside templates with
|
||||
[`{% component %}`](https://django-components.github.io/django-components/latest/reference/template_tags#component) tag.
|
||||
- Compose them with [`{% slot %}`](https://django-components.github.io/django-components/latest/reference/template_tags#slot)
|
||||
and [`{% fill %}`](https://django-components.github.io/django-components/latest/reference/template_tags#fill) tags.
|
||||
- Vue-like slot system, including [scoped slots](https://django-components.github.io/django-components/latest/concepts/fundamentals/slots/#slot-data).
|
||||
|
||||
```django
|
||||
{% component "Layout"
|
||||
|
@ -169,14 +173,17 @@ class Calendar(Component):
|
|||
|
||||
### Extended template tags
|
||||
|
||||
`django-components` extends Django's template tags syntax with:
|
||||
`django-components` is designed for flexibility, making working with templates a breeze.
|
||||
|
||||
- Literal lists and dictionaries in template tags
|
||||
- Self-closing tags `{% mytag / %}`
|
||||
- Multi-line template tags
|
||||
- Spread operator `...` to dynamically pass args or kwargs into the template tag
|
||||
- Nested template tags like `"{{ first_name }} {{ last_name }}"`
|
||||
- Flat definition of dictionary keys `attr:key=val`
|
||||
It extends Django's template tags syntax with:
|
||||
|
||||
<!-- TODO - Document literal lists and dictionaries -->
|
||||
- Literal lists and dictionaries in the template
|
||||
- [Self-closing tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#self-closing-tags) `{% mytag / %}`
|
||||
- [Multi-line template tags](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#multiline-tags)
|
||||
- [Spread operator](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#spread-operator) `...` to dynamically pass args or kwargs into the template tag
|
||||
- [Template tags inside literal strings](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#template-tags-inside-literal-strings) like `"{{ first_name }} {{ last_name }}"`
|
||||
- [Pass dictonaries by their key-value pairs](https://django-components.github.io/django-components/latest/concepts/fundamentals/template_tag_syntax#pass-dictonary-by-its-key-value-pairs) `attr:key=val`
|
||||
|
||||
```django
|
||||
{% component "table"
|
||||
|
@ -201,13 +208,70 @@ class Calendar(Component):
|
|||
/ %}
|
||||
```
|
||||
|
||||
You too can define template tags with these features by using
|
||||
[`@template_tag()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.template_tag)
|
||||
or [`BaseNode`](https://django-components.github.io/django-components/latest/reference/api/#django_components.BaseNode).
|
||||
|
||||
Read more on [Custom template tags](https://django-components.github.io/django-components/latest/concepts/advanced/template_tags/).
|
||||
|
||||
### Full programmatic access
|
||||
|
||||
When you render a component, you can access everything about the component:
|
||||
|
||||
- Component input: [args, kwargs, slots and context](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-inputs)
|
||||
- Component's template, CSS and JS
|
||||
- Django's [context processors](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#request-and-context-processors)
|
||||
- Unique [render ID](https://django-components.github.io/django-components/latest/concepts/fundamentals/render_api/#component-id)
|
||||
|
||||
```python
|
||||
class Table(Component):
|
||||
js_file = "table.js"
|
||||
css_file = "table.css"
|
||||
|
||||
template = """
|
||||
<div class="table">
|
||||
<span>{{ variable }}</span>
|
||||
</div>
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
# Access component's ID
|
||||
assert self.id == "djc1A2b3c"
|
||||
|
||||
# Access component's inputs and slots
|
||||
assert self.args == [123, "str"]
|
||||
assert self.kwargs == {"variable": "test", "another": 1}
|
||||
footer_slot = self.slots["footer"]
|
||||
some_var = self.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": kwargs["variable"],
|
||||
}
|
||||
|
||||
# Access component's HTML / JS / CSS
|
||||
Table.template
|
||||
Table.js
|
||||
Table.css
|
||||
|
||||
# Render the component
|
||||
rendered = Table.render(
|
||||
kwargs={"variable": "test", "another": 1},
|
||||
args=(123, "str"),
|
||||
slots={"footer": "MY_FOOTER"},
|
||||
)
|
||||
```
|
||||
|
||||
### Granular HTML attributes
|
||||
|
||||
Use the [`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) template tag to render HTML attributes.
|
||||
|
||||
It supports:
|
||||
|
||||
- Defining attributes as dictionaries
|
||||
- Defining attributes as keyword arguments
|
||||
- Defining attributes as whole dictionaries or keyword arguments
|
||||
- Merging attributes from multiple sources
|
||||
- Boolean attributes
|
||||
- Appending attributes
|
||||
|
@ -224,13 +288,19 @@ It supports:
|
|||
>
|
||||
```
|
||||
|
||||
`{% html_attrs %}` offers a Vue-like granular control over `class` and `style` HTML attributes,
|
||||
[`{% html_attrs %}`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/) offers a Vue-like granular control for
|
||||
[`class`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-class-attributes)
|
||||
and [`style`](https://django-components.github.io/django-components/latest/concepts/fundamentals/html_attributes/#merging-style-attributes)
|
||||
HTML attributes,
|
||||
where you can use a dictionary to manage each class name or style property separately.
|
||||
|
||||
```django
|
||||
{% html_attrs
|
||||
class="foo bar"
|
||||
class={"baz": True, "foo": False}
|
||||
class={
|
||||
"baz": True,
|
||||
"foo": False,
|
||||
}
|
||||
class="extra"
|
||||
%}
|
||||
```
|
||||
|
@ -238,7 +308,11 @@ where you can use a dictionary to manage each class name or style property separ
|
|||
```django
|
||||
{% html_attrs
|
||||
style="text-align: center; background-color: blue;"
|
||||
style={"background-color": "green", "color": None, "width": False}
|
||||
style={
|
||||
"background-color": "green",
|
||||
"color": None,
|
||||
"width": False,
|
||||
}
|
||||
style="position: absolute; height: 12px;"
|
||||
%}
|
||||
```
|
||||
|
@ -247,11 +321,13 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
|
|||
|
||||
### HTML fragment support
|
||||
|
||||
`django-components` makes integration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as HTML fragments:
|
||||
`django-components` makes integration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as [HTML fragments](https://django-components.github.io/django-components/latest/concepts/advanced/html_fragments/):
|
||||
|
||||
- Components's JS and CSS is loaded automatically when the fragment is inserted into the DOM.
|
||||
- Components's JS and CSS files are loaded automatically when the fragment is inserted into the DOM.
|
||||
|
||||
- Expose components as views with `get`, `post`, `put`, `patch`, `delete` methods
|
||||
- Components can be [exposed as Django Views](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/) with `get()`, `post()`, `put()`, `patch()`, `delete()` methods
|
||||
|
||||
- Automatically create an endpoint for a component with [`Component.View.public`](https://django-components.github.io/django-components/latest/concepts/fundamentals/component_views_urls/#register-urls-automatically)
|
||||
|
||||
```py
|
||||
# components/calendar/calendar.py
|
||||
|
@ -259,94 +335,163 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
|
|||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
page = request.GET.get("page", 1)
|
||||
return self.render_to_response(
|
||||
kwargs={
|
||||
"page": page,
|
||||
}
|
||||
)
|
||||
class View:
|
||||
# Register Component with `urlpatterns`
|
||||
public = True
|
||||
|
||||
def get_context_data(self, page):
|
||||
# Define handlers
|
||||
def get(self, request, *args, **kwargs):
|
||||
page = request.GET.get("page", 1)
|
||||
return self.component.render_to_response(
|
||||
request=request,
|
||||
kwargs={
|
||||
"page": page,
|
||||
},
|
||||
)
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"page": page,
|
||||
"page": kwargs["page"],
|
||||
}
|
||||
|
||||
# urls.py
|
||||
path("calendar/", Calendar.as_view()),
|
||||
# Get auto-generated URL for the component
|
||||
url = get_component_url(Calendar)
|
||||
|
||||
# Or define explicit URL in urls.py
|
||||
path("calendar/", Calendar.as_view())
|
||||
```
|
||||
|
||||
### Type hints
|
||||
### Provide / Inject
|
||||
|
||||
Opt-in to type hints by defining types for component's args, kwargs, slots, and more:
|
||||
`django-components` supports the provide / inject pattern, similarly to React's [Context Providers](https://react.dev/learn/passing-data-deeply-with-context) or Vue's [provide / inject](https://vuejs.org/guide/components/provide-inject):
|
||||
|
||||
- Use the [`{% provide %}`](https://django-components.github.io/django-components/latest/reference/template_tags/#provide) tag to provide data to the component tree
|
||||
- Use the [`Component.inject()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.inject) method to inject data into the component
|
||||
|
||||
Read more about [Provide / Inject](https://django-components.github.io/django-components/latest/concepts/advanced/provide_inject).
|
||||
|
||||
```django
|
||||
<body>
|
||||
{% provide "theme" variant="light" %}
|
||||
{% component "header" / %}
|
||||
{% endprovide %}
|
||||
</body>
|
||||
```
|
||||
|
||||
```djc_py
|
||||
@register("header")
|
||||
class Header(Component):
|
||||
template = "..."
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
theme = self.inject("theme").variant
|
||||
return {
|
||||
"theme": theme,
|
||||
}
|
||||
```
|
||||
|
||||
### Input validation and static type hints
|
||||
|
||||
Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/fundamentals/typing_and_validation/).
|
||||
|
||||
To opt-in to input validation, define types for component's args, kwargs, slots, and more:
|
||||
|
||||
```py
|
||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
|
||||
from typing import NamedTuple, Optional
|
||||
from django.template import Context
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
ButtonArgs = Tuple[int, str]
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
size: int
|
||||
text: str
|
||||
|
||||
class ButtonKwargs(TypedDict):
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: NotRequired[int] # May be omitted
|
||||
class Kwargs(NamedTuple):
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class ButtonData(TypedDict):
|
||||
variable: str
|
||||
class Slots(NamedTuple):
|
||||
my_slot: Optional[SlotInput] = None
|
||||
another_slot: SlotInput
|
||||
|
||||
class ButtonSlots(TypedDict):
|
||||
my_slot: NotRequired[SlotFunc]
|
||||
another_slot: SlotContent
|
||||
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
|
||||
|
||||
class Button(ButtonType):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
self.input.args[0] # int
|
||||
self.input.kwargs["variable"] # str
|
||||
self.input.slots["my_slot"] # SlotFunc[MySlotData]
|
||||
|
||||
return {} # Error: Key "variable" is missing
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
args.size # int
|
||||
kwargs.variable # str
|
||||
slots.my_slot # Slot[MySlotData]
|
||||
```
|
||||
|
||||
When you then call `Button.render()` or `Button.render_to_response()`, you will get type hints:
|
||||
To have type hints when calling
|
||||
[`Button.render()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render) or
|
||||
[`Button.render_to_response()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render_to_response),
|
||||
wrap the inputs in their respective `Args`, `Kwargs`, and `Slots` classes:
|
||||
|
||||
```py
|
||||
Button.render(
|
||||
# Error: First arg must be `int`, got `float`
|
||||
args=(1.25, "abc"),
|
||||
args=Button.Args(
|
||||
size=1.25,
|
||||
text="abc",
|
||||
),
|
||||
# Error: Key "another" is missing
|
||||
kwargs={
|
||||
"variable": "text",
|
||||
},
|
||||
kwargs=Button.Kwargs(
|
||||
variable="text",
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Extensions
|
||||
|
||||
Django-components functionality can be extended with "extensions". Extensions allow for powerful customization and integrations. They can:
|
||||
Django-components functionality can be extended with [Extensions](https://django-components.github.io/django-components/latest/concepts/advanced/extensions/).
|
||||
Extensions allow for powerful customization and integrations. They can:
|
||||
|
||||
- Tap into lifecycle events, such as when a component is created, deleted, or registered.
|
||||
- Add new attributes and methods to the components under an extension-specific nested class.
|
||||
- Tap into lifecycle events, such as when a component is created, deleted, or registered
|
||||
- Add new attributes and methods to the components
|
||||
- Add custom CLI commands
|
||||
- Add custom URLs
|
||||
|
||||
Some of the extensions include:
|
||||
|
||||
- [Component caching](https://github.com/django-components/django-components/blob/master/src/django_components/extensions/cache.py)
|
||||
- [Django View integration](https://github.com/django-components/django-components/blob/master/src/django_components/extensions/view.py)
|
||||
- [Component defaults](https://github.com/django-components/django-components/blob/master/src/django_components/extensions/defaults.py)
|
||||
- [Pydantic integration (input validation)](https://github.com/django-components/djc-ext-pydantic)
|
||||
|
||||
Some of the planned extensions include:
|
||||
|
||||
- Caching
|
||||
- AlpineJS integration
|
||||
- Storybook integration
|
||||
- Pydantic validation
|
||||
- Component-level benchmarking with asv
|
||||
|
||||
### Caching
|
||||
|
||||
- [Components can be cached](https://django-components.github.io/django-components/latest/concepts/advanced/component_caching/) using Django's cache framework.
|
||||
- Caching rules can be configured on a per-component basis.
|
||||
- Components are cached based on their input. Or you can write custom caching logic.
|
||||
|
||||
```py
|
||||
from django_components import Component
|
||||
|
||||
class MyComponent(Component):
|
||||
class Cache:
|
||||
enabled = True
|
||||
ttl = 60 * 60 * 24 # 1 day
|
||||
|
||||
def hash(self, *args, **kwargs):
|
||||
return hash(f"{json.dumps(args)}:{json.dumps(kwargs)}")
|
||||
```
|
||||
|
||||
### Simple testing
|
||||
|
||||
- Write tests for components with `@djc_test` decorator.
|
||||
- Write tests for components with [`@djc_test`](https://django-components.github.io/django-components/latest/concepts/advanced/testing/) decorator.
|
||||
- The decorator manages global state, ensuring that tests don't leak.
|
||||
- If using `pytest`, the decorator allows you to parametrize Django or Components settings.
|
||||
- The decorator also serves as a stand-in for Django's `@override_settings`.
|
||||
- The decorator also serves as a stand-in for Django's [`@override_settings`](https://docs.djangoproject.com/en/5.2/topics/testing/tools/#django.test.override_settings).
|
||||
|
||||
```python
|
||||
from djc_test import djc_test
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from components.my_component import MyTable
|
||||
from components.my_table import MyTable
|
||||
|
||||
@djc_test
|
||||
def test_my_table():
|
||||
|
@ -358,11 +503,6 @@ def test_my_table():
|
|||
assert rendered == "<table>My table</table>"
|
||||
```
|
||||
|
||||
### Handle large projects with ease
|
||||
|
||||
- Components can be infinitely nested.
|
||||
- (Soon) Optimize performance with component-level caching
|
||||
|
||||
### Debugging features
|
||||
|
||||
- **Visual component inspection**: Highlight components and slots directly in your browser.
|
||||
|
@ -386,10 +526,6 @@ def test_my_table():
|
|||
{% endcalendar %}
|
||||
```
|
||||
|
||||
### Other features
|
||||
|
||||
- Vue-like provide / inject system
|
||||
|
||||
## Documentation
|
||||
|
||||
[Read the full documentation here](https://django-components.github.io/django-components/latest/).
|
||||
|
@ -418,7 +554,7 @@ to see the latest features and fixes.
|
|||
|
||||
One of our goals with `django-components` is to make it easy to share components between projects. If you have a set of components that you think would be useful to others, please open a pull request to add them to the list below.
|
||||
|
||||
- [django-htmx-components](https://github.com/iwanalabs/django-htmx-components): A set of components for use with [htmx](https://htmx.org/). Try out the [live demo](https://dhc.iwanalabs.com/).
|
||||
- [django-htmx-components](https://github.com/iwanalabs/django-htmx-components): A set of components for use with [htmx](https://htmx.org/).
|
||||
|
||||
- [djc-heroicons](https://pypi.org/project/djc-heroicons/): A component that renders icons from [Heroicons.com](https://heroicons.com/).
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ django-components uses `asv` for these use cases:
|
|||
1. When a git tag is created and pushed, we also update the documentation website (see `docs.yml`).
|
||||
2. Before we publish the docs website, we generate the HTML report for the benchmark results.
|
||||
3. The generated report is placed in the `docs/benchmarks/` directory, and is thus
|
||||
published with the rest of the docs website and available under [`/benchmarks/`](https://django-components.github.io/django-components/benchmarks).
|
||||
published with the rest of the docs website and available under [`/benchmarks/`](https://django-components.github.io/django-components/latest/benchmarks).
|
||||
- NOTE: The location where the report is placed is defined in `asv.conf.json`.
|
||||
|
||||
- Compare performance between commits on pull requests:
|
||||
|
|
|
@ -1 +1 @@
|
|||
[[1662, [52920320.0, 54566912.0]], [1672, [52350976.0, 54599680.0]], [1687, [52109312.0, 54779904.0]], [1691, [52899840.0, 54730752.0]]]
|
||||
[[1662, [52920320.0, 54566912.0]], [1672, [52350976.0, 54599680.0]], [1687, [52109312.0, 54779904.0]], [1691, [52899840.0, 54730752.0]], [1709, [52936704.0, 55009280.0]], [1726, [52379648.0, 54992896.0]], [1766, [53084160.0, 55382016.0]], [1770, [53047296.0, 55373824.0]], [1776, [52490240.0, 55361536.0]], [1801, [53153792.0, 55410688.0]], [1937, [52957184.0, 55177216.0]], [1960, [52932608.0, 55693312.0]], [1996, [53096448.0, 55484416.0]], [2029, [52715520.0, 56090624.0]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [53800960.0, 54734848.0]], [1672, [52289536.0, 55099392.0]], [1687, [52142080.0, 55255040.0]], [1691, [53796864.0, 55238656.0]]]
|
||||
[[1662, [53800960.0, 54734848.0]], [1672, [52289536.0, 55099392.0]], [1687, [52142080.0, 55255040.0]], [1691, [53796864.0, 55238656.0]], [1709, [53768192.0, 55455744.0]], [1726, [51998720.0, 55451648.0]], [1766, [53739520.0, 55812096.0]], [1770, [53948416.0, 55824384.0]], [1776, [52097024.0, 55791616.0]], [1801, [53919744.0, 55799808.0]], [1937, [52822016.0, 56242176.0]], [1960, [53063680.0, 56180736.0]], [1996, [53018624.0, 56389632.0]], [2029, [52736000.0, 56791040.0]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [44191744.0, 44191744.0]], [1672, [44056576.0, 44048384.0]], [1687, [44191744.0, 44310528.0]], [1691, [44183552.0, 44175360.0]]]
|
||||
[[1662, [44191744.0, 44191744.0]], [1672, [44056576.0, 44048384.0]], [1687, [44191744.0, 44310528.0]], [1691, [44183552.0, 44175360.0]], [1709, [44191744.0, 44314624.0]], [1726, [44195840.0, 44314624.0]], [1766, [44322816.0, 44314624.0]], [1770, [44326912.0, 44322816.0]], [1776, [44183552.0, 44306432.0]], [1801, [44195840.0, 44453888.0]], [1937, [44756992.0, 44744704.0]], [1960, [44716032.0, 44834816.0]], [1996, [44716032.0, 44969984.0]], [2029, [44871680.0, 44912640.0]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [44195840.0, 44187648.0]], [1672, [44060672.0, 43917312.0]], [1687, [44105728.0, 44310528.0]], [1691, [44187648.0, 44183552.0]]]
|
||||
[[1662, [44195840.0, 44187648.0]], [1672, [44060672.0, 43917312.0]], [1687, [44105728.0, 44310528.0]], [1691, [44187648.0, 44183552.0]], [1709, [44191744.0, 44437504.0]], [1726, [44322816.0, 44314624.0]], [1766, [44322816.0, 44310528.0]], [1770, [44101632.0, 44310528.0]], [1776, [44314624.0, 44437504.0]], [1801, [44191744.0, 44453888.0]], [1937, [44527616.0, 44744704.0]], [1960, [44716032.0, 44838912.0]], [1996, [44724224.0, 44969984.0]], [2029, [44617728.0, 44986368.0]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.06960565700001098, 0.25608221199996706]], [1672, [0.07114163800000028, 0.26389872900000455]], [1687, [0.06910802600003763, 0.25746033199999374]], [1691, [0.07048037500001669, 0.2598985070000026]]]
|
||||
[[1662, [0.06960565700001098, 0.25608221199996706]], [1672, [0.07114163800000028, 0.26389872900000455]], [1687, [0.06910802600003763, 0.25746033199999374]], [1691, [0.07048037500001669, 0.2598985070000026]], [1709, [0.07402671400001282, 0.26584690599997884]], [1726, [0.07297276199997782, 0.2569234329999972]], [1766, [0.07308550800001967, 0.26274096600002395]], [1770, [0.0749189080000292, 0.26436952000000247]], [1776, [0.07303507899999317, 0.2628890319999755]], [1801, [0.07360306399999672, 0.2678246009999725]], [1937, [0.07941284200001064, 0.26779402600004687]], [1960, [0.08026317200000221, 0.26819844099998136]], [1996, [0.0814841690000776, 0.28364495499999975]], [2029, [0.08105427499998541, 0.29477426600001877]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.03327357099999517, 0.1421111020000012]], [1672, [0.033918617999972867, 0.14395761299999776]], [1687, [0.03317536700001256, 0.14245594600001255]], [1691, [0.034316510999985894, 0.1444248799999741]]]
|
||||
[[1662, [0.03327357099999517, 0.1421111020000012]], [1672, [0.033918617999972867, 0.14395761299999776]], [1687, [0.03317536700001256, 0.14245594600001255]], [1691, [0.034316510999985894, 0.1444248799999741]], [1709, [0.03742426899998463, 0.14901454800002512]], [1726, [0.03658580800001232, 0.1459621130000528]], [1766, [0.03723830100000214, 0.15196534300002895]], [1770, [0.03752758399997447, 0.15356457899997622]], [1776, [0.03678920999999491, 0.14955294699998944]], [1801, [0.037022983000014165, 0.15138703899998518]], [1937, [0.043317416999911984, 0.15457556900003055]], [1960, [0.04349111400000538, 0.15453611999998884]], [1996, [0.04362213900003553, 0.16551773399999092]], [2029, [0.043648402000002307, 0.17461173199995983]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.0035443229999998493, 0.00467639600003622]], [1672, [0.0036137869999777195, 0.004807943000002979]], [1687, [0.0035223549999727766, 0.004706463999980315]], [1691, [0.00364059099999281, 0.004926952999994683]]]
|
||||
[[1662, [0.0035443229999998493, 0.00467639600003622]], [1672, [0.0036137869999777195, 0.004807943000002979]], [1687, [0.0035223549999727766, 0.004706463999980315]], [1691, [0.00364059099999281, 0.004926952999994683]], [1709, [0.003602947999979733, 0.004853936999950292]], [1726, [0.0035008030000085455, 0.004695608999981005]], [1766, [0.003566315000000486, 0.004791812000007667]], [1770, [0.0036766670000361046, 0.004929383999979109]], [1776, [0.0035613420000117912, 0.004760385999986738]], [1801, [0.003639607999986083, 0.004848561000017071]], [1937, [0.0036632869999948525, 0.00493345400002454]], [1960, [0.0036145729999930154, 0.004811176000004025]], [1996, [0.00375721499995052, 0.0049729269999261305]], [2029, [0.0037106409999978496, 0.004899473999955717]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.00010400499999718704, 0.0005328339999977061]], [1672, [0.00010086800000408402, 0.0005549249999887707]], [1687, [9.818199998790078e-05, 0.0005511469999817109]], [1691, [0.0001005780000014056, 0.0005555879999974422]]]
|
||||
[[1662, [0.00010400499999718704, 0.0005328339999977061]], [1672, [0.00010086800000408402, 0.0005549249999887707]], [1687, [9.818199998790078e-05, 0.0005511469999817109]], [1691, [0.0001005780000014056, 0.0005555879999974422]], [1709, [0.00012266099997759738, 0.0005711430000019391]], [1726, [0.00011641800000461444, 0.0005489540000098714]], [1766, [0.00011609900002440554, 0.0005779780000239043]], [1770, [0.0001176700000087294, 0.0005864990000077341]], [1776, [0.00011622699999236374, 0.0005842630000074678]], [1801, [0.00011665800002447213, 0.000582710000003317]], [1937, [0.00012153600005149201, 0.0005999570000199128]], [1960, [0.00012332000000014887, 0.0005915369999911491]], [1996, [0.00012686900004155177, 0.0006182140000419167]], [2029, [0.00012706900002967814, 0.0006100459999629493]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.21775109000003567, 0.21398552899995593]], [1672, [0.22476057199997967, 0.22048105400000395]], [1687, [0.21809406599999193, 0.2131839880000257]], [1691, [0.22356123500000535, 0.22167734499998915]]]
|
||||
[[1662, [0.21775109000003567, 0.21398552899995593]], [1672, [0.22476057199997967, 0.22048105400000395]], [1687, [0.21809406599999193, 0.2131839880000257]], [1691, [0.22356123500000535, 0.22167734499998915]], [1709, [0.22133603999998286, 0.21805855799999563]], [1726, [0.2166100470000174, 0.21420494400001644]], [1766, [0.22339861599999722, 0.22020213500002228]], [1770, [0.22985272800002576, 0.22544496099999378]], [1776, [0.22073260000001937, 0.2182690520000392]], [1801, [0.224061646999985, 0.2246476189999953]], [1937, [0.22743783699991127, 0.226070988999993]], [1960, [0.2252378419999843, 0.2247263650000093]], [1996, [0.23076480500003527, 0.23163660399995933]], [2029, [0.22799248500001568, 0.22723498599998493]]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.19832900800003017], [1672, 0.20217585500000723], [1687, 0.19726691500000015], [1691, 0.20350580199999513]]
|
||||
[[1662, 0.19832900800003017], [1672, 0.20217585500000723], [1687, 0.19726691500000015], [1691, 0.20350580199999513], [1709, 0.19950735400001918], [1726, 0.19625152499997967], [1766, 0.20073733000003813], [1770, 0.20376683500001036], [1776, 0.19919827600000417], [1801, 0.2053688209999791], [1937, 0.2063091950000171], [1960, 0.20468290799999522], [1996, 0.21042045099989082], [2029, 0.2056691309999792]]
|
|
@ -1 +1 @@
|
|||
[[1662, [54439936.0, 53968896.0]], [1672, [54616064.0, 54140928.0]], [1687, [54767616.0, 54296576.0]], [1691, [54743040.0, 54087680.0]]]
|
||||
[[1662, [54439936.0, 53968896.0]], [1672, [54616064.0, 54140928.0]], [1687, [54767616.0, 54296576.0]], [1691, [54743040.0, 54087680.0]], [1709, [55001088.0, 54312960.0]], [1726, [54992896.0, 54345728.0]], [1766, [55373824.0, 54894592.0]], [1770, [55246848.0, 54898688.0]], [1776, [55357440.0, 54874112.0]], [1801, [55382016.0, 54882304.0]], [1937, [55222272.0, 54722560.0]], [1960, [55263232.0, 54693888.0]], [1996, [55476224.0, 54968320.0]], [2029, [56090624.0, 55582720.0]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [54968320.0, 54792192.0]], [1672, [54849536.0, 54841344.0]], [1687, [55271424.0, 55304192.0]], [1691, [54984704.0, 54964224.0]]]
|
||||
[[1662, [54968320.0, 54792192.0]], [1672, [54849536.0, 54841344.0]], [1687, [55271424.0, 55304192.0]], [1691, [54984704.0, 54964224.0]], [1709, [55439360.0, 55369728.0]], [1726, [55455744.0, 55177216.0]], [1766, [55545856.0, 55631872.0]], [1770, [55812096.0, 55611392.0]], [1776, [55640064.0, 55631872.0]], [1801, [55812096.0, 55902208.0]], [1937, [56008704.0, 56143872.0]], [1960, [55783424.0, 56160256.0]], [1996, [56352768.0, 56516608.0]], [2029, [56786944.0, 56778752.0]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [44187648.0, 44183552.0]], [1672, [44048384.0, 44048384.0]], [1687, [44314624.0, 44310528.0]], [1691, [44179456.0, 44175360.0]]]
|
||||
[[1662, [44187648.0, 44183552.0]], [1672, [44048384.0, 44048384.0]], [1687, [44314624.0, 44310528.0]], [1691, [44179456.0, 44175360.0]], [1709, [44314624.0, 44310528.0]], [1726, [44314624.0, 44314624.0]], [1766, [44318720.0, 44314624.0]], [1770, [44322816.0, 44314624.0]], [1776, [44306432.0, 44240896.0]], [1801, [44453888.0, 44453888.0]], [1937, [44744704.0, 44744704.0]], [1960, [44838912.0, 44838912.0]], [1996, [44969984.0, 44969984.0]], [2029, [44843008.0, 44851200.0]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [44187648.0, 44187648.0]], [1672, [44052480.0, 44052480.0]], [1687, [44314624.0, 44310528.0]], [1691, [44179456.0, 44179456.0]]]
|
||||
[[1662, [44187648.0, 44187648.0]], [1672, [44052480.0, 44052480.0]], [1687, [44314624.0, 44310528.0]], [1691, [44179456.0, 44179456.0]], [1709, [44310528.0, 44314624.0]], [1726, [44314624.0, 44314624.0]], [1766, [44310528.0, 44314624.0]], [1770, [44314624.0, 44318720.0]], [1776, [44437504.0, 44437504.0]], [1801, [44449792.0, 44449792.0]], [1937, [44744704.0, 44744704.0]], [1960, [44965888.0, 44834816.0]], [1996, [44974080.0, 44974080.0]], [2029, [44982272.0, 44986368.0]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.2574955810000006, 0.2591010970000127]], [1672, [0.2600247560000071, 0.26185358800000813]], [1687, [0.2567828300000201, 0.2602957870000182]], [1691, [0.259077934000004, 0.2619792840000059]]]
|
||||
[[1662, [0.2574955810000006, 0.2591010970000127]], [1672, [0.2600247560000071, 0.26185358800000813]], [1687, [0.2567828300000201, 0.2602957870000182]], [1691, [0.259077934000004, 0.2619792840000059]], [1709, [0.2646600410000133, 0.2676605120000204]], [1726, [0.2570519909999689, 0.2606809000000112]], [1766, [0.262679922000018, 0.2686107789999994]], [1770, [0.265977821000007, 0.26914772099999595]], [1776, [0.2626667089999728, 0.2663110299999971]], [1801, [0.2658582709999848, 0.2712929850000023]], [1937, [0.2675778039999841, 0.2724974679999832]], [1960, [0.26819597400000816, 0.2740507329999957]], [1996, [0.2794132599999557, 0.28440619299999526]], [2029, [0.2920349000000044, 0.2976166970000236]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.14273938200000202, 0.1464969190000147]], [1672, [0.14515931700000806, 0.14909453600000688]], [1687, [0.1423055980000072, 0.14642362500001127]], [1691, [0.1436571560000175, 0.14915657599999577]]]
|
||||
[[1662, [0.14273938200000202, 0.1464969190000147]], [1672, [0.14515931700000806, 0.14909453600000688]], [1687, [0.1423055980000072, 0.14642362500001127]], [1691, [0.1436571560000175, 0.14915657599999577]], [1709, [0.14860135300000366, 0.15305296299999327]], [1726, [0.14520097999997006, 0.14991973799999414]], [1766, [0.15071133700001837, 0.15540660900001058]], [1770, [0.15150350199996865, 0.1558047899999906]], [1776, [0.14876902899999322, 0.15549233400000162]], [1801, [0.15248822700002052, 0.15465820200000735]], [1937, [0.15459265900005903, 0.15926110200007315]], [1960, [0.15396625699997912, 0.16023626799997714]], [1996, [0.16650312799993117, 0.17177308600003016]], [2029, [0.17414895399997476, 0.178393189000019]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.004720848000005162, 0.004705489000002672]], [1672, [0.004856270999994194, 0.00490694800001279]], [1687, [0.00473016699999107, 0.004734037999980956]], [1691, [0.004871503999993365, 0.0048899079999955575]]]
|
||||
[[1662, [0.004720848000005162, 0.004705489000002672]], [1672, [0.004856270999994194, 0.00490694800001279]], [1687, [0.00473016699999107, 0.004734037999980956]], [1691, [0.004871503999993365, 0.0048899079999955575]], [1709, [0.0048215560000244295, 0.004858458999990489]], [1726, [0.004671787999996013, 0.004672599999992144]], [1766, [0.00478528000002143, 0.0047485900000197034]], [1770, [0.004901490999998259, 0.004895917999988342]], [1776, [0.00480728600001612, 0.00472804499997892]], [1801, [0.004847185000016907, 0.004857667999999649]], [1937, [0.004923484000073586, 0.004925836999973399]], [1960, [0.004825538000005736, 0.0047952310000027865]], [1996, [0.005049280000093859, 0.004947880000145233]], [2029, [0.004897051999989799, 0.004863266000029398]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.0005377129999999397, 0.0005395769999836375]], [1672, [0.000547750000009728, 0.0005677989999810507]], [1687, [0.0005471899999918151, 0.0005447550000212686]], [1691, [0.0005559489999882317, 0.0005480739999939033]]]
|
||||
[[1662, [0.0005377129999999397, 0.0005395769999836375]], [1672, [0.000547750000009728, 0.0005677989999810507]], [1687, [0.0005471899999918151, 0.0005447550000212686]], [1691, [0.0005559489999882317, 0.0005480739999939033]], [1709, [0.0005736080000247057, 0.0005720849999875099]], [1726, [0.000542692999999872, 0.0005430530000012368]], [1766, [0.0005853119999983392, 0.000582014999963576]], [1770, [0.0005929909999622396, 0.000583071999983531]], [1776, [0.0005810670000130358, 0.000576186999978745]], [1801, [0.0005717709999828458, 0.0005785939999896073]], [1937, [0.0005969709999362749, 0.0005864510000037626]], [1960, [0.0005953940000154034, 0.0005933700000468889]], [1996, [0.0006160310000495883, 0.0006166809999967882]], [2029, [0.0006159270000125616, 0.0006080119999865019]]]
|
|
@ -1 +1 @@
|
|||
[[1662, [0.21402431699999624, 0.21364062999998623]], [1672, [0.2221746719999942, 0.2222580240000127]], [1687, [0.2142312400000037, 0.21397752699999728]], [1691, [0.22129613300000983, 0.21942976399998315]]]
|
||||
[[1662, [0.21402431699999624, 0.21364062999998623]], [1672, [0.2221746719999942, 0.2222580240000127]], [1687, [0.2142312400000037, 0.21397752699999728]], [1691, [0.22129613300000983, 0.21942976399998315]], [1709, [0.2199001029999863, 0.22046102699999892]], [1726, [0.2147675530000015, 0.21506381099999317]], [1766, [0.22056839900000114, 0.21916191200000412]], [1770, [0.22394285699999728, 0.22330144500000415]], [1776, [0.21867883100003382, 0.21859779499999377]], [1801, [0.22378945699995256, 0.22211803700002974]], [1937, [0.22545313400001987, 0.22602228000005198]], [1960, [0.22564571399999522, 0.22598634599995648]], [1996, [0.2295973340000046, 0.23030742100002044]], [2029, [0.22777395400004252, 0.2292747939999913]]]
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
[[1662, 53737309.613078326], [1672, 53463506.59363525], [1687, 53427924.42970294], [1691, 53807508.99158667]]
|
||||
[[1662, 53737309.613078326], [1672, 53463506.59363525], [1687, 53427924.42970294], [1691, 53807508.99158667], [1709, 53963042.655257314], [1726, 53670369.245800875], [1766, 54220916.6140389], [1770, 54198077.75539557], [1776, 53906774.26269022], [1801, 54270509.344660625], [1937, 54055804.31664803], [1960, 54295416.494559616], [1996, 54277301.04707094], [2029, 54376892.25474807]]
|
|
@ -1 +1 @@
|
|||
[[1662, 54265895.0709751], [1672, 53676080.7209516], [1687, 53675997.57883592], [1691, 54512993.537089705]]
|
||||
[[1662, 54265895.0709751], [1672, 53676080.7209516], [1687, 53675997.57883592], [1691, 54512993.537089705], [1709, 54605449.27839023], [1726, 53697436.790693834], [1766, 54766004.5031032], [1770, 54878384.55144014], [1776, 53912680.86221259], [1801, 54851721.60114168], [1937, 54505276.07990639], [1960, 54599968.83944605], [1996, 54678155.56971878], [2029, 54725974.50425164]]
|
|
@ -1 +1 @@
|
|||
[[1662, 44191743.99999999], [1672, 44052479.80957694], [1687, 44251096.14326895], [1691, 44179455.81012423]]
|
||||
[[1662, 44191743.99999999], [1672, 44052479.80957694], [1687, 44251096.14326895], [1691, 44179455.81012423], [1709, 44253141.3491094], [1726, 44255192.14695785], [1766, 44318719.81072088], [1770, 44324863.95268679], [1776, 44244949.34121254], [1801, 44324676.21343578], [1937, 44750847.578234404], [1960, 44775384.609963], [1996, 44842828.229087956], [2029, 44892155.328466915]]
|
|
@ -1 +1 @@
|
|||
[[1662, 44191743.81017703], [1672, 43988933.59873213], [1687, 44208009.40445502], [1691, 44185599.95253766]]
|
||||
[[1662, 44191743.81017703], [1672, 43988933.59873213], [1687, 44208009.40445502], [1691, 44185599.95253766], [1709, 44314453.63272547], [1726, 44318719.81072088], [1766, 44316671.57410231], [1770, 44205956.60747199], [1776, 44376021.4672124], [1801, 44322622.19567646], [1937, 44636028.0238471], [1960, 44777429.84849827], [1996, 44846935.655543014], [2029, 44801668.84315699]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.13350944016163727], [1672, 0.1370189324406613], [1687, 0.13338881256624893], [1691, 0.13534306127506]]
|
||||
[[1662, 0.13350944016163727], [1672, 0.1370189324406613], [1687, 0.13338881256624893], [1691, 0.13534306127506], [1709, 0.14028461383291016], [1726, 0.1369248426273554], [1766, 0.13857329097819557], [1770, 0.14073477092350728], [1776, 0.1385645020210802], [1801, 0.14040196312080028], [1937, 0.14582964264952603], [1960, 0.14671897491501892], [1996, 0.15202819951982394], [2029, 0.15457268328939747]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.0687644082522681], [1672, 0.06987734456556612], [1687, 0.06874611472573841], [1691, 0.07039998567606925]]
|
||||
[[1662, 0.0687644082522681], [1672, 0.06987734456556612], [1687, 0.06874611472573841], [1691, 0.07039998567606925], [1709, 0.07467771106069107], [1726, 0.07307627413528986], [1766, 0.0752258677863117], [1770, 0.07591381717343901], [1776, 0.0741750279629251], [1801, 0.07486521068773488], [1937, 0.08182795598310513], [1960, 0.08198138820511656], [1996, 0.08497198126158123], [2029, 0.08730133488241124]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.004071198582731586], [1672, 0.004168318834979474], [1687, 0.004071589002161507], [1691, 0.004235212007582172]]
|
||||
[[1662, 0.004071198582731586], [1672, 0.004168318834979474], [1687, 0.004071589002161507], [1691, 0.004235212007582172], [1709, 0.004181923314217816], [1726, 0.004054429932062044], [1766, 0.004133897799028137], [1770, 0.004257194320585938], [1776, 0.004117446125697445], [1801, 0.004200816754404154], [1937, 0.004251194879485355], [1960, 0.0041701734817425696], [1996, 0.004322540447211732], [2029, 0.004263823296369016]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.00023540900613243872], [1672, 0.0002365886195511814], [1687, 0.0002326213978668684], [1691, 0.0002363893607261623]]
|
||||
[[1662, 0.00023540900613243872], [1672, 0.0002365886195511814], [1687, 0.0002326213978668684], [1691, 0.0002363893607261623], [1709, 0.0002646827752432008], [1726, 0.00025280056719810247], [1766, 0.00025904182642747317], [1770, 0.0002627038966898471], [1776, 0.00026058997620285855], [1801, 0.000260725493948419], [1937, 0.0002700303204925571], [1960, 0.00027008950893915996], [1996, 0.0002800574798090668], [2029, 0.000278420428825539]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.21586009863792485], [1672, 0.22261052942796597], [1687, 0.21562505130206716], [1691, 0.2226172972159168]]
|
||||
[[1662, 0.21586009863792485], [1672, 0.22261052942796597], [1687, 0.21562505130206716], [1691, 0.2226172972159168], [1709, 0.21969118716012626], [1726, 0.21540413874268913], [1766, 0.2217946171557135], [1770, 0.22763817627917332], [1776, 0.21949736979633283], [1801, 0.22435444169386096], [1937, 0.22675338309844276], [1960, 0.22498195815021013], [1996, 0.23120029358312028], [2029, 0.22761342037999505]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.19832900800003017], [1672, 0.20217585500000723], [1687, 0.19726691500000015], [1691, 0.20350580199999513]]
|
||||
[[1662, 0.19832900800003017], [1672, 0.20217585500000723], [1687, 0.19726691500000015], [1691, 0.20350580199999513], [1709, 0.19950735400001918], [1726, 0.19625152499997967], [1766, 0.20073733000003813], [1770, 0.20376683500001036], [1776, 0.19919827600000417], [1801, 0.2053688209999791], [1937, 0.2063091950000171], [1960, 0.20468290799999522], [1996, 0.21042045099989082], [2029, 0.2056691309999792]]
|
|
@ -1 +1 @@
|
|||
[[1662, 54203904.32644733], [1672, 54377977.05567385], [1687, 54531587.401090905], [1691, 54414373.37457081]]
|
||||
[[1662, 54203904.32644733], [1672, 54377977.05567385], [1687, 54531587.401090905], [1691, 54414373.37457081], [1709, 54655941.05401974], [1726, 54668354.35558938], [1766, 55133687.30603648], [1770, 55072492.873806104], [1776, 55115246.19008138], [1801, 55131593.83007953], [1937, 54971848.18483294], [1960, 54977822.99733244], [1996, 55221688.06930552], [2029, 55836094.494666085]]
|
|
@ -1 +1 @@
|
|||
[[1662, 54880185.34368702], [1672, 54845439.84705003], [1687, 55287805.57238104], [1691, 54974463.04630629]]
|
||||
[[1662, 54880185.34368702], [1672, 54845439.84705003], [1687, 55287805.57238104], [1691, 54974463.04630629], [1709, 55404533.06087942], [1726, 55316304.695168346], [1766, 55588847.36277981], [1770, 55711653.6193069], [1776, 55635967.849223286], [1801, 55857133.82825839], [1937, 56076247.273349956], [1960, 55971522.87008585], [1996, 56434628.542863145], [2029, 56782847.85226863]]
|
|
@ -1 +1 @@
|
|||
[[1662, 44185599.95253766], [1672, 44048383.99999999], [1687, 44312575.95267366], [1691, 44177407.95252886]]
|
||||
[[1662, 44185599.95253766], [1672, 44048383.99999999], [1687, 44312575.95267366], [1691, 44177407.95252886], [1709, 44312575.95267366], [1726, 44314624.0], [1766, 44316671.95267803], [1770, 44318719.81072088], [1776, 44273651.87380721], [1801, 44453888.0], [1937, 44744704.0], [1960, 44838912.00000001], [1996, 44969983.99999999], [2029, 44847103.812950954]]
|
|
@ -1 +1 @@
|
|||
[[1662, 44187648.0], [1672, 44052479.99999999], [1687, 44312575.95267366], [1691, 44179455.99999999]]
|
||||
[[1662, 44187648.0], [1672, 44052479.99999999], [1687, 44312575.95267366], [1691, 44179455.99999999], [1709, 44312575.95267366], [1726, 44314624.0], [1766, 44312575.95267366], [1770, 44316671.95267803], [1776, 44437504.0], [1801, 44449792.0], [1937, 44744704.0], [1960, 44900304.17220587], [1996, 44974080.0], [2029, 44984319.953380376]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.2582970915627115], [1672, 0.2609375697890752], [1687, 0.2585333418012986], [1691, 0.2605245701455466]]
|
||||
[[1662, 0.2582970915627115], [1672, 0.2609375697890752], [1687, 0.2585333418012986], [1691, 0.2605245701455466], [1709, 0.26615604836262874], [1726, 0.25886008645727265], [1766, 0.2656287982807661], [1770, 0.2675580766089799], [1776, 0.2644825926606367], [1801, 0.26856188100049755], [1937, 0.2700264321932048], [1960, 0.2711075492537049], [1996, 0.28189867248766043], [2029, 0.29481258851469266]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.144605946222714], [1672, 0.14711376894836906], [1687, 0.14434992731884352], [1691, 0.14638104217028877]]
|
||||
[[1662, 0.144605946222714], [1672, 0.14711376894836906], [1687, 0.14434992731884352], [1691, 0.14638104217028877], [1709, 0.1508107336447194], [1726, 0.14754149544768042], [1766, 0.15304096778650703], [1770, 0.1536390943522132], [1776, 0.15209353551720362], [1801, 0.15356938175949056], [1937, 0.15690951925702573], [1960, 0.15706997937098616], [1996, 0.16911758076913888], [2029, 0.17625829701058932]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.004713162243622524], [1672, 0.004881543738505435], [1687, 0.004732102104161917], [1691, 0.00488069732533968]]
|
||||
[[1662, 0.004713162243622524], [1672, 0.004881543738505435], [1687, 0.004732102104161917], [1691, 0.00488069732533968], [1709, 0.004839972328668506], [1726, 0.004672193982353972], [1766, 0.004766899700580667], [1770, 0.004898703707479391], [1776, 0.004767500868992566], [1801, 0.004852423669122516], [1937, 0.004924660359490744], [1960, 0.004810360631940079], [1996, 0.004998322871483767], [2029, 0.004880129761791827]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.0005386441936864901], [1672, 0.0005576844109755481], [1687, 0.0005459711425132094], [1691, 0.0005519974567116778]]
|
||||
[[1662, 0.0005386441936864901], [1672, 0.0005576844109755481], [1687, 0.0005459711425132094], [1691, 0.0005519974567116778], [1709, 0.0005728459938648165], [1726, 0.0005428729701593198], [1766, 0.0005836611719634209], [1770, 0.0005880105852110292], [1776, 0.0005786218553806627], [1801, 0.0005751723828193878], [1937, 0.0005916876201898046], [1960, 0.000594381138510516], [1996, 0.0006163559143381376], [2029, 0.0006119567036345985]]
|
|
@ -1 +1 @@
|
|||
[[1662, 0.21383238744211777], [1672, 0.22221634409189991], [1687, 0.21410434591886193], [1691, 0.2203609725843055]]
|
||||
[[1662, 0.21383238744211777], [1672, 0.22221634409189991], [1687, 0.21410434591886193], [1691, 0.2203609725843055], [1709, 0.22018038637622225], [1726, 0.2149156309515977], [1766, 0.2198640308272821], [1770, 0.22362192103085216], [1776, 0.21863830924562072], [1801, 0.22295218072522197], [1937, 0.22573752762853083], [1960, 0.22581596577171012], [1996, 0.22995210340856065], [2029, 0.2285231418957897]]
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"asv-version": "0.6.4",
|
||||
"timestamp": 1742766051964
|
||||
"timestamp": 1753049912703
|
||||
}
|
|
@ -1 +1 @@
|
|||
{"regressions": []}
|
||||
{"regressions": [["Components vs Django.timeraw_render_lg_subsequent('django')", "graphs/branch-master/django-5.1/djc-core-html-parser/machine-ci-linux/python-3.13/Components vs Django.timeraw_render_lg_subsequent.json", {}, 0, 0.04362213900003553, 0.03327357099999517, [[1691, 1709, 0.03327357099999517, 0.03723830100000214], [1801, 1937, 0.03723830100000214, 0.04362213900003553]]]]}
|
|
@ -1,2 +1,8 @@
|
|||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"><id>tag:django-components.asv,1970-01-01:/cddbdcca8b398afd301fbfc73cc4d51103d4e3059c0e6b938d4c467ad3d1aa25</id><author><name>Airspeed Velocity</name></author><title xml:lang="en">django-components performance regressions</title><updated>2025-03-23T21:40:51Z</updated></feed>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom"><id>tag:django-components.asv,1970-01-01:/cddbdcca8b398afd301fbfc73cc4d51103d4e3059c0e6b938d4c467ad3d1aa25</id><author><name>Airspeed Velocity</name></author><title xml:lang="en">django-components performance regressions</title><updated>2025-06-04T22:41:21Z</updated><entry><id>tag:django-components.asv,2025-06-04:/38b868a38890eb5bccfd51983abf3a15cedf2846ef6cf2452e941122dbde2bde</id><title xml:lang="en">17.14% Components vs Django.timeraw_render_lg_subsequent('django')</title><updated>2025-06-04T22:41:21Z</updated><link href="index.html#Components vs Django.timeraw_render_lg_subsequent?p-renderer=%27django%27&commits=4c909486069f3c3c8ee7915239174f820f081da4-7b24b86f4a836c697acba926d9d6602afa45418d" /><content xml:lang="en" type="html"><a href="index.html#Components vs Django.timeraw_render_lg_subsequent?p-renderer=%27django%27&commits=4c909486069f3c3c8ee7915239174f820f081da4-7b24b86f4a836c697acba926d9d6602afa45418d">17.14% regression</a> on 2025-06-04 22:35:21 in commits <a href="#4c909486069f3c3c8ee7915239174f820f081da4">4c909486...7b24b86f</a>.<br>
|
||||
New value: 43.6ms, old value: 37.2ms.<br>
|
||||
Latest value: 43.6ms (31.10% worse
|
||||
than best value 33.3ms).</content></entry><entry><id>tag:django-components.asv,2025-03-31:/7a13128cbc4d175ca09ebda40e8a303789275bd84b00a5d496cfa08a26ad2f8b</id><title xml:lang="en">11.92% Components vs Django.timeraw_render_lg_subsequent('django')</title><updated>2025-03-31T14:16:53Z</updated><link href="index.html#Components vs Django.timeraw_render_lg_subsequent?p-renderer=%27django%27&commits=42818ad6ffb47bd650d8a379b84c3d48394f9f77-a6455d70f6c28ddbd4be8e58902f6cbc101e5ff3" /><content xml:lang="en" type="html"><a href="index.html#Components vs Django.timeraw_render_lg_subsequent?p-renderer=%27django%27&commits=42818ad6ffb47bd650d8a379b84c3d48394f9f77-a6455d70f6c28ddbd4be8e58902f6cbc101e5ff3">11.92% regression</a> on 2025-03-31 14:10:42 in commits <a href="#42818ad6ffb47bd650d8a379b84c3d48394f9f77">42818ad6...a6455d70</a>.<br>
|
||||
New value: 37.2ms, old value: 33.3ms.<br>
|
||||
Latest value: 43.6ms (31.10% worse
|
||||
than best value 33.3ms).</content></entry></feed>
|
|
@ -5,9 +5,10 @@ nav:
|
|||
- Prop drilling and provide / inject: provide_inject.md
|
||||
- Lifecycle hooks: hooks.md
|
||||
- Registering components: component_registry.md
|
||||
- Typing and validation: typing_and_validation.md
|
||||
- Component caching: component_caching.md
|
||||
- Component context and scope: component_context_scope.md
|
||||
- Custom template tags: template_tags.md
|
||||
- Tag formatters: tag_formatter.md
|
||||
- Tag formatters: tag_formatters.md
|
||||
- Extensions: extensions.md
|
||||
- Testing: testing.md
|
||||
- Authoring component libraries: authoring_component_libraries.md
|
||||
- Component libraries: component_libraries.md
|
||||
|
|
181
docs/concepts/advanced/component_caching.md
Normal file
181
docs/concepts/advanced/component_caching.md
Normal file
|
@ -0,0 +1,181 @@
|
|||
Component caching allows you to store the rendered output of a component. Next time the component is rendered
|
||||
with the same input, the cached output is returned instead of re-rendering the component.
|
||||
|
||||
This is particularly useful for components that are expensive to render or do not change frequently.
|
||||
|
||||
!!! info
|
||||
|
||||
Component caching uses [Django's cache framework](https://docs.djangoproject.com/en/5.2/topics/cache/),
|
||||
so you can use any cache backend that is supported by Django.
|
||||
|
||||
### Enabling caching
|
||||
|
||||
Caching is disabled by default.
|
||||
|
||||
To enable caching for a component, set [`Component.Cache.enabled`](../../reference/api.md#django_components.ComponentCache.enabled) to `True`:
|
||||
|
||||
```python
|
||||
from django_components import Component
|
||||
|
||||
class MyComponent(Component):
|
||||
class Cache:
|
||||
enabled = True
|
||||
```
|
||||
|
||||
### Time-to-live (TTL)
|
||||
|
||||
You can specify a time-to-live (TTL) for the cache entry with [`Component.Cache.ttl`](../../reference/api.md#django_components.ComponentCache.ttl), which determines how long the entry remains valid. The TTL is specified in seconds.
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class Cache:
|
||||
enabled = True
|
||||
ttl = 60 * 60 * 24 # 1 day
|
||||
```
|
||||
|
||||
- If `ttl > 0`, entries are cached for the specified number of seconds.
|
||||
- If `ttl = -1`, entries are cached indefinitely.
|
||||
- If `ttl = 0`, entries are not cached.
|
||||
- If `ttl = None`, the default TTL is used.
|
||||
|
||||
### Custom cache name
|
||||
|
||||
Since component caching uses Django's cache framework, you can specify a custom cache name with [`Component.Cache.cache_name`](../../reference/api.md#django_components.ComponentCache.cache_name) to use a different cache backend:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class Cache:
|
||||
enabled = True
|
||||
cache_name = "my_cache"
|
||||
```
|
||||
|
||||
### Cache key generation
|
||||
|
||||
By default, the cache key is generated based on the component's input (args and kwargs). So the following two calls would generate separate entries in the cache:
|
||||
|
||||
```py
|
||||
MyComponent.render(name="Alice")
|
||||
MyComponent.render(name="Bob")
|
||||
```
|
||||
|
||||
However, you have full control over the cache key generation. As such, you can:
|
||||
|
||||
- Cache the component on all inputs (default)
|
||||
- Cache the component on particular inputs
|
||||
- Cache the component irrespective of the inputs
|
||||
|
||||
To achieve that, you can override
|
||||
the [`Component.Cache.hash()`](../../reference/api.md#django_components.ComponentCache.hash)
|
||||
method to customize how arguments are hashed into the cache key.
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class Cache:
|
||||
enabled = True
|
||||
|
||||
def hash(self, *args, **kwargs):
|
||||
return f"{json.dumps(args)}:{json.dumps(kwargs)}"
|
||||
```
|
||||
|
||||
For even more control, you can override other methods available on the [`ComponentCache`](../../reference/api.md#django_components.ComponentCache) class.
|
||||
|
||||
!!! warning
|
||||
|
||||
The default implementation of `Cache.hash()` simply serializes the input into a string.
|
||||
As such, it might not be suitable if you need to hash complex objects like Models.
|
||||
|
||||
### Caching slots
|
||||
|
||||
By default, the cache key is generated based ONLY on the args and kwargs.
|
||||
|
||||
To cache the component based on the slots, set [`Component.Cache.include_slots`](../../reference/api.md#django_components.ComponentCache.include_slots) to `True`:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class Cache:
|
||||
enabled = True
|
||||
include_slots = True
|
||||
```
|
||||
|
||||
with `include_slots = True`, the cache key will be generated also based on the given slots.
|
||||
|
||||
As such, the following two calls would generate separate entries in the cache:
|
||||
|
||||
```django
|
||||
{% component "my_component" position="left" %}
|
||||
Hello, Alice
|
||||
{% endcomponent %}
|
||||
|
||||
{% component "my_component" position="left" %}
|
||||
Hello, Bob
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
Same when using [`Component.render()`](../../reference/api.md#django_components.Component.render) with string slots:
|
||||
|
||||
```py
|
||||
MyComponent.render(
|
||||
kwargs={"position": "left"},
|
||||
slots={"content": "Hello, Alice"}
|
||||
)
|
||||
MyComponent.render(
|
||||
kwargs={"position": "left"},
|
||||
slots={"content": "Hello, Bob"}
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
Passing slots as functions to cached components with `include_slots=True` will raise an error.
|
||||
|
||||
```py
|
||||
MyComponent.render(
|
||||
kwargs={"position": "left"},
|
||||
slots={"content": lambda ctx: "Hello, Alice"}
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
Slot caching DOES NOT account for context variables within
|
||||
the [`{% fill %}`](../../reference/template_tags.md#fill) tag.
|
||||
|
||||
For example, the following two cases will be treated as the same entry:
|
||||
|
||||
```django
|
||||
{% with my_var="foo" %}
|
||||
{% component "mycomponent" name="foo" %}
|
||||
{{ my_var }}
|
||||
{% endcomponent %}
|
||||
{% endwith %}
|
||||
|
||||
{% with my_var="bar" %}
|
||||
{% component "mycomponent" name="bar" %}
|
||||
{{ my_var }}
|
||||
{% endcomponent %}
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
Currently it's impossible to capture used variables. This will be addressed in v2.
|
||||
Read more about it in [django-components/#1164](https://github.com/django-components/django-components/issues/1164).
|
||||
|
||||
### Example
|
||||
|
||||
Here's a complete example of a component with caching enabled:
|
||||
|
||||
```python
|
||||
from django_components import Component
|
||||
|
||||
class MyComponent(Component):
|
||||
template = "Hello, {{ name }}"
|
||||
|
||||
class Cache:
|
||||
enabled = True
|
||||
ttl = 300 # Cache for 5 minutes
|
||||
cache_name = "my_cache"
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"name": kwargs["name"]}
|
||||
```
|
||||
|
||||
In this example, the component's rendered output is cached for 5 minutes using the `my_cache` backend.
|
|
@ -12,7 +12,7 @@ NOTE: `{% csrf_token %}` tags need access to the top-level context, and they wil
|
|||
|
||||
If you find yourself using the `only` modifier often, you can set the [context_behavior](#context-behavior) option to `"isolated"`, which automatically applies the `only` modifier. This is useful if you want to make sure that components don't accidentally access the outer context.
|
||||
|
||||
Components can also access the outer context in their context methods like `get_context_data` by accessing the property `self.outer_context`.
|
||||
Components can also access the outer context in their context methods like `get_template_data` by accessing the property `self.outer_context`.
|
||||
|
||||
## Example of Accessing Outer Context
|
||||
|
||||
|
@ -22,14 +22,14 @@ Components can also access the outer context in their context methods like `get_
|
|||
</div>
|
||||
```
|
||||
|
||||
Assuming that the rendering context has variables such as `date`, you can use `self.outer_context` to access them from within `get_context_data`. Here's how you might implement it:
|
||||
Assuming that the rendering context has variables such as `date`, you can use `self.outer_context` to access them from within `get_template_data`. Here's how you might implement it:
|
||||
|
||||
```python
|
||||
class Calender(Component):
|
||||
|
||||
...
|
||||
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
outer_field = self.outer_context["date"]
|
||||
return {
|
||||
"date": outer_fields,
|
||||
|
@ -53,10 +53,10 @@ This has two modes:
|
|||
you can access are a union of:
|
||||
|
||||
- All the variables that were OUTSIDE the fill tag, including any\
|
||||
[`{% with %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#with) tags.
|
||||
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#cycle))
|
||||
[`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#with) tags.
|
||||
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#cycle))
|
||||
that the `{% fill %}` tag is part of.
|
||||
- Data returned from [`Component.get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
|
||||
- Data returned from [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
of the component that owns the fill tag.
|
||||
|
||||
- `"isolated"`
|
||||
|
@ -67,14 +67,14 @@ This has two modes:
|
|||
|
||||
Inside the [`{% fill %}`](../../../reference/template_tags#fill) tag, you can ONLY access variables from 2 places:
|
||||
|
||||
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#cycle))
|
||||
- Any loops ([`{% for ... %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#cycle))
|
||||
that the `{% fill %}` tag is part of.
|
||||
- [`Component.get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
|
||||
- [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
of the component which defined the template (AKA the "root" component).
|
||||
|
||||
!!! warning
|
||||
|
||||
Notice that the component whose `get_context_data()` we use inside
|
||||
Notice that the component whose `get_template_data()` we use inside
|
||||
[`{% fill %}`](../../../reference/template_tags#fill)
|
||||
is NOT the same across the two modes!
|
||||
|
||||
|
@ -93,10 +93,10 @@ This has two modes:
|
|||
"""
|
||||
```
|
||||
|
||||
- `"django"` - `my_var` has access to data from `get_context_data()` of both `Inner` and `Outer`.
|
||||
- `"django"` - `my_var` has access to data from `get_template_data()` of both `Inner` and `Outer`.
|
||||
If there are variables defined in both, then `Inner` overshadows `Outer`.
|
||||
|
||||
- `"isolated"` - `my_var` has access to data from `get_context_data()` of ONLY `Outer`.
|
||||
- `"isolated"` - `my_var` has access to data from `get_template_data()` of ONLY `Outer`.
|
||||
|
||||
|
||||
### Example "django"
|
||||
|
@ -115,11 +115,11 @@ class RootComp(Component):
|
|||
{% endwith %}
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return { "my_var": 123 }
|
||||
```
|
||||
|
||||
Then if [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
|
||||
Then if [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
of the component `"my_comp"` returns following data:
|
||||
|
||||
```py
|
||||
|
@ -154,11 +154,11 @@ class RootComp(Component):
|
|||
{% endwith %}
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return { "my_var": 123 }
|
||||
```
|
||||
|
||||
Then if [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
|
||||
Then if [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
of the component `"my_comp"` returns following data:
|
||||
|
||||
```py
|
||||
|
@ -172,10 +172,10 @@ Then the template will be rendered as:
|
|||
# cheese
|
||||
```
|
||||
|
||||
Because variables `"my_var"` and `"cheese"` are searched only inside `RootComponent.get_context_data()`.
|
||||
Because variables `"my_var"` and `"cheese"` are searched only inside `RootComponent.get_template_data()`.
|
||||
But since `"cheese"` is not defined there, it's empty.
|
||||
|
||||
!!! info
|
||||
|
||||
Notice that the variables defined with the [`{% with %}`](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#with)
|
||||
Notice that the variables defined with the [`{% with %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#with)
|
||||
tag are ignored inside the [`{% fill %}`](../../../reference/template_tags#fill) tag with the `"isolated"` mode.
|
|
@ -23,13 +23,13 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
|-- 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)
|
||||
2. Create custom [`Library`](https://docs.djangoproject.com/en/5.2/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)):
|
||||
Remember that Django requires the [`Library`](https://docs.djangoproject.com/en/5.2/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/5.2/howto/custom-template-tags)):
|
||||
|
||||
```py
|
||||
from django.template import Library
|
||||
|
@ -101,35 +101,30 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
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`.
|
||||
|
||||
```djc_py
|
||||
from typing import Dict, NotRequired, Optional, Tuple, TypedDict
|
||||
|
||||
from django_components import Component, SlotFunc, register, types
|
||||
from typing import NamedTuple, Optional
|
||||
from django_components import Component, SlotInput, 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,
|
||||
):
|
||||
class MyMenu(Component):
|
||||
# Define the types
|
||||
class Args(NamedTuple):
|
||||
size: int
|
||||
text: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
vertical: Optional[bool] = None
|
||||
klass: Optional[str] = None
|
||||
style: Optional[str] = None
|
||||
|
||||
class Slots(NamedTuple):
|
||||
default: Optional[SlotInput] = None
|
||||
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
attrs = ...
|
||||
return {
|
||||
"attrs": attrs,
|
||||
}
|
||||
|
@ -153,7 +148,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
|
||||
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)
|
||||
We recommend doing this in the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
|
||||
hook of your `apps.py`:
|
||||
|
||||
```py
|
||||
|
@ -175,7 +170,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
```
|
||||
|
||||
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).
|
||||
[`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready).
|
||||
|
||||
And that's it! The next step is to publish it.
|
||||
|
||||
|
@ -190,7 +185,7 @@ django_components uses the [`build`](https://build.pypa.io/en/stable/) utility t
|
|||
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)
|
||||
And to publish to PyPI, you can use [`twine`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.apps.AppConfig.ready)
|
||||
([See Python user guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/#uploading-the-distribution-archives))
|
||||
|
||||
```bash
|
||||
|
@ -224,7 +219,7 @@ After the package has been published, all that remains is to install it in other
|
|||
]
|
||||
```
|
||||
|
||||
3. Optionally add the template tags to the [`builtins`](https://docs.djangoproject.com/en/5.1/topics/templates/#django.template.backends.django.DjangoTemplates),
|
||||
3. Optionally add the template tags to the [`builtins`](https://docs.djangoproject.com/en/5.2/topics/templates/#django.template.backends.django.DjangoTemplates),
|
||||
so you don't have to call `{% load mytags %}` in every template:
|
||||
|
||||
```python
|
|
@ -12,9 +12,9 @@ class Calendar(Component):
|
|||
template_file = "template.html"
|
||||
|
||||
# This component takes one parameter, a date string to show in the template
|
||||
def get_context_data(self, date):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": date,
|
||||
"date": kwargs["date"],
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -6,7 +6,11 @@ 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.
|
||||
|
||||
## Setting up extensions
|
||||
## Live examples
|
||||
|
||||
- [djc-ext-pydantic](https://github.com/django-components/djc-ext-pydantic)
|
||||
|
||||
## Install extensions
|
||||
|
||||
Extensions are configured in the Django settings under [`COMPONENTS.extensions`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions).
|
||||
|
||||
|
@ -18,7 +22,7 @@ Extensions can be set by either as an import string or by passing in a class:
|
|||
class MyExtension(ComponentExtension):
|
||||
name = "my_extension"
|
||||
|
||||
class ExtensionClass(ComponentExtension.ExtensionClass):
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
...
|
||||
|
||||
COMPONENTS = ComponentsSettings(
|
||||
|
@ -38,16 +42,22 @@ Extensions can define methods to hook into lifecycle events, such as:
|
|||
- Un/registering a component
|
||||
- Creating or deleting a registry
|
||||
- Pre-processing data passed to a component on render
|
||||
- Post-processing data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
|
||||
- Post-processing data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
and others.
|
||||
|
||||
See the full list in [Extension Hooks Reference](../../../reference/extension_hooks).
|
||||
|
||||
## Configuring extensions per component
|
||||
## Per-component configuration
|
||||
|
||||
Each extension has a corresponding nested class within the [`Component`](../../../reference/api#django_components.Component) class. These allow
|
||||
to configure the extensions on a per-component basis.
|
||||
|
||||
E.g.:
|
||||
|
||||
- `"view"` extension -> [`Component.View`](../../../reference/api#django_components.Component.View)
|
||||
- `"cache"` extension -> [`Component.Cache`](../../../reference/api#django_components.Component.Cache)
|
||||
- `"defaults"` extension -> [`Component.Defaults`](../../../reference/api#django_components.Component.Defaults)
|
||||
|
||||
!!! note
|
||||
|
||||
**Accessing the component instance from inside the nested classes:**
|
||||
|
@ -57,31 +67,32 @@ to configure the extensions on a per-component basis.
|
|||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
class MyExtension:
|
||||
def get(self, request):
|
||||
# `self.component` points to the instance of `MyTable` Component.
|
||||
return self.component.get(request)
|
||||
return self.component.render_to_response(request=request)
|
||||
```
|
||||
|
||||
### Example: Component as View
|
||||
|
||||
The [Components as Views](../../fundamentals/components_as_views) feature is actually implemented as an extension
|
||||
The [Components as Views](../../fundamentals/component_views_urls) feature is actually implemented as an extension
|
||||
that is configured by a `View` nested class.
|
||||
|
||||
You can override the `get`, `post`, etc methods to customize the behavior of the component as a view:
|
||||
You can override the `get()`, `post()`, etc methods to customize the behavior of the component as a view:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component.get(request)
|
||||
return self.component_class.render_to_response(request=request)
|
||||
|
||||
def post(self, request):
|
||||
return self.component.post(request)
|
||||
return self.component_class.render_to_response(request=request)
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
<!-- TODO - LINK TO IT ONCE RELEASED -->
|
||||
### Example: Storybook integration
|
||||
|
||||
The Storybook integration (work in progress) is an extension that is configured by a `Storybook` nested class.
|
||||
|
@ -93,12 +104,12 @@ JSON file from the component.
|
|||
class MyTable(Component):
|
||||
class Storybook:
|
||||
def title(self):
|
||||
return self.component.__class__.__name__
|
||||
return self.component_cls.__name__
|
||||
|
||||
def parameters(self) -> Parameters:
|
||||
return {
|
||||
"server": {
|
||||
"id": self.component.__class__.__name__,
|
||||
"id": self.component_cls.__name__,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,17 +119,84 @@ class MyTable(Component):
|
|||
...
|
||||
```
|
||||
|
||||
## Accessing extensions in components
|
||||
### Extension defaults
|
||||
|
||||
Extensions are incredibly flexible, but configuring the same extension for every component can be a pain.
|
||||
|
||||
For this reason, django-components allows for extension defaults. This is like setting the extension config for every component.
|
||||
|
||||
To set extension defaults, use the [`COMPONENTS.extensions_defaults`](../../../reference/settings#django_components.app_settings.ComponentsSettings.extensions_defaults) setting.
|
||||
|
||||
The `extensions_defaults` setting is a dictionary where the key is the extension name and the value is a dictionary of config attributes:
|
||||
|
||||
```python
|
||||
COMPONENTS = ComponentsSettings(
|
||||
extensions=[
|
||||
"my_extension.MyExtension",
|
||||
"storybook.StorybookExtension",
|
||||
],
|
||||
extensions_defaults={
|
||||
"my_extension": {
|
||||
"key": "value",
|
||||
},
|
||||
"view": {
|
||||
"public": True,
|
||||
},
|
||||
"cache": {
|
||||
"ttl": 60,
|
||||
},
|
||||
"storybook": {
|
||||
"title": lambda self: self.component_cls.__name__,
|
||||
},
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Which is equivalent to setting the following for every component:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class MyExtension:
|
||||
key = "value"
|
||||
|
||||
class View:
|
||||
public = True
|
||||
|
||||
class Cache:
|
||||
ttl = 60
|
||||
|
||||
class Storybook:
|
||||
def title(self):
|
||||
return self.component_cls.__name__
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
If you define an attribute as a function, it is like defining a method on the extension class.
|
||||
|
||||
E.g. in the example above, `title` is a method on the `Storybook` extension class.
|
||||
|
||||
As the name suggests, these are defaults, and so you can still selectively override them on a per-component basis:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View:
|
||||
public = False
|
||||
```
|
||||
|
||||
### Extensions in component instances
|
||||
|
||||
Above, we've configured extensions `View` and `Storybook` for the `MyTable` component.
|
||||
|
||||
You can access the instances of these extension classes in the component instance.
|
||||
|
||||
Extensions are available under their names (e.g. `self.view`, `self.storybook`).
|
||||
|
||||
For example, the View extension is available as `self.view`:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
def get_context_data(self, request):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
# `self.view` points to the instance of `View` extension.
|
||||
return {
|
||||
"view": self.view,
|
||||
|
@ -129,30 +207,30 @@ And the Storybook extension is available as `self.storybook`:
|
|||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
def get_context_data(self, request):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
# `self.storybook` points to the instance of `Storybook` extension.
|
||||
return {
|
||||
"title": self.storybook.title(),
|
||||
}
|
||||
```
|
||||
|
||||
Thus, you can use extensions to add methods or attributes that will be available to all components
|
||||
in their component context.
|
||||
|
||||
## Writing extensions
|
||||
|
||||
Creating extensions in django-components involves defining a class that inherits from
|
||||
[`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension).
|
||||
This class can implement various lifecycle hooks and define new attributes or methods to be added to components.
|
||||
|
||||
### Defining an extension
|
||||
### Extension class
|
||||
|
||||
To create an extension, define a class that inherits from [`ComponentExtension`](../../../reference/api/#django_components.ComponentExtension)
|
||||
and implement the desired hooks.
|
||||
|
||||
- Each extension MUST have a `name` attribute. The name MUST be a valid Python identifier.
|
||||
- The extension MAY implement any of the [hook methods](../../../reference/extension_hooks).
|
||||
- Each hook method receives a context object with relevant data.
|
||||
- The extension may implement any of the [hook methods](../../../reference/extension_hooks).
|
||||
|
||||
Each hook method receives a context object with relevant data.
|
||||
|
||||
- Extension may own [URLs](#extension-urls) or [CLI commands](#extension-commands).
|
||||
|
||||
```python
|
||||
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext
|
||||
|
@ -165,9 +243,18 @@ class MyExtension(ComponentExtension):
|
|||
ctx.component_cls.my_attr = "my_value"
|
||||
```
|
||||
|
||||
### Defining the extension class
|
||||
!!! warning
|
||||
|
||||
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested within the `Component` class:
|
||||
The `name` attribute MUST be unique across all extensions.
|
||||
|
||||
Moreover, the `name` attribute MUST NOT conflict with existing Component class API.
|
||||
|
||||
So if you name an extension `render`, it will conflict with the [`render()`](../../../reference/api/#django_components.Component.render) method of the `Component` class.
|
||||
|
||||
### Component config
|
||||
|
||||
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested
|
||||
within the [`Component`](../../../reference/api/#django_components.Component) class:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
|
@ -180,32 +267,30 @@ class MyComponent(Component):
|
|||
|
||||
These can be understood as component-specific overrides or configuration.
|
||||
|
||||
The nested extension classes like `View` or `Storybook` will actually subclass from a base extension
|
||||
class as defined on the [`ComponentExtension.ExtensionClass`](../../../reference/api/#django_components.ComponentExtension.ExtensionClass).
|
||||
Whether it's `Component.View` or `Component.Storybook`, their respective extensions
|
||||
defined how these nested classes will behave.
|
||||
|
||||
This is how extensions define the "default" behavior of their nested extension classes.
|
||||
|
||||
For example, the `View` base extension class defines the handlers for GET, POST, etc:
|
||||
For example, the View extension defines the API that users may override in `ViewExtension.ComponentConfig`:
|
||||
|
||||
```python
|
||||
from django_components.extension import ComponentExtension
|
||||
from django_components.extension import ComponentExtension, ExtensionComponentConfig
|
||||
|
||||
class ViewExtension(ComponentExtension):
|
||||
name = "view"
|
||||
|
||||
# The default behavior of the `View` extension class.
|
||||
class ExtensionClass(ComponentExtension.ExtensionClass):
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
def get(self, request):
|
||||
return self.component.get(request)
|
||||
raise NotImplementedError("You must implement the `get` method.")
|
||||
|
||||
def post(self, request):
|
||||
return self.component.post(request)
|
||||
raise NotImplementedError("You must implement the `post` method.")
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
In any component that then defines a nested `View` extension class, the `View` extension class will actually
|
||||
subclass from the `ViewExtension.ExtensionClass` class.
|
||||
In any component that then defines a nested `Component.View` extension class, the resulting `View` class
|
||||
will actually subclass from the `ViewExtension.ComponentConfig` class.
|
||||
|
||||
In other words, when you define a component like this:
|
||||
|
||||
|
@ -217,11 +302,11 @@ class MyTable(Component):
|
|||
...
|
||||
```
|
||||
|
||||
It will actually be implemented as if the `View` class subclassed from base class `ViewExtension.ExtensionClass`:
|
||||
Behind the scenes it is as if you defined the following:
|
||||
|
||||
```python
|
||||
class MyTable(Component):
|
||||
class View(ViewExtension.ExtensionClass):
|
||||
class View(ViewExtension.ComponentConfig):
|
||||
def get(self, request):
|
||||
# Do something
|
||||
...
|
||||
|
@ -229,13 +314,13 @@ class MyTable(Component):
|
|||
|
||||
!!! warning
|
||||
|
||||
When writing an extension, the `ExtensionClass` MUST subclass the base class [`ComponentExtension.ExtensionClass`](../../../reference/api/#django_components.ComponentExtension.ExtensionClass).
|
||||
When writing an extension, the `ComponentConfig` MUST subclass the base class [`ExtensionComponentConfig`](../../../reference/api/#django_components.ExtensionComponentConfig).
|
||||
|
||||
This base class ensures that the extension class will have access to the component instance.
|
||||
|
||||
### Registering extensions
|
||||
### Install your extension
|
||||
|
||||
Once the extension is defined, it needs to be registered in the Django settings to be used by the application.
|
||||
Once the extension is defined, it needs to be installed in the Django settings to be used by the application.
|
||||
|
||||
Extensions can be given either as an extension class, or its import string:
|
||||
|
||||
|
@ -271,30 +356,30 @@ To tie it all together, here's an example of a custom logging extension that log
|
|||
```python
|
||||
from django_components.extension import (
|
||||
ComponentExtension,
|
||||
ExtensionComponentConfig,
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentClassDeletedContext,
|
||||
OnComponentInputContext,
|
||||
)
|
||||
|
||||
class ColorLoggerExtensionClass(ComponentExtension.ExtensionClass):
|
||||
color: str
|
||||
|
||||
|
||||
class ColorLoggerExtension(ComponentExtension):
|
||||
name = "color_logger"
|
||||
|
||||
# All `Component.ColorLogger` classes will inherit from this class.
|
||||
ExtensionClass = ColorLoggerExtensionClass
|
||||
class ComponentConfig(ExtensionComponentConfig):
|
||||
color: str
|
||||
|
||||
# These hooks don't have access to the Component instance, only to the Component class,
|
||||
# so we access the color as `Component.ColorLogger.color`.
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
|
||||
# These hooks don't have access to the Component instance,
|
||||
# only to the Component class, so we access the color
|
||||
# as `Component.ColorLogger.color`.
|
||||
def on_component_class_created(self, ctx: OnComponentClassCreatedContext):
|
||||
log.info(
|
||||
f"Component {ctx.component_cls} created.",
|
||||
color=ctx.component_cls.ColorLogger.color,
|
||||
)
|
||||
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext):
|
||||
log.info(
|
||||
f"Component {ctx.component_cls} deleted.",
|
||||
color=ctx.component_cls.ColorLogger.color,
|
||||
|
@ -302,7 +387,7 @@ class ColorLoggerExtension(ComponentExtension):
|
|||
|
||||
# This hook has access to the Component instance, so we access the color
|
||||
# as `self.component.color_logger.color`.
|
||||
def on_component_input(self, ctx: OnComponentInputContext) -> None:
|
||||
def on_component_input(self, ctx: OnComponentInputContext):
|
||||
log.info(
|
||||
f"Rendering component {ctx.component_cls}.",
|
||||
color=ctx.component.color_logger.color,
|
||||
|
@ -320,7 +405,7 @@ COMPONENTS = {
|
|||
}
|
||||
```
|
||||
|
||||
Once registered, in any component, you can define a `ColorLogger` attribute:
|
||||
Once installed, in any component, you can define a `ColorLogger` attribute:
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
|
@ -337,45 +422,77 @@ django-components provides a few utility functions to help with writing extensio
|
|||
- [`all_components()`](../../../reference/api#django_components.all_components) - returns a list of all created component classes.
|
||||
- [`all_registries()`](../../../reference/api#django_components.all_registries) - returns a list of all created registry instances.
|
||||
|
||||
### Accessing the component class from within an extension
|
||||
### Access component class
|
||||
|
||||
When you are writing the extension class that will be nested inside a Component class, e.g.
|
||||
You can access the owner [`Component`](../../../reference/api/#django_components.Component) class (`MyTable`) from within methods
|
||||
of the extension class (`MyExtension`) by using
|
||||
the [`component_cls`](../../../reference/api/#django_components.ExtensionComponentConfig.component_cls) attribute:
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
class MyExtension:
|
||||
def some_method(self):
|
||||
...
|
||||
print(self.component_cls)
|
||||
```
|
||||
|
||||
You can access the owner Component class (`MyTable`) from within methods of the extension class (`MyExtension`) by using the `component_class` attribute:
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
class MyExtension:
|
||||
def some_method(self):
|
||||
print(self.component_class)
|
||||
```
|
||||
|
||||
Here is how the `component_class` attribute may be used with our `ColorLogger`
|
||||
Here is how the `component_cls` attribute may be used with our `ColorLogger`
|
||||
extension shown above:
|
||||
|
||||
```python
|
||||
class ColorLoggerExtensionClass(ComponentExtension.ExtensionClass):
|
||||
class ColorLoggerComponentConfig(ExtensionComponentConfig):
|
||||
color: str
|
||||
|
||||
def log(self, msg: str) -> None:
|
||||
print(f"{self.component_class.name}: {msg}")
|
||||
print(f"{self.component_cls.__name__}: {msg}")
|
||||
|
||||
|
||||
class ColorLoggerExtension(ComponentExtension):
|
||||
name = "color_logger"
|
||||
|
||||
# All `Component.ColorLogger` classes will inherit from this class.
|
||||
ExtensionClass = ColorLoggerExtensionClass
|
||||
ComponentConfig = ColorLoggerComponentConfig
|
||||
```
|
||||
|
||||
## Extension Commands
|
||||
### Pass slot metadata
|
||||
|
||||
When a slot is passed to a component, it is copied, so that the original slot is not modified
|
||||
with rendering metadata.
|
||||
|
||||
Therefore, don't use slot's identity to associate metadata with the slot:
|
||||
|
||||
```py
|
||||
# ❌ Don't do this:
|
||||
slots_cache = {}
|
||||
|
||||
class LoggerExtension(ComponentExtension):
|
||||
name = "logger"
|
||||
|
||||
def on_component_input(self, ctx: OnComponentInputContext):
|
||||
for slot in ctx.component.slots.values():
|
||||
slots_cache[id(slot)] = {"some": "metadata"}
|
||||
```
|
||||
|
||||
Instead, use the [`Slot.extra`](../../../reference/api#django_components.Slot.extra) attribute,
|
||||
which is copied from the original slot:
|
||||
|
||||
```python
|
||||
# ✅ Do this:
|
||||
class LoggerExtension(ComponentExtension):
|
||||
name = "logger"
|
||||
|
||||
# Save component-level logger settings for each slot when a component is rendered.
|
||||
def on_component_input(self, ctx: OnComponentInputContext):
|
||||
for slot in ctx.component.slots.values():
|
||||
slot.extra["logger"] = ctx.component.logger
|
||||
|
||||
# Then, when a fill is rendered with `{% slot %}`, we can access the logger settings
|
||||
# from the slot's metadata.
|
||||
def on_slot_rendered(self, ctx: OnSlotRenderedContext):
|
||||
logger = ctx.slot.extra["logger"]
|
||||
logger.log(...)
|
||||
```
|
||||
|
||||
## Extension commands
|
||||
|
||||
Extensions in django-components can define custom commands that can be executed via the Django management command interface. This allows for powerful automation and customization capabilities.
|
||||
|
||||
|
@ -392,9 +509,9 @@ Where:
|
|||
- `my_ext` - is the extension name
|
||||
- `hello` - is the command name
|
||||
|
||||
### Defining Commands
|
||||
### Define commands
|
||||
|
||||
To define a command, subclass from [`ComponentCommand`](../../../reference/api#django_components.ComponentCommand).
|
||||
To define a command, subclass from [`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand).
|
||||
This subclass should define:
|
||||
|
||||
- `name` - the command's name
|
||||
|
@ -416,15 +533,15 @@ class MyExt(ComponentExtension):
|
|||
commands = [HelloCommand]
|
||||
```
|
||||
|
||||
### Defining Command Arguments and Options
|
||||
### Define arguments and options
|
||||
|
||||
Commands can accept positional arguments and options (e.g. `--foo`), which are defined using the
|
||||
[`arguments`](../../../reference/api#django_components.ComponentCommand.arguments)
|
||||
attribute of the [`ComponentCommand`](../../../reference/api#django_components.ComponentCommand) class.
|
||||
[`arguments`](../../../reference/extension_commands#django_components.ComponentCommand.arguments)
|
||||
attribute of the [`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand) class.
|
||||
|
||||
The arguments are parsed with [`argparse`](https://docs.python.org/3/library/argparse.html)
|
||||
into a dictionary of arguments and options. These are then available
|
||||
as keyword arguments to the [`handle`](../../../reference/api#django_components.ComponentCommand.handle)
|
||||
as keyword arguments to the [`handle`](../../../reference/extension_commands#django_components.ComponentCommand.handle)
|
||||
method of the command.
|
||||
|
||||
```python
|
||||
|
@ -470,20 +587,20 @@ python manage.py components ext run my_ext hello John --shout
|
|||
See the [argparse documentation](https://docs.python.org/3/library/argparse.html) for more information.
|
||||
|
||||
django-components defines types as
|
||||
[`CommandArg`](../../../reference/api#django_components.CommandArg),
|
||||
[`CommandArgGroup`](../../../reference/api#django_components.CommandArgGroup),
|
||||
[`CommandSubcommand`](../../../reference/api#django_components.CommandSubcommand),
|
||||
and [`CommandParserInput`](../../../reference/api#django_components.CommandParserInput)
|
||||
[`CommandArg`](../../../reference/extension_commands#django_components.CommandArg),
|
||||
[`CommandArgGroup`](../../../reference/extension_commands#django_components.CommandArgGroup),
|
||||
[`CommandSubcommand`](../../../reference/extension_commands#django_components.CommandSubcommand),
|
||||
and [`CommandParserInput`](../../../reference/extension_commands#django_components.CommandParserInput)
|
||||
to help with type checking.
|
||||
|
||||
!!! note
|
||||
|
||||
If a command doesn't have the [`handle`](../../../reference/api#django_components.ComponentCommand.handle)
|
||||
If a command doesn't have the [`handle`](../../../reference/extension_commands#django_components.ComponentCommand.handle)
|
||||
method defined, the command will print a help message and exit.
|
||||
|
||||
### Grouping Arguments
|
||||
### Argument groups
|
||||
|
||||
Arguments can be grouped using [`CommandArgGroup`](../../../reference/api#django_components.CommandArgGroup)
|
||||
Arguments can be grouped using [`CommandArgGroup`](../../../reference/extension_commands#django_components.CommandArgGroup)
|
||||
to provide better organization and help messages.
|
||||
|
||||
Read more on [argparse argument groups](https://docs.python.org/3/library/argparse.html#argument-groups).
|
||||
|
@ -539,12 +656,12 @@ class HelloCommand(ComponentCommand):
|
|||
Extensions can define subcommands, allowing for more complex command structures.
|
||||
|
||||
Subcommands are defined similarly to root commands, as subclasses of
|
||||
[`ComponentCommand`](../../../reference/api#django_components.ComponentCommand) class.
|
||||
[`ComponentCommand`](../../../reference/extension_commands#django_components.ComponentCommand) class.
|
||||
|
||||
However, instead of defining the subcommands in the
|
||||
[`commands`](../../../reference/api#django_components.ComponentExtension.commands)
|
||||
[`commands`](../../../reference/extension_commands#django_components.ComponentExtension.commands)
|
||||
attribute of the extension, you define them in the
|
||||
[`subcommands`](../../../reference/api#django_components.ComponentCommand.subcommands)
|
||||
[`subcommands`](../../../reference/extension_commands#django_components.ComponentCommand.subcommands)
|
||||
attribute of the parent command:
|
||||
|
||||
```python
|
||||
|
@ -600,7 +717,7 @@ python manage.py components ext run parent child
|
|||
python manage.py components ext run parent child --foo --bar
|
||||
```
|
||||
|
||||
### Print command help
|
||||
### Help message
|
||||
|
||||
By default, all commands will print their help message when run with the `--help` / `-h` flag.
|
||||
|
||||
|
@ -610,9 +727,9 @@ python manage.py components ext run my_ext --help
|
|||
|
||||
The help message prints out all the arguments and options available for the command, as well as any subcommands.
|
||||
|
||||
### Testing Commands
|
||||
### Testing commands
|
||||
|
||||
Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.1/ref/django-admin/#running-management-commands-from-your-code)
|
||||
Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.2/ref/django-admin/#running-management-commands-from-your-code)
|
||||
function, which allows you to simulate running the command in tests.
|
||||
|
||||
```python
|
||||
|
@ -663,7 +780,7 @@ def test_hello_command(self):
|
|||
|
||||
Extensions can define custom views and endpoints that can be accessed through the Django application.
|
||||
|
||||
To define URLs for an extension, set them in the [`urls`](../../../reference/api#django_components.ComponentExtension.urls) attribute of your [`ComponentExtension`](../../../reference/api#django_components.ComponentExtension) class. Each URL is defined using the [`URLRoute`](../../../reference/api#django_components.URLRoute) class, which specifies the path, handler, and optional name for the route.
|
||||
To define URLs for an extension, set them in the [`urls`](../../../reference/api#django_components.ComponentExtension.urls) attribute of your [`ComponentExtension`](../../../reference/api#django_components.ComponentExtension) class. Each URL is defined using the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class, which specifies the path, handler, and optional name for the route.
|
||||
|
||||
Here's an example of how to define URLs within an extension:
|
||||
|
||||
|
@ -685,17 +802,17 @@ class MyExtension(ComponentExtension):
|
|||
|
||||
!!! warning
|
||||
|
||||
The [`URLRoute`](../../../reference/api#django_components.URLRoute) objects
|
||||
The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) objects
|
||||
are different from objects created with Django's
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path).
|
||||
Do NOT use `URLRoute` objects in Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.1/topics/http/urls/#example)
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path).
|
||||
Do NOT use `URLRoute` objects in Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/#example)
|
||||
and vice versa!
|
||||
|
||||
django-components uses a custom [`URLRoute`](../../../reference/api#django_components.URLRoute) class to define framework-agnostic routing rules.
|
||||
django-components uses a custom [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class to define framework-agnostic routing rules.
|
||||
|
||||
As of v0.131, `URLRoute` objects are directly converted to Django's `URLPattern` and `URLResolver` objects.
|
||||
|
||||
### Accessing Extension URLs
|
||||
### URL paths
|
||||
|
||||
The URLs defined in an extension are available under the path
|
||||
|
||||
|
@ -713,9 +830,9 @@ For example, if you have defined a URL with the path `my-view/<str:name>/` in an
|
|||
|
||||
Extensions can also define nested URLs to allow for more complex routing structures.
|
||||
|
||||
To define nested URLs, set the [`children`](../../../reference/api#django_components.URLRoute.children)
|
||||
attribute of the [`URLRoute`](../../../reference/api#django_components.URLRoute) object to
|
||||
a list of child [`URLRoute`](../../../reference/api#django_components.URLRoute) objects:
|
||||
To define nested URLs, set the [`children`](../../../reference/extension_urls#django_components.URLRoute.children)
|
||||
attribute of the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) object to
|
||||
a list of child [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) objects:
|
||||
|
||||
```python
|
||||
class MyExtension(ComponentExtension):
|
||||
|
@ -740,17 +857,17 @@ In this example, the URL
|
|||
|
||||
would call the `my_view` handler with the parameter `name` set to `"John"`.
|
||||
|
||||
### Passing kwargs and other extra fields to URL routes
|
||||
### Extra URL data
|
||||
|
||||
The [`URLRoute`](../../../reference/api#django_components.URLRoute) class is framework-agnostic,
|
||||
The [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) class is framework-agnostic,
|
||||
so that extensions could be used with non-Django frameworks in the future.
|
||||
|
||||
However, that means that there may be some extra fields that Django's
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.1/ref/urls/#path)
|
||||
[`django.urls.path()`](https://docs.djangoproject.com/en/5.2/ref/urls/#path)
|
||||
accepts, but which are not defined on the `URLRoute` object.
|
||||
|
||||
To address this, the [`URLRoute`](../../../reference/api#django_components.URLRoute) object has
|
||||
an [`extra`](../../../reference/api#django_components.URLRoute.extra) attribute,
|
||||
To address this, the [`URLRoute`](../../../reference/extension_urls#django_components.URLRoute) object has
|
||||
an [`extra`](../../../reference/extension_urls#django_components.URLRoute.extra) attribute,
|
||||
which is a dictionary that can be used to pass any extra kwargs to `django.urls.path()`:
|
||||
|
||||
```python
|
||||
|
|
|
@ -1,58 +1,333 @@
|
|||
_New in version 0.96_
|
||||
|
||||
Component hooks are functions that allow you to intercept the rendering process at specific positions.
|
||||
Intercept the rendering lifecycle with Component hooks.
|
||||
|
||||
Unlike the [extension hooks](../../../reference/extension_hooks/), these are defined directly
|
||||
on the [`Component`](../../../reference/api#django_components.Component) class.
|
||||
|
||||
## Available hooks
|
||||
|
||||
- `on_render_before`
|
||||
### `on_render_before`
|
||||
|
||||
```py
|
||||
def on_render_before(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Template
|
||||
) -> None:
|
||||
```
|
||||
```py
|
||||
def on_render_before(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
) -> None:
|
||||
```
|
||||
|
||||
Hook that runs just before the component's template is rendered.
|
||||
[`Component.on_render_before`](../../../reference/api#django_components.Component.on_render_before) runs just before the component's template is rendered.
|
||||
|
||||
You can use this hook to access or modify the context or the template:
|
||||
It is called for every component, including nested ones, as part of
|
||||
the component render lifecycle.
|
||||
|
||||
```py
|
||||
def on_render_before(self, context, template) -> None:
|
||||
# Insert value into the Context
|
||||
context["from_on_before"] = ":)"
|
||||
It receives the [Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
|
||||
and the [Template](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
|
||||
as arguments.
|
||||
|
||||
# Append text into the Template
|
||||
template.nodelist.append(TextNode("FROM_ON_BEFORE"))
|
||||
```
|
||||
The `template` argument is `None` if the component has no template.
|
||||
|
||||
- `on_render_after`
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
def on_render_after(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Template,
|
||||
content: str
|
||||
) -> None | str | SafeString:
|
||||
```
|
||||
You can use this hook to access the context or the template:
|
||||
|
||||
Hook that runs just after the component's template was rendered.
|
||||
It receives the rendered output as the last argument.
|
||||
```py
|
||||
from django.template import Context, Template
|
||||
from django_components import Component
|
||||
|
||||
You can use this hook to access the context or the template, but modifying
|
||||
them won't have any effect.
|
||||
class MyTable(Component):
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
# Insert value into the Context
|
||||
context["from_on_before"] = ":)"
|
||||
|
||||
To override the content that gets rendered, you can return a string or SafeString from this hook:
|
||||
assert isinstance(template, Template)
|
||||
```
|
||||
|
||||
```py
|
||||
def on_render_after(self, context, template, content):
|
||||
# Prepend text to the rendered content
|
||||
return "Chocolate cookie recipe: " + content
|
||||
```
|
||||
!!! warning
|
||||
|
||||
## Component hooks example
|
||||
If you want to pass data to the template, prefer using
|
||||
[`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
instead of this hook.
|
||||
|
||||
!!! warning
|
||||
|
||||
Do NOT modify the template in this hook. The template is reused across renders.
|
||||
|
||||
Since this hook is called for every component, this means that the template would be modified
|
||||
every time a component is rendered.
|
||||
|
||||
### `on_render`
|
||||
|
||||
_New in version 0.140_
|
||||
|
||||
```py
|
||||
def on_render(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
) -> Union[str, SafeString, OnRenderGenerator, None]:
|
||||
```
|
||||
|
||||
[`Component.on_render`](../../../reference/api#django_components.Component.on_render) does the actual rendering.
|
||||
|
||||
You can override this method to:
|
||||
|
||||
- Change what template gets rendered
|
||||
- Modify the context
|
||||
- Modify the rendered output after it has been rendered
|
||||
- Handle errors
|
||||
|
||||
The default implementation renders the component's
|
||||
[Template](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
|
||||
with the given
|
||||
[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context).
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
if template is None:
|
||||
return None
|
||||
else:
|
||||
return template.render(context)
|
||||
```
|
||||
|
||||
The `template` argument is `None` if the component has no template.
|
||||
|
||||
#### Modifying rendered template
|
||||
|
||||
To change what gets rendered, you can:
|
||||
|
||||
- Render a different template
|
||||
- Render a component
|
||||
- Return a different string or SafeString
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
return "Hello"
|
||||
```
|
||||
|
||||
You can also use [`on_render()`](../../../reference/api#django_components.Component.on_render) as a router,
|
||||
rendering other components based on the parent component's arguments:
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
# Select different component based on `feature_new_table` kwarg
|
||||
if self.kwargs.get("feature_new_table"):
|
||||
comp_cls = NewTable
|
||||
else:
|
||||
comp_cls = OldTable
|
||||
|
||||
# Render the selected component
|
||||
return comp_cls.render(
|
||||
args=self.args,
|
||||
kwargs=self.kwargs,
|
||||
slots=self.slots,
|
||||
context=context,
|
||||
)
|
||||
```
|
||||
|
||||
#### Post-processing rendered template
|
||||
|
||||
When you render the original template in [`on_render()`](../../../reference/api#django_components.Component.on_render) as:
|
||||
|
||||
```py
|
||||
template.render(context)
|
||||
```
|
||||
|
||||
The result is NOT the final output, but an intermediate result. Nested components
|
||||
are not rendered yet.
|
||||
|
||||
Instead, django-components needs to take this result and process it
|
||||
to actually render the child components.
|
||||
|
||||
To access the final output, you can `yield` the result instead of returning it.
|
||||
|
||||
This will return a tuple of (rendered HTML, error). The error is `None` if the rendering succeeded.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
|
||||
if error is None:
|
||||
# The rendering succeeded
|
||||
return html
|
||||
else:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
At this point you can do 3 things:
|
||||
|
||||
1. Return a new HTML
|
||||
|
||||
The new HTML will be used as the final output.
|
||||
|
||||
If the original template raised an error, it will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
|
||||
return "NEW HTML"
|
||||
```
|
||||
|
||||
2. Raise a new exception
|
||||
|
||||
The new exception is what will bubble up from the component.
|
||||
|
||||
The original HTML and original error will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
|
||||
raise Exception("Error message")
|
||||
```
|
||||
|
||||
3. Return nothing (or `None`) to handle the result as usual
|
||||
|
||||
If you don't raise an exception, and neither return a new HTML,
|
||||
then original HTML / error will be used:
|
||||
|
||||
- If rendering succeeded, the original HTML will be used as the final output.
|
||||
- If rendering failed, the original error will be propagated.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render(self, context, template):
|
||||
html, error = yield template.render(context)
|
||||
|
||||
if error is not None:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
#### Example: ErrorBoundary
|
||||
|
||||
[`on_render()`](../../../reference/api#django_components.Component.on_render) can be used to
|
||||
implement React's [ErrorBoundary](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary).
|
||||
|
||||
That is, a component that catches errors in nested components and displays a fallback UI instead:
|
||||
|
||||
```django
|
||||
{% component "error_boundary" %}
|
||||
{% fill "content" %}
|
||||
{% component "nested_component" %}
|
||||
{% endfill %}
|
||||
{% fill "fallback" %}
|
||||
Sorry, something went wrong.
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
To implement this, we render the fallback slot in [`on_render()`](../../../reference/api#django_components.Component.on_render)
|
||||
and return it if an error occured:
|
||||
|
||||
```djc_py
|
||||
class ErrorFallback(Component):
|
||||
template = """
|
||||
{% slot "content" default / %}
|
||||
"""
|
||||
|
||||
def on_render(self, context, template):
|
||||
fallback = self.slots.fallback
|
||||
|
||||
if fallback is None:
|
||||
raise ValueError("fallback slot is required")
|
||||
|
||||
html, error = yield template.render(context)
|
||||
|
||||
if error is not None:
|
||||
return fallback()
|
||||
else:
|
||||
return html
|
||||
```
|
||||
|
||||
### `on_render_after`
|
||||
|
||||
```py
|
||||
def on_render_after(
|
||||
self: Component,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
result: Optional[str | SafeString],
|
||||
error: Optional[Exception],
|
||||
) -> Union[str, SafeString, None]:
|
||||
```
|
||||
|
||||
[`on_render_after()`](../../../reference/api#django_components.Component.on_render_after) runs when the component was fully rendered,
|
||||
including all its children.
|
||||
|
||||
It receives the same arguments as [`on_render_before()`](#on_render_before),
|
||||
plus the outcome of the rendering:
|
||||
|
||||
- `result`: The rendered output of the component. `None` if the rendering failed.
|
||||
- `error`: The error that occurred during the rendering, or `None` if the rendering succeeded.
|
||||
|
||||
[`on_render_after()`](../../../reference/api#django_components.Component.on_render_after) behaves the same way
|
||||
as the second part of [`on_render()`](#on_render) (after the `yield`).
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
if error is None:
|
||||
# The rendering succeeded
|
||||
return result
|
||||
else:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
Same as [`on_render()`](#on_render),
|
||||
you can return a new HTML, raise a new exception, or return nothing:
|
||||
|
||||
1. Return a new HTML
|
||||
|
||||
The new HTML will be used as the final output.
|
||||
|
||||
If the original template raised an error, it will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
return "NEW HTML"
|
||||
```
|
||||
|
||||
2. Raise a new exception
|
||||
|
||||
The new exception is what will bubble up from the component.
|
||||
|
||||
The original HTML and original error will be ignored.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
raise Exception("Error message")
|
||||
```
|
||||
|
||||
3. Return nothing (or `None`) to handle the result as usual
|
||||
|
||||
If you don't raise an exception, and neither return a new HTML,
|
||||
then original HTML / error will be used:
|
||||
|
||||
- If rendering succeeded, the original HTML will be used as the final output.
|
||||
- If rendering failed, the original error will be propagated.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
def on_render_after(self, context, template, result, error):
|
||||
if error is not None:
|
||||
# The rendering failed
|
||||
print(f"Error: {error}")
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
Django-components provides a seamless integration with HTML fragments ([HTML over the wire](https://hotwired.dev/)),
|
||||
whether you're using HTMX, AlpineJS, or vanilla JavaScript.
|
||||
Django-components provides a seamless integration with HTML fragments with AJAX ([HTML over the wire](https://hotwired.dev/)),
|
||||
whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.
|
||||
|
||||
When you define a component that has extra JS or CSS, and you use django-components
|
||||
to render the fragment, django-components will:
|
||||
If the fragment component has any JS or CSS, django-components will:
|
||||
|
||||
- Automatically load the associated JS and CSS
|
||||
- Ensure that JS is loaded and executed only once even if the fragment is inserted multiple times
|
||||
|
@ -22,19 +21,23 @@ to render the fragment, django-components will:
|
|||
4. A library like HTMX, AlpineJS, or custom function inserts the new HTML into
|
||||
the correct place.
|
||||
|
||||
## Document and fragment types
|
||||
## Document and fragment strategies
|
||||
|
||||
Components support two modes of rendering - As a "document" or as a "fragment".
|
||||
Components support different ["strategies"](../../advanced/rendering_js_css#dependencies-strategies)
|
||||
for rendering JS and CSS.
|
||||
|
||||
Two of them are used to enable HTML fragments - ["document"](../../advanced/rendering_js_css#document)
|
||||
and ["fragment"](../../advanced/rendering_js_css#fragment).
|
||||
|
||||
What's the difference?
|
||||
|
||||
### Document mode
|
||||
### Document strategy
|
||||
|
||||
Document mode assumes that the rendered components will be embedded into the HTML
|
||||
Document strategy assumes that the rendered components will be embedded into the HTML
|
||||
of the initial page load. This means that:
|
||||
|
||||
- The JS and CSS is embedded into the HTML as `<script>` and `<style>` tags
|
||||
(see [JS and CSS output locations](./rendering_js_css.md#js-and-css-output-locations))
|
||||
(see [Default JS / CSS locations](./rendering_js_css.md#default-js-css-locations))
|
||||
- Django-components injects a JS script for managing JS and CSS assets
|
||||
|
||||
A component is rendered as a "document" when:
|
||||
|
@ -42,7 +45,7 @@ A component is rendered as a "document" when:
|
|||
- It is embedded inside a template as [`{% component %}`](../../reference/template_tags.md#component)
|
||||
- It is rendered with [`Component.render()`](../../../reference/api#django_components.Component.render)
|
||||
or [`Component.render_to_response()`](../../../reference/api#django_components.Component.render_to_response)
|
||||
with the `type` kwarg set to `"document"` (default)
|
||||
with the `deps_strategy` kwarg set to `"document"` (default)
|
||||
|
||||
Example:
|
||||
|
||||
|
@ -55,13 +58,13 @@ MyTable.render(
|
|||
|
||||
MyTable.render(
|
||||
kwargs={...},
|
||||
type="document",
|
||||
deps_strategy="document",
|
||||
)
|
||||
```
|
||||
|
||||
### Fragment mode
|
||||
### Fragment strategy
|
||||
|
||||
Fragment mode assumes that the main HTML has already been rendered and loaded on the page.
|
||||
Fragment strategy assumes that the main HTML has already been rendered and loaded on the page.
|
||||
The component renders HTML that will be inserted into the page as a fragment, at a LATER time:
|
||||
|
||||
- JS and CSS is not directly embedded to avoid duplicately executing the same JS scripts.
|
||||
|
@ -75,14 +78,14 @@ A component is rendered as "fragment" when:
|
|||
|
||||
- It is rendered with [`Component.render()`](../../../reference/api#django_components.Component.render)
|
||||
or [`Component.render_to_response()`](../../../reference/api#django_components.Component.render_to_response)
|
||||
with the `type` kwarg set to `"fragment"`
|
||||
with the `deps_strategy` kwarg set to `"fragment"`
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
MyTable.render(
|
||||
kwargs={...},
|
||||
type="fragment",
|
||||
deps_strategy="fragment",
|
||||
)
|
||||
```
|
||||
|
||||
|
@ -101,14 +104,12 @@ Then navigate to these URLs:
|
|||
|
||||
### 1. Define document HTML
|
||||
|
||||
This is the HTML into which a fragment will be loaded using HTMX.
|
||||
|
||||
```djc_py title="[root]/components/demo.py"
|
||||
from django_components import Component, types
|
||||
|
||||
# HTML into which a fragment will be loaded using HTMX
|
||||
class MyPage(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response()
|
||||
|
||||
template = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -132,18 +133,20 @@ class MyPage(Component):
|
|||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component_cls.render_to_response(request=request)
|
||||
```
|
||||
|
||||
### 2. Define fragment HTML
|
||||
|
||||
The fragment to be inserted into the document.
|
||||
|
||||
IMPORTANT: Don't forget to set `deps_strategy="fragment"`
|
||||
|
||||
```djc_py title="[root]/components/demo.py"
|
||||
class Frag(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response(
|
||||
# IMPORTANT: Don't forget `type="fragment"`
|
||||
type="fragment",
|
||||
)
|
||||
|
||||
template = """
|
||||
<div class="frag">
|
||||
123
|
||||
|
@ -160,6 +163,14 @@ class Frag(Component):
|
|||
background: blue;
|
||||
}
|
||||
"""
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component_cls.render_to_response(
|
||||
request=request,
|
||||
# IMPORTANT: Don't forget `deps_strategy="fragment"`
|
||||
deps_strategy="fragment",
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Create view and URLs
|
||||
|
@ -179,14 +190,12 @@ urlpatterns = [
|
|||
|
||||
### 1. Define document HTML
|
||||
|
||||
This is the HTML into which a fragment will be loaded using AlpineJS.
|
||||
|
||||
```djc_py title="[root]/components/demo.py"
|
||||
from django_components import Component, types
|
||||
|
||||
# HTML into which a fragment will be loaded using AlpineJS
|
||||
class MyPage(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response()
|
||||
|
||||
template = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -216,18 +225,20 @@ class MyPage(Component):
|
|||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component_cls.render_to_response(request=request)
|
||||
```
|
||||
|
||||
### 2. Define fragment HTML
|
||||
|
||||
The fragment to be inserted into the document.
|
||||
|
||||
IMPORTANT: Don't forget to set `deps_strategy="fragment"`
|
||||
|
||||
```djc_py title="[root]/components/demo.py"
|
||||
class Frag(Component):
|
||||
def get(self, request):
|
||||
# IMPORTANT: Don't forget `type="fragment"`
|
||||
return self.render_to_response(
|
||||
type="fragment",
|
||||
)
|
||||
|
||||
# NOTE: We wrap the actual fragment in a template tag with x-if="false" to prevent it
|
||||
# from being rendered until we have registered the component with AlpineJS.
|
||||
template = """
|
||||
|
@ -257,6 +268,14 @@ class Frag(Component):
|
|||
background: blue;
|
||||
}
|
||||
"""
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component_cls.render_to_response(
|
||||
request=request,
|
||||
# IMPORTANT: Don't forget `deps_strategy="fragment"`
|
||||
deps_strategy="fragment",
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Create view and URLs
|
||||
|
@ -276,14 +295,12 @@ urlpatterns = [
|
|||
|
||||
### 1. Define document HTML
|
||||
|
||||
This is the HTML into which a fragment will be loaded using vanilla JS.
|
||||
|
||||
```djc_py title="[root]/components/demo.py"
|
||||
from django_components import Component, types
|
||||
|
||||
# HTML into which a fragment will be loaded using JS
|
||||
class MyPage(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response()
|
||||
|
||||
template = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -312,18 +329,20 @@ class MyPage(Component):
|
|||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component_cls.render_to_response(request=request)
|
||||
```
|
||||
|
||||
### 2. Define fragment HTML
|
||||
|
||||
The fragment to be inserted into the document.
|
||||
|
||||
IMPORTANT: Don't forget to set `deps_strategy="fragment"`
|
||||
|
||||
```djc_py title="[root]/components/demo.py"
|
||||
class Frag(Component):
|
||||
def get(self, request):
|
||||
return self.render_to_response(
|
||||
# IMPORTANT: Don't forget `type="fragment"`
|
||||
type="fragment",
|
||||
)
|
||||
|
||||
template = """
|
||||
<div class="frag">
|
||||
123
|
||||
|
@ -340,6 +359,14 @@ class Frag(Component):
|
|||
background: blue;
|
||||
}
|
||||
"""
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component_cls.render_to_response(
|
||||
request=request,
|
||||
# IMPORTANT: Don't forget `deps_strategy="fragment"`
|
||||
deps_strategy="fragment",
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Create view and URLs
|
||||
|
|
|
@ -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,84 @@ 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):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
my_data = self.inject("my_data")
|
||||
print(my_data.key) # hi
|
||||
print(my_data.another) # 123
|
||||
return {}
|
||||
print(my_data.hello) # hi
|
||||
print(my_data.another) # 123
|
||||
```
|
||||
|
||||
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):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
my_data = self.inject("invalid_key", DEFAULT_DATA)
|
||||
assert my_data == DEFAUKT_DATA
|
||||
return {}
|
||||
assert my_data == DEFAULT_DATA
|
||||
```
|
||||
|
||||
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,17 +113,17 @@ 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>
|
||||
"""
|
||||
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
my_data = self.inject("my_data", "default")
|
||||
return {"my_data": my_data}
|
||||
|
||||
template_str = """
|
||||
{% load component_tags %}
|
||||
{% provide "my_data" key="hi" another=123 %}
|
||||
{% provide "my_data" hello="hi" another=123 %}
|
||||
{% component "child" / %}
|
||||
{% endprovide %}
|
||||
"""
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
### JS and CSS output locations
|
||||
## Introduction
|
||||
|
||||
If:
|
||||
Components consist of 3 parts - HTML, JS and CSS.
|
||||
|
||||
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
|
||||
Handling of HTML is straightforward - it is rendered as is, and inserted where
|
||||
the [`{% component %}`](../../../reference/template_tags#component) tag is.
|
||||
|
||||
Then, by default, the components' JS and CSS will be automatically inserted into the HTML:
|
||||
However, handling of JS and CSS is more complex:
|
||||
|
||||
- JS and CSS is are inserted elsewhere in the HTML. As a best practice, JS is placed in the `<body>` HTML tag, and CSS in the `<head>`.
|
||||
- Multiple components may use the same JS and CSS files. We don't want to load the same files multiple times.
|
||||
- Fetching of JS and CSS may block the page, so the JS / CSS should be embedded in the HTML.
|
||||
- Components inserted as HTML fragments need different handling for JS and CSS.
|
||||
|
||||
## Default JS / CSS locations
|
||||
|
||||
If your components use JS and CSS then, by default, the 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>`
|
||||
|
@ -17,8 +22,8 @@ Then, by default, the components' JS and CSS will be automatically inserted into
|
|||
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
|
||||
- [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies) - Set new location(s) for JS scripts
|
||||
- [`{% component_css_dependencies %}`](../../../reference/template_tags#component_css_dependencies) - Set new location(s) for CSS styles
|
||||
|
||||
So if you have a component with JS and CSS:
|
||||
|
||||
|
@ -31,6 +36,7 @@ class MyButton(Component):
|
|||
Click me!
|
||||
</button>
|
||||
"""
|
||||
|
||||
js: types.js = """
|
||||
for (const btnEl of document.querySelectorAll(".my-button")) {
|
||||
btnEl.addEventListener("click", () => {
|
||||
|
@ -38,6 +44,7 @@ class MyButton(Component):
|
|||
});
|
||||
}
|
||||
"""
|
||||
|
||||
css: types.css """
|
||||
.my-button {
|
||||
background: green;
|
||||
|
@ -49,11 +56,13 @@ class MyButton(Component):
|
|||
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).
|
||||
Then:
|
||||
|
||||
And the CSS from `MyButton.css` and `MyButton.Media.css` will be rendered at the default place,
|
||||
or in [`{% component_css_dependencies %}`](#TODO).
|
||||
- JS from `MyButton.js` and `MyButton.Media.js` will be rendered at the default place (`<body>`),
|
||||
or in [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies).
|
||||
|
||||
- CSS from `MyButton.css` and `MyButton.Media.css` will be rendered at the default place (`<head>`),
|
||||
or in [`{% component_css_dependencies %}`](../../../reference/template_tags#component_css_dependencies).
|
||||
|
||||
And if you don't specify `{% component_dependencies %}` tags, it is the equivalent of:
|
||||
|
||||
|
@ -74,52 +83,307 @@ And if you don't specify `{% component_dependencies %}` tags, it is the equivale
|
|||
</html>
|
||||
```
|
||||
|
||||
### Setting up the middleware
|
||||
!!! warning
|
||||
|
||||
[`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.
|
||||
If the rendered HTML does NOT contain neither `{% component_dependencies %}` template tags,
|
||||
nor `<head>` and `<body>` HTML tags, then the JS and CSS will NOT be inserted!
|
||||
|
||||
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`:
|
||||
To force the JS and CSS to be inserted, use the [`"append"`](#append) or [`"prepend"`](#prepend)
|
||||
strategies.
|
||||
|
||||
## Dependencies strategies
|
||||
|
||||
The rendered HTML may be used in different contexts (browser, email, etc).
|
||||
If your components use JS and CSS scripts, you may need to handle them differently.
|
||||
|
||||
The different ways for handling JS / CSS are called **"dependencies strategies"**.
|
||||
|
||||
[`render()`](../../../reference/api/#django_components.Component.render) and [`render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
|
||||
accept a `deps_strategy` parameter, which controls where and how the JS / CSS are inserted into the HTML.
|
||||
|
||||
```python
|
||||
MIDDLEWARE = [
|
||||
# ... other middleware classes ...
|
||||
'django_components.middleware.ComponentDependencyMiddleware'
|
||||
# ... other middleware classes ...
|
||||
]
|
||||
main_page = MainPage.render(deps_strategy="document")
|
||||
fragment = MyComponent.render_to_response(deps_strategy="fragment")
|
||||
```
|
||||
|
||||
### `render_dependencies` and rendering JS / CSS without the middleware
|
||||
The `deps_strategy` parameter is set at the root of a component render tree, which is why it is not available for
|
||||
the [`{% component %}`](../../../reference/template_tags#component) tag.
|
||||
|
||||
For most scenarios, using the [`ComponentDependencyMiddleware`](#TODO) middleware will be just fine.
|
||||
When you use Django's [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
|
||||
or [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render) to render templates,
|
||||
you can't directly set the `deps_strategy` parameter.
|
||||
|
||||
However, this section is for you if you want to:
|
||||
In this case, you can set the `deps_strategy` with the `DJC_DEPS_STRATEGY` context variable.
|
||||
|
||||
- Render HTML that will NOT be sent as a server response
|
||||
- Insert pre-rendered HTML into another component
|
||||
- Render HTML fragments (partials)
|
||||
```python
|
||||
from django.template.context import Context
|
||||
from django.shortcuts import render
|
||||
|
||||
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).
|
||||
ctx = Context({"DJC_DEPS_STRATEGY": "fragment"})
|
||||
fragment = render(request, "my_component.html", ctx=ctx)
|
||||
```
|
||||
|
||||
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.
|
||||
!!! info
|
||||
|
||||
#### Render JS / CSS without the middleware
|
||||
The `deps_strategy` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
|
||||
|
||||
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.
|
||||
!!! note "Why is `deps_strategy` required?"
|
||||
|
||||
Here is how you can achieve the same, without the middleware, using [`render_dependencies()`](#TODO):
|
||||
This is a technical limitation of the current implementation.
|
||||
|
||||
When a component is rendered, django-components embeds metadata about the component's JS and CSS into the HTML.
|
||||
|
||||
This way we can compose components together, and know which JS / CSS dependencies are needed.
|
||||
|
||||
As the last step of rendering, django-components extracts this metadata and uses a selected strategy
|
||||
to insert the JS / CSS into the HTML.
|
||||
|
||||
There are six dependencies strategies:
|
||||
|
||||
- [`document`](../../advanced/rendering_js_css#document) (default)
|
||||
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||
- Inserts extra script to allow `fragment` strategy to work.
|
||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
||||
- [`fragment`](../../advanced/rendering_js_css#fragment)
|
||||
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
||||
- No JS / CSS included.
|
||||
- [`simple`](../../advanced/rendering_js_css#simple)
|
||||
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||
- No extra script loaded.
|
||||
- [`prepend`](../../advanced/rendering_js_css#prepend)
|
||||
- Insert JS / CSS before the rendered HTML.
|
||||
- No extra script loaded.
|
||||
- [`append`](../../advanced/rendering_js_css#append)
|
||||
- Insert JS / CSS after the rendered HTML.
|
||||
- No extra script loaded.
|
||||
- [`ignore`](../../advanced/rendering_js_css#ignore)
|
||||
- HTML is left as-is. You can still process it with a different strategy later with
|
||||
[`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
|
||||
- Used for inserting rendered HTML into other components.
|
||||
|
||||
### `document`
|
||||
|
||||
`deps_strategy="document"` is the default. Use this if you are rendering a whole page, or if no other option suits better.
|
||||
|
||||
```python
|
||||
html = Button.render(deps_strategy="document")
|
||||
```
|
||||
|
||||
When you render a component tree with the `"document"` strategy, it is expected that:
|
||||
|
||||
- The HTML will be rendered at page load.
|
||||
- The HTML will be inserted into a page / browser where JS can be executed.
|
||||
|
||||
**Location:**
|
||||
|
||||
JS and CSS is inserted:
|
||||
|
||||
- Preferentially into JS / CSS placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)
|
||||
- Otherwise, JS into `<body>` element, and CSS into `<head>` element
|
||||
- If neither found, JS / CSS are NOT inserted
|
||||
|
||||
**Included scripts:**
|
||||
|
||||
For the `"document"` strategy, the JS and CSS is set up to avoid any delays when the end user loads
|
||||
the page in the browser:
|
||||
|
||||
- Components' primary JS and CSS scripts ([`Component.js`](../../../reference/api/#django_components.Component.js)
|
||||
and [`Component.css`](../../../reference/api/#django_components.Component.css)) - fully inlined:
|
||||
|
||||
```html
|
||||
<script>
|
||||
console.log("Hello from Button!");
|
||||
</script>
|
||||
<style>
|
||||
.button {
|
||||
background-color: blue;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- Components' secondary JS and CSS scripts
|
||||
([`Component.Media`](../../../reference/api/#django_components.Component.Media)) - inserted as links:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://example.com/styles.css" />
|
||||
<script src="https://example.com/script.js"></script>
|
||||
```
|
||||
|
||||
- A JS script is injected to manage component dependencies, enabling lazy loading of JS and CSS
|
||||
for HTML fragments.
|
||||
|
||||
!!! info
|
||||
|
||||
This strategy is required for fragments to work properly, as it sets up the dependency manager that fragments rely on.
|
||||
|
||||
!!! note "How the dependency manager works"
|
||||
|
||||
The dependency manager is a JS script that keeps track of all the JS and CSS dependencies that have already been loaded.
|
||||
|
||||
When a fragment is inserted into the page, it will also insert a JSON `<script>` tag with fragment metadata.
|
||||
|
||||
The dependency manager will pick up on that, and check which scripts the fragment needs.
|
||||
|
||||
It will then fetch only the scripts that haven't been loaded yet.
|
||||
|
||||
### `fragment`
|
||||
|
||||
`deps_strategy="fragment"` is used when rendering a piece of HTML that will be inserted into a page
|
||||
that has already been rendered with the [`"document"`](#document) strategy:
|
||||
|
||||
```python
|
||||
fragment = MyComponent.render(deps_strategy="fragment")
|
||||
```
|
||||
|
||||
The HTML of fragments is very lightweight because it doesn't include the JS and CSS scripts
|
||||
of the rendered components.
|
||||
|
||||
With fragments, even if a component has JS and CSS, you can insert the same component into a page
|
||||
hundreds of times, and the JS and CSS will only ever be loaded once.
|
||||
|
||||
This is intended for dynamic content that's loaded with AJAX after the initial page load, such as with [jQuery](https://jquery.com/), [HTMX](https://htmx.org/), [AlpineJS](https://alpinejs.dev/) or similar libraries.
|
||||
|
||||
**Location:**
|
||||
|
||||
None. The fragment's JS and CSS files will be loaded dynamically into the page.
|
||||
|
||||
**Included scripts:**
|
||||
|
||||
- A special JSON `<script>` tag that tells the dependency manager what JS and CSS to load.
|
||||
|
||||
### `simple`
|
||||
|
||||
`deps_strategy="simple"` is used either for non-browser use cases, or when you don't want to use the dependency manager.
|
||||
|
||||
Practically, this is the same as the [`"document"`](#document) strategy, except that the dependency manager is not used.
|
||||
|
||||
```python
|
||||
html = MyComponent.render(deps_strategy="simple")
|
||||
```
|
||||
|
||||
**Location:**
|
||||
|
||||
JS and CSS is inserted:
|
||||
|
||||
- Preferentially into JS / CSS placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)
|
||||
- Otherwise, JS into `<body>` element, and CSS into `<head>` element
|
||||
- If neither found, JS / CSS are NOT inserted
|
||||
|
||||
**Included scripts:**
|
||||
|
||||
- Components' primary JS and CSS scripts ([`Component.js`](../../../reference/api/#django_components.Component.js)
|
||||
and [`Component.css`](../../../reference/api/#django_components.Component.css)) - fully inlined:
|
||||
|
||||
```html
|
||||
<script>
|
||||
console.log("Hello from Button!");
|
||||
</script>
|
||||
<style>
|
||||
.button {
|
||||
background-color: blue;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- Components' secondary JS and CSS scripts
|
||||
([`Component.Media`](../../../reference/api/#django_components.Component.Media)) - inserted as links:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://example.com/styles.css" />
|
||||
<script src="https://example.com/script.js"></script>
|
||||
```
|
||||
|
||||
- No extra scripts are inserted.
|
||||
|
||||
### `prepend`
|
||||
|
||||
This is the same as [`"simple"`](#simple), but placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies) and HTML tags `<head>` and `<body>` are all ignored. The JS and CSS are **always** inserted **before** the rendered content.
|
||||
|
||||
```python
|
||||
html = MyComponent.render(deps_strategy="prepend")
|
||||
```
|
||||
|
||||
**Location:**
|
||||
|
||||
JS and CSS is **always** inserted before the rendered content.
|
||||
|
||||
**Included scripts:**
|
||||
|
||||
Same as for the [`"simple"`](#simple) strategy.
|
||||
|
||||
### `append`
|
||||
|
||||
This is the same as [`"simple"`](#simple), but placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies) and HTML tags `<head>` and `<body>` are all ignored. The JS and CSS are **always** inserted **after** the rendered content.
|
||||
|
||||
```python
|
||||
html = MyComponent.render(deps_strategy="append")
|
||||
```
|
||||
|
||||
**Location:**
|
||||
|
||||
JS and CSS is **always** inserted after the rendered content.
|
||||
|
||||
**Included scripts:**
|
||||
|
||||
Same as for the [`"simple"`](#simple) strategy.
|
||||
|
||||
### `ignore`
|
||||
|
||||
`deps_strategy="ignore"` is used when you do NOT want to process JS and CSS of the rendered HTML.
|
||||
|
||||
```python
|
||||
html = MyComponent.render(deps_strategy="ignore")
|
||||
```
|
||||
|
||||
The rendered HTML is left as-is. You can still process it with a different strategy later with `render_dependencies()`.
|
||||
|
||||
This is useful when you want to insert rendered HTML into another component.
|
||||
|
||||
```python
|
||||
html = MyComponent.render(deps_strategy="ignore")
|
||||
html = AnotherComponent.render(slots={"content": html})
|
||||
```
|
||||
|
||||
## Manually rendering JS / CSS
|
||||
|
||||
When rendering templates or components, django-components covers all the traditional ways how components
|
||||
or templates can be rendered:
|
||||
|
||||
- [`Component.render()`](../../../reference/api/#django_components.Component.render)
|
||||
- [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
|
||||
- [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
|
||||
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
|
||||
|
||||
This way you don't need to manually handle rendering of JS / CSS.
|
||||
|
||||
However, for advanced or low-level use cases, you may need to control when to render JS / CSS.
|
||||
|
||||
In such case you can directly pass rendered HTML to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
|
||||
|
||||
This function will extract all used components in the HTML string, and insert the components' JS and CSS
|
||||
based on given strategy.
|
||||
|
||||
!!! info
|
||||
|
||||
The truth is that all the methods listed above call [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies)
|
||||
internally.
|
||||
|
||||
**Example:**
|
||||
|
||||
To see how [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies) works,
|
||||
let's render a template with a component.
|
||||
|
||||
We will render it twice:
|
||||
|
||||
- First time, we let `template.render()` handle the rendering.
|
||||
- Second time, we prevent `template.render()` from inserting the component's JS and CSS with `deps_strategy="ignore"`.
|
||||
|
||||
Instead, we pass the "unprocessed" HTML to `render_dependencies()` ourselves to insert the component's JS and CSS.
|
||||
|
||||
```python
|
||||
from django.template.base import Template
|
||||
from django.template.context import Context
|
||||
from django_component import render_dependencies
|
||||
from django_components import render_dependencies
|
||||
|
||||
template = Template("""
|
||||
{% load component_tags %}
|
||||
|
@ -138,85 +402,30 @@ template = Template("""
|
|||
</html>
|
||||
""")
|
||||
|
||||
rendered = template.render(Context())
|
||||
rendered = render_dependencies(rendered)
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
rendered2_raw = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
|
||||
rendered2 = render_dependencies(rendered2_raw)
|
||||
|
||||
assert rendered == rendered2
|
||||
```
|
||||
|
||||
Same applies if you render a template using Django's [`django.shortcuts.render`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render):
|
||||
Same applies to other strategies and other methods of rendering:
|
||||
|
||||
```python
|
||||
from django.shortcuts import render
|
||||
raw_html = MyComponent.render(deps_strategy="ignore")
|
||||
html = render_dependencies(raw_html, deps_strategy="document")
|
||||
|
||||
def my_view(request):
|
||||
rendered = render(request, "pages/home.html")
|
||||
rendered = render_dependencies(rendered)
|
||||
return rendered
|
||||
html2 = MyComponent.render(deps_strategy="document")
|
||||
|
||||
assert html == html2
|
||||
```
|
||||
|
||||
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:
|
||||
## HTML fragments
|
||||
|
||||
```python
|
||||
from django_components import Component
|
||||
Django-components provides a seamless integration with HTML fragments with AJAX ([HTML over the wire](https://hotwired.dev/)),
|
||||
whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.
|
||||
|
||||
class MyButton(Component):
|
||||
...
|
||||
This is achieved by the combination of the [`"document"`](#document) and [`"fragment"`](#fragment) strategies.
|
||||
|
||||
# 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).
|
||||
Read more about [HTML fragments](../../advanced/html_fragments).
|
||||
|
|
|
@ -52,7 +52,7 @@ This will allow you to use the tag in your templates like this:
|
|||
|
||||
### Parameters
|
||||
|
||||
The `@template_tag` decorator accepts the following parameters:
|
||||
The [`@template_tag`](../../../reference/api#django_components.template_tag) decorator accepts the following parameters:
|
||||
|
||||
- `library`: The Django template library to register the tag with
|
||||
- `tag`: The name of the template tag (e.g. `"mytag"` for `{% mytag %}`)
|
||||
|
@ -61,7 +61,8 @@ The `@template_tag` decorator accepts the following parameters:
|
|||
|
||||
### Function signature
|
||||
|
||||
The function decorated with `@template_tag` must accept at least two arguments:
|
||||
The function decorated with [`@template_tag`](../../../reference/api#django_components.template_tag)
|
||||
must accept at least two arguments:
|
||||
|
||||
1. `node`: The node instance (we'll explain this in detail in the next section)
|
||||
2. `context`: The Django template context
|
||||
|
@ -150,15 +151,18 @@ GreetNode.register(library)
|
|||
|
||||
### Node properties
|
||||
|
||||
When using `BaseNode`, you have access to several useful properties:
|
||||
When using [`BaseNode`](../../../reference/api#django_components.BaseNode), you have access to several useful properties:
|
||||
|
||||
- `node_id`: A unique identifier for this node instance
|
||||
- `flags`: Dictionary of flag values (e.g. `{"required": True}`)
|
||||
- `params`: List of raw parameters passed to the tag
|
||||
- `nodelist`: The template nodes between the start and end tags
|
||||
- `active_flags`: List of flags that are currently set to True
|
||||
- [`node_id`](../../../reference/api#django_components.BaseNode.node_id): A unique identifier for this node instance
|
||||
- [`flags`](../../../reference/api#django_components.BaseNode.flags): Dictionary of flag values (e.g. `{"required": True}`)
|
||||
- [`params`](../../../reference/api#django_components.BaseNode.params): List of raw parameters passed to the tag
|
||||
- [`nodelist`](../../../reference/api#django_components.BaseNode.nodelist): The template nodes between the start and end tags
|
||||
- [`contents`](../../../reference/api#django_components.BaseNode.contents): The raw contents between the start and end tags
|
||||
- [`active_flags`](../../../reference/api#django_components.BaseNode.active_flags): List of flags that are currently set to True
|
||||
- [`template_name`](../../../reference/api#django_components.BaseNode.template_name): The name of the `Template` instance inside which the node was defined
|
||||
- [`template_component`](../../../reference/api#django_components.BaseNode.template_component): The component class that the `Template` belongs to
|
||||
|
||||
This is what the `node` parameter in the `@template_tag` decorator gives you access to - it's the instance of the node class that was automatically created for your template tag.
|
||||
This is what the `node` parameter in the [`@template_tag`](../../../reference/api#django_components.template_tag) decorator gives you access to - it's the instance of the node class that was automatically created for your template tag.
|
||||
|
||||
### Rendering content between tags
|
||||
|
||||
|
|
|
@ -4,7 +4,10 @@ The [`@djc_test`](../../../reference/testing_api#djc_test) decorator is a powerf
|
|||
|
||||
## Usage
|
||||
|
||||
The [`@djc_test`](../../../reference/testing_api#djc_test) decorator can be applied to functions, methods, or classes. When applied to a class, it recursively decorates all methods starting with `test_`, including those in nested classes. This allows for comprehensive testing of component behavior.
|
||||
The [`@djc_test`](../../../reference/testing_api#djc_test) decorator can be applied to functions, methods, or classes.
|
||||
|
||||
When applied to a class, it decorates all methods starting with `test_`, and all nested classes starting with `Test`,
|
||||
recursively.
|
||||
|
||||
### Applying to a Function
|
||||
|
||||
|
@ -15,8 +18,6 @@ simply decorate the function as shown below:
|
|||
import django
|
||||
from django_components.testing import djc_test
|
||||
|
||||
django.setup()
|
||||
|
||||
@djc_test
|
||||
def test_my_component():
|
||||
@register("my_component")
|
||||
|
@ -27,38 +28,34 @@ def test_my_component():
|
|||
|
||||
### Applying to a Class
|
||||
|
||||
When applied to a class, `djc_test` decorates each `test_` method individually:
|
||||
When applied to a class, `djc_test` decorates each `test_` method, as well as all nested classes starting with `Test`.
|
||||
|
||||
```python
|
||||
import django
|
||||
from django_components.testing import djc_test
|
||||
|
||||
django.setup()
|
||||
|
||||
@djc_test
|
||||
class TestMyComponent:
|
||||
def test_something(self):
|
||||
...
|
||||
|
||||
class Nested:
|
||||
class TestNested:
|
||||
def test_something_else(self):
|
||||
...
|
||||
```
|
||||
|
||||
This is equivalent to applying the decorator to each method individually:
|
||||
This is equivalent to applying the decorator to both of the methods individually:
|
||||
|
||||
```python
|
||||
import django
|
||||
from django_components.testing import djc_test
|
||||
|
||||
django.setup()
|
||||
|
||||
class TestMyComponent:
|
||||
@djc_test
|
||||
def test_something(self):
|
||||
...
|
||||
|
||||
class Nested:
|
||||
class TestNested:
|
||||
@djc_test
|
||||
def test_something_else(self):
|
||||
...
|
||||
|
@ -70,19 +67,26 @@ See the API reference for [`@djc_test`](../../../reference/testing_api#djc_test)
|
|||
|
||||
### Setting Up Django
|
||||
|
||||
Before using [`djc_test`](../../../reference/testing_api#djc_test), ensure Django is set up:
|
||||
If you want to define a common Django settings that would be the baseline for all tests,
|
||||
you can call [`django.setup()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.setup)
|
||||
before the `@djc_test` decorator:
|
||||
|
||||
```python
|
||||
import django
|
||||
from django_components.testing import djc_test
|
||||
|
||||
django.setup()
|
||||
django.setup(...)
|
||||
|
||||
@djc_test
|
||||
def test_my_component():
|
||||
...
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
If you omit [`django.setup()`](https://docs.djangoproject.com/en/5.2/ref/applications/#django.setup)
|
||||
in the example above, `@djc_test` will call it for you, so you don't need to do it manually.
|
||||
|
||||
## Example: Parametrizing Context Behavior
|
||||
|
||||
You can parametrize the [context behavior](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior) using [`djc_test`](../../../reference/testing_api#djc_test):
|
||||
|
@ -106,6 +110,6 @@ from django_components.testing import djc_test
|
|||
)
|
||||
)
|
||||
def test_context_behavior(components_settings):
|
||||
rendered = MyComponent().render()
|
||||
rendered = MyComponent.render()
|
||||
...
|
||||
```
|
||||
|
|
|
@ -1,173 +0,0 @@
|
|||
## 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.
|
|
@ -1,15 +1,17 @@
|
|||
# `.nav.yml` is provided by https://lukasgeiter.github.io/mkdocs-awesome-nav
|
||||
nav:
|
||||
- Single-file components: single_file_components.md
|
||||
- Components in Python: components_in_python.md
|
||||
- Accessing component inputs: access_component_input.md
|
||||
- HTML / JS / CSS files: html_js_css_files.md
|
||||
- HTML / JS / CSS variables: html_js_css_variables.md
|
||||
- Secondary JS / CSS files: secondary_js_css_files.md
|
||||
- Component defaults: component_defaults.md
|
||||
- Component context and scope: component_context_scope.md
|
||||
- Template tag syntax: template_tag_syntax.md
|
||||
- Render API: render_api.md
|
||||
- Rendering components: rendering_components.md
|
||||
- Slots: slots.md
|
||||
- Template tag syntax: template_tag_syntax.md
|
||||
- HTML attributes: html_attributes.md
|
||||
- Defining HTML / JS / CSS files: defining_js_css_html_files.md
|
||||
- Autodiscovery: autodiscovery.md
|
||||
- Components as views: components_as_views.md
|
||||
- Component views and URLs: component_views_urls.md
|
||||
- HTTP Request: http_request.md
|
||||
- Typing and validation: typing_and_validation.md
|
||||
- Subclassing components: subclassing_components.md
|
||||
- Autodiscovery: autodiscovery.md
|
||||
|
|
|
@ -1,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.
|
|
@ -1,6 +1,15 @@
|
|||
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 searches for files containing components in the
|
||||
[`COMPONENTS.dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.dirs) and
|
||||
[`COMPONENTS.app_dirs`](../../../reference/settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
directories.
|
||||
|
||||
### 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).
|
||||
|
||||
We use the [`@register`](../../../reference/api#django_components.register) decorator for that:
|
||||
|
||||
```python
|
||||
from django_components import Component, register
|
||||
|
@ -12,6 +21,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 +41,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 execute the code that registers the components.
|
||||
|
||||
Autodiscovery occurs when Django is loaded, during the [`AppConfig.ready()`](https://docs.djangoproject.com/en/5.2/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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
When a component is being rendered, the component inputs are passed to various methods like
|
||||
[`get_context_data()`](../../../reference/api#django_components.Component.get_context_data),
|
||||
[`get_template_data()`](../../../reference/api#django_components.Component.get_template_data),
|
||||
[`get_js_data()`](../../../reference/api#django_components.Component.get_js_data),
|
||||
or [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
|
||||
|
||||
|
@ -10,8 +10,8 @@ no value is provided, or when the value is set to `None` for a particular input.
|
|||
|
||||
### Defining defaults
|
||||
|
||||
To define defaults for a component, you create a nested `Defaults` class within your
|
||||
[`Component`](../../../reference/api#django_components.Component) class.
|
||||
To define defaults for a component, you create a nested [`Defaults`](../../../reference/api#django_components.Component.Defaults)
|
||||
class within your [`Component`](../../../reference/api#django_components.Component) class.
|
||||
Each attribute in the `Defaults` class represents a default value for a corresponding input.
|
||||
|
||||
```py
|
||||
|
@ -24,16 +24,16 @@ class MyTable(Component):
|
|||
position = "left"
|
||||
selected_items = Default(lambda: [1, 2, 3])
|
||||
|
||||
def get_context_data(self, position, selected_items):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"position": position,
|
||||
"selected_items": selected_items,
|
||||
"position": kwargs["position"],
|
||||
"selected_items": kwargs["selected_items"],
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
In this example, `position` is a simple default value, while `selected_items` uses a factory function wrapped in `Default` to ensure a new list is created each time the default is used.
|
||||
In this example, `position` is a simple default value, while `selected_items` uses a factory function wrapped in [`Default`](../../../reference/api#django_components.Default) to ensure a new list is created each time the default is used.
|
||||
|
||||
Now, when we render the component, the defaults will be applied:
|
||||
|
||||
|
@ -65,6 +65,26 @@ and so `selected_items` will be set to `[1, 2, 3]`.
|
|||
|
||||
The defaults are aplied only to keyword arguments. They are NOT applied to positional arguments!
|
||||
|
||||
!!! warning
|
||||
|
||||
When [typing](../fundamentals/typing_and_validation.md) your components with [`Args`](../../../reference/api/#django_components.Component.Args),
|
||||
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
||||
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
|
||||
you may be inclined to define the defaults in the classes.
|
||||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
show_details: bool = True
|
||||
```
|
||||
|
||||
This is **NOT recommended**, because:
|
||||
|
||||
- The defaults will NOT be applied to inputs when using [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs) property.
|
||||
- The defaults will NOT be applied when a field is given but set to `None`.
|
||||
|
||||
Instead, define the defaults in the [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class.
|
||||
|
||||
### Default factories
|
||||
|
||||
For objects such as lists, dictionaries or other instances, you have to be careful - if you simply set a default value, this instance will be shared across all instances of the component!
|
||||
|
@ -78,7 +98,7 @@ class MyTable(Component):
|
|||
selected_items = [1, 2, 3]
|
||||
```
|
||||
|
||||
To avoid this, you can use a factory function wrapped in `Default`.
|
||||
To avoid this, you can use a factory function wrapped in [`Default`](../../../reference/api#django_components.Default).
|
||||
|
||||
```py
|
||||
from django_components import Component, Default
|
||||
|
@ -104,7 +124,7 @@ class MyTable(Component):
|
|||
|
||||
### Accessing defaults
|
||||
|
||||
Since the defaults are defined on the component class, you can access the defaults for a component with the `Component.Defaults` property.
|
||||
Since the defaults are defined on the component class, you can access the defaults for a component with the [`Component.Defaults`](../../../reference/api#django_components.Component.Defaults) property.
|
||||
|
||||
So if we have a component like this:
|
||||
|
||||
|
@ -118,10 +138,10 @@ class MyTable(Component):
|
|||
position = "left"
|
||||
selected_items = Default(lambda: [1, 2, 3])
|
||||
|
||||
def get_context_data(self, position, selected_items):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"position": position,
|
||||
"selected_items": selected_items,
|
||||
"position": kwargs["position"],
|
||||
"selected_items": kwargs["selected_items"],
|
||||
}
|
||||
```
|
||||
|
||||
|
|
151
docs/concepts/fundamentals/component_views_urls.md
Normal file
151
docs/concepts/fundamentals/component_views_urls.md
Normal file
|
@ -0,0 +1,151 @@
|
|||
_New in version 0.34_
|
||||
|
||||
_Note: Since 0.92, `Component` is no longer a subclass of Django's `View`. Instead, the nested
|
||||
[`Component.View`](../../../reference/api#django_components.Component.View) class is a subclass of Django's `View`._
|
||||
|
||||
---
|
||||
|
||||
For web applications, it's common to define endpoints that serve HTML content (AKA views).
|
||||
|
||||
django-components has a suite of features that help you write and manage views and their URLs:
|
||||
|
||||
- For each component, you can define methods for handling HTTP requests (GET, POST, etc.) - `get()`, `post()`, etc.
|
||||
|
||||
- Use [`Component.as_view()`](../../../reference/api#django_components.Component.as_view) to be able to use your Components with Django's [`urlpatterns`](https://docs.djangoproject.com/en/5.2/topics/http/urls/). This works the same way as [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view).
|
||||
|
||||
- To avoid having to manually define the endpoints for each component, you can set the component to be "public" with [`Component.View.public = True`](../../../reference/api#django_components.ComponentView.public). This will automatically create a URL for the component. To retrieve the component URL, use [`get_component_url()`](../../../reference/api#django_components.get_component_url).
|
||||
|
||||
- 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.
|
||||
|
||||
## 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:
|
||||
|
||||
```djc_py title="[project root]/components/calendar.py"
|
||||
from django_components import Component, ComponentView, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template = """
|
||||
<div class="calendar-component">
|
||||
<div class="header">
|
||||
{% slot "header" / %}
|
||||
</div>
|
||||
<div class="body">
|
||||
Today's date is <span>{{ date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
class View:
|
||||
# Handle GET requests
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Return HttpResponse with the rendered content
|
||||
return Calendar.render_to_response(
|
||||
request=request,
|
||||
kwargs={
|
||||
"date": request.GET.get("date", "2020-06-06"),
|
||||
},
|
||||
slots={
|
||||
"header": "Calendar header",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
|
||||
|
||||
`get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()`, `trace()`
|
||||
|
||||
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
||||
|
||||
|
||||
<!-- TODO_V1 REMOVE -->
|
||||
|
||||
!!! warning
|
||||
|
||||
**Deprecation warning:**
|
||||
|
||||
Previously, the handler methods such as `get()` and `post()` could be defined directly on the `Component` class:
|
||||
|
||||
```py
|
||||
class Calendar(Component):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.render_to_response(
|
||||
kwargs={
|
||||
"date": request.GET.get("date", "2020-06-06"),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
This is deprecated from v0.137 onwards, and will be removed in v1.0.
|
||||
|
||||
### Acccessing component class
|
||||
|
||||
You can access the component class from within the View methods by using the [`View.component_cls`](../../../reference/api#django_components.ComponentView.component_cls) attribute:
|
||||
|
||||
```py
|
||||
class Calendar(Component):
|
||||
...
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
return self.component_cls.render_to_response(request=request)
|
||||
```
|
||||
|
||||
## 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.2/topics/http/urls/).
|
||||
In place of the view function, create a view object with [`Component.as_view()`](../../../reference/api#django_components.Component.as_view):
|
||||
|
||||
```python title="[project root]/urls.py"
|
||||
from django.urls import path
|
||||
from components.calendar.calendar import Calendar
|
||||
|
||||
urlpatterns = [
|
||||
path("calendar/", Calendar.as_view()),
|
||||
]
|
||||
```
|
||||
|
||||
[`Component.as_view()`](../../../reference/api#django_components.Component.as_view)
|
||||
internally calls [`View.as_view()`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View.as_view), passing the component
|
||||
instance as one of the arguments.
|
||||
|
||||
## 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.View.public`](../../../reference/api#django_components.ComponentView.public) attribute to `True`:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class View:
|
||||
public = True
|
||||
|
||||
def get(self, request):
|
||||
return self.component_cls.render_to_response(request=request)
|
||||
...
|
||||
```
|
||||
|
||||
Then, to get the URL for the component, use [`get_component_url()`](../../../reference/api#django_components.get_component_url):
|
||||
|
||||
```py
|
||||
from django_components import get_component_url
|
||||
|
||||
url = get_component_url(MyComponent)
|
||||
```
|
||||
|
||||
This way you don't have to mix your app URLs with component URLs.
|
||||
|
||||
!!! info
|
||||
|
||||
If you need to pass query parameters or a fragment to the component URL, you can do so by passing the `query` and `fragment` arguments to [`get_component_url()`](../../../reference/api#django_components.get_component_url):
|
||||
|
||||
```py
|
||||
url = get_component_url(
|
||||
MyComponent,
|
||||
query={"foo": "bar"},
|
||||
fragment="baz",
|
||||
)
|
||||
# /components/ext/view/components/c1ab2c3?foo=bar#baz
|
||||
```
|
|
@ -1,148 +0,0 @@
|
|||
_New in version 0.34_
|
||||
|
||||
_Note: Since 0.92, Component no longer subclasses View. To configure the View class, set the nested `Component.View` class_
|
||||
|
||||
Components can now be used as views:
|
||||
|
||||
- Components define the `Component.as_view()` class method that can be used the same as [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view).
|
||||
|
||||
- By default, you can define GET, POST or other HTTP handlers directly on the Component, same as you do with [View](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#view). For example, you can override `get` and `post` to handle GET and POST requests, respectively.
|
||||
|
||||
- In addition, `Component` now has a [`render_to_response`](#inputs-of-render-and-render_to_response) method that renders the component template based on the provided context and slots' data and returns an `HttpResponse` object.
|
||||
|
||||
## Component as view example
|
||||
|
||||
Here's an example of a calendar component defined as a view:
|
||||
|
||||
```djc_py
|
||||
# In a file called [project root]/components/calendar.py
|
||||
from django_components import Component, ComponentView, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
|
||||
template = """
|
||||
<div class="calendar-component">
|
||||
<div class="header">
|
||||
{% slot "header" / %}
|
||||
</div>
|
||||
<div class="body">
|
||||
Today's date is <span>{{ date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Handle GET requests
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {
|
||||
"date": request.GET.get("date", "2020-06-06"),
|
||||
}
|
||||
slots = {
|
||||
"header": "Calendar header",
|
||||
}
|
||||
# Return HttpResponse with the rendered content
|
||||
return self.render_to_response(
|
||||
context=context,
|
||||
slots=slots,
|
||||
)
|
||||
```
|
||||
|
||||
Then, to use this component as a view, you should create a `urls.py` file in your components directory, and add a path to the component's view:
|
||||
|
||||
```python
|
||||
# In a file called [project root]/components/urls.py
|
||||
from django.urls import path
|
||||
from components.calendar.calendar import Calendar
|
||||
|
||||
urlpatterns = [
|
||||
path("calendar/", Calendar.as_view()),
|
||||
]
|
||||
```
|
||||
|
||||
`Component.as_view()` is a shorthand for calling [`View.as_view()`](https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.as_view) and passing the component
|
||||
instance as one of the arguments.
|
||||
|
||||
Remember to add `__init__.py` to your components directory, so that Django can find the `urls.py` file.
|
||||
|
||||
Finally, include the component's urls in your project's `urls.py` file:
|
||||
|
||||
```python
|
||||
# In a file called [project root]/urls.py
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path("components/", include("components.urls")),
|
||||
]
|
||||
```
|
||||
|
||||
Note: Slots content are automatically escaped by default to prevent XSS attacks. To disable escaping, set `escape_slots_content=False` in the `render_to_response` method. If you do so, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
|
||||
|
||||
If you're planning on passing an HTML string, check Django's use of [`format_html`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.html.format_html) and [`mark_safe`](https://docs.djangoproject.com/en/5.0/ref/utils/#django.utils.safestring.mark_safe).
|
||||
|
||||
## Modifying the View class
|
||||
|
||||
The View class that handles the requests is defined on `Component.View`.
|
||||
|
||||
When you define a GET or POST handlers on the `Component` class, like so:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.render_to_response(
|
||||
context={
|
||||
"date": request.GET.get("date", "2020-06-06"),
|
||||
},
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs) -> HttpResponse:
|
||||
variable = request.POST.get("variable")
|
||||
return self.render_to_response(
|
||||
kwargs={"variable": variable}
|
||||
)
|
||||
```
|
||||
|
||||
Then the request is still handled by `Component.View.get()` or `Component.View.post()`
|
||||
methods. However, by default, `Component.View.get()` points to `Component.get()`, and so on.
|
||||
|
||||
```py
|
||||
class ComponentView(View):
|
||||
component: Component = None
|
||||
...
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.component.get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
return self.component.post(request, *args, **kwargs)
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
If you want to define your own `View` class, you need to:
|
||||
|
||||
1. Set the class as `Component.View`
|
||||
2. Subclass from `ComponentView`, so the View instance has access to the component instance.
|
||||
|
||||
In the example below, we added extra logic into `View.setup()`.
|
||||
|
||||
Note that the POST handler is still defined at the top. This is because `View` subclasses `ComponentView`, which defines the `post()` method that calls `Component.post()`.
|
||||
|
||||
If you were to overwrite the `View.post()` method, then `Component.post()` would be ignored.
|
||||
|
||||
```py
|
||||
from django_components import Component, ComponentView
|
||||
|
||||
class MyComponent(Component):
|
||||
|
||||
def post(self, request, *args, **kwargs) -> HttpResponse:
|
||||
variable = request.POST.get("variable")
|
||||
return self.component.render_to_response(
|
||||
kwargs={"variable": variable}
|
||||
)
|
||||
|
||||
class View(ComponentView):
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super(request, *args, **kwargs)
|
||||
|
||||
do_something_extra(request, *args, **kwargs)
|
||||
```
|
|
@ -1,134 +0,0 @@
|
|||
_New in version 0.81_
|
||||
|
||||
Components can be rendered outside of Django templates, calling them as regular functions ("React-style").
|
||||
|
||||
The component class defines `render` and `render_to_response` class methods. These methods accept positional args, kwargs, and slots, offering the same flexibility as the `{% component %}` tag:
|
||||
|
||||
```djc_py
|
||||
class SimpleComponent(Component):
|
||||
template = """
|
||||
{% load component_tags %}
|
||||
hello: {{ hello }}
|
||||
foo: {{ foo }}
|
||||
kwargs: {{ kwargs|safe }}
|
||||
slot_first: {% slot "first" required / %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, arg1, arg2, **kwargs):
|
||||
return {
|
||||
"hello": arg1,
|
||||
"foo": arg2,
|
||||
"kwargs": kwargs,
|
||||
}
|
||||
|
||||
rendered = SimpleComponent.render(
|
||||
args=["world", "bar"],
|
||||
kwargs={"kw1": "test", "kw2": "ooo"},
|
||||
slots={"first": "FIRST_SLOT"},
|
||||
context={"from_context": 98},
|
||||
)
|
||||
```
|
||||
|
||||
Renders:
|
||||
|
||||
```
|
||||
hello: world
|
||||
foo: bar
|
||||
kwargs: {'kw1': 'test', 'kw2': 'ooo'}
|
||||
slot_first: FIRST_SLOT
|
||||
```
|
||||
|
||||
## Inputs of `render` and `render_to_response`
|
||||
|
||||
Both `render` and `render_to_response` accept the same input:
|
||||
|
||||
```py
|
||||
Component.render(
|
||||
context: Mapping | django.template.Context | None = None,
|
||||
args: List[Any] | None = None,
|
||||
kwargs: Dict[str, Any] | None = None,
|
||||
slots: Dict[str, str | SafeString | SlotFunc] | None = None,
|
||||
escape_slots_content: bool = True
|
||||
) -> str:
|
||||
```
|
||||
|
||||
- _`args`_ - Positional args for the component. This is the same as calling the component
|
||||
as `{% component "my_comp" arg1 arg2 ... %}`
|
||||
|
||||
- _`kwargs`_ - Keyword args for the component. This is the same as calling the component
|
||||
as `{% component "my_comp" key1=val1 key2=val2 ... %}`
|
||||
|
||||
- _`slots`_ - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
|
||||
Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
|
||||
or [`SlotFunc`](#slotfunc).
|
||||
|
||||
- _`escape_slots_content`_ - Whether the content from `slots` should be escaped. `True` by default to prevent XSS attacks. If you disable escaping, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
|
||||
|
||||
- _`context`_ - A context (dictionary or Django's Context) within which the component
|
||||
is rendered. The keys on the context can be accessed from within the template.
|
||||
- NOTE: In "isolated" mode, context is NOT accessible, and data MUST be passed via
|
||||
component's args and kwargs.
|
||||
|
||||
- _`request`_ - A Django request object. This is used to enable Django template `context_processors` to run,
|
||||
allowing for template tags like `{% csrf_token %}` and variables like `{{ debug }}`.
|
||||
- Similar behavior can be achieved with [provide / inject](#how-to-use-provide--inject).
|
||||
- This is used internally to convert `context` to a RequestContext. It does nothing if `context` is already
|
||||
a `Context` instance.
|
||||
|
||||
### `SlotFunc`
|
||||
|
||||
When rendering components with slots in `render` or `render_to_response`, you can pass either a string or a function.
|
||||
|
||||
The function has following signature:
|
||||
|
||||
```py
|
||||
def render_func(
|
||||
context: Context,
|
||||
data: Dict[str, Any],
|
||||
slot_ref: SlotRef,
|
||||
) -> str | SafeString:
|
||||
return nodelist.render(ctx)
|
||||
```
|
||||
|
||||
- _`context`_ - Django's Context available to the Slot Node.
|
||||
- _`data`_ - Data passed to the `{% slot %}` tag. See [Scoped Slots](#scoped-slots).
|
||||
- _`slot_ref`_ - The default slot content. See [Accessing original content of slots](#accessing-original-content-of-slots).
|
||||
- NOTE: The slot is lazily evaluated. To render the slot, convert it to string with `str(slot_ref)`.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
def footer_slot(ctx, data, slot_ref):
|
||||
return f"""
|
||||
SLOT_DATA: {data['abc']}
|
||||
ORIGINAL: {slot_ref}
|
||||
"""
|
||||
|
||||
MyComponent.render_to_response(
|
||||
slots={
|
||||
"footer": footer_slot,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
## Response class of `render_to_response`
|
||||
|
||||
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is `django.http.HttpResponse`.
|
||||
|
||||
If you want to use a different Response class in `render_to_response`, set the `Component.response_class` attribute:
|
||||
|
||||
```py
|
||||
class MyResponse(HttpResponse):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
# Configure response
|
||||
self.headers = ...
|
||||
self.status = ...
|
||||
|
||||
class SimpleComponent(Component):
|
||||
response_class = MyResponse
|
||||
template: types.django_html = "HELLO"
|
||||
|
||||
response = SimpleComponent.render_to_response()
|
||||
assert isinstance(response, MyResponse)
|
||||
```
|
|
@ -47,10 +47,10 @@ You can use this for example to allow users of your component to add extra attri
|
|||
```djc_py
|
||||
@register("my_comp")
|
||||
class MyComp(Component):
|
||||
# Capture extra kwargs in `attrs`
|
||||
def get_context_data(self, **attrs):
|
||||
# Pass all kwargs as `attrs`
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"attrs": attrs,
|
||||
"attrs": kwargs,
|
||||
"classes": "text-red",
|
||||
"my_id": 123,
|
||||
}
|
||||
|
@ -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:**
|
||||
|
||||
|
@ -607,10 +607,11 @@ class MyComp(Component):
|
|||
</div>
|
||||
"""
|
||||
|
||||
def get_context_data(self, date: Date, attrs: dict):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
date = kwargs.pop("date")
|
||||
return {
|
||||
"date": date,
|
||||
"attrs": attrs,
|
||||
"attrs": kwargs,
|
||||
"class_from_var": "extra-class"
|
||||
}
|
||||
|
||||
|
@ -625,7 +626,7 @@ class Parent(Component):
|
|||
/ %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, date: Date):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": datetime.now(),
|
||||
"json_data": json.dumps({"value": 456})
|
||||
|
@ -669,7 +670,7 @@ So all kwargs that start with `attrs:` will be collected into an `attrs` dict.
|
|||
attrs:@click="(e) => onClick(e, 'from_parent')"
|
||||
```
|
||||
|
||||
And `get_context_data` of `MyComp` will receive `attrs` input with following keys:
|
||||
And `get_template_data` of `MyComp` will receive a kwarg named `attrs` with following keys:
|
||||
|
||||
```py
|
||||
attrs = {
|
||||
|
|
441
docs/concepts/fundamentals/html_js_css_files.md
Normal file
441
docs/concepts/fundamentals/html_js_css_files.md
Normal file
|
@ -0,0 +1,441 @@
|
|||
## Overview
|
||||
|
||||
Each component can have single "primary" HTML, CSS and JS file associated with them.
|
||||
|
||||
Each of these can be either defined inline, or in a separate file:
|
||||
|
||||
- HTML files are defined using [`Component.template`](../../reference/api.md#django_components.Component.template) or [`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
|
||||
- CSS files are defined using [`Component.css`](../../reference/api.md#django_components.Component.css) or [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
|
||||
- JS files are defined using [`Component.js`](../../reference/api.md#django_components.Component.js) or [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
|
||||
|
||||
```py
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
css_file = "calendar.css"
|
||||
js_file = "calendar.js"
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```djc_py
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template = """
|
||||
<div class="welcome">
|
||||
Hi there!
|
||||
</div>
|
||||
"""
|
||||
css = """
|
||||
.welcome {
|
||||
color: red;
|
||||
}
|
||||
"""
|
||||
js = """
|
||||
console.log("Hello, world!");
|
||||
"""
|
||||
```
|
||||
|
||||
These "primary" files will have special behavior. For example, each will receive variables from the component's data methods.
|
||||
Read more about each file type below:
|
||||
|
||||
- [HTML](#html)
|
||||
- [CSS](#css)
|
||||
- [JS](#js)
|
||||
|
||||
In addition, you can define extra "secondary" CSS / JS files using the nested [`Component.Media`](../../reference/api.md#django_components.Component.Media) class,
|
||||
by setting [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js) and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
|
||||
|
||||
Single component can have many secondary files. There is no special behavior for them.
|
||||
|
||||
You can use these for third-party libraries, or for shared CSS / JS files.
|
||||
|
||||
Read more about [Secondary JS / CSS files](../secondary_js_css_files).
|
||||
|
||||
!!! warning
|
||||
|
||||
You **cannot** use both inlined code **and** separate file for a single language type (HTML, CSS, JS).
|
||||
|
||||
However, you can freely mix these for different languages:
|
||||
|
||||
```djc_py
|
||||
class MyTable(Component):
|
||||
template: types.django_html = """
|
||||
<div class="welcome">
|
||||
Hi there!
|
||||
</div>
|
||||
"""
|
||||
js_file = "my_table.js"
|
||||
css_file = "my_table.css"
|
||||
```
|
||||
|
||||
## HTML
|
||||
|
||||
Components use Django's template system to define their HTML.
|
||||
This means that you can use [Django's template syntax](https://docs.djangoproject.com/en/5.2/ref/templates/language/) to define your HTML.
|
||||
|
||||
Inside the template, you can access the data returned from the [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) method.
|
||||
|
||||
You can define the HTML directly in your Python code using the [`template`](../../reference/api.md#django_components.Component.template) attribute:
|
||||
|
||||
```djc_py
|
||||
class Button(Component):
|
||||
template = """
|
||||
<button class="btn">
|
||||
{% if icon %}
|
||||
<i class="{{ icon }}"></i>
|
||||
{% endif %}
|
||||
{{ text }}
|
||||
</button>
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"text": kwargs.get("text", "Click me"),
|
||||
"icon": kwargs.get("icon", None),
|
||||
}
|
||||
```
|
||||
|
||||
Or you can define the HTML in a separate file and reference it using [`template_file`](../../reference/api.md#django_components.Component.template_file):
|
||||
|
||||
```python
|
||||
class Button(Component):
|
||||
template_file = "button.html"
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"text": kwargs.get("text", "Click me"),
|
||||
"icon": kwargs.get("icon", None),
|
||||
}
|
||||
```
|
||||
|
||||
```django title="button.html"
|
||||
<button class="btn">
|
||||
{% if icon %}
|
||||
<i class="{{ icon }}"></i>
|
||||
{% endif %}
|
||||
{{ text }}
|
||||
</button>
|
||||
```
|
||||
|
||||
### Dynamic templates
|
||||
|
||||
Each component has only a single template associated with it.
|
||||
|
||||
However, whether it's for A/B testing or for preserving public API
|
||||
when sharing your components, sometimes you may need to render different templates
|
||||
based on the input to your component.
|
||||
|
||||
You can use [`Component.on_render()`](../../reference/api.md#django_components.Component.on_render)
|
||||
to dynamically override what template gets rendered.
|
||||
|
||||
By default, the component's template is rendered as-is.
|
||||
|
||||
```py
|
||||
class Table(Component):
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is not None:
|
||||
return template.render(context)
|
||||
```
|
||||
|
||||
If you want to render a different template in its place,
|
||||
we recommended you to:
|
||||
|
||||
1. Wrap the substitute templates as new Components
|
||||
2. Then render those Components inside [`Component.on_render()`](../../reference/api.md#django_components.Component.on_render):
|
||||
|
||||
```py
|
||||
class TableNew(Component):
|
||||
template_file = "table_new.html"
|
||||
|
||||
class TableOld(Component):
|
||||
template_file = "table_old.html"
|
||||
|
||||
class Table(Component):
|
||||
def on_render(self, context, template):
|
||||
if self.kwargs.get("feat_table_new_ui"):
|
||||
return TableNew.render(
|
||||
args=self.args,
|
||||
kwargs=self.kwargs,
|
||||
slots=self.slots,
|
||||
)
|
||||
else:
|
||||
return TableOld.render(
|
||||
args=self.args,
|
||||
kwargs=self.kwargs,
|
||||
slots=self.slots,
|
||||
)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
If you do not wrap the templates as Components,
|
||||
there is a risk that some [extensions](../../advanced/extensions) will not work as expected.
|
||||
|
||||
```py
|
||||
new_template = Template("""
|
||||
{% load django_components %}
|
||||
<div>
|
||||
{% slot "content" %}
|
||||
Other template
|
||||
{% endslot %}
|
||||
</div>
|
||||
""")
|
||||
|
||||
class Table(Component):
|
||||
def on_render(self, context, template):
|
||||
if self.kwargs.get("feat_table_new_ui"):
|
||||
return new_template.render(context)
|
||||
else:
|
||||
return template.render(context)
|
||||
```
|
||||
|
||||
### Template-less components
|
||||
|
||||
Since you can use [`Component.on_render()`](../../reference/api.md#django_components.Component.on_render)
|
||||
to render *other* components, there is no need to define a template for the component.
|
||||
|
||||
So even an empty component like this is valid:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
pass
|
||||
```
|
||||
|
||||
These "template-less" components can be useful as base classes for other components, or as mixins.
|
||||
|
||||
### HTML processing
|
||||
|
||||
Django Components expects the rendered template to be a valid HTML. This is needed to enable features like [CSS / JS variables](../html_js_css_variables).
|
||||
|
||||
Here is how the HTML is post-processed:
|
||||
|
||||
1. **Insert component ID**: Each root element in the rendered HTML automatically receives a `data-djc-id-cxxxxxx` attribute containing a unique component instance ID.
|
||||
|
||||
```html
|
||||
<!-- Output HTML -->
|
||||
<div class="card" data-djc-id-c1a2b3c>
|
||||
...
|
||||
</div>
|
||||
<div class="backdrop" data-djc-id-c1a2b3c>
|
||||
...
|
||||
</div>
|
||||
```
|
||||
|
||||
2. **Insert CSS ID**: If the component defines CSS variables through [`get_css_data()`](../../../reference/api/#django_components.Component.get_css_data), the root elements also receive a `data-djc-css-xxxxxx` attribute. This attribute links the element to its specific CSS variables.
|
||||
|
||||
```html
|
||||
<!-- Output HTML -->
|
||||
<div class="card" data-djc-id-c1a2b3c data-djc-css-d4e5f6>
|
||||
<!-- Component content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
3. **Insert JS and CSS**: After the HTML is rendered, Django Components handles inserting JS and CSS dependencies into the page based on the [dependencies rendering strategy](../rendering_components/#dependencies-rendering) (document, fragment, or inline).
|
||||
|
||||
For example, if your component contains the
|
||||
[`{% component_js_dependencies %}`](../../reference/template_tags.md#component_js_dependencies)
|
||||
or
|
||||
[`{% component_css_dependencies %}`](../../reference/template_tags.md#component_css_dependencies)
|
||||
tags, or the `<head>` and `<body>` elements, the JS and CSS scripts will be inserted into the HTML.
|
||||
|
||||
For more information on how JS and CSS dependencies are rendered, see [Rendering JS / CSS](../../advanced/rendering_js_css).
|
||||
|
||||
## JS
|
||||
|
||||
The component's JS script is executed in the browser:
|
||||
|
||||
- It is executed AFTER the "secondary" JS files from [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js) are loaded.
|
||||
- The script is only executed once, even if there are multiple instances of the component on the page.
|
||||
- Component JS scripts are executed in the order how they appeared in the template / HTML (top to bottom).
|
||||
|
||||
You can define the JS directly in your Python code using the [`js`](../../reference/api.md#django_components.Component.js) attribute:
|
||||
|
||||
```djc_py
|
||||
class Button(Component):
|
||||
js = """
|
||||
console.log("Hello, world!");
|
||||
"""
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"text": kwargs.get("text", "Click me"),
|
||||
}
|
||||
```
|
||||
|
||||
Or you can define the JS in a separate file and reference it using [`js_file`](../../reference/api.md#django_components.Component.js_file):
|
||||
|
||||
```python
|
||||
class Button(Component):
|
||||
js_file = "button.js"
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"text": kwargs.get("text", "Click me"),
|
||||
}
|
||||
```
|
||||
|
||||
```django title="button.js"
|
||||
console.log("Hello, world!");
|
||||
```
|
||||
|
||||
## CSS
|
||||
|
||||
You can define the CSS directly in your Python code using the [`css`](../../reference/api.md#django_components.Component.css) attribute:
|
||||
|
||||
```djc_py
|
||||
class Button(Component):
|
||||
css = """
|
||||
.btn {
|
||||
width: 100px;
|
||||
color: var(--color);
|
||||
}
|
||||
"""
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"color": kwargs.get("color", "red"),
|
||||
}
|
||||
```
|
||||
|
||||
Or you can define the CSS in a separate file and reference it using [`css_file`](../../reference/api.md#django_components.Component.css_file):
|
||||
|
||||
```python
|
||||
class Button(Component):
|
||||
css_file = "button.css"
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"text": kwargs.get("text", "Click me"),
|
||||
}
|
||||
```
|
||||
|
||||
```django title="button.css"
|
||||
.btn {
|
||||
color: red;
|
||||
}
|
||||
```
|
||||
|
||||
## File paths
|
||||
|
||||
Compared to the [secondary JS / CSS files](../secondary_js_css_files), the definition of file paths for the main HTML / JS / CSS files is quite simple - just strings, without any lists, objects, or globs.
|
||||
|
||||
However, similar to the secondary JS / CSS files, you can specify the file paths [relative to the component's directory](../secondary_js_css_files/#relative-to-component).
|
||||
|
||||
So if you have a directory with following files:
|
||||
|
||||
```
|
||||
[project root]/components/calendar/
|
||||
├── calendar.html
|
||||
├── calendar.css
|
||||
├── calendar.js
|
||||
└── calendar.py
|
||||
```
|
||||
|
||||
You can define the component like this:
|
||||
|
||||
```py title="[project root]/components/calendar/calendar.py"
|
||||
from django_components import Component, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
css_file = "calendar.css"
|
||||
js_file = "calendar.js"
|
||||
```
|
||||
|
||||
Assuming that
|
||||
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
|
||||
contains path `[project root]/components`, the example above is the same as writing out:
|
||||
|
||||
```py title="[project root]/components/calendar/calendar.py"
|
||||
from django_components import Component, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template_file = "calendar/template.html"
|
||||
css_file = "calendar/style.css"
|
||||
js_file = "calendar/script.js"
|
||||
```
|
||||
|
||||
If the path cannot be resolved relative to the component, django-components will attempt
|
||||
to resolve the path relative to the component directories, as set in
|
||||
[`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
|
||||
or
|
||||
[`COMPONENTS.app_dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs).
|
||||
|
||||
Read more about [file path resolution](../secondary_js_css_files/#relative-to-component).
|
||||
|
||||
## Access component definition
|
||||
|
||||
Component's HTML / CSS / JS is resolved and loaded lazily.
|
||||
|
||||
This means that, when you specify any of
|
||||
[`template_file`](../../reference/api.md#django_components.Component.template_file),
|
||||
[`js_file`](../../reference/api.md#django_components.Component.js_file),
|
||||
[`css_file`](../../reference/api.md#django_components.Component.css_file),
|
||||
or [`Media.js/css`](../../reference/api.md#django_components.Component.Media),
|
||||
these file paths will be resolved only once you either:
|
||||
|
||||
1. Access any of the following attributes on the component:
|
||||
|
||||
- [`media`](../../reference/api.md#django_components.Component.media),
|
||||
[`template`](../../reference/api.md#django_components.Component.template),
|
||||
[`template_file`](../../reference/api.md#django_components.Component.template_file),
|
||||
[`js`](../../reference/api.md#django_components.Component.js),
|
||||
[`js_file`](../../reference/api.md#django_components.Component.js_file),
|
||||
[`css`](../../reference/api.md#django_components.Component.css),
|
||||
[`css_file`](../../reference/api.md#django_components.Component.css_file)
|
||||
|
||||
2. Render the component.
|
||||
|
||||
Once the component's media files have been loaded once, they will remain in-memory
|
||||
on the Component class:
|
||||
|
||||
- HTML from [`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
|
||||
will be available under [`Component.template`](../../reference/api.md#django_components.Component.template)
|
||||
- CSS from [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
|
||||
will be available under [`Component.css`](../../reference/api.md#django_components.Component.css)
|
||||
- JS from [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
|
||||
will be available under [`Component.js`](../../reference/api.md#django_components.Component.js)
|
||||
|
||||
Thus, whether you define HTML via
|
||||
[`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
|
||||
or [`Component.template`](../../reference/api.md#django_components.Component.template),
|
||||
you can always access the HTML content under [`Component.template`](../../reference/api.md#django_components.Component.template).
|
||||
And the same applies for JS and CSS.
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
# When we create Calendar component, the files like `calendar/template.html`
|
||||
# are not yet loaded!
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template_file = "calendar/template.html"
|
||||
css_file = "calendar/style.css"
|
||||
js_file = "calendar/script.js"
|
||||
|
||||
class Media:
|
||||
css = "calendar/style1.css"
|
||||
js = "calendar/script2.js"
|
||||
|
||||
# It's only at this moment that django-components reads the files like `calendar/template.html`
|
||||
print(Calendar.css)
|
||||
# Output:
|
||||
# .calendar {
|
||||
# width: 200px;
|
||||
# background: pink;
|
||||
# }
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
**Do NOT modify HTML / CSS / JS after it has been loaded**
|
||||
|
||||
django-components assumes that the component's media files like `js_file` or `Media.js/css` are static.
|
||||
|
||||
If you need to dynamically change these media files, consider instead defining multiple Components.
|
||||
|
||||
Modifying these files AFTER the component has been loaded at best does nothing. However, this is
|
||||
an untested behavior, which may lead to unexpected errors.
|
496
docs/concepts/fundamentals/html_js_css_variables.md
Normal file
496
docs/concepts/fundamentals/html_js_css_variables.md
Normal file
|
@ -0,0 +1,496 @@
|
|||
When a component recieves input through [`{% component %}`](../../../reference/template_tags/#component) tag,
|
||||
or the [`Component.render()`](../../../reference/api/#django_components.Component.render) or [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response) methods, you can define how the input is handled, and what variables will be available to the template, JavaScript and CSS.
|
||||
|
||||
## Overview
|
||||
|
||||
Django Components offers three key methods for passing variables to different parts of your component:
|
||||
|
||||
- [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) - Provides variables to your HTML template
|
||||
- [`get_js_data()`](../../../reference/api/#django_components.Component.get_js_data) - Provides variables to your JavaScript code
|
||||
- [`get_css_data()`](../../../reference/api/#django_components.Component.get_css_data) - Provides variables to your CSS styles
|
||||
|
||||
These methods let you pre-process inputs before they're used in rendering.
|
||||
|
||||
Each method handles the data independently - you can define different data for the template, JS, and CSS.
|
||||
|
||||
```python
|
||||
class ProfileCard(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
user_id: int
|
||||
show_details: bool
|
||||
|
||||
class Defaults:
|
||||
show_details = True
|
||||
|
||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||
user = User.objects.get(id=kwargs.user_id)
|
||||
return {
|
||||
"user": user,
|
||||
"show_details": kwargs.show_details,
|
||||
}
|
||||
|
||||
def get_js_data(self, args, kwargs: Kwargs, slots, context):
|
||||
return {
|
||||
"user_id": kwargs.user_id,
|
||||
}
|
||||
|
||||
def get_css_data(self, args, kwargs: Kwargs, slots, context):
|
||||
text_color = "red" if kwargs.show_details else "blue"
|
||||
return {
|
||||
"text_color": text_color,
|
||||
}
|
||||
```
|
||||
|
||||
## Template variables
|
||||
|
||||
The [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) method is the primary way to provide variables to your HTML template. It receives the component inputs and returns a dictionary of data that will be available in the template.
|
||||
|
||||
If [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) returns `None`, an empty dictionary will be used.
|
||||
|
||||
```python
|
||||
class ProfileCard(Component):
|
||||
template_file = "profile_card.html"
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
user_id: int
|
||||
show_details: bool
|
||||
|
||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||
user = User.objects.get(id=kwargs.user_id)
|
||||
|
||||
# Process and transform inputs
|
||||
return {
|
||||
"user": user,
|
||||
"show_details": kwargs.show_details,
|
||||
"user_joined_days": (timezone.now() - user.date_joined).days,
|
||||
}
|
||||
```
|
||||
|
||||
In your template, you can then use these variables:
|
||||
|
||||
```django
|
||||
<div class="profile-card">
|
||||
<h2>{{ user.username }}</h2>
|
||||
|
||||
{% if show_details %}
|
||||
<p>Member for {{ user_joined_days }} days</p>
|
||||
<p>Email: {{ user.email }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Legacy `get_context_data()`
|
||||
|
||||
The [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data) method is the legacy way to provide variables to your HTML template. It serves the same purpose as [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data) - it receives the component inputs and returns a dictionary of data that will be available in the template.
|
||||
|
||||
However, [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data) has a few drawbacks:
|
||||
|
||||
- It does NOT receive the `slots` and `context` parameters.
|
||||
- The `args` and `kwargs` parameters are given as variadic `*args` and `**kwargs` parameters. As such, they cannot be typed.
|
||||
|
||||
```python
|
||||
class ProfileCard(Component):
|
||||
template_file = "profile_card.html"
|
||||
|
||||
def get_context_data(self, user_id, show_details=False, *args, **kwargs):
|
||||
user = User.objects.get(id=user_id)
|
||||
return {
|
||||
"user": user,
|
||||
"show_details": show_details,
|
||||
}
|
||||
```
|
||||
|
||||
There is a slight difference between [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data) and [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data)
|
||||
when rendering a component with the [`{% component %}`](../../../reference/template_tags/#component) tag.
|
||||
|
||||
For example if you have component that accepts kwarg `date`:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
def get_context_data(self, date, *args, **kwargs):
|
||||
return {
|
||||
"date": date,
|
||||
}
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": kwargs["date"],
|
||||
}
|
||||
```
|
||||
|
||||
The difference is that:
|
||||
|
||||
- With [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data), you can pass `date` either as arg or kwarg:
|
||||
|
||||
```django
|
||||
✅
|
||||
{% component "my_component" date=some_date %}
|
||||
{% component "my_component" some_date %}
|
||||
```
|
||||
|
||||
- But with [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data), `date` MUST be passed as kwarg:
|
||||
|
||||
```django
|
||||
✅
|
||||
{% component "my_component" date=some_date %}
|
||||
|
||||
❌
|
||||
{% component "my_component" some_date %}
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
[`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data)
|
||||
and [`get_context_data()`](../../../reference/api/#django_components.Component.get_context_data)
|
||||
are mutually exclusive.
|
||||
|
||||
If both methods return non-empty dictionaries, an error will be raised.
|
||||
|
||||
!!! note
|
||||
|
||||
The `get_context_data()` method will be removed in v2.
|
||||
|
||||
## Accessing component inputs
|
||||
|
||||
The component inputs are available in 3 ways:
|
||||
|
||||
### Function arguments
|
||||
|
||||
The data methods receive the inputs as parameters directly.
|
||||
|
||||
```python
|
||||
class ProfileCard(Component):
|
||||
# Access inputs directly as parameters
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"user_id": args[0],
|
||||
"show_details": kwargs["show_details"],
|
||||
}
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
By default, the `args` parameter is a list, while `kwargs` and `slots` are dictionaries.
|
||||
|
||||
If you add typing to your component with
|
||||
[`Args`](../../../reference/api/#django_components.Component.Args),
|
||||
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
||||
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
|
||||
the respective inputs will be given as instances of these classes.
|
||||
|
||||
Learn more about [Component typing](../../fundamentals/typing_and_validation).
|
||||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
class Args(NamedTuple):
|
||||
user_id: int
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
show_details: bool
|
||||
|
||||
# Access inputs directly as parameters
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
|
||||
return {
|
||||
"user_id": args.user_id,
|
||||
"show_details": kwargs.show_details,
|
||||
}
|
||||
```
|
||||
|
||||
### `args`, `kwargs`, `slots` properties
|
||||
|
||||
In other methods, you can access the inputs via
|
||||
[`self.args`](../../../reference/api/#django_components.Component.args),
|
||||
[`self.kwargs`](../../../reference/api/#django_components.Component.kwargs),
|
||||
and [`self.slots`](../../../reference/api/#django_components.Component.slots) properties:
|
||||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
def on_render_before(self, context: Context, template: Optional[Template]):
|
||||
# Access inputs via self.args, self.kwargs, self.slots
|
||||
self.args[0]
|
||||
self.kwargs.get("show_details", False)
|
||||
self.slots["footer"]
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
These properties work the same way as `args`, `kwargs`, and `slots` parameters in the data methods:
|
||||
|
||||
By default, the `args` property is a list, while `kwargs` and `slots` are dictionaries.
|
||||
|
||||
If you add typing to your component with
|
||||
[`Args`](../../../reference/api/#django_components.Component.Args),
|
||||
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
||||
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
|
||||
the respective inputs will be given as instances of these classes.
|
||||
|
||||
Learn more about [Component typing](../../fundamentals/typing_and_validation).
|
||||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
class Args(NamedTuple):
|
||||
user_id: int
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
show_details: bool
|
||||
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
|
||||
return {
|
||||
"user_id": self.args.user_id,
|
||||
"show_details": self.kwargs.show_details,
|
||||
}
|
||||
```
|
||||
|
||||
<!-- TODO_v1 - Remove -->
|
||||
|
||||
### `input` property (low-level)
|
||||
|
||||
!!! warning
|
||||
|
||||
The `input` property is deprecated and will be removed in v1.
|
||||
|
||||
Instead, use properties defined on the
|
||||
[`Component`](../../../reference/api/#django_components.Component) class
|
||||
directly like
|
||||
[`self.context`](../../../reference/api/#django_components.Component.context).
|
||||
|
||||
To access the unmodified inputs, use
|
||||
[`self.raw_args`](../../../reference/api/#django_components.Component.raw_args),
|
||||
[`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs),
|
||||
and [`self.raw_slots`](../../../reference/api/#django_components.Component.raw_slots) properties.
|
||||
|
||||
The previous two approaches allow you to access only the most important inputs.
|
||||
|
||||
There are additional settings that may be passed to components.
|
||||
If you need to access these, you can use [`self.input`](../../../reference/api/#django_components.Component.input) property
|
||||
for a low-level access to all the inputs.
|
||||
|
||||
The `input` property contains all the inputs passed to the component (instance of [`ComponentInput`](../../../reference/api/#django_components.ComponentInput)).
|
||||
|
||||
This includes:
|
||||
|
||||
- [`input.args`](../../../reference/api/#django_components.ComponentInput.args) - List of positional arguments
|
||||
- [`input.kwargs`](../../../reference/api/#django_components.ComponentInput.kwargs) - Dictionary of keyword arguments
|
||||
- [`input.slots`](../../../reference/api/#django_components.ComponentInput.slots) - Dictionary of slots. Values are normalized to [`Slot`](../../../reference/api/#django_components.Slot) instances
|
||||
- [`input.context`](../../../reference/api/#django_components.ComponentInput.context) - [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) object that should be used to render the component
|
||||
- [`input.type`](../../../reference/api/#django_components.ComponentInput.type) - The type of the component (document, fragment)
|
||||
- [`input.render_dependencies`](../../../reference/api/#django_components.ComponentInput.render_dependencies) - Whether to render dependencies (CSS, JS)
|
||||
|
||||
```python
|
||||
class ProfileCard(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
# Access positional arguments
|
||||
user_id = self.input.args[0] if self.input.args else None
|
||||
|
||||
# Access keyword arguments
|
||||
show_details = self.input.kwargs.get("show_details", False)
|
||||
|
||||
# Render component differently depending on the type
|
||||
if self.input.type == "fragment":
|
||||
...
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"show_details": show_details,
|
||||
}
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
Unlike the parameters passed to the data methods, the `args`, `kwargs`, and `slots` in `self.input` property are always lists and dictionaries,
|
||||
regardless of whether you added typing classes to your component (like [`Args`](../../../reference/api/#django_components.Component.Args),
|
||||
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
||||
or [`Slots`](../../../reference/api/#django_components.Component.Slots)).
|
||||
|
||||
## Default values
|
||||
|
||||
You can use [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class to provide default values for your inputs.
|
||||
|
||||
These defaults will be applied either when:
|
||||
|
||||
- The input is not provided at rendering time
|
||||
- The input is provided as `None`
|
||||
|
||||
When you then access the inputs in your data methods, the default values will be already applied.
|
||||
|
||||
Read more about [Component Defaults](./component_defaults.md).
|
||||
|
||||
```py
|
||||
from django_components import Component, Default, register
|
||||
|
||||
@register("profile_card")
|
||||
class ProfileCard(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
show_details: bool
|
||||
|
||||
class Defaults:
|
||||
show_details = True
|
||||
|
||||
# show_details will be set to True if `None` or missing
|
||||
def get_template_data(self, args, kwargs: Kwargs, slots, context):
|
||||
return {
|
||||
"show_details": kwargs.show_details,
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
When typing your components with [`Args`](../../../reference/api/#django_components.Component.Args),
|
||||
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
||||
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
|
||||
you may be inclined to define the defaults in the classes.
|
||||
|
||||
```py
|
||||
class ProfileCard(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
show_details: bool = True
|
||||
```
|
||||
|
||||
This is **NOT recommended**, because:
|
||||
|
||||
- The defaults will NOT be applied to inputs when using [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs) property.
|
||||
- The defaults will NOT be applied when a field is given but set to `None`.
|
||||
|
||||
Instead, define the defaults in the [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class.
|
||||
|
||||
## Accessing Render API
|
||||
|
||||
All three data methods have access to the Component's [Render API](../render_api), which includes:
|
||||
|
||||
- [`self.args`](../render_api/#args) - The positional arguments for the current render call
|
||||
- [`self.kwargs`](../render_api/#kwargs) - The keyword arguments for the current render call
|
||||
- [`self.slots`](../render_api/#slots) - The slots for the current render call
|
||||
- [`self.raw_args`](../render_api/#args) - Unmodified positional arguments for the current render call
|
||||
- [`self.raw_kwargs`](../render_api/#kwargs) - Unmodified keyword arguments for the current render call
|
||||
- [`self.raw_slots`](../render_api/#slots) - Unmodified slots for the current render call
|
||||
- [`self.context`](../render_api/#context) - The context for the current render call
|
||||
- [`self.id`](../render_api/#component-id) - The unique ID for the current render call
|
||||
- [`self.request`](../render_api/#request-and-context-processors) - The request object
|
||||
- [`self.context_processors_data`](../render_api/#request-and-context-processors) - Data from Django's context processors
|
||||
- [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
|
||||
- [`self.registry`](../render_api/#template-tag-metadata) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
|
||||
- [`self.registered_name`](../render_api/#template-tag-metadata) - The name under which the component was registered
|
||||
- [`self.outer_context`](../render_api/#template-tag-metadata) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
|
||||
- `self.deps_strategy` - The strategy for rendering dependencies
|
||||
|
||||
## Type hints
|
||||
|
||||
### Typing inputs
|
||||
|
||||
You can add type hints for the component inputs to ensure that the component logic is correct.
|
||||
|
||||
For this, define the [`Args`](../../../reference/api/#django_components.Component.Args),
|
||||
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
||||
and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
|
||||
and then add type hints to the data methods.
|
||||
|
||||
This will also validate the inputs at runtime, as the type classes will be instantiated with the inputs.
|
||||
|
||||
Read more about [Component typing](../../fundamentals/typing_and_validation).
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
surname: str
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
my_slot: Optional[SlotInput] = None
|
||||
footer: SlotInput
|
||||
|
||||
# Use the above classes to add type hints to the data method
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
# The parameters are instances of the classes we defined
|
||||
assert isinstance(args, Button.Args)
|
||||
assert isinstance(kwargs, Button.Kwargs)
|
||||
assert isinstance(slots, Button.Slots)
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
To access "untyped" inputs, use [`self.raw_args`](../../../reference/api/#django_components.Component.raw_args),
|
||||
[`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs),
|
||||
and [`self.raw_slots`](../../../reference/api/#django_components.Component.raw_slots) properties.
|
||||
|
||||
These are plain lists and dictionaries, even when you added typing to your component.
|
||||
|
||||
### Typing data
|
||||
|
||||
In the same fashion, you can add types and validation for the data that should be RETURNED from each data method.
|
||||
|
||||
For this, set the [`TemplateData`](../../../reference/api/#django_components.Component.TemplateData),
|
||||
[`JsData`](../../../reference/api/#django_components.Component.JsData),
|
||||
and [`CssData`](../../../reference/api/#django_components.Component.CssData) classes on the component class.
|
||||
|
||||
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Button(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
class JsData(NamedTuple):
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
class CssData(NamedTuple):
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return Button.TemplateData(
|
||||
data1="...",
|
||||
data2=123,
|
||||
)
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return Button.JsData(
|
||||
js_data1="...",
|
||||
js_data2=123,
|
||||
)
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return Button.CssData(
|
||||
css_data1="...",
|
||||
css_data2=123,
|
||||
)
|
||||
```
|
||||
|
||||
## Pass-through kwargs
|
||||
|
||||
It's best practice to explicitly define what args and kwargs a component accepts.
|
||||
|
||||
However, if you want a looser setup, you can easily write components that accept any number
|
||||
of kwargs, and pass them all to the template
|
||||
(similar to [django-cotton](https://github.com/wrabit/django-cotton)).
|
||||
|
||||
To do that, simply return the `kwargs` dictionary itself from [`get_template_data()`](../../../reference/api/#django_components.Component.get_template_data):
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return kwargs
|
||||
```
|
||||
|
||||
You can do the same for [`get_js_data()`](../../../reference/api/#django_components.Component.get_js_data) and [`get_css_data()`](../../../reference/api/#django_components.Component.get_css_data), if needed:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return kwargs
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return kwargs
|
||||
```
|
|
@ -1,25 +1,22 @@
|
|||
The most common use of django-components is to render HTML for a given request. As such,
|
||||
The most common use of django-components is to render HTML when the server receives a request. As such,
|
||||
there are a few features that are dependent on the request object.
|
||||
|
||||
## Passing and accessing HttpRequest
|
||||
## Passing the HttpRequest object
|
||||
|
||||
In regular Django templates, the request object is available only within the `RequestContext`.
|
||||
In regular Django templates, the request object is available only within the [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext).
|
||||
|
||||
In Components, you can either use `RequestContext`, or pass the `request` object
|
||||
In Components, you can either use [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext), or pass the `request` object
|
||||
explicitly to [`Component.render()`](../../../reference/api#django_components.Component.render) and
|
||||
[`Component.render_to_response()`](../../../reference/api#django_components.Component.render_to_response).
|
||||
|
||||
When a component is nested in another, the child component uses parent's `request` object.
|
||||
So the request object is available to components either when:
|
||||
|
||||
You can access the request object under [`Component.request`](../../../reference/api#django_components.Component.request):
|
||||
- The component is rendered with [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext) (Regular Django behavior)
|
||||
- The component is rendered with a regular [`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context) (or none), but you set the `request` kwarg
|
||||
of [`Component.render()`](../../../reference/api#django_components.Component.render).
|
||||
- The component is nested and the parent has access to the request object.
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
def get_context_data(self):
|
||||
return {
|
||||
'user_id': self.request.GET['user_id'],
|
||||
}
|
||||
|
||||
# ✅ With request
|
||||
MyComponent.render(request=request)
|
||||
MyComponent.render(context=RequestContext(request, {}))
|
||||
|
@ -29,31 +26,7 @@ MyComponent.render()
|
|||
MyComponent.render(context=Context({}))
|
||||
```
|
||||
|
||||
## Context Processors
|
||||
|
||||
Components support Django's [context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#using-requestcontext).
|
||||
|
||||
In regular Django templates, the context processors are applied only when the template is rendered with `RequestContext`.
|
||||
|
||||
Components allow you to pass the `request` object explicitly. Thus, the context processors are applied to components either when:
|
||||
|
||||
- The component is rendered with `RequestContext` (Regular Django behavior)
|
||||
- The component is rendered with a regular `Context` (or none), but you set the `request` kwarg
|
||||
of [`Component.render()`](../../../reference/api#django_components.Component.render).
|
||||
- The component is nested in another component that matches one of the two conditions above.
|
||||
|
||||
```python
|
||||
# ❌ No context processors
|
||||
rendered = MyComponent.render()
|
||||
rendered = MyComponent.render(Context({}))
|
||||
|
||||
# ✅ With context processors
|
||||
rendered = MyComponent.render(request=request)
|
||||
rendered = MyComponent.render(Context({}), request=request)
|
||||
rendered = MyComponent.render(RequestContext(request, {}))
|
||||
```
|
||||
|
||||
When a component is rendered within a template with [`{% component %}`](../../../reference/template_tags#component) tag, context processors are available depending on whether the template is rendered with `RequestContext` or not.
|
||||
When a component is rendered within a template with [`{% component %}`](../../../reference/template_tags#component) tag, the request object is available depending on whether the template is rendered with [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext) or not.
|
||||
|
||||
```python
|
||||
template = Template("""
|
||||
|
@ -62,13 +35,33 @@ template = Template("""
|
|||
</div>
|
||||
""")
|
||||
|
||||
# ❌ No context processors
|
||||
# ❌ No request
|
||||
rendered = template.render(Context({}))
|
||||
|
||||
# ✅ With context processors
|
||||
# ✅ With request
|
||||
rendered = template.render(RequestContext(request, {}))
|
||||
```
|
||||
|
||||
## Accessing the HttpRequest object
|
||||
|
||||
When the component has access to the `request` object, the request object will be available in [`Component.request`](../../../reference/api/#django_components.Component.request).
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
'user_id': self.request.GET['user_id'],
|
||||
}
|
||||
```
|
||||
|
||||
## Context Processors
|
||||
|
||||
Components support Django's [context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
|
||||
|
||||
In regular Django templates, the context processors are applied only when the template is rendered with [`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext).
|
||||
|
||||
In Components, the context processors are applied when the component has access to the `request` object.
|
||||
|
||||
### Accessing context processors data
|
||||
|
||||
The data from context processors is automatically available within the component's template.
|
||||
|
@ -84,13 +77,21 @@ class MyComponent(Component):
|
|||
MyComponent.render(request=request)
|
||||
```
|
||||
|
||||
You can also access the context processors data from within [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data) and other methods under [`Component.context_processors_data`](../../../reference/api#django_components.Component.context_processors_data).
|
||||
You can also access the context processors data from within [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data) and other methods under [`Component.context_processors_data`](../../../reference/api#django_components.Component.context_processors_data).
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
csrf_token = self.context_processors_data['csrf_token']
|
||||
return {
|
||||
'csrf_token': csrf_token,
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
!!! 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.
|
||||
|
|
376
docs/concepts/fundamentals/render_api.md
Normal file
376
docs/concepts/fundamentals/render_api.md
Normal file
|
@ -0,0 +1,376 @@
|
|||
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.
|
||||
|
||||
We refer to these render-time-only methods and attributes as the "Render API".
|
||||
|
||||
Render API is available inside these [`Component`](../../../reference/api#django_components.Component) methods:
|
||||
|
||||
- [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data)
|
||||
- [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data)
|
||||
- [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data)
|
||||
- [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)
|
||||
- [`on_render_before()`](../../../reference/api#django_components.Component.on_render_before)
|
||||
- [`on_render()`](../../../reference/api#django_components.Component.on_render)
|
||||
- [`on_render_after()`](../../../reference/api#django_components.Component.on_render_after)
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
class Table(Component):
|
||||
def on_render_before(self, context, template):
|
||||
# Access component's ID
|
||||
assert self.id == "c1A2b3c"
|
||||
|
||||
# Access component's inputs, slots and context
|
||||
assert self.args == [123, "str"]
|
||||
assert self.kwargs == {"variable": "test", "another": 1}
|
||||
footer_slot = self.slots["footer"]
|
||||
some_var = self.context["some_var"]
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
# 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"
|
||||
|
||||
rendered = Table.render(
|
||||
kwargs={"variable": "test", "another": 1},
|
||||
args=(123, "str"),
|
||||
slots={"footer": "MY_SLOT"},
|
||||
)
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
The Render API includes:
|
||||
|
||||
- Component inputs:
|
||||
- [`self.args`](../render_api/#args) - The positional arguments for the current render call
|
||||
- [`self.kwargs`](../render_api/#kwargs) - The keyword arguments for the current render call
|
||||
- [`self.slots`](../render_api/#slots) - The slots for the current render call
|
||||
- [`self.raw_args`](../render_api/#args) - Unmodified positional arguments for the current render call
|
||||
- [`self.raw_kwargs`](../render_api/#kwargs) - Unmodified keyword arguments for the current render call
|
||||
- [`self.raw_slots`](../render_api/#slots) - Unmodified slots for the current render call
|
||||
- [`self.context`](../render_api/#context) - The context for the current render call
|
||||
- [`self.deps_strategy`](../../advanced/rendering_js_css#dependencies-strategies) - The strategy for rendering dependencies
|
||||
|
||||
- Request-related:
|
||||
- [`self.request`](../render_api/#request-and-context-processors) - The request object (if available)
|
||||
- [`self.context_processors_data`](../render_api/#request-and-context-processors) - Data from Django's context processors
|
||||
|
||||
- Provide / inject:
|
||||
- [`self.inject()`](../render_api/#provide-inject) - Inject data into the component
|
||||
|
||||
- Template tag metadata:
|
||||
- [`self.node`](../render_api/#template-tag-metadata) - The [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) instance
|
||||
- [`self.registry`](../render_api/#template-tag-metadata) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
|
||||
- [`self.registered_name`](../render_api/#template-tag-metadata) - The name under which the component was registered
|
||||
- [`self.outer_context`](../render_api/#template-tag-metadata) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
|
||||
|
||||
- Other metadata:
|
||||
- [`self.id`](../render_api/#component-id) - The unique ID for the current render call
|
||||
|
||||
## Component inputs
|
||||
|
||||
### Args
|
||||
|
||||
The `args` argument as passed to
|
||||
[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
|
||||
|
||||
If you defined the [`Component.Args`](../../../reference/api/#django_components.Component.Args) class,
|
||||
then the [`Component.args`](../../../reference/api/#django_components.Component.args) property will return an instance of that class.
|
||||
|
||||
Otherwise, `args` will be a plain list.
|
||||
|
||||
Use [`self.raw_args`](../../../reference/api/#django_components.Component.raw_args)
|
||||
to access the positional arguments as a plain list irrespective of [`Component.Args`](../../../reference/api/#django_components.Component.Args).
|
||||
|
||||
**Example:**
|
||||
|
||||
With `Args` class:
|
||||
|
||||
```python
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert self.args.page == 123
|
||||
assert self.args.per_page == 10
|
||||
|
||||
rendered = Table.render(
|
||||
args=[123, 10],
|
||||
)
|
||||
```
|
||||
|
||||
Without `Args` class:
|
||||
|
||||
```python
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert self.args[0] == 123
|
||||
assert self.args[1] == 10
|
||||
```
|
||||
|
||||
### Kwargs
|
||||
|
||||
The `kwargs` argument as passed to
|
||||
[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
|
||||
|
||||
If you defined the [`Component.Kwargs`](../../../reference/api/#django_components.Component.Kwargs) class,
|
||||
then the [`Component.kwargs`](../../../reference/api/#django_components.Component.kwargs) property will return an instance of that class.
|
||||
|
||||
Otherwise, `kwargs` will be a plain dictionary.
|
||||
|
||||
Use [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs)
|
||||
to access the keyword arguments as a plain dictionary irrespective of [`Component.Kwargs`](../../../reference/api/#django_components.Component.Kwargs).
|
||||
|
||||
**Example:**
|
||||
|
||||
With `Kwargs` class:
|
||||
|
||||
```python
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
page: int
|
||||
per_page: int
|
||||
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert self.kwargs.page == 123
|
||||
assert self.kwargs.per_page == 10
|
||||
|
||||
rendered = Table.render(
|
||||
kwargs={"page": 123, "per_page": 10},
|
||||
)
|
||||
```
|
||||
|
||||
Without `Kwargs` class:
|
||||
|
||||
```python
|
||||
from django_components import Component
|
||||
|
||||
class Table(Component):
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert self.kwargs["page"] == 123
|
||||
assert self.kwargs["per_page"] == 10
|
||||
```
|
||||
|
||||
### Slots
|
||||
|
||||
The `slots` argument as passed to
|
||||
[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
|
||||
|
||||
If you defined the [`Component.Slots`](../../../reference/api/#django_components.Component.Slots) class,
|
||||
then the [`Component.slots`](../../../reference/api/#django_components.Component.slots) property will return an instance of that class.
|
||||
|
||||
Otherwise, `slots` will be a plain dictionary.
|
||||
|
||||
Use [`self.raw_slots`](../../../reference/api/#django_components.Component.raw_slots)
|
||||
to access the slots as a plain dictionary irrespective of [`Component.Slots`](../../../reference/api/#django_components.Component.Slots).
|
||||
|
||||
**Example:**
|
||||
|
||||
With `Slots` class:
|
||||
|
||||
```python
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Table(Component):
|
||||
class Slots(NamedTuple):
|
||||
header: SlotInput
|
||||
footer: SlotInput
|
||||
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert isinstance(self.slots.header, Slot)
|
||||
assert isinstance(self.slots.footer, Slot)
|
||||
|
||||
rendered = Table.render(
|
||||
slots={
|
||||
"header": "MY_HEADER",
|
||||
"footer": lambda ctx: "FOOTER: " + ctx.data["user_id"],
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Without `Slots` class:
|
||||
|
||||
```python
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
class Table(Component):
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
assert isinstance(self.slots["header"], Slot)
|
||||
assert isinstance(self.slots["footer"], Slot)
|
||||
```
|
||||
|
||||
### Context
|
||||
|
||||
The `context` argument as passed to
|
||||
[`Component.get_template_data()`](../../../reference/api/#django_components.Component.get_template_data).
|
||||
|
||||
This is Django's [Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
|
||||
with which the component template is rendered.
|
||||
|
||||
If the root component or template was rendered with
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
|
||||
then this will be an instance of `RequestContext`.
|
||||
|
||||
Whether the context variables defined in `context` are available to the template depends on the
|
||||
[context behavior mode](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior):
|
||||
|
||||
- In `"django"` context behavior mode, the template will have access to the keys of this context.
|
||||
|
||||
- In `"isolated"` context behavior mode, the template will NOT have access to this context,
|
||||
and data MUST be passed via component's args and kwargs.
|
||||
|
||||
## 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).
|
||||
|
||||
The ID is a 7-letter alphanumeric string in the format `cXXXXXX`,
|
||||
where `XXXXXX` is a random string of 6 alphanumeric characters (case-sensitive).
|
||||
|
||||
E.g. `c1a2b3c`.
|
||||
|
||||
A single render ID has a chance of collision 1 in 57 billion. However, due to birthday paradox, the chance of collision increases to 1% when approaching ~33K render IDs.
|
||||
|
||||
Thus, there is currently a soft-cap of ~30K components rendered on a single page.
|
||||
|
||||
If you need to expand this limit, please open an issue on GitHub.
|
||||
|
||||
```python
|
||||
class Table(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
# Access component's ID
|
||||
assert self.id == "c1A2b3c"
|
||||
```
|
||||
|
||||
## Request and context processors
|
||||
|
||||
Components have access to the request object and context processors data if the component was:
|
||||
|
||||
- Given a [`request`](../../../reference/api/#django_components.Component.render) kwarg directly
|
||||
- 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.
|
||||
|
||||
Read more about the request object and context processors in the [HTTP Request](./http_request.md) section.
|
||||
|
||||
```python
|
||||
from django.http import HttpRequest
|
||||
|
||||
class Table(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
# Access the request object and Django's context processors
|
||||
assert self.request.GET == {"query": "something"}
|
||||
assert self.context_processors_data['user'].username == "admin"
|
||||
|
||||
rendered = Table.render(
|
||||
request=HttpRequest(),
|
||||
)
|
||||
```
|
||||
|
||||
## Provide / Inject
|
||||
|
||||
Components support a provide / inject system as known from Vue or React.
|
||||
|
||||
When rendering the component, you can call [`self.inject()`](../../../reference/api/#django_components.Component.inject) with the key of the data you want to inject.
|
||||
|
||||
The object returned by [`self.inject()`](../../../reference/api/#django_components.Component.inject)
|
||||
|
||||
To provide data to components, use the [`{% provide %}`](../../../reference/template_tags#provide) template tag.
|
||||
|
||||
Read more about [Provide / Inject](../advanced/provide_inject.md).
|
||||
|
||||
```python
|
||||
class Table(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
# Access provided data
|
||||
data = self.inject("some_data")
|
||||
assert data.some_data == "some_data"
|
||||
```
|
||||
|
||||
## Template tag metadata
|
||||
|
||||
If the component is rendered with [`{% component %}`](../../../reference/template_tags#component) template tag,
|
||||
the following metadata is available:
|
||||
|
||||
- [`self.node`](../../../reference/api/#django_components.Component.node) - The [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) instance
|
||||
- [`self.registry`](../../../reference/api/#django_components.Component.registry) - The [`ComponentRegistry`](../../../reference/api/#django_components.ComponentRegistry) instance
|
||||
that was used to render the component
|
||||
- [`self.registered_name`](../../../reference/api/#django_components.Component.registered_name) - The name under which the component was registered
|
||||
- [`self.outer_context`](../../../reference/api/#django_components.Component.outer_context) - The context outside of the [`{% component %}`](../../../reference/template_tags#component) tag
|
||||
|
||||
```django
|
||||
{% with abc=123 %}
|
||||
{{ abc }} {# <--- This is in outer context #}
|
||||
{% component "my_component" / %}
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
You can use these to check whether the component was rendered inside a template with [`{% component %}`](../../../reference/template_tags#component) tag
|
||||
or in Python with [`Component.render()`](../../../reference/api/#django_components.Component.render).
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
if self.registered_name is None:
|
||||
# Do something for the render() function
|
||||
else:
|
||||
# Do something for the {% component %} template tag
|
||||
```
|
||||
|
||||
You can access the [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) under [`Component.node`](../../../reference/api/#django_components.Component.node):
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
def get_template_data(self, context, template):
|
||||
if self.node is not None:
|
||||
assert self.node.name == "my_component"
|
||||
```
|
||||
|
||||
Accessing the [`ComponentNode`](../../../reference/api/#django_components.ComponentNode) is mostly useful for extensions, which can modify their behaviour based on the source of the Component.
|
||||
|
||||
For example, if `MyComponent` was used in another component - that is,
|
||||
with a `{% component "my_component" %}` tag
|
||||
in a template that belongs to another component - then you can use
|
||||
[`self.node.template_component`](../../../reference/api/#django_components.ComponentNode.template_component)
|
||||
to access the owner [`Component`](../../../reference/api/#django_components.Component) class.
|
||||
|
||||
```djc_py
|
||||
class Parent(Component):
|
||||
template: types.django_html = """
|
||||
<div>
|
||||
{% component "my_component" / %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
@register("my_component")
|
||||
class MyComponent(Component):
|
||||
def get_template_data(self, context, template):
|
||||
if self.node is not None:
|
||||
assert self.node.template_component == Parent
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
`Component.node` is `None` if the component is created by [`Component.render()`](../../../reference/api/#django_components.Component.render)
|
||||
(but you can pass in the `node` kwarg yourself).
|
627
docs/concepts/fundamentals/rendering_components.md
Normal file
627
docs/concepts/fundamentals/rendering_components.md
Normal file
|
@ -0,0 +1,627 @@
|
|||
Your components can be rendered either within your Django templates, or directly in Python code.
|
||||
|
||||
## Overview
|
||||
|
||||
Django Components provides three main methods to render components:
|
||||
|
||||
- [`{% component %}` tag](#component-tag) - Renders the component within your Django templates
|
||||
- [`Component.render()` method](#render-method) - Renders the component to a string
|
||||
- [`Component.render_to_response()` method](#render-to-response-method) - Renders the component and wraps it in an HTTP response
|
||||
|
||||
## `{% component %}` tag
|
||||
|
||||
Use the [`{% component %}`](../../../reference/template_tags#component) tag to render a component within your Django templates.
|
||||
|
||||
The [`{% component %}`](../../../reference/template_tags#component) tag takes:
|
||||
|
||||
- Component's registered name as the first positional argument,
|
||||
- Followed by any number of positional and keyword arguments.
|
||||
|
||||
```django
|
||||
{% load component_tags %}
|
||||
<div>
|
||||
{% component "button" name="John" job="Developer" / %}
|
||||
</div>
|
||||
```
|
||||
|
||||
To pass in slots content, you can insert [`{% fill %}`](../../../reference/template_tags#fill) tags,
|
||||
directly within the [`{% component %}`](../../../reference/template_tags#component) tag to "fill" the slots:
|
||||
|
||||
```django
|
||||
{% component "my_table" rows=rows headers=headers %}
|
||||
{% fill "pagination" %}
|
||||
< 1 | 2 | 3 >
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
You can even nest [`{% fill %}`](../../../reference/template_tags#fill) tags within
|
||||
[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
|
||||
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
|
||||
and other tags:
|
||||
|
||||
```django
|
||||
{% component "my_table" rows=rows headers=headers %}
|
||||
{% if rows %}
|
||||
{% fill "pagination" %}
|
||||
< 1 | 2 | 3 >
|
||||
{% endfill %}
|
||||
{% endif %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
!!! info "Omitting the `component` keyword"
|
||||
|
||||
If you would like to omit the `component` keyword, and simply refer to your
|
||||
components by their registered names:
|
||||
|
||||
```django
|
||||
{% button name="John" job="Developer" / %}
|
||||
```
|
||||
|
||||
You can do so by setting the "shorthand" [Tag formatter](../../advanced/tag_formatters) in the settings:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
COMPONENTS = {
|
||||
"tag_formatter": "django_components.component_shorthand_formatter",
|
||||
}
|
||||
```
|
||||
|
||||
!!! info "Extended template tag syntax"
|
||||
|
||||
Unlike regular Django template tags, django-components' tags offer extra features like
|
||||
defining literal lists and dicts, and more. Read more about [Template tag syntax](../template_tag_syntax).
|
||||
|
||||
### Registering components
|
||||
|
||||
For a component to be renderable with the [`{% component %}`](../../../reference/template_tags#component) tag, it must be first registered with the [`@register()`](../../../reference/api/#django_components.register) decorator.
|
||||
|
||||
For example, if you register a component under the name `"button"`:
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from django_components import Component, register
|
||||
|
||||
@register("button")
|
||||
class Button(Component):
|
||||
template_file = "button.html"
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
name: str
|
||||
job: str
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
...
|
||||
```
|
||||
|
||||
Then you can render this component by using its registered name `"button"` in the template:
|
||||
|
||||
```django
|
||||
{% component "button" name="John" job="Developer" / %}
|
||||
```
|
||||
|
||||
As you can see above, the args and kwargs passed to the [`{% component %}`](../../../reference/template_tags#component) tag correspond
|
||||
to the component's input.
|
||||
|
||||
For more details, read [Registering components](../../advanced/component_registry).
|
||||
|
||||
!!! note "Why do I need to register components?"
|
||||
|
||||
TL;DR: To be able to share components as libraries, and because components can be registed with multiple registries / libraries.
|
||||
|
||||
Django-components allows to [share components across projects](../../advanced/component_libraries).
|
||||
|
||||
However, different projects may use different settings. For example, one project may prefer the "long" format:
|
||||
|
||||
```django
|
||||
{% component "button" name="John" job="Developer" / %}
|
||||
```
|
||||
|
||||
While the other may use the "short" format:
|
||||
|
||||
```django
|
||||
{% button name="John" job="Developer" / %}
|
||||
```
|
||||
|
||||
Both approaches are supported simultaneously for backwards compatibility, because django-components
|
||||
started out with only the "long" format.
|
||||
|
||||
To avoid ambiguity, when you use a 3rd party library, it uses the syntax that the author
|
||||
had configured for it.
|
||||
|
||||
So when you are creating a component, django-components need to know which registry the component
|
||||
belongs to, so it knows which syntax to use.
|
||||
|
||||
### Rendering templates
|
||||
|
||||
If you have embedded the component in a Django template using the
|
||||
[`{% component %}`](../../reference/template_tags#component) tag:
|
||||
|
||||
```django title="[project root]/templates/my_template.html"
|
||||
{% load component_tags %}
|
||||
<div>
|
||||
{% component "calendar" date="2024-12-13" / %}
|
||||
</div>
|
||||
```
|
||||
|
||||
You can simply render the template with the Django's API:
|
||||
|
||||
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
|
||||
|
||||
```python
|
||||
from django.shortcuts import render
|
||||
|
||||
context = {"date": "2024-12-13"}
|
||||
rendered_template = render(request, "my_template.html", context)
|
||||
```
|
||||
|
||||
- [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
|
||||
|
||||
```python
|
||||
from django.template import Template
|
||||
from django.template.loader import get_template
|
||||
|
||||
# Either from a file
|
||||
template = get_template("my_template.html")
|
||||
|
||||
# or inlined
|
||||
template = Template("""
|
||||
{% load component_tags %}
|
||||
<div>
|
||||
{% component "calendar" date="2024-12-13" / %}
|
||||
</div>
|
||||
""")
|
||||
|
||||
rendered_template = template.render()
|
||||
```
|
||||
|
||||
### Isolating components
|
||||
|
||||
By default, components behave similarly to Django's
|
||||
[`{% include %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#include),
|
||||
and the template inside the component has access to the variables defined in the outer template.
|
||||
|
||||
You can selectively isolate a component, using the `only` flag, so that the inner template
|
||||
can access only the data that was explicitly passed to it:
|
||||
|
||||
```django
|
||||
{% component "name" positional_arg keyword_arg=value ... only / %}
|
||||
```
|
||||
|
||||
Alternatively, you can set all components to be isolated by default, by setting
|
||||
[`context_behavior`](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior)
|
||||
to `"isolated"` in your settings:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
COMPONENTS = {
|
||||
"context_behavior": "isolated",
|
||||
}
|
||||
```
|
||||
|
||||
## `render()` method
|
||||
|
||||
The [`Component.render()`](../../../reference/api/#django_components.Component.render) method renders a component to a string.
|
||||
|
||||
This is the equivalent of calling the [`{% component %}`](../template_tags#component) tag.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
template_file = "button.html"
|
||||
|
||||
class Args(NamedTuple):
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
surname: str
|
||||
age: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
footer: Optional[SlotInput] = None
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
...
|
||||
|
||||
Button.render(
|
||||
args=["John"],
|
||||
kwargs={
|
||||
"surname": "Doe",
|
||||
"age": 30,
|
||||
},
|
||||
slots={
|
||||
"footer": "i AM A SLOT",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
[`Component.render()`](../../../reference/api/#django_components.Component.render) accepts the following arguments:
|
||||
|
||||
- `args` - Positional arguments to pass to the component (as a list or tuple)
|
||||
- `kwargs` - Keyword arguments to pass to the component (as a dictionary)
|
||||
- `slots` - Slot content to pass to the component (as a dictionary)
|
||||
- `context` - Django context for rendering (can be a dictionary or a `Context` object)
|
||||
- `deps_strategy` - [Dependencies rendering strategy](#dependencies-rendering) (default: `"document"`)
|
||||
- `request` - [HTTP request object](../http_request), used for context processors (optional)
|
||||
|
||||
All arguments are optional. If not provided, they default to empty values or sensible defaults.
|
||||
|
||||
See the API reference for [`Component.render()`](../../../reference/api/#django_components.Component.render)
|
||||
for more details on the arguments.
|
||||
|
||||
## `render_to_response()` method
|
||||
|
||||
The [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
|
||||
method works just like [`Component.render()`](../../../reference/api/#django_components.Component.render),
|
||||
but wraps the result in an HTTP response.
|
||||
|
||||
It accepts all the same arguments as [`Component.render()`](../../../reference/api/#django_components.Component.render).
|
||||
|
||||
Any extra arguments are passed to the [`HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse)
|
||||
constructor.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
template_file = "button.html"
|
||||
|
||||
class Args(NamedTuple):
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
surname: str
|
||||
age: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
footer: Optional[SlotInput] = None
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
...
|
||||
|
||||
# Render the component to an HttpResponse
|
||||
response = Button.render_to_response(
|
||||
args=["John"],
|
||||
kwargs={
|
||||
"surname": "Doe",
|
||||
"age": 30,
|
||||
},
|
||||
slots={
|
||||
"footer": "i AM A SLOT",
|
||||
},
|
||||
# Additional response arguments
|
||||
status=200,
|
||||
headers={"X-Custom-Header": "Value"},
|
||||
)
|
||||
```
|
||||
|
||||
This method is particularly useful in view functions, as you can return the result of the component directly:
|
||||
|
||||
```python
|
||||
def profile_view(request, user_id):
|
||||
return Button.render_to_response(
|
||||
kwargs={
|
||||
"surname": "Doe",
|
||||
"age": 30,
|
||||
},
|
||||
request=request,
|
||||
)
|
||||
```
|
||||
|
||||
### Custom response classes
|
||||
|
||||
By default, [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
|
||||
returns a standard Django [`HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse).
|
||||
|
||||
You can customize this by setting the [`response_class`](../../../reference/api/#django_components.Component.response_class)
|
||||
attribute on your component:
|
||||
|
||||
```python
|
||||
from django.http import HttpResponse
|
||||
from django_components import Component
|
||||
|
||||
class MyHttpResponse(HttpResponse):
|
||||
...
|
||||
|
||||
class MyComponent(Component):
|
||||
response_class = MyHttpResponse
|
||||
|
||||
response = MyComponent.render_to_response()
|
||||
assert isinstance(response, MyHttpResponse)
|
||||
```
|
||||
|
||||
## Dependencies rendering
|
||||
|
||||
The rendered HTML may be used in different contexts (browser, email, etc), and each may need different handling of JS and CSS scripts.
|
||||
|
||||
[`render()`](../../../reference/api/#django_components.Component.render) and [`render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
|
||||
accept a `deps_strategy` parameter, which controls where and how the JS / CSS are inserted into the HTML.
|
||||
|
||||
The `deps_strategy` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
|
||||
|
||||
Learn more about [Rendering JS / CSS](../../advanced/rendering_js_css).
|
||||
|
||||
There are six dependencies rendering strategies:
|
||||
|
||||
- [`document`](../../advanced/rendering_js_css#document) (default)
|
||||
- Smartly inserts JS / CSS into placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) or into `<head>` and `<body>` tags.
|
||||
- Inserts extra script to allow `fragment` components to work.
|
||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
||||
- [`fragment`](../../advanced/rendering_js_css#fragment)
|
||||
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
||||
- Assumes the page was already rendered with `"document"` strategy.
|
||||
- No JS / CSS included.
|
||||
- [`simple`](../../advanced/rendering_js_css#simple)
|
||||
- Smartly insert JS / CSS into placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) or into `<head>` and `<body>` tags.
|
||||
- No extra script loaded.
|
||||
- [`prepend`](../../advanced/rendering_js_css#prepend)
|
||||
- Insert JS / CSS before the rendered HTML.
|
||||
- Ignores the placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) and any `<head>`/`<body>` HTML tags.
|
||||
- No extra script loaded.
|
||||
- [`append`](../../advanced/rendering_js_css#append)
|
||||
- Insert JS / CSS after the rendered HTML.
|
||||
- Ignores the placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) and any `<head>`/`<body>` HTML tags.
|
||||
- No extra script loaded.
|
||||
- [`ignore`](../../advanced/rendering_js_css#ignore)
|
||||
- HTML is left as-is. You can still process it with a different strategy later with
|
||||
[`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
|
||||
- Used for inserting rendered HTML into other components.
|
||||
|
||||
!!! info
|
||||
|
||||
You can use the `"prepend"` and `"append"` strategies to force to output JS / CSS for components
|
||||
that don't have neither the placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies), nor any `<head>`/`<body>` HTML tags:
|
||||
|
||||
```py
|
||||
rendered = Calendar.render_to_response(
|
||||
request=request,
|
||||
kwargs={
|
||||
"date": request.GET.get("date", ""),
|
||||
},
|
||||
deps_strategy="append",
|
||||
)
|
||||
```
|
||||
|
||||
Renders something like this:
|
||||
|
||||
```html
|
||||
<!-- Calendar component -->
|
||||
<div class="calendar">
|
||||
...
|
||||
</div>
|
||||
<!-- Appended JS / CSS -->
|
||||
<script src="..."></script>
|
||||
<link href="..."></link>
|
||||
```
|
||||
|
||||
## Passing context
|
||||
|
||||
The [`render()`](../../../reference/api/#django_components.Component.render) and [`render_to_response()`](../../../reference/api/#django_components.Component.render_to_response) methods accept an optional `context` argument.
|
||||
This sets the context within which the component is rendered.
|
||||
|
||||
When a component is rendered within a template with the [`{% component %}`](../../../reference/template_tags#component)
|
||||
tag, this will be automatically set to the
|
||||
[Context](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
|
||||
instance that is used for rendering the template.
|
||||
|
||||
When you call [`Component.render()`](../../../reference/api/#django_components.Component.render) directly from Python,
|
||||
there is no context object, so you can ignore this input most of the time.
|
||||
Instead, use `args`, `kwargs`, and `slots` to pass data to the component.
|
||||
|
||||
However, you can pass
|
||||
[`RequestContext`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext)
|
||||
to the `context` argument, so that the component will gain access to the request object and will use
|
||||
[context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#using-requestcontext).
|
||||
Read more on [Working with HTTP requests](../http_request).
|
||||
|
||||
```py
|
||||
Button.render(
|
||||
context=RequestContext(request),
|
||||
)
|
||||
```
|
||||
|
||||
For advanced use cases, you can use `context` argument to "pre-render" the component in Python, and then
|
||||
pass the rendered output as plain string to the template. With this, the inner component is rendered as if
|
||||
it was within the template with [`{% component %}`](../../../reference/template_tags#component).
|
||||
|
||||
```py
|
||||
class Button(Component):
|
||||
def render(self, context, template):
|
||||
# Pass `context` to Icon component so it is rendered
|
||||
# as if nested within Button.
|
||||
icon = Icon.render(
|
||||
context=context,
|
||||
args=["icon-name"],
|
||||
deps_strategy="ignore",
|
||||
)
|
||||
# Update context with icon
|
||||
with context.update({"icon": icon}):
|
||||
return template.render(context)
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
Whether the variables defined in `context` are actually available in the template depends on the
|
||||
[context behavior mode](../../../reference/settings#django_components.app_settings.ComponentsSettings.context_behavior):
|
||||
|
||||
- In `"django"` context behavior mode, the template will have access to the keys of this context.
|
||||
|
||||
- In `"isolated"` context behavior mode, the template will NOT have access to this context,
|
||||
and data MUST be passed via component's args and kwargs.
|
||||
|
||||
Therefore, it's **strongly recommended** to not rely on defining variables on the context object,
|
||||
but instead passing them through as `args` and `kwargs`
|
||||
|
||||
❌ Don't do this:
|
||||
|
||||
```python
|
||||
html = ProfileCard.render(
|
||||
context={"name": "John"},
|
||||
)
|
||||
```
|
||||
|
||||
✅ Do this:
|
||||
|
||||
```python
|
||||
html = ProfileCard.render(
|
||||
kwargs={"name": "John"},
|
||||
)
|
||||
```
|
||||
|
||||
## Typing render methods
|
||||
|
||||
Neither [`Component.render()`](../../../reference/api/#django_components.Component.render)
|
||||
nor [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
|
||||
are typed, due to limitations of Python's type system.
|
||||
|
||||
To add type hints, you can wrap the inputs
|
||||
in component's [`Args`](../../../reference/api/#django_components.Component.Args),
|
||||
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
|
||||
and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes.
|
||||
|
||||
Read more on [Typing and validation](../../fundamentals/typing_and_validation).
|
||||
|
||||
```python
|
||||
from typing import NamedTuple, Optional
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
# Define the component with the types
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
surname: str
|
||||
age: int
|
||||
|
||||
class Slots(NamedTuple):
|
||||
my_slot: Optional[SlotInput] = None
|
||||
footer: SlotInput
|
||||
|
||||
# Add type hints to the render call
|
||||
Button.render(
|
||||
args=Button.Args(
|
||||
name="John",
|
||||
),
|
||||
kwargs=Button.Kwargs(
|
||||
surname="Doe",
|
||||
age=30,
|
||||
),
|
||||
slots=Button.Slots(
|
||||
footer=Slot(lambda ctx: "Click me!"),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Components as input
|
||||
|
||||
django_components makes it possible to compose components in a "React-like" way,
|
||||
where you can render one component and use its output as input to another component:
|
||||
|
||||
```python
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
# Render the inner component
|
||||
inner_html = InnerComponent.render(
|
||||
kwargs={"some_data": "value"},
|
||||
deps_strategy="ignore", # Important for nesting!
|
||||
)
|
||||
|
||||
# Use inner component's output in the outer component
|
||||
outer_html = OuterComponent.render(
|
||||
kwargs={
|
||||
"content": mark_safe(inner_html), # Mark as safe to prevent escaping
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
The key here is setting [`deps_strategy="ignore"`](../../advanced/rendering_js_css#ignore) for the inner component. This prevents duplicate
|
||||
rendering of JS / CSS dependencies when the outer component is rendered.
|
||||
|
||||
When `deps_strategy="ignore"`:
|
||||
|
||||
- No JS or CSS dependencies will be added to the output HTML
|
||||
- The component's content is rendered as-is
|
||||
- The outer component will take care of including all needed dependencies
|
||||
|
||||
Read more about [Rendering JS / CSS](../../advanced/rendering_js_css).
|
||||
|
||||
## Dynamic components
|
||||
|
||||
Django components defines a special "dynamic" component ([`DynamicComponent`](../../../reference/components#django_components.components.dynamic.DynamicComponent)).
|
||||
|
||||
Normally, you have to hard-code the component name in the template:
|
||||
|
||||
```django
|
||||
{% component "button" / %}
|
||||
```
|
||||
|
||||
The dynamic component allows you to dynamically render any component based on the `is` kwarg. This is similar
|
||||
to [Vue's dynamic components](https://vuejs.org/guide/essentials/component-basics#dynamic-components) (`<component :is>`).
|
||||
|
||||
```django
|
||||
{% component "dynamic" is=table_comp data=table_data headers=table_headers %}
|
||||
{% fill "pagination" %}
|
||||
{% component "pagination" / %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
The args, kwargs, and slot fills are all passed down to the underlying component.
|
||||
|
||||
As with other components, the dynamic component can be rendered from Python:
|
||||
|
||||
```py
|
||||
from django_components import DynamicComponent
|
||||
|
||||
DynamicComponent.render(
|
||||
kwargs={
|
||||
"is": table_comp,
|
||||
"data": table_data,
|
||||
"headers": table_headers,
|
||||
},
|
||||
slots={
|
||||
"pagination": PaginationComponent.render(
|
||||
deps_strategy="ignore",
|
||||
),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Dynamic component name
|
||||
|
||||
By default, the dynamic component is registered under the name `"dynamic"`. In case of a conflict,
|
||||
you can set the
|
||||
[`COMPONENTS.dynamic_component_name`](../../../reference/settings#django_components.app_settings.ComponentsSettings.dynamic_component_name)
|
||||
setting to change the name used for the dynamic components.
|
||||
|
||||
```py
|
||||
# settings.py
|
||||
COMPONENTS = ComponentsSettings(
|
||||
dynamic_component_name="my_dynamic",
|
||||
)
|
||||
```
|
||||
|
||||
After which you will be able to use the dynamic component with the new name:
|
||||
|
||||
```django
|
||||
{% component "my_dynamic" is=table_comp data=table_data headers=table_headers %}
|
||||
{% fill "pagination" %}
|
||||
{% component "pagination" / %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
## HTML fragments
|
||||
|
||||
Django-components provides a seamless integration with HTML fragments with AJAX ([HTML over the wire](https://hotwired.dev/)),
|
||||
whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.
|
||||
|
||||
This is achieved by the combination of the [`"document"`](../../advanced/rendering_js_css#document)
|
||||
and [`"fragment"`](../../advanced/rendering_js_css#fragment) dependencies rendering strategies.
|
||||
|
||||
Read more about [HTML fragments](../../advanced/html_fragments) and [Rendering JS / CSS](../../advanced/rendering_js_css).
|
|
@ -1,48 +1,266 @@
|
|||
As you could have seen in [the tutorial](../../getting_started/adding_js_and_css.md), there's multiple ways how you can associate
|
||||
HTML / JS / CSS with a component:
|
||||
## Overview
|
||||
|
||||
- You can set [`Component.template`](../../reference/api.md#django_components.Component.template),
|
||||
[`Component.css`](../../reference/api.md#django_components.Component.css) and
|
||||
[`Component.js`](../../reference/api.md#django_components.Component.js) to define the main HTML / CSS / JS for a component
|
||||
as inlined code.
|
||||
- You can set [`Component.template_file`](../../reference/api.md#django_components.Component.template_file),
|
||||
[`Component.css_file`](../../reference/api.md#django_components.Component.css_file) and
|
||||
[`Component.js_file`](../../reference/api.md#django_components.Component.js_file) to define the main HTML / CSS / JS
|
||||
for a component in separate files.
|
||||
- You can link additional CSS / JS files using
|
||||
[`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js)
|
||||
and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
|
||||
Each component can define extra or "secondary" CSS / JS files using the nested [`Component.Media`](../../reference/api.md#django_components.Component.Media) class,
|
||||
by setting [`Component.Media.js`](../../reference/api.md#django_components.ComponentMediaInput.js) and [`Component.Media.css`](../../reference/api.md#django_components.ComponentMediaInput.css).
|
||||
|
||||
!!! warning
|
||||
The [main HTML / JS / CSS files](../html_js_css_files) are limited to 1 per component. This is not the case for the secondary files,
|
||||
where components can have many of them.
|
||||
|
||||
You **cannot** use both inlined code **and** separate file for a single language type:
|
||||
There is also no special behavior or post-processing for these secondary files, they are loaded as is.
|
||||
|
||||
- You can only either set `Component.template` or `Component.template_file`
|
||||
- You can only either set `Component.css` or `Component.css_file`
|
||||
- You can only either set `Component.js` or `Component.js_file`
|
||||
You can use these for third-party libraries, or for shared CSS / JS files.
|
||||
|
||||
However, you can freely mix these for different languages:
|
||||
These must be set as paths, URLs, or [custom objects](#paths-as-objects).
|
||||
|
||||
```djc_py
|
||||
class MyTable(Component):
|
||||
template: types.django_html = """
|
||||
<div class="welcome">
|
||||
Hi there!
|
||||
</div>
|
||||
"""
|
||||
js_file = "my_table.js"
|
||||
css_file = "my_table.css"
|
||||
```
|
||||
```py
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
class Media:
|
||||
js = [
|
||||
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js",
|
||||
"calendar/script.js",
|
||||
]
|
||||
css = [
|
||||
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css",
|
||||
"calendar/style.css",
|
||||
]
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
django-component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media/).
|
||||
django-component's management of files is inspired by [Django's `Media` class](https://docs.djangoproject.com/en/5.2/topics/forms/media/).
|
||||
|
||||
To be familiar with how Django handles static files, we recommend reading also:
|
||||
|
||||
- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.0/howto/static-files/)
|
||||
- [How to manage static files (e.g. images, JavaScript, CSS)](https://docs.djangoproject.com/en/5.2/howto/static-files/)
|
||||
|
||||
## Defining file paths relative to component
|
||||
## `Media` class
|
||||
|
||||
<!-- TODO: This section deserves to be expanded with more examples,
|
||||
so it's easier to follow. Right now it assumes that the read
|
||||
is familiar with Django's Media class, as we describe our Media class
|
||||
in constrast to it.
|
||||
|
||||
Instead we should go over all features / behaviours of the `Media` class.
|
||||
|
||||
We should also make `Media` class into a separate extension,
|
||||
and then have a separate page on "Secondary JS / CSS files".
|
||||
-->
|
||||
|
||||
Use the `Media` class to define secondary JS / CSS files for a component.
|
||||
|
||||
This `Media` class behaves similarly to
|
||||
[Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition):
|
||||
|
||||
- **Static paths** - Paths are handled as static file paths, and are resolved to URLs with Django's
|
||||
[`{% static %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#static) template tag.
|
||||
- **URLs** - A path that starts with `http`, `https`, or `/` is considered a URL. URLs are NOT resolved with [`{% static %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#static).
|
||||
- **HTML tags** - Both static paths and URLs are rendered to `<script>` and `<link>` HTML tags with
|
||||
`media_class.render_js()` and `media_class.render_css()`.
|
||||
- **Bypass formatting** - A [`SafeString`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.SafeString),
|
||||
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
|
||||
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
|
||||
- **Inheritance** - You can set [`extend`](../../../reference/api#django_components.ComponentMediaInput.extend) to configure
|
||||
whether to inherit JS / CSS from parent components. See [Media inheritance](#media-inheritance).
|
||||
|
||||
However, there's a few differences from Django's Media class:
|
||||
|
||||
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list,
|
||||
or (CSS-only) a dictonary (See [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
|
||||
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
|
||||
[`SafeString`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.SafeString), or a function
|
||||
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
|
||||
3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
|
||||
4. If you set [`Media.extend`](../../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
|
||||
it should be a list of [`Component`](../../../reference/api/#django_components.Component) classes.
|
||||
|
||||
```py
|
||||
from components.layout import LayoutComp
|
||||
|
||||
class MyTable(Component):
|
||||
class Media:
|
||||
js = [
|
||||
"path/to/script.js",
|
||||
"path/to/*.js", # Or as a glob
|
||||
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
|
||||
]
|
||||
css = {
|
||||
"all": [
|
||||
"path/to/style.css",
|
||||
"path/to/*.css", # Or as a glob
|
||||
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # TailwindCSS
|
||||
],
|
||||
"print": ["path/to/style2.css"],
|
||||
}
|
||||
|
||||
# Reuse JS / CSS from LayoutComp
|
||||
extend = [
|
||||
LayoutComp,
|
||||
]
|
||||
```
|
||||
|
||||
### CSS media types
|
||||
|
||||
You can define which stylesheets will be associated with which
|
||||
[CSS media types](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#targeting_media_types). You do so by defining CSS files as a dictionary.
|
||||
|
||||
See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.2/topics/forms/media/#css).
|
||||
|
||||
Again, you can set either a single file or a list of files per media type:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Media:
|
||||
css = {
|
||||
"all": "path/to/style1.css",
|
||||
"print": ["path/to/style2.css", "path/to/style3.css"],
|
||||
}
|
||||
```
|
||||
|
||||
Which will render the following HTML:
|
||||
|
||||
```html
|
||||
<link href="/static/path/to/style1.css" media="all" rel="stylesheet">
|
||||
<link href="/static/path/to/style2.css" media="print" rel="stylesheet">
|
||||
<link href="/static/path/to/style3.css" media="print" rel="stylesheet">
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
When you define CSS as a string or a list, the `all` media type is implied.
|
||||
|
||||
So these two examples are the same:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Media:
|
||||
css = "path/to/style1.css"
|
||||
```
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Media:
|
||||
css = {
|
||||
"all": ["path/to/style1.css"],
|
||||
}
|
||||
```
|
||||
|
||||
### Media inheritance
|
||||
|
||||
By default, the media files are inherited from the parent component.
|
||||
|
||||
```python
|
||||
class ParentComponent(Component):
|
||||
class Media:
|
||||
js = ["parent.js"]
|
||||
|
||||
class MyComponent(ParentComponent):
|
||||
class Media:
|
||||
js = ["script.js"]
|
||||
|
||||
print(MyComponent.media._js) # ["parent.js", "script.js"]
|
||||
```
|
||||
|
||||
You can set the component NOT to inherit from the parent component by setting the [`extend`](../../reference/api.md#django_components.ComponentMediaInput.extend) attribute to `False`:
|
||||
|
||||
```python
|
||||
class ParentComponent(Component):
|
||||
class Media:
|
||||
js = ["parent.js"]
|
||||
|
||||
class MyComponent(ParentComponent):
|
||||
class Media:
|
||||
extend = False # Don't inherit parent media
|
||||
js = ["script.js"]
|
||||
|
||||
print(MyComponent.media._js) # ["script.js"]
|
||||
```
|
||||
|
||||
Alternatively, you can specify which components to inherit from. In such case, the media files are inherited ONLY from the specified components, and NOT from the original parent components:
|
||||
|
||||
```python
|
||||
class ParentComponent(Component):
|
||||
class Media:
|
||||
js = ["parent.js"]
|
||||
|
||||
class MyComponent(ParentComponent):
|
||||
class Media:
|
||||
# Only inherit from these, ignoring the files from the parent
|
||||
extend = [OtherComponent1, OtherComponent2]
|
||||
|
||||
js = ["script.js"]
|
||||
|
||||
print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
The `extend` behaves consistently with
|
||||
[Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#extend),
|
||||
with one exception:
|
||||
|
||||
- When you set `extend` to a list, the list is expected to contain Component classes (or other classes that have a nested `Media` class).
|
||||
|
||||
### Accessing Media files
|
||||
|
||||
To access the files that you defined under [`Component.Media`](../../../reference/api#django_components.Component.Media),
|
||||
use [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
|
||||
|
||||
This is consistent behavior with
|
||||
[Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition).
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Media:
|
||||
js = "path/to/script.js"
|
||||
css = "path/to/style.css"
|
||||
|
||||
print(MyComponent.media)
|
||||
# Output:
|
||||
# <script src="/static/path/to/script.js"></script>
|
||||
# <link href="/static/path/to/style.css" media="all" rel="stylesheet">
|
||||
```
|
||||
|
||||
When working with component media files, it is important to understand the difference:
|
||||
|
||||
- `Component.Media`
|
||||
|
||||
- Is the "raw" media definition, or the input, which holds only the component's **own** media definition
|
||||
- This class is NOT instantiated, it merely holds the JS / CSS files.
|
||||
|
||||
- `Component.media`
|
||||
- Returns all resolved media files, **including** those inherited from parent components
|
||||
- Is an instance of [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
|
||||
|
||||
```python
|
||||
class ParentComponent(Component):
|
||||
class Media:
|
||||
js = ["parent.js"]
|
||||
|
||||
class ChildComponent(ParentComponent):
|
||||
class Media:
|
||||
js = ["child.js"]
|
||||
|
||||
# Access only this component's media
|
||||
print(ChildComponent.Media.js) # ["child.js"]
|
||||
|
||||
# Access all inherited media
|
||||
print(ChildComponent.media._js) # ["parent.js", "child.js"]
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
You should **not** manually modify `Component.media` or `Component.Media` after the component has been resolved, as this may lead to unexpected behavior.
|
||||
|
||||
If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media),
|
||||
you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
|
||||
([See example](#rendering-paths)).
|
||||
|
||||
## File paths
|
||||
|
||||
Unlike the [main HTML / JS / CSS files](../html_js_css_files), the path definition for the secondary files are quite ergonomic.
|
||||
|
||||
### Relative to component
|
||||
|
||||
As seen in the [getting started example](../../getting_started/your_first_component.md), to associate HTML / JS / CSS
|
||||
files with a component, you can set them as
|
||||
|
@ -117,93 +335,30 @@ class Calendar(Component):
|
|||
|
||||
NOTE: In case of ambiguity, the preference goes to resolving the files relative to the component's directory.
|
||||
|
||||
## Defining additional JS and CSS files
|
||||
### Globs
|
||||
|
||||
Each component can have only a single template, and single main JS and CSS. However, you can define additional JS or CSS
|
||||
using the nested [`Component.Media` class](../../../reference/api#django_components.Component.Media).
|
||||
Components can have many secondary files. To simplify their declaration, you can use globs.
|
||||
|
||||
This `Media` class behaves similarly to
|
||||
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition):
|
||||
Globs MUST be relative to the component's directory.
|
||||
|
||||
- Paths are generally handled as static file paths, and resolved URLs are rendered to HTML with
|
||||
`media_class.render_js()` or `media_class.render_css()`.
|
||||
- A path that starts with `http`, `https`, or `/` is considered a URL, skipping the static file resolution.
|
||||
This path is still rendered to HTML with `media_class.render_js()` or `media_class.render_css()`.
|
||||
- A [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
|
||||
or a function (with `__html__` method) is considered an already-formatted HTML tag, skipping both static file
|
||||
resolution and rendering with `media_class.render_js()` or `media_class.render_css()`.
|
||||
- You can set [`extend`](../../../reference/api#django_components.ComponentMediaInput.extend) to configure
|
||||
whether to inherit JS / CSS from parent components. See
|
||||
[Controlling Media Inheritance](../../fundamentals/defining_js_css_html_files/#controlling-media-inheritance).
|
||||
```py title="[project root]/components/calendar/calendar.py"
|
||||
from django_components import Component, register
|
||||
|
||||
However, there's a few differences from Django's Media class:
|
||||
|
||||
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list,
|
||||
or (CSS-only) a dictonary (See [`ComponentMediaInput`](../../../reference/api#django_components.ComponentMediaInput)).
|
||||
2. Individual JS / CSS files can be any of `str`, `bytes`, `Path`,
|
||||
[`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString), or a function
|
||||
(See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath)).
|
||||
3. Individual JS / CSS files can be glob patterns, e.g. `*.js` or `styles/**/*.css`.
|
||||
4. If you set [`Media.extend`](../../../reference/api/#django_components.ComponentMediaInput.extend) to a list,
|
||||
it should be a list of [`Component`](../../../reference/api/#django_components.Component) classes.
|
||||
|
||||
```py
|
||||
class MyTable(Component):
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
class Media:
|
||||
js = [
|
||||
"path/to/script.js",
|
||||
"path/to/*.js", # Or as a glob
|
||||
"https://unpkg.com/alpinejs@3.14.7/dist/cdn.min.js", # AlpineJS
|
||||
"path/to/*.js",
|
||||
"another/path/*.js",
|
||||
]
|
||||
css = {
|
||||
"all": [
|
||||
"path/to/style.css",
|
||||
"path/to/*.css", # Or as a glob
|
||||
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # TailwindCSS
|
||||
],
|
||||
"print": ["path/to/style2.css"],
|
||||
}
|
||||
css = "*.css"
|
||||
```
|
||||
|
||||
## Configuring CSS Media Types
|
||||
How this works is that django-components will detect that the path is a glob, and will try to resolve all files matching the glob pattern relative to the component's directory.
|
||||
|
||||
You can define which stylesheets will be associated with which
|
||||
[CSS Media types](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries#targeting_media_types). You do so by defining CSS files as a dictionary.
|
||||
After that, the file paths are handled the same way as if you defined them explicitly.
|
||||
|
||||
See the corresponding [Django Documentation](https://docs.djangoproject.com/en/5.0/topics/forms/media/#css).
|
||||
|
||||
Again, you can set either a single file or a list of files per media type:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Media:
|
||||
css = {
|
||||
"all": "path/to/style1.css",
|
||||
"print": ["path/to/style2.css", "path/to/style3.css"],
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
When you define CSS as a string or a list, the `all` media type is implied.
|
||||
|
||||
So these two examples are the same:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Media:
|
||||
css = "path/to/style1.css"
|
||||
```
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Media:
|
||||
css = {
|
||||
"all": ["path/to/style1.css"],
|
||||
}
|
||||
```
|
||||
|
||||
## Supported types for file paths
|
||||
### Supported types
|
||||
|
||||
File paths can be any of:
|
||||
|
||||
|
@ -213,7 +368,7 @@ File paths can be any of:
|
|||
- `SafeData` (`__html__` method)
|
||||
- `Callable` that returns any of the above, evaluated at class creation (`__new__`)
|
||||
|
||||
See [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath).
|
||||
To help with typing the union, use [`ComponentMediaInputPath`](../../../reference/api#django_components.ComponentMediaInputPath).
|
||||
|
||||
```py
|
||||
from pathlib import Path
|
||||
|
@ -238,18 +393,18 @@ class SimpleComponent(Component):
|
|||
]
|
||||
```
|
||||
|
||||
## Paths as objects
|
||||
### Paths as objects
|
||||
|
||||
In the example [above](#supported-types-for-file-paths), you can see that when we used Django's
|
||||
[`mark_safe()`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.mark_safe)
|
||||
to mark a string as a [`SafeString`](https://docs.djangoproject.com/en/5.1/ref/utils/#django.utils.safestring.SafeString),
|
||||
we had to define the full `<script>`/`<link>` tag.
|
||||
In the example [above](#supported-types), you can see that when we used Django's
|
||||
[`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe)
|
||||
to mark a string as a [`SafeString`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.SafeString),
|
||||
we had to define the URL / path as an HTML `<script>`/`<link>` elements.
|
||||
|
||||
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.0/topics/forms/media/#paths-as-objects)
|
||||
feature, where "safe" strings are taken as is, and accessed only at render time.
|
||||
This is an extension of Django's [Paths as objects](https://docs.djangoproject.com/en/5.2/topics/forms/media/#paths-as-objects)
|
||||
feature, where "safe" strings are taken as is, and are accessed only at render time.
|
||||
|
||||
Because of that, the paths defined as "safe" strings are NEVER resolved, neither relative to component's directory,
|
||||
nor relative to [`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs).
|
||||
nor relative to [`COMPONENTS.dirs`](../../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs). It is assumed that you will define the full `<script>`/`<link>` tag with the correct URL / path.
|
||||
|
||||
"Safe" strings can be used to lazily resolve a path, or to customize the `<script>` or `<link>` tag for individual paths:
|
||||
|
||||
|
@ -257,10 +412,12 @@ In the example below, we make use of "safe" strings to add `type="module"` to th
|
|||
In this case, we implemented a "safe" string by defining a `__html__` method.
|
||||
|
||||
```py
|
||||
# Path object
|
||||
class ModuleJsPath:
|
||||
def __init__(self, static_path: str) -> None:
|
||||
self.static_path = static_path
|
||||
|
||||
# Lazily resolve the path
|
||||
def __html__(self):
|
||||
full_path = static(self.static_path)
|
||||
return format_html(
|
||||
|
@ -271,11 +428,6 @@ class ModuleJsPath:
|
|||
class Calendar(Component):
|
||||
template_file = "calendar/template.html"
|
||||
|
||||
def get_context_data(self, date):
|
||||
return {
|
||||
"date": date,
|
||||
}
|
||||
|
||||
class Media:
|
||||
css = "calendar/style1.css"
|
||||
js = [
|
||||
|
@ -286,13 +438,17 @@ class Calendar(Component):
|
|||
]
|
||||
```
|
||||
|
||||
## Customize how paths are rendered into HTML tags
|
||||
### Rendering paths
|
||||
|
||||
Sometimes you may need to change how all CSS `<link>` or JS `<script>` tags are rendered for a given component. You can achieve this by providing your own subclass of [Django's `Media` class](https://docs.djangoproject.com/en/5.0/topics/forms/media) to component's `media_class` attribute.
|
||||
As part of the rendering process, the secondary JS / CSS files are resolved and rendered into `<link>` and `<script>` HTML tags, so they can be inserted into the render.
|
||||
|
||||
Normally, the JS and CSS paths are passed to `Media` class, which decides how the paths are resolved and how the `<link>` and `<script>` tags are constructed.
|
||||
In the [Paths as objects](#paths-as-objects) section, we saw that we can use that to selectively change
|
||||
how the HTML tags are constructed.
|
||||
|
||||
To change how the tags are constructed, you can override the [`Media.render_js` and `Media.render_css` methods](https://github.com/django/django/blob/fa7848146738a9fe1d415ee4808664e54739eeb7/django/forms/widgets.py#L102):
|
||||
However, if you need to change how ALL CSS and JS files are rendered for a given component,
|
||||
you can provide your own subclass of [Django's `Media` class](https://docs.djangoproject.com/en/5.2/topics/forms/media) to the [`Component.media_class`](../../reference/api.md#django_components.Component.media_class) attribute.
|
||||
|
||||
To change how the tags are constructed, you can override the [`Media.render_js()` and `Media.render_css()` methods](https://github.com/django/django/blob/fa7848146738a9fe1d415ee4808664e54739eeb7/django/forms/widgets.py#L102):
|
||||
|
||||
```py
|
||||
from django.forms.widgets import Media
|
||||
|
@ -326,189 +482,3 @@ class Calendar(Component):
|
|||
# Override the behavior of Media class
|
||||
media_class = MyMedia
|
||||
```
|
||||
|
||||
## Accessing component's HTML / JS / CSS
|
||||
|
||||
Component's HTML / CSS / JS is resolved and loaded lazily.
|
||||
|
||||
This means that, when you specify any of
|
||||
[`template_file`](../../reference/api.md#django_components.Component.template_file),
|
||||
[`js_file`](../../reference/api.md#django_components.Component.js_file),
|
||||
[`css_file`](../../reference/api.md#django_components.Component.css_file),
|
||||
or [`Media.js/css`](../../reference/api.md#django_components.Component.Media),
|
||||
these file paths will be resolved only once you either:
|
||||
|
||||
1. Access any of the following attributes on the component:
|
||||
|
||||
- [`media`](../../reference/api.md#django_components.Component.media),
|
||||
[`template`](../../reference/api.md#django_components.Component.template),
|
||||
[`template_file`](../../reference/api.md#django_components.Component.template_file),
|
||||
[`js`](../../reference/api.md#django_components.Component.js),
|
||||
[`js_file`](../../reference/api.md#django_components.Component.js_file),
|
||||
[`css`](../../reference/api.md#django_components.Component.css),
|
||||
[`css_file`](../../reference/api.md#django_components.Component.css_file)
|
||||
|
||||
2. Render the component.
|
||||
|
||||
Once the component's media files have been loaded once, they will remain in-memory
|
||||
on the Component class:
|
||||
|
||||
- HTML from [`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
|
||||
will be available under [`Component.template`](../../reference/api.md#django_components.Component.template)
|
||||
- CSS from [`Component.css_file`](../../reference/api.md#django_components.Component.css_file)
|
||||
will be available under [`Component.css`](../../reference/api.md#django_components.Component.css)
|
||||
- JS from [`Component.js_file`](../../reference/api.md#django_components.Component.js_file)
|
||||
will be available under [`Component.js`](../../reference/api.md#django_components.Component.js)
|
||||
|
||||
Thus, whether you define HTML via
|
||||
[`Component.template_file`](../../reference/api.md#django_components.Component.template_file)
|
||||
or [`Component.template`](../../reference/api.md#django_components.Component.template),
|
||||
you can always access the HTML content under [`Component.template`](../../reference/api.md#django_components.Component.template).
|
||||
And the same applies for JS and CSS.
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
# When we create Calendar component, the files like `calendar/template.html`
|
||||
# are not yet loaded!
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template_file = "calendar/template.html"
|
||||
css_file = "calendar/style.css"
|
||||
js_file = "calendar/script.js"
|
||||
|
||||
class Media:
|
||||
css = "calendar/style1.css"
|
||||
js = "calendar/script2.js"
|
||||
|
||||
# It's only at this moment that django-components reads the files like `calendar/template.html`
|
||||
print(Calendar.css)
|
||||
# Output:
|
||||
# .calendar {
|
||||
# width: 200px;
|
||||
# background: pink;
|
||||
# }
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
**Do NOT modify HTML / CSS / JS after it has been loaded**
|
||||
|
||||
django-components assumes that the component's media files like `js_file` or `Media.js/css` are static.
|
||||
|
||||
If you need to dynamically change these media files, consider instead defining multiple Components.
|
||||
|
||||
Modifying these files AFTER the component has been loaded at best does nothing. However, this is
|
||||
an untested behavior, which may lead to unexpected errors.
|
||||
|
||||
## Accessing component's Media files
|
||||
|
||||
To access the files that you defined under [`Component.Media`](../../../reference/api#django_components.Component.Media),
|
||||
use [`Component.media`](../../reference/api.md#django_components.Component.media) (lowercase).
|
||||
This is consistent behavior with
|
||||
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition).
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
class Media:
|
||||
js = "path/to/script.js"
|
||||
css = "path/to/style.css"
|
||||
|
||||
print(MyComponent.media)
|
||||
# Output:
|
||||
# <script src="/static/path/to/script.js"></script>
|
||||
# <link href="/static/path/to/style.css" media="all" rel="stylesheet">
|
||||
```
|
||||
|
||||
### `Component.Media` vs `Component.media`
|
||||
|
||||
When working with component media files, there are a few important concepts to understand:
|
||||
|
||||
- `Component.Media`
|
||||
|
||||
- Is the "raw" media definition, or the input, which holds only the component's **own** media definition
|
||||
- This class is NOT instantiated, it merely holds the JS / CSS files.
|
||||
|
||||
- `Component.media`
|
||||
- Returns all resolved media files, **including** those inherited from parent components
|
||||
- Is an instance of [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
|
||||
|
||||
```python
|
||||
class ParentComponent(Component):
|
||||
class Media:
|
||||
js = ["parent.js"]
|
||||
|
||||
class ChildComponent(ParentComponent):
|
||||
class Media:
|
||||
js = ["child.js"]
|
||||
|
||||
# Access only this component's media
|
||||
print(ChildComponent.Media.js) # ["child.js"]
|
||||
|
||||
# Access all inherited media
|
||||
print(ChildComponent.media._js) # ["parent.js", "child.js"]
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
You should **not** manually modify `Component.media` or `Component.Media` after the component has been resolved, as this may lead to unexpected behavior.
|
||||
|
||||
If you want to modify the class that is instantiated for [`Component.media`](../../reference/api.md#django_components.Component.media),
|
||||
you can configure [`Component.media_class`](../../reference/api.md#django_components.Component.media_class)
|
||||
([See example](#customize-how-paths-are-rendered-into-html-tags)).
|
||||
|
||||
## Controlling Media Inheritance
|
||||
|
||||
By default, the media files are inherited from the parent component.
|
||||
|
||||
```python
|
||||
class ParentComponent(Component):
|
||||
class Media:
|
||||
js = ["parent.js"]
|
||||
|
||||
class MyComponent(ParentComponent):
|
||||
class Media:
|
||||
js = ["script.js"]
|
||||
|
||||
print(MyComponent.media._js) # ["parent.js", "script.js"]
|
||||
```
|
||||
|
||||
You can set the component NOT to inherit from the parent component by setting the [`extend`](../../reference/api.md#django_components.ComponentMediaInput.extend) attribute to `False`:
|
||||
|
||||
```python
|
||||
class ParentComponent(Component):
|
||||
class Media:
|
||||
js = ["parent.js"]
|
||||
|
||||
class MyComponent(ParentComponent):
|
||||
class Media:
|
||||
extend = False # Don't inherit parent media
|
||||
js = ["script.js"]
|
||||
|
||||
print(MyComponent.media._js) # ["script.js"]
|
||||
```
|
||||
|
||||
Alternatively, you can specify which components to inherit from. In such case, the media files are inherited ONLY from the specified components, and NOT from the original parent components:
|
||||
|
||||
```python
|
||||
class ParentComponent(Component):
|
||||
class Media:
|
||||
js = ["parent.js"]
|
||||
|
||||
class MyComponent(ParentComponent):
|
||||
class Media:
|
||||
# Only inherit from these, ignoring the files from the parent
|
||||
extend = [OtherComponent1, OtherComponent2]
|
||||
|
||||
js = ["script.js"]
|
||||
|
||||
print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
The `extend` behaves consistently with
|
||||
[Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#extend),
|
||||
with one exception:
|
||||
|
||||
- When you set `extend` to a list, the list is expected to contain Component classes (or other classes that have a nested `Media` class).
|
|
@ -1,17 +1,38 @@
|
|||
Components can be defined in a single file, which is useful for small components. To do this, you can use the `template`, `js`, and `css` class attributes instead of the `template_file`, `js_file`, and `css_file`.
|
||||
Components can be defined in a single file, inlining the HTML, JS and CSS within the Python code.
|
||||
|
||||
## Writing single file components
|
||||
|
||||
To do this, you can use the
|
||||
[`template`](../../../reference/api#django_components.Component.template),
|
||||
[`js`](../../../reference/api#django_components.Component.js),
|
||||
and [`css`](../../../reference/api#django_components.Component.css)
|
||||
class attributes instead of the
|
||||
[`template_file`](../../../reference/api#django_components.Component.template_file),
|
||||
[`js_file`](../../../reference/api#django_components.Component.js_file),
|
||||
and [`css_file`](../../../reference/api#django_components.Component.css_file).
|
||||
|
||||
For example, here's the calendar component from
|
||||
the [Getting started](../../getting_started/your_first_component.md) tutorial,
|
||||
defined in a single file:
|
||||
the [Getting started](../../getting_started/your_first_component.md) tutorial:
|
||||
|
||||
```py title="calendar.py"
|
||||
from django_components import Component
|
||||
|
||||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
js_file = "calendar.js"
|
||||
css_file = "calendar.css"
|
||||
```
|
||||
|
||||
And here is the same component, rewritten in a single file:
|
||||
|
||||
```djc_py title="[project root]/components/calendar.py"
|
||||
from django_components import Component, register, types
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
def get_context_data(self, date):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": date,
|
||||
"date": kwargs["date"],
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
|
@ -41,6 +62,194 @@ class Calendar(Component):
|
|||
"""
|
||||
```
|
||||
|
||||
This makes it easy to create small components without having to create a separate template, CSS, and JS file.
|
||||
You can mix and match, so you can have a component with inlined HTML,
|
||||
while the JS and CSS are in separate files:
|
||||
|
||||
To add syntax highlighting to these snippets, head over to [Syntax highlighting](../../guides/setup/syntax_highlight.md).
|
||||
```djc_py title="[project root]/components/calendar.py"
|
||||
from django_components import Component, register, types
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
js_file = "calendar.js"
|
||||
css_file = "calendar.css"
|
||||
|
||||
template: types.django_html = """
|
||||
<div class="calendar">
|
||||
Today's date is <span>{{ date }}</span>
|
||||
</div>
|
||||
"""
|
||||
```
|
||||
|
||||
## Syntax highlighting
|
||||
|
||||
If you "inline" the HTML, JS and CSS code into the Python class, you should set up
|
||||
syntax highlighting to let your code editor know that the inlined code is HTML, JS and CSS.
|
||||
|
||||
In the examples above, we've annotated the
|
||||
[`template`](../../../reference/api#django_components.Component.template),
|
||||
[`js`](../../../reference/api#django_components.Component.js),
|
||||
and [`css`](../../../reference/api#django_components.Component.css)
|
||||
attributes with
|
||||
the `types.django_html`, `types.js` and `types.css` types. These are used for syntax highlighting in VSCode.
|
||||
|
||||
!!! warning
|
||||
|
||||
Autocompletion / intellisense does not work in the inlined code.
|
||||
|
||||
Help us add support for intellisense in the inlined code! Start a conversation in the
|
||||
[GitHub Discussions](https://github.com/django-components/django-components/discussions).
|
||||
|
||||
### VSCode
|
||||
|
||||
1. First install [Python Inline Source Syntax Highlighting](https://marketplace.visualstudio.com/items?itemName=samwillis.python-inline-source) extension, it will give you syntax highlighting for the template, CSS, and JS.
|
||||
|
||||
2. Next, in your component, set typings of
|
||||
[`Component.template`](../../../reference/api#django_components.Component.template),
|
||||
[`Component.js`](../../../reference/api#django_components.Component.js),
|
||||
[`Component.css`](../../../reference/api#django_components.Component.css)
|
||||
to `types.django_html`, `types.css`, and `types.js` respectively. The extension will recognize these and will activate syntax highlighting.
|
||||
|
||||
```djc_py title="[project root]/components/calendar.py"
|
||||
from django_components import Component, register, types
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": kwargs["date"],
|
||||
}
|
||||
|
||||
template: types.django_html = """
|
||||
<div class="calendar-component">
|
||||
Today's date is <span>{{ date }}</span>
|
||||
</div>
|
||||
"""
|
||||
|
||||
css: types.css = """
|
||||
.calendar-component {
|
||||
width: 200px;
|
||||
background: pink;
|
||||
}
|
||||
.calendar-component span {
|
||||
font-weight: bold;
|
||||
}
|
||||
"""
|
||||
|
||||
js: types.js = """
|
||||
(function(){
|
||||
if (document.querySelector(".calendar-component")) {
|
||||
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
|
||||
}
|
||||
})()
|
||||
"""
|
||||
```
|
||||
|
||||
### Pycharm (or other Jetbrains IDEs)
|
||||
|
||||
With PyCharm (or any other editor from Jetbrains), you don't need to use `types.django_html`, `types.css`, `types.js` since Pycharm uses [language injections](https://www.jetbrains.com/help/pycharm/using-language-injections.html).
|
||||
|
||||
You only need to write the comments `# language=<lang>` above the variables.
|
||||
|
||||
```djc_py
|
||||
from django_components import Component, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": kwargs["date"],
|
||||
}
|
||||
|
||||
# language=HTML
|
||||
template= """
|
||||
<div class="calendar-component">
|
||||
Today's date is <span>{{ date }}</span>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# language=CSS
|
||||
css = """
|
||||
.calendar-component {
|
||||
width: 200px;
|
||||
background: pink;
|
||||
}
|
||||
.calendar-component span {
|
||||
font-weight: bold;
|
||||
}
|
||||
"""
|
||||
|
||||
# language=JS
|
||||
js = """
|
||||
(function(){
|
||||
if (document.querySelector(".calendar-component")) {
|
||||
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
|
||||
}
|
||||
})()
|
||||
"""
|
||||
```
|
||||
|
||||
### Markdown code blocks with Pygments
|
||||
|
||||
[Pygments](https://pygments.org/) is a syntax highlighting library written in Python. It's also what's used by this documentation site ([mkdocs-material](https://squidfunk.github.io/mkdocs-material/)) to highlight code blocks.
|
||||
|
||||
To write code blocks with syntax highlighting, you need to install the [`pygments-djc`](https://pypi.org/project/pygments-djc/) package.
|
||||
|
||||
```bash
|
||||
pip install pygments-djc
|
||||
```
|
||||
|
||||
And then initialize it by importing `pygments_djc` somewhere in your project:
|
||||
|
||||
```python
|
||||
import pygments_djc
|
||||
```
|
||||
|
||||
Now you can use the `djc_py` code block to write code blocks with syntax highlighting for components.
|
||||
|
||||
```txt
|
||||
\```djc_py
|
||||
from django_components import Component, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template = """
|
||||
<div class="calendar-component">
|
||||
Today's date is <span>{{ date }}</span>
|
||||
</div>
|
||||
"""
|
||||
|
||||
css = """
|
||||
.calendar-component {
|
||||
width: 200px;
|
||||
background: pink;
|
||||
}
|
||||
.calendar-component span {
|
||||
font-weight: bold;
|
||||
}
|
||||
"""
|
||||
\```
|
||||
```
|
||||
|
||||
Will be rendered as below. Notice that the CSS and HTML are highlighted correctly:
|
||||
|
||||
```djc_py
|
||||
from django_components import Component, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template= """
|
||||
<div class="calendar-component">
|
||||
Today's date is <span>{{ date }}</span>
|
||||
</div>
|
||||
"""
|
||||
|
||||
css = """
|
||||
.calendar-component {
|
||||
width: 200px;
|
||||
background: pink;
|
||||
}
|
||||
.calendar-component span {
|
||||
font-weight: bold;
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@ In such cases, you can extract shared behavior into a standalone component class
|
|||
|
||||
When subclassing a component, there's a couple of things to keep in mind:
|
||||
|
||||
### Template, JS, and CSS Inheritance
|
||||
## Template, JS, and CSS inheritance
|
||||
|
||||
When it comes to the pairs:
|
||||
|
||||
|
@ -52,13 +52,13 @@ class CustomCard(BaseCard):
|
|||
"""
|
||||
```
|
||||
|
||||
### Media Class Inheritance
|
||||
## Media inheritance
|
||||
|
||||
The [`Component.Media`](../../reference/api.md#django_components.Component.Media) nested class follows Django's media inheritance rules:
|
||||
|
||||
- If both parent and child define a `Media` class, the child's media will automatically include both its own and the parent's JS and CSS files.
|
||||
- This behavior can be configured using the [`extend`](../../reference/api.md#django_components.Component.Media.extend) attribute in the Media class, similar to Django's forms.
|
||||
Read more on this in [Controlling Media Inheritance](./defining_js_css_html_files.md#controlling-media-inheritance).
|
||||
Read more on this in [Media inheritance](./secondary_js_css_files/#media-inheritance).
|
||||
|
||||
For example:
|
||||
|
||||
|
@ -83,7 +83,35 @@ class SimpleModal(BaseModal):
|
|||
js = ["simple_modal.js"] # Only this JS will be included
|
||||
```
|
||||
|
||||
### Regular Python Inheritance
|
||||
## Opt out of inheritance
|
||||
|
||||
For the following media attributes, when you don't want to inherit from the parent,
|
||||
but you also don't need to set the template / JS / CSS to any specific value,
|
||||
you can set these attributes to `None`.
|
||||
|
||||
- [`template`](../../reference/api.md#django_components.Component.template) / [`template_file`](../../reference/api.md#django_components.Component.template_file)
|
||||
- [`js`](../../reference/api.md#django_components.Component.js) / [`js_file`](../../reference/api.md#django_components.Component.js_file)
|
||||
- [`css`](../../reference/api.md#django_components.Component.css) / [`css_file`](../../reference/api.md#django_components.Component.css_file)
|
||||
- [`Media`](../../reference/api.md#django_components.Component.Media) class
|
||||
|
||||
For example:
|
||||
|
||||
```djc_py
|
||||
class BaseForm(Component):
|
||||
template = "..."
|
||||
css = "..."
|
||||
js = "..."
|
||||
|
||||
class Media:
|
||||
js = ["form.js"]
|
||||
|
||||
# Use parent's template and CSS, but no JS
|
||||
class ContactForm(BaseForm):
|
||||
js = None
|
||||
Media = None
|
||||
```
|
||||
|
||||
## Regular Python inheritance
|
||||
|
||||
All other attributes and methods (including the [`Component.View`](../../reference/api.md#django_components.ComponentView) class and its methods) follow standard Python inheritance rules.
|
||||
|
||||
|
@ -100,7 +128,7 @@ class BaseForm(Component):
|
|||
</form>
|
||||
"""
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"form_content": self.get_form_content(),
|
||||
"submit_text": "Submit"
|
||||
|
@ -112,8 +140,8 @@ class BaseForm(Component):
|
|||
class ContactForm(BaseForm):
|
||||
# Extend parent's "context"
|
||||
# but override "submit_text"
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
context = super().get_template_data(args, kwargs, slots, context)
|
||||
context["submit_text"] = "Send Message"
|
||||
return context
|
||||
|
||||
|
|
|
@ -30,14 +30,12 @@ so are still valid:
|
|||
</body>
|
||||
```
|
||||
|
||||
These can then be accessed inside `get_context_data` so:
|
||||
These can then be accessed inside `get_template_data` so:
|
||||
|
||||
```py
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
# Since # . @ - are not valid identifiers, we have to
|
||||
# use `**kwargs` so the method can accept these args.
|
||||
def get_context_data(self, **kwargs):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": kwargs["my-date"],
|
||||
"id": kwargs["#some_id"],
|
||||
|
@ -85,24 +83,24 @@ 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_
|
||||
|
||||
When passing data around, sometimes you may need to do light transformations, like negating booleans or filtering lists.
|
||||
|
||||
Normally, what you would have to do is to define ALL the variables
|
||||
inside `get_context_data()`. But this can get messy if your components contain a lot of logic.
|
||||
inside `get_template_data()`. But this can get messy if your components contain a lot of logic.
|
||||
|
||||
```py
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
def get_context_data(self, id: str, editable: bool):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"editable": editable,
|
||||
"readonly": not editable,
|
||||
"input_id": f"input-{id}",
|
||||
"icon_id": f"icon-{id}",
|
||||
"editable": kwargs["editable"],
|
||||
"readonly": not kwargs["editable"],
|
||||
"input_id": f"input-{kwargs['id']}",
|
||||
"icon_id": f"icon-{kwargs['id']}",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
@ -119,26 +117,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:
|
||||
|
||||
|
@ -204,10 +198,10 @@ class MyComp(Component):
|
|||
{% component "other" attrs=attrs / %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, some_id: str):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
attrs = {
|
||||
"class": "pa-4 flex",
|
||||
"data-some-id": some_id,
|
||||
"data-some-id": kwargs["some_id"],
|
||||
"@click.stop": "onClickHandler",
|
||||
}
|
||||
return {"attrs": attrs}
|
||||
|
@ -235,8 +229,8 @@ class MyComp(Component):
|
|||
/ %}
|
||||
"""
|
||||
|
||||
def get_context_data(self, some_id: str):
|
||||
return {"some_id": some_id}
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"some_id": kwargs["some_id"]}
|
||||
```
|
||||
|
||||
Sweet! Now all the relevant HTML is inside the template, and we can move it to a separate file with confidence:
|
||||
|
|
668
docs/concepts/fundamentals/typing_and_validation.md
Normal file
668
docs/concepts/fundamentals/typing_and_validation.md
Normal file
|
@ -0,0 +1,668 @@
|
|||
## Typing overview
|
||||
|
||||
<!-- TODO_V1 - REMOVE IN v1 -->
|
||||
|
||||
!!! warning
|
||||
|
||||
In versions 0.92 to 0.139 (inclusive), the component typing was specified through generics.
|
||||
|
||||
Since v0.140, the types must be specified as class attributes of the Component class - `Args`, `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData`.
|
||||
|
||||
See [Migrating from generics to class attributes](#migrating-from-generics-to-class-attributes) for more info.
|
||||
|
||||
!!! warning
|
||||
|
||||
Input validation was NOT part of Django Components between versions 0.136 and 0.139 (inclusive).
|
||||
|
||||
The [`Component`](../../../reference/api#django_components.Component) class optionally accepts class attributes
|
||||
that allow you to define the types of args, kwargs, slots, as well as the data returned from the data methods.
|
||||
|
||||
Use this to add type hints to your components, to validate the inputs at runtime, and to document them.
|
||||
|
||||
```py
|
||||
from typing import NamedTuple, Optional
|
||||
from django.template import Context
|
||||
from django_components import Component, SlotInput
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
size: int
|
||||
text: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
variable: str
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
my_slot: Optional[SlotInput] = None
|
||||
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
...
|
||||
|
||||
template_file = "button.html"
|
||||
```
|
||||
|
||||
The class attributes are:
|
||||
|
||||
- [`Args`](../../../reference/api#django_components.Component.Args) - Type for positional arguments.
|
||||
- [`Kwargs`](../../../reference/api#django_components.Component.Kwargs) - Type for keyword arguments.
|
||||
- [`Slots`](../../../reference/api#django_components.Component.Slots) - Type for slots.
|
||||
- [`TemplateData`](../../../reference/api#django_components.Component.TemplateData) - Type for data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data).
|
||||
- [`JsData`](../../../reference/api#django_components.Component.JsData) - Type for data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data).
|
||||
- [`CssData`](../../../reference/api#django_components.Component.CssData) - Type for data returned from [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
|
||||
|
||||
You can specify as many or as few of these as you want, the rest will default to `None`.
|
||||
|
||||
## Typing inputs
|
||||
|
||||
You can use [`Component.Args`](../../../reference/api#django_components.Component.Args),
|
||||
[`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
||||
and [`Component.Slots`](../../../reference/api#django_components.Component.Slots) to type the component inputs.
|
||||
|
||||
When you set these classes, at render time the `args`, `kwargs`, and `slots` parameters of the data methods
|
||||
([`get_template_data()`](../../../reference/api#django_components.Component.get_template_data),
|
||||
[`get_js_data()`](../../../reference/api#django_components.Component.get_js_data),
|
||||
[`get_css_data()`](../../../reference/api#django_components.Component.get_css_data))
|
||||
will be instances of these classes.
|
||||
|
||||
This way, each component can have runtime validation of the inputs:
|
||||
|
||||
- When you use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
or [`@dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
|
||||
instantiating these classes will check ONLY for the presence of the attributes.
|
||||
- When you use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/),
|
||||
instantiating these classes will check for the presence AND type of the attributes.
|
||||
|
||||
If you omit the [`Args`](../../../reference/api#django_components.Component.Args),
|
||||
[`Kwargs`](../../../reference/api#django_components.Component.Kwargs), or
|
||||
[`Slots`](../../../reference/api#django_components.Component.Slots) classes,
|
||||
or set them to `None`, the inputs will be passed as plain lists or dictionaries,
|
||||
and will not be validated.
|
||||
|
||||
```python
|
||||
from typing_extensions import NamedTuple, TypedDict
|
||||
from django.template import Context
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
# The data available to the `footer` scoped slot
|
||||
class ButtonFooterSlotData(TypedDict):
|
||||
value: int
|
||||
|
||||
# Define the component with the types
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
name: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
surname: str
|
||||
age: int
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||
# plain string, or a function that returns a string.
|
||||
my_slot: Optional[SlotInput] = None
|
||||
# Use `Slot` to allow ONLY `Slot` instances.
|
||||
# The generic is optional, and it specifies the data available
|
||||
# to the slot function.
|
||||
footer: Slot[ButtonFooterSlotData]
|
||||
|
||||
# Add type hints to the data method
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
# The parameters are instances of the classes we defined
|
||||
assert isinstance(args, Button.Args)
|
||||
assert isinstance(kwargs, Button.Kwargs)
|
||||
assert isinstance(slots, Button.Slots)
|
||||
|
||||
args.name # str
|
||||
kwargs.age # int
|
||||
slots.footer # Slot[ButtonFooterSlotData]
|
||||
|
||||
# Add type hints to the render call
|
||||
Button.render(
|
||||
args=Button.Args(
|
||||
name="John",
|
||||
),
|
||||
kwargs=Button.Kwargs(
|
||||
surname="Doe",
|
||||
age=30,
|
||||
),
|
||||
slots=Button.Slots(
|
||||
footer=Slot(lambda ctx: "Click me!"),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
If you don't want to validate some parts, set them to `None` or omit them.
|
||||
|
||||
The following will validate only the keyword inputs:
|
||||
|
||||
```python
|
||||
class Button(Component):
|
||||
# We could also omit these
|
||||
Args = None
|
||||
Slots = None
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
# Only `kwargs` is instantiated. `args` and `slots` are not.
|
||||
def get_template_data(self, args, kwargs: Kwargs, slots, context: Context):
|
||||
assert isinstance(args, list)
|
||||
assert isinstance(slots, dict)
|
||||
assert isinstance(kwargs, Button.Kwargs)
|
||||
|
||||
args[0] # str
|
||||
slots["footer"] # Slot[ButtonFooterSlotData]
|
||||
kwargs.age # int
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
Components can receive slots as strings, functions, or instances of [`Slot`](../../../reference/api#django_components.Slot).
|
||||
|
||||
Internally these are all normalized to instances of [`Slot`](../../../reference/api#django_components.Slot).
|
||||
|
||||
Therefore, the `slots` dictionary available in data methods (like
|
||||
[`get_template_data()`](../../../reference/api#django_components.Component.get_template_data))
|
||||
will always be a dictionary of [`Slot`](../../../reference/api#django_components.Slot) instances.
|
||||
|
||||
To correctly type this dictionary, you should set the fields of `Slots` to
|
||||
[`Slot`](../../../reference/api#django_components.Slot) or [`SlotInput`](../../../reference/api#django_components.SlotInput):
|
||||
|
||||
[`SlotInput`](../../../reference/api#django_components.SlotInput) is a union of `Slot`, string, and function types.
|
||||
|
||||
## Typing data
|
||||
|
||||
You can use [`Component.TemplateData`](../../../reference/api#django_components.Component.TemplateData),
|
||||
[`Component.JsData`](../../../reference/api#django_components.Component.JsData),
|
||||
and [`Component.CssData`](../../../reference/api#django_components.Component.CssData) to type the data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data), [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), and [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
|
||||
|
||||
When you set these classes, at render time they will be instantiated with the data returned from these methods.
|
||||
|
||||
This way, each component can have runtime validation of the returned data:
|
||||
|
||||
- When you use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
or [`@dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
|
||||
instantiating these classes will check ONLY for the presence of the attributes.
|
||||
- When you use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/),
|
||||
instantiating these classes will check for the presence AND type of the attributes.
|
||||
|
||||
If you omit the [`TemplateData`](../../../reference/api#django_components.Component.TemplateData),
|
||||
[`JsData`](../../../reference/api#django_components.Component.JsData), or
|
||||
[`CssData`](../../../reference/api#django_components.Component.CssData) classes,
|
||||
or set them to `None`, the validation and instantiation will be skipped.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Button(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
class JsData(NamedTuple):
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
class CssData(NamedTuple):
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"data1": "...",
|
||||
"data2": 123,
|
||||
}
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"js_data1": "...",
|
||||
"js_data2": 123,
|
||||
}
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"css_data1": "...",
|
||||
"css_data2": 123,
|
||||
}
|
||||
```
|
||||
|
||||
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class directly.
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Button(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
class JsData(NamedTuple):
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
class CssData(NamedTuple):
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return Button.TemplateData(
|
||||
data1="...",
|
||||
data2=123,
|
||||
)
|
||||
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return Button.JsData(
|
||||
js_data1="...",
|
||||
js_data2=123,
|
||||
)
|
||||
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return Button.CssData(
|
||||
css_data1="...",
|
||||
css_data2=123,
|
||||
)
|
||||
```
|
||||
|
||||
## Custom types
|
||||
|
||||
We recommend to use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||
for the `Args` class, and [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple),
|
||||
[dataclasses](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
|
||||
or [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
|
||||
for `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData` classes.
|
||||
|
||||
However, you can use any class, as long as they meet the conditions below.
|
||||
|
||||
For example, here is how you can use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
|
||||
to validate the inputs at runtime.
|
||||
|
||||
```py
|
||||
from django_components import Component
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Table(Component):
|
||||
class Kwargs(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
assert isinstance(kwargs, Table.Kwargs)
|
||||
|
||||
Table.render(
|
||||
kwargs=Table.Kwargs(name="John", age=30),
|
||||
)
|
||||
```
|
||||
|
||||
### `Args` class
|
||||
|
||||
The [`Args`](../../../reference/api#django_components.Component.Args) class
|
||||
represents a list of positional arguments. It must meet two conditions:
|
||||
|
||||
1. The constructor for the `Args` class must accept positional arguments.
|
||||
|
||||
```py
|
||||
Args(*args)
|
||||
```
|
||||
|
||||
2. The `Args` instance must be convertable to a list.
|
||||
|
||||
```py
|
||||
list(Args(1, 2, 3))
|
||||
```
|
||||
|
||||
To implement the conversion to a list, you can implement the `__iter__()` method:
|
||||
|
||||
```py
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
self.y = 2
|
||||
|
||||
def __iter__(self):
|
||||
return iter([('x', self.x), ('y', self.y)])
|
||||
```
|
||||
|
||||
### Dictionary classes
|
||||
|
||||
On the other hand, other types
|
||||
([`Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
||||
[`Slots`](../../../reference/api#django_components.Component.Slots),
|
||||
[`TemplateData`](../../../reference/api#django_components.Component.TemplateData),
|
||||
[`JsData`](../../../reference/api#django_components.Component.JsData),
|
||||
and [`CssData`](../../../reference/api#django_components.Component.CssData))
|
||||
represent dictionaries. They must meet these two conditions:
|
||||
|
||||
1. The constructor must accept keyword arguments.
|
||||
|
||||
```py
|
||||
Kwargs(**kwargs)
|
||||
Slots(**slots)
|
||||
```
|
||||
|
||||
2. The instance must be convertable to a dictionary.
|
||||
|
||||
```py
|
||||
dict(Kwargs(a=1, b=2))
|
||||
dict(Slots(a=1, b=2))
|
||||
```
|
||||
|
||||
To implement the conversion to a dictionary, you can implement either:
|
||||
|
||||
1. `_asdict()` method
|
||||
```py
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
self.y = 2
|
||||
|
||||
def _asdict(self):
|
||||
return {'x': self.x, 'y': self.y}
|
||||
```
|
||||
|
||||
2. Or make the class dict-like with `__iter__()` and `__getitem__()`
|
||||
```py
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
self.y = 2
|
||||
|
||||
def __iter__(self):
|
||||
return iter([('x', self.x), ('y', self.y)])
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
```
|
||||
|
||||
## Passing variadic args and kwargs
|
||||
|
||||
You may have a component that accepts any number of args or kwargs.
|
||||
|
||||
However, this cannot be described with the current Python's typing system (as of v0.140).
|
||||
|
||||
As a workaround:
|
||||
|
||||
- For a variable number of positional arguments (`*args`), set a positional argument that accepts a list of values:
|
||||
|
||||
```py
|
||||
class Table(Component):
|
||||
class Args(NamedTuple):
|
||||
args: List[str]
|
||||
|
||||
Table.render(
|
||||
args=Table.Args(args=["a", "b", "c"]),
|
||||
)
|
||||
```
|
||||
|
||||
- For a variable number of keyword arguments (`**kwargs`), set a keyword argument that accepts a dictionary of values:
|
||||
|
||||
```py
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
variable: str
|
||||
another: int
|
||||
# Pass any extra keys under `extra`
|
||||
extra: Dict[str, any]
|
||||
|
||||
Table.render(
|
||||
kwargs=Table.Kwargs(
|
||||
variable="a",
|
||||
another=1,
|
||||
extra={"foo": "bar"},
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Handling no args or no kwargs
|
||||
|
||||
To declare that a component accepts no args, kwargs, etc, define the types with no attributes using the `pass` keyword:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
pass
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
pass
|
||||
|
||||
class Slots(NamedTuple):
|
||||
pass
|
||||
```
|
||||
|
||||
This can get repetitive, so we added a [`Empty`](../../../reference/api#django_components.Empty) type to make it easier:
|
||||
|
||||
```py
|
||||
from django_components import Component, Empty
|
||||
|
||||
class Button(Component):
|
||||
Args = Empty
|
||||
Kwargs = Empty
|
||||
Slots = Empty
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
|
||||
Subclassing components with types is simple.
|
||||
|
||||
Since each type class is a separate class attribute, you can just override them in the Component subclass.
|
||||
|
||||
In the example below, `ButtonExtra` inherits `Kwargs` from `Button`, but overrides the `Args` class.
|
||||
|
||||
```py
|
||||
from django_components import Component, Empty
|
||||
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
size: int
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
color: str
|
||||
|
||||
class ButtonExtra(Button):
|
||||
class Args(NamedTuple):
|
||||
name: str
|
||||
size: int
|
||||
|
||||
# Stil works the same way!
|
||||
ButtonExtra.render(
|
||||
args=ButtonExtra.Args(name="John", size=30),
|
||||
kwargs=ButtonExtra.Kwargs(color="red"),
|
||||
)
|
||||
```
|
||||
|
||||
The only difference is when it comes to type hints to the data methods like
|
||||
[`get_template_data()`](../../../reference/api#django_components.Component.get_template_data).
|
||||
|
||||
When you define the nested classes like `Args` and `Kwargs` directly on the class, you
|
||||
can reference them just by their class name (`Args` and `Kwargs`).
|
||||
|
||||
But when you have a Component subclass, and it uses `Args` or `Kwargs` from the parent,
|
||||
you will have to reference the type as a [forward reference](https://peps.python.org/pep-0563/#forward-references), including the full name of the component
|
||||
(`Button.Args` and `Button.Kwargs`).
|
||||
|
||||
Compare the following:
|
||||
|
||||
```py
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
size: int
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
color: str
|
||||
|
||||
# Both `Args` and `Kwargs` are defined on the class
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
|
||||
pass
|
||||
|
||||
class ButtonExtra(Button):
|
||||
class Args(NamedTuple):
|
||||
name: str
|
||||
size: int
|
||||
|
||||
# `Args` is defined on the subclass, `Kwargs` is defined on the parent
|
||||
def get_template_data(self, args: Args, kwargs: "ButtonExtra.Kwargs", slots, context):
|
||||
pass
|
||||
|
||||
class ButtonSame(Button):
|
||||
# Both `Args` and `Kwargs` are defined on the parent
|
||||
def get_template_data(self, args: "ButtonSame.Args", kwargs: "ButtonSame.Kwargs", slots, context):
|
||||
pass
|
||||
```
|
||||
|
||||
## Runtime type validation
|
||||
|
||||
When you add types to your component, and implement
|
||||
them as [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) or [`dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass), the validation will check only for the presence of the attributes.
|
||||
|
||||
So this will not catch if you pass a string to an `int` attribute.
|
||||
|
||||
To enable runtime type validation, you need to use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/), and install the [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic) extension.
|
||||
|
||||
The `djc-ext-pydantic` extension ensures compatibility between django-components' classes such as `Component`, or `Slot` and Pydantic models.
|
||||
|
||||
First install the extension:
|
||||
|
||||
```bash
|
||||
pip install djc-ext-pydantic
|
||||
```
|
||||
|
||||
And then add the extension to your project:
|
||||
|
||||
```py
|
||||
COMPONENTS = {
|
||||
"extensions": [
|
||||
"djc_pydantic.PydanticExtension",
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
<!-- TODO_V1 - REMOVE IN v1 -->
|
||||
|
||||
## Migrating from generics to class attributes
|
||||
|
||||
In versions 0.92 to 0.139 (inclusive), the component typing was specified through generics.
|
||||
|
||||
Since v0.140, the types must be specified as class attributes of the [Component](../../../reference/api#django_components.Component) class -
|
||||
[`Args`](../../../reference/api#django_components.Component.Args),
|
||||
[`Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
||||
[`Slots`](../../../reference/api#django_components.Component.Slots),
|
||||
[`TemplateData`](../../../reference/api#django_components.Component.TemplateData),
|
||||
[`JsData`](../../../reference/api#django_components.Component.JsData),
|
||||
and [`CssData`](../../../reference/api#django_components.Component.CssData).
|
||||
|
||||
This change was necessary to make it possible to subclass components. Subclassing with generics was otherwise too complicated. Read the discussion [here](https://github.com/django-components/django-components/issues/1122).
|
||||
|
||||
Because of this change, the [`Component.render()`](../../../reference/api#django_components.Component.render)
|
||||
method is no longer typed.
|
||||
To type-check the inputs, you should wrap the inputs in [`Component.Args`](../../../reference/api#django_components.Component.Args),
|
||||
[`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
||||
[`Component.Slots`](../../../reference/api#django_components.Component.Slots), etc.
|
||||
|
||||
For example, if you had a component like this:
|
||||
|
||||
```py
|
||||
from typing import NotRequired, Tuple, TypedDict
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
ButtonArgs = Tuple[int, str]
|
||||
|
||||
class ButtonKwargs(TypedDict):
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: NotRequired[int] # May be omitted
|
||||
|
||||
class ButtonSlots(TypedDict):
|
||||
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||
# plain string, or a function that returns a string.
|
||||
my_slot: NotRequired[SlotInput]
|
||||
# Use `Slot` to allow ONLY `Slot` instances.
|
||||
another_slot: Slot
|
||||
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
|
||||
class Button(ButtonType):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
self.input.args[0] # int
|
||||
self.input.kwargs["variable"] # str
|
||||
self.input.slots["my_slot"] # Slot[MySlotData]
|
||||
|
||||
Button.render(
|
||||
args=(1, "hello"),
|
||||
kwargs={
|
||||
"variable": "world",
|
||||
"another": 123,
|
||||
},
|
||||
slots={
|
||||
"my_slot": "...",
|
||||
"another_slot": Slot(lambda ctx: ...),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
The steps to migrate are:
|
||||
|
||||
1. Convert all the types (`ButtonArgs`, `ButtonKwargs`, `ButtonSlots`) to subclasses
|
||||
of [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple).
|
||||
2. Move these types inside the Component class (`Button`), and rename them to `Args`, `Kwargs`, and `Slots`.
|
||||
3. If you defined typing for the data methods (like [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)), move them inside the Component class, and rename them to `TemplateData`, `JsData`, and `CssData`.
|
||||
4. Remove the `Component` generic.
|
||||
5. If you accessed the `args`, `kwargs`, or `slots` attributes via
|
||||
[`self.input`](../../../reference/api#django_components.Component.input), you will need to add the type hints yourself, because [`self.input`](../../../reference/api#django_components.Component.input) is no longer typed.
|
||||
|
||||
Otherwise, you may use [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data) instead of [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), as `get_template_data()` receives `args`, `kwargs`, `slots` and `context` as arguments. You will still need to add the type hints yourself.
|
||||
|
||||
6. Lastly, you will need to update the [`Component.render()`](../../../reference/api#django_components.Component.render)
|
||||
calls to wrap the inputs in [`Component.Args`](../../../reference/api#django_components.Component.Args), [`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs), and [`Component.Slots`](../../../reference/api#django_components.Component.Slots), to manually add type hints.
|
||||
|
||||
Thus, the code above will become:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple, Optional
|
||||
from django.template import Context
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
# The Component class does not take any generics
|
||||
class Button(Component):
|
||||
# Types are now defined inside the component class
|
||||
class Args(NamedTuple):
|
||||
size: int
|
||||
text: str
|
||||
|
||||
class Kwargs(NamedTuple):
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: Optional[int] = None # May be omitted
|
||||
|
||||
class Slots(NamedTuple):
|
||||
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||
# plain string, or a function that returns a string.
|
||||
my_slot: Optional[SlotInput] = None
|
||||
# Use `Slot` to allow ONLY `Slot` instances.
|
||||
another_slot: Slot
|
||||
|
||||
# The args, kwargs, slots are instances of the component's Args, Kwargs, and Slots classes
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
args.size # int
|
||||
kwargs.variable # str
|
||||
slots.my_slot # Slot[MySlotData]
|
||||
|
||||
Button.render(
|
||||
# Wrap the inputs in the component's Args, Kwargs, and Slots classes
|
||||
args=Button.Args(1, "hello"),
|
||||
kwargs=Button.Kwargs(
|
||||
variable="world",
|
||||
another=123,
|
||||
),
|
||||
slots=Button.Slots(
|
||||
my_slot="...",
|
||||
another_slot=Slot(lambda ctx: ...),
|
||||
),
|
||||
)
|
||||
```
|
|
@ -52,7 +52,8 @@ Be sure to prefix your rules with unique CSS class like `calendar`, so the CSS d
|
|||
|
||||
This CSS will be inserted into the page as an inlined `<style>` tag, at the position defined by
|
||||
[`{% component_css_dependencies %}`](../../reference/template_tags#component_css_dependencies),
|
||||
or at the end of the inside the `<head>` tag (See [JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations)).
|
||||
or at the end of the inside the `<head>` tag
|
||||
(See [Default JS / CSS locations](../../concepts/advanced/rendering_js_css#default-js-css-locations)).
|
||||
|
||||
So in your HTML, you may see something like this:
|
||||
|
||||
|
@ -103,7 +104,7 @@ This makes all variables defined only be defined inside this component and not a
|
|||
|
||||
Similarly to CSS, JS will be inserted into the page as an inlined `<script>` tag, at the position defined by
|
||||
[`{% component_js_dependencies %}`](../../reference/template_tags#component_js_dependencies),
|
||||
or at the end of the inside the `<body>` tag (See [JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations)).
|
||||
or at the end of the inside the `<body>` tag (See [Default JS / CSS locations](../../concepts/advanced/rendering_js_css#default-js-css-locations)).
|
||||
|
||||
So in your HTML, you may see something like this:
|
||||
|
||||
|
@ -184,7 +185,7 @@ class Calendar(Component):
|
|||
js_file = "calendar.js" # <--- new
|
||||
css_file = "calendar.css" # <--- new
|
||||
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": "1970-01-01",
|
||||
}
|
||||
|
@ -225,7 +226,7 @@ automatically embed the associated JS and CSS.
|
|||
Your components may depend on third-party packages or styling, or other shared logic.
|
||||
To load these additional dependencies, you can use a nested [`Media` class](../../reference/api#django_components.Component.Media).
|
||||
|
||||
This `Media` class behaves similarly to [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/#assets-as-a-static-definition),
|
||||
This `Media` class behaves similarly to [Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/#assets-as-a-static-definition),
|
||||
with a few differences:
|
||||
|
||||
1. Our Media class accepts various formats for the JS and CSS files: either a single file, a list, or (CSS-only) a dictonary (see below).
|
||||
|
@ -256,7 +257,7 @@ class Calendar(Component):
|
|||
"https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css", # Tailwind
|
||||
]
|
||||
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": "1970-01-01",
|
||||
}
|
||||
|
@ -274,7 +275,7 @@ class Calendar(Component):
|
|||
|
||||
!!! info
|
||||
|
||||
The `Media` nested class is shaped based on [Django's Media class](https://docs.djangoproject.com/en/5.1/topics/forms/media/).
|
||||
The `Media` nested class is shaped based on [Django's Media class](https://docs.djangoproject.com/en/5.2/topics/forms/media/).
|
||||
|
||||
As such, django-components allows multiple formats to define the nested Media class:
|
||||
|
||||
|
|
|
@ -134,7 +134,7 @@ Which will render as:
|
|||
{% endcomponent %}
|
||||
```
|
||||
|
||||
### 5. Wait, there's a bug
|
||||
### 4. Wait, there's a bug
|
||||
|
||||
There is a mistake in our code! `2024-12-13` is Friday, so that's fine. But if we updated
|
||||
the to `2024-12-14`, which is Saturday, our template from previous step would render this:
|
||||
|
@ -158,7 +158,7 @@ the to `2024-12-14`, which is Saturday, our template from previous step would re
|
|||
|
||||
The first instance rendered `2024-12-16`, while the rest rendered `2024-12-14`!
|
||||
|
||||
Why? Remember that in the [`get_context_data()`](../../reference/api#django_components.Component.get_context_data)
|
||||
Why? Remember that in the [`get_template_data()`](../../reference/api#django_components.Component.get_template_data)
|
||||
method of our Calendar component, we pre-process the date. If the date falls on Saturday or Sunday, we shift it to next Monday:
|
||||
|
||||
```python title="[project root]/components/calendar/calendar.py"
|
||||
|
@ -174,11 +174,11 @@ def to_workweek_date(d: date):
|
|||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
...
|
||||
def get_context_data(self, date: date, extra_class: str | None = None):
|
||||
workweek_date = to_workweek_date(date)
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
workweek_date = to_workweek_date(kwargs["date"])
|
||||
return {
|
||||
"date": workweek_date,
|
||||
"extra_class": extra_class,
|
||||
"extra_class": kwargs.get("extra_class", "text-blue"),
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -189,7 +189,8 @@ which is NOT the same as the `date` variable used inside Calendar's template.
|
|||
|
||||
We want to use the same `date` variable that's used inside Calendar's template.
|
||||
|
||||
Luckily, django-components allows passing data to the slot, also known as [Scoped slots](../../concepts/fundamentals/slots#scoped-slots).
|
||||
Luckily, django-components allows [passing data to slots](../../concepts/fundamentals/slots#slot-data),
|
||||
also known as [Scoped slots](https://vuejs.org/guide/components/slots#scoped-slots).
|
||||
|
||||
This consists of two steps:
|
||||
|
||||
|
@ -284,7 +285,12 @@ each time:
|
|||
Generally, slots are more flexible - you can access the slot data, even the original slot content.
|
||||
Thus, slots behave more like functions that render content based on their context.
|
||||
|
||||
On the other hand, variables are static - the variable you pass to a component is what will be used.
|
||||
On the other hand, variables are simpler - the variable you pass to a component is what will be used.
|
||||
|
||||
Moreover, slots are treated as part of the template - for example the CSS scoping (work in progress)
|
||||
is applied to the slot content too.
|
||||
|
||||
---
|
||||
|
||||
So far we've rendered components using template tag. [Next, let’s explore other ways to render components ➡️]
|
||||
(./rendering_components.md)
|
||||
|
|
|
@ -30,7 +30,7 @@ class Calendar(Component):
|
|||
js_file = "calendar.js"
|
||||
css_file = "calendar.css"
|
||||
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": "1970-01-01",
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ by calling `{% load component_tags %}` inside the template.
|
|||
like `{% component "calendar" / %}`.
|
||||
|
||||
`ComponentRegistries` also make it possible to group and share components as standalone packages.
|
||||
[Learn more here](../../concepts/advanced/authoring_component_libraries).
|
||||
[Learn more here](../../concepts/advanced/component_libraries).
|
||||
|
||||
!!! note
|
||||
|
||||
|
@ -102,7 +102,7 @@ and render the component inside a template:
|
|||
|
||||
!!! info
|
||||
|
||||
Component tags should end with `/` if they do not contain any [Slot fills](../../concepts/fundamentals/slots#slot-fills).
|
||||
Component tags should end with `/` if they do not contain any [Slot fills](../../concepts/fundamentals/slots).
|
||||
But you can also use `{% endcomponent %}` instead:
|
||||
|
||||
```htmldjango
|
||||
|
@ -159,7 +159,8 @@ and keeping your CSS and Javascript in the static directory.
|
|||
Remember that you can use
|
||||
[`{% component_js_dependencies %}`](../../reference/template_tags#component_js_dependencies)
|
||||
and [`{% component_css_dependencies %}`](../../reference/template_tags#component_css_dependencies)
|
||||
to change where the `<script>` and `<style>` tags will be rendered (See [JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations)).
|
||||
to change where the `<script>` and `<style>` tags will be rendered
|
||||
(See [Default JS / CSS locations](../../concepts/advanced/rendering_js_css#default-js-css-locations)).
|
||||
|
||||
!!! info
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ What we want is to be able to use the Calendar component within the template lik
|
|||
### 1. Understading component inputs
|
||||
|
||||
In section [Create your first component](../your_first_component), we defined
|
||||
the [`get_context_data()`](../../reference/api#django_components.Component.get_context_data) method
|
||||
the [`get_template_data()`](../../reference/api#django_components.Component.get_template_data) method
|
||||
that defines what variables will be available within the template:
|
||||
|
||||
```python title="[project root]/components/calendar/calendar.py"
|
||||
|
@ -20,13 +20,13 @@ from django_components import Component, register
|
|||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
...
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": "1970-01-01",
|
||||
}
|
||||
```
|
||||
|
||||
What we didn't say is that [`get_context_data()`](../../reference/api#django_components.Component.get_context_data)
|
||||
What we didn't say is that [`get_template_data()`](../../reference/api#django_components.Component.get_template_data)
|
||||
actually receives the args and kwargs that were passed to a component.
|
||||
|
||||
So if we call a component with a `date` and `extra_class` keywords:
|
||||
|
@ -38,7 +38,10 @@ So if we call a component with a `date` and `extra_class` keywords:
|
|||
This is the same as calling:
|
||||
|
||||
```py
|
||||
Calendar.get_context_data(date="2024-12-13", extra_class="text-red")
|
||||
Calendar.get_template_data(
|
||||
args=[],
|
||||
kwargs={"date": "2024-12-13", "extra_class": "text-red"},
|
||||
)
|
||||
```
|
||||
|
||||
And same applies to positional arguments, or mixing args and kwargs, where:
|
||||
|
@ -50,13 +53,16 @@ And same applies to positional arguments, or mixing args and kwargs, where:
|
|||
is same as
|
||||
|
||||
```py
|
||||
Calendar.get_context_data("2024-12-13", extra_class="text-red")
|
||||
Calendar.get_template_data(
|
||||
args=["2024-12-13"],
|
||||
kwargs={"extra_class": "text-red"},
|
||||
)
|
||||
```
|
||||
|
||||
### 2. Define inputs for `get_context_data`
|
||||
### 2. Define inputs
|
||||
|
||||
Let's put this to test. We want to pass `date` and `extra_class` kwargs to the component.
|
||||
And so, we can write the [`get_context_data()`](../../reference/api#django_components.Component.get_context_data)
|
||||
And so, we can write the [`get_template_data()`](../../reference/api#django_components.Component.get_template_data)
|
||||
method such that it expects those parameters:
|
||||
|
||||
```python title="[project root]/components/calendar/calendar.py"
|
||||
|
@ -68,53 +74,47 @@ from django_components import Component, register
|
|||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
...
|
||||
def get_context_data(self, date: date, extra_class: str = "text-blue"):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": date,
|
||||
"extra_class": extra_class,
|
||||
"date": kwargs["date"],
|
||||
"extra_class": kwargs.get("extra_class", "text-blue"),
|
||||
}
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
Since [`get_context_data()`](../../reference/api#django_components.Component.get_context_data)
|
||||
is just a regular Python function, type hints annotations work the same way as anywhere else.
|
||||
|
||||
!!! warning
|
||||
|
||||
Since [`get_context_data()`](../../reference/api#django_components.Component.get_context_data)
|
||||
is just a regular Python function, it will raise TypeError if it receives incorrect parameters.
|
||||
|
||||
Since `extra_class` is optional in the function signature, it's optional also in the template.
|
||||
So both following calls are valid:
|
||||
|
||||
```htmldjango
|
||||
{% component "calendar" "2024-12-13" / %}
|
||||
{% component "calendar" "2024-12-13" extra_class="text-red" / %}
|
||||
{% component "calendar" date="2024-12-13" / %}
|
||||
{% component "calendar" date="2024-12-13" extra_class="text-red" / %}
|
||||
```
|
||||
|
||||
However, `date` is required. Thus we MUST provide it. Same with regular Python functions,
|
||||
`date` can be set either as positional or keyword argument. But either way it MUST be set:
|
||||
!!! warning
|
||||
|
||||
```htmldjango
|
||||
✅
|
||||
{% component "calendar" "2024-12-13" / %}
|
||||
{% component "calendar" extra_class="text-red" date="2024-12-13" / %}
|
||||
[`get_template_data()`](../../reference/api#django_components.Component.get_template_data)
|
||||
differentiates between positional and keyword arguments,
|
||||
so you have to make sure to pass the arguments correctly.
|
||||
|
||||
❌
|
||||
{% component "calendar" extra_class="text-red" / %}
|
||||
```
|
||||
Since `date` is expected to be a keyword argument, it MUST be provided as such:
|
||||
|
||||
### 3. Process inputs in `get_context_data`
|
||||
```htmldjango
|
||||
✅ `date` is kwarg
|
||||
{% component "calendar" date="2024-12-13" / %}
|
||||
|
||||
The [`get_context_data()`](../../reference/api#django_components.Component.get_context_data)
|
||||
❌ `date` is arg
|
||||
{% component "calendar" "2024-12-13" / %}
|
||||
```
|
||||
|
||||
### 3. Process inputs
|
||||
|
||||
The [`get_template_data()`](../../reference/api#django_components.Component.get_template_data)
|
||||
method is powerful, because it allows us to decouple
|
||||
component inputs from the template variables. In other words, we can pre-process
|
||||
the component inputs, and massage them into a shape that's most appropriate for
|
||||
what the template needs. And it also allows us to pass in static data into the template.
|
||||
|
||||
Imagine our component receives data from the database that looks like below
|
||||
([taken from Django](https://docs.djangoproject.com/en/5.1/ref/templates/builtins/#regroup)).
|
||||
([taken from Django](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#regroup)).
|
||||
|
||||
```py
|
||||
cities = [
|
||||
|
@ -160,12 +160,14 @@ cities_by_pop = [
|
|||
]
|
||||
```
|
||||
|
||||
Without the `get_context_data()` method, we'd have to either:
|
||||
Without the [`get_template_data()`](../../reference/api#django_components.Component.get_template_data) method,
|
||||
we'd have to either:
|
||||
|
||||
1. Pre-process the data in Python before passing it to the components.
|
||||
2. Define a Django filter or template tag to take the data and process it on the spot.
|
||||
|
||||
Instead, with `get_context_data()`, we can keep this transformation private to this component,
|
||||
Instead, with [`get_template_data()`](../../reference/api#django_components.Component.get_template_data),
|
||||
we can keep this transformation private to this component,
|
||||
and keep the rest of the codebase clean.
|
||||
|
||||
```py
|
||||
|
@ -176,13 +178,14 @@ def group_by_pop(data):
|
|||
class PopulationTable(Component):
|
||||
template_file = "population_table.html"
|
||||
|
||||
def get_context_data(self, data):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"data": group_by_pop(data),
|
||||
"data": group_by_pop(kwargs["data"]),
|
||||
}
|
||||
```
|
||||
|
||||
Similarly we can make use of `get_context_data()` to pre-process the date that was given to the component:
|
||||
Similarly we can make use of [`get_template_data()`](../../reference/api#django_components.Component.get_template_data)
|
||||
to pre-process the date that was given to the component:
|
||||
|
||||
```python title="[project root]/components/calendar/calendar.py"
|
||||
from datetime import date
|
||||
|
@ -197,17 +200,17 @@ def to_workweek_date(d: date):
|
|||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
...
|
||||
def get_context_data(self, date: date, extra_class: str = "text-blue"):
|
||||
workweek_date = to_workweek_date(date) # <--- new
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
workweek_date = to_workweek_date(kwargs["date"]) # <--- new
|
||||
return {
|
||||
"date": workweek_date, # <--- changed
|
||||
"extra_class": extra_class,
|
||||
"extra_class": kwargs.get("extra_class", "text-blue"),
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Pass inputs to components
|
||||
|
||||
Once we're happy with `Calendar.get_contex_data()`, we can update our templates to use
|
||||
Once we're happy with `Calendar.get_template_data()`, we can update our templates to use
|
||||
the parametrized version of the component:
|
||||
|
||||
```htmldjango
|
||||
|
@ -217,14 +220,10 @@ the parametrized version of the component:
|
|||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next, you will learn [how to use slots give your components even more flexibility ➡️](./adding_slots.md)
|
||||
|
||||
### 5. Add defaults
|
||||
|
||||
In our example, we've set the `extra_class` to default to `"text-blue"` by setting it in the
|
||||
[`get_context_data()`](../../reference/api#django_components.Component.get_context_data)
|
||||
[`get_template_data()`](../../reference/api#django_components.Component.get_template_data)
|
||||
method.
|
||||
|
||||
However, you may want to use the same default value in multiple methods, like
|
||||
|
@ -248,10 +247,14 @@ class Calendar(Component):
|
|||
class Defaults: # <--- new
|
||||
extra_class = "text-blue"
|
||||
|
||||
def get_context_data(self, date: date, extra_class: str): # <--- changed
|
||||
workweek_date = to_workweek_date(date)
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
workweek_date = to_workweek_date(kwargs["date"])
|
||||
return {
|
||||
"date": workweek_date,
|
||||
"extra_class": extra_class,
|
||||
"extra_class": kwargs["extra_class"], # <--- changed
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Next, you will learn [how to use slots give your components even more flexibility ➡️](./adding_slots.md)
|
||||
|
|
|
@ -19,13 +19,13 @@ class Calendar(Component):
|
|||
js_file = "calendar.js"
|
||||
css_file = "calendar.css"
|
||||
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": "1970-01-01",
|
||||
}
|
||||
```
|
||||
|
||||
### 1. Render the template that contains the `{% component %}` tag
|
||||
### 1. Render the template
|
||||
|
||||
If you have embedded the component in a Django template using the
|
||||
[`{% component %}`](../../reference/template_tags#component) tag:
|
||||
|
@ -39,7 +39,7 @@ If you have embedded the component in a Django template using the
|
|||
|
||||
You can simply render the template with the Django tooling:
|
||||
|
||||
#### With [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
|
||||
#### With [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.2/topics/http/shortcuts/#render)
|
||||
|
||||
```python
|
||||
from django.shortcuts import render
|
||||
|
@ -48,9 +48,9 @@ context = {"date": "2024-12-13"}
|
|||
rendered_template = render(request, "my_template.html", context)
|
||||
```
|
||||
|
||||
#### With [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render)
|
||||
#### With [`Template.render()`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template.render)
|
||||
|
||||
Either loading the template with [`get_template()`](https://docs.djangoproject.com/en/5.1/topics/templates/#django.template.loader.get_template):
|
||||
Either loading the template with [`get_template()`](https://docs.djangoproject.com/en/5.2/topics/templates/#django.template.loader.get_template):
|
||||
|
||||
```python
|
||||
from django.template.loader import get_template
|
||||
|
@ -60,7 +60,7 @@ context = {"date": "2024-12-13"}
|
|||
rendered_template = template.render(context)
|
||||
```
|
||||
|
||||
Or creating a new [`Template`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template) instance:
|
||||
Or creating a new [`Template`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template) instance:
|
||||
|
||||
```python
|
||||
from django.template import Template
|
||||
|
@ -74,7 +74,7 @@ template = Template("""
|
|||
rendered_template = template.render()
|
||||
```
|
||||
|
||||
### 2. Render the component directly with [`Component.render()`](../../reference/api#django_components.Component.render)
|
||||
### 2. Render the component
|
||||
|
||||
You can also render the component directly with [`Component.render()`](../../reference/api#django_components.Component.render), without wrapping the component in a template.
|
||||
|
||||
|
@ -113,15 +113,15 @@ rendered_component = calendar.render(
|
|||
rendered_component = calendar.render(request=request)
|
||||
```
|
||||
|
||||
The `request` object is required for some of the component's features, like using [Django's context processors](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.RequestContext).
|
||||
The `request` object is required for some of the component's features, like using [Django's context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.RequestContext).
|
||||
|
||||
### 3. Render the component directly with [`Component.render_to_response()`](../../reference/api#django_components.Component.render_to_response)
|
||||
### 3. Render the component to HttpResponse
|
||||
|
||||
A common pattern in Django is to render the component and then return the resulting HTML as a response to an HTTP request.
|
||||
|
||||
For this, you can use the [`Component.render_to_response()`](../../reference/api#django_components.Component.render_to_response) convenience method.
|
||||
|
||||
`render_to_response()` accepts the same args, kwargs, slots, and more, as [`Component.render()`](../../reference/api#django_components.Component.render), but wraps the result in an [`HttpResponse`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpResponse).
|
||||
`render_to_response()` accepts the same args, kwargs, slots, and more, as [`Component.render()`](../../reference/api#django_components.Component.render), but wraps the result in an [`HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse).
|
||||
|
||||
```python
|
||||
from components.calendar import Calendar
|
||||
|
@ -144,7 +144,7 @@ def my_view(request):
|
|||
|
||||
**Response class of `render_to_response`**
|
||||
|
||||
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is [`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.1/ref/request-response/#django.http.HttpResponse).
|
||||
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is [`django.http.HttpResponse`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpResponse).
|
||||
|
||||
If you want to use a different Response class in `render_to_response`, set the [`Component.response_class`](../../reference/api#django_components.Component.response_class) attribute:
|
||||
|
||||
|
@ -159,3 +159,137 @@ def my_view(request):
|
|||
class SimpleComponent(Component):
|
||||
response_class = MyCustomResponse
|
||||
```
|
||||
|
||||
### 4. Rendering slots
|
||||
|
||||
Slots content are automatically escaped by default to prevent XSS attacks.
|
||||
|
||||
In other words, it's as if you would be using Django's [`escape()`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#std-templatefilter-escape) on the slot contents / result:
|
||||
|
||||
```python
|
||||
from django.utils.html import escape
|
||||
|
||||
class Calendar(Component):
|
||||
template = """
|
||||
<div>
|
||||
{% slot "date" default date=date / %}
|
||||
</div>
|
||||
"""
|
||||
|
||||
Calendar.render(
|
||||
slots={
|
||||
"date": escape("<b>Hello</b>"),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
To disable escaping, you can wrap the slot string or slot result in Django's [`mark_safe()`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe):
|
||||
|
||||
```py
|
||||
Calendar.render(
|
||||
slots={
|
||||
# string
|
||||
"date": mark_safe("<b>Hello</b>"),
|
||||
|
||||
# function
|
||||
"date": lambda ctx: mark_safe("<b>Hello</b>"),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
Read more about Django's
|
||||
[`format_html`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.html.format_html)
|
||||
and [`mark_safe`](https://docs.djangoproject.com/en/5.2/ref/utils/#django.utils.safestring.mark_safe).
|
||||
|
||||
### 5. Component views and URLs
|
||||
|
||||
For web applications, it's common to define endpoints that serve HTML content (AKA views).
|
||||
|
||||
If this is your case, you can define the view request handlers directly on your component by using the nested[`Component.View`](../../reference/api#django_components.Component.View) class.
|
||||
|
||||
This is a great place for:
|
||||
|
||||
- Endpoints that render whole pages, if your component
|
||||
is a page component.
|
||||
|
||||
- Endpoints that render the component as HTML fragments, to be used with HTMX or similar libraries.
|
||||
|
||||
Read more on [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||
|
||||
```djc_py title="[project root]/components/calendar.py"
|
||||
from django_components import Component, ComponentView, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template = """
|
||||
<div class="calendar-component">
|
||||
<div class="header">
|
||||
{% slot "header" / %}
|
||||
</div>
|
||||
<div class="body">
|
||||
Today's date is <span>{{ date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
class View:
|
||||
# Handle GET requests
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Return HttpResponse with the rendered content
|
||||
return Calendar.render_to_response(
|
||||
request=request,
|
||||
kwargs={
|
||||
"date": request.GET.get("date", "2020-06-06"),
|
||||
},
|
||||
slots={
|
||||
"header": "Calendar header",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
The View class supports all the same HTTP methods as Django's [`View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.View) class. These are:
|
||||
|
||||
`get()`, `post()`, `put()`, `patch()`, `delete()`, `head()`, `options()`, `trace()`
|
||||
|
||||
Each of these receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument.
|
||||
|
||||
Next, you need to set the URL for the component.
|
||||
|
||||
You can either:
|
||||
|
||||
1. Automatically assign the URL by setting the [`Component.View.public`](../../reference/api#django_components.ComponentView.public) attribute to `True`.
|
||||
|
||||
In this case, use [`get_component_url()`](../../reference/api#django_components.get_component_url) to get the URL for the component view.
|
||||
|
||||
```djc_py
|
||||
from django_components import Component, get_component_url
|
||||
|
||||
class Calendar(Component):
|
||||
class View:
|
||||
public = True
|
||||
|
||||
url = get_component_url(Calendar)
|
||||
```
|
||||
|
||||
2. Manually assign the URL by setting [`Component.as_view()`](../../reference/api#django_components.Component.as_view) to your `urlpatterns`:
|
||||
|
||||
```djc_py
|
||||
from django.urls import path
|
||||
from components.calendar import Calendar
|
||||
|
||||
urlpatterns = [
|
||||
path("calendar/", Calendar.as_view()),
|
||||
]
|
||||
```
|
||||
|
||||
And with that, you're all set! When you visit the URL, the component will be rendered and the content will be returned.
|
||||
|
||||
The `get()`, `post()`, etc methods will receive the [`HttpRequest`](https://docs.djangoproject.com/en/5.2/ref/request-response/#django.http.HttpRequest) object as the first argument. So you can parametrize how the component is rendered for example by passing extra query parameters to the URL:
|
||||
|
||||
```
|
||||
http://localhost:8000/calendar/?date=2024-12-13
|
||||
```
|
||||
|
|
|
@ -35,8 +35,9 @@ document.querySelector(".calendar").onclick = function () {
|
|||
```
|
||||
|
||||
```py title="calendar.py"
|
||||
from django_components import Component
|
||||
from django_components import Component, register
|
||||
|
||||
@register("calendar")
|
||||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
js_file = "calendar.js"
|
||||
|
@ -72,7 +73,7 @@ class Calendar(Component):
|
|||
!!! note
|
||||
|
||||
If you "inline" the HTML, JS and CSS code into the Python class, you can set up
|
||||
[syntax highlighting](../../guides/setup/syntax_highlight) for better experience.
|
||||
[syntax highlighting](../../concepts/fundamentals/single_file_components#syntax-highlighting) for better experience.
|
||||
However, autocompletion / intellisense does not work with syntax highlighting.
|
||||
|
||||
We'll start by creating a component that defines only a Django template:
|
||||
|
@ -104,7 +105,7 @@ Inside `calendar.html`, write:
|
|||
```
|
||||
|
||||
In this example we've defined one template variable `date`. You can use any and as many variables as you like. These variables will be
|
||||
defined in the Python file in [`get_context_data()`](../../reference/api#django_components.Component.get_context_data)
|
||||
defined in the Python file in [`get_template_data()`](../../reference/api#django_components.Component.get_template_data)
|
||||
when creating an instance of this component.
|
||||
|
||||
!!! note
|
||||
|
@ -142,7 +143,7 @@ class Calendar(Component):
|
|||
|
||||
In `calendar.html`, we've used the variable `date`. So we need to define it for the template to work.
|
||||
|
||||
This is done using [`Component.get_context_data()`](../../reference/api#django_components.Component.get_context_data).
|
||||
This is done using [`Component.get_template_data()`](../../reference/api#django_components.Component.get_template_data).
|
||||
It's a function that returns a dictionary. The entries in this dictionary
|
||||
will become available within the template as variables, e.g. as `{{ date }}`.
|
||||
|
||||
|
@ -152,7 +153,7 @@ from django_components import Component
|
|||
class Calendar(Component):
|
||||
template_file = "calendar.html"
|
||||
|
||||
def get_context_data(self):
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"date": "1970-01-01",
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue