feat: add "simple", "prepend", and "append" render types (#1156)

* feat: add "simple", "prepend", and "append" render types

* refactor: explicitly set strategy for "document" in tests
This commit is contained in:
Juro Oravec 2025-05-02 15:07:16 +02:00 committed by GitHub
parent e74e1241ac
commit bf7a204e92
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1210 additions and 408 deletions

View file

@ -29,12 +29,14 @@
- The interface of the not-yet-released `get_js_data()` and `get_css_data()` methods has changed to
match `get_template_data()`.
Before:
```py
def get_js_data(self, *args, **kwargs):
def get_css_data(self, *args, **kwargs):
```
to:
After:
```py
def get_js_data(self, args, kwargs, slots, context):
@ -78,7 +80,7 @@
escape_slots_content: bool = True,
args: Optional[ArgsType] = None,
kwargs: Optional[KwargsType] = None,
type: RenderType = "document",
deps_strategy: DependenciesStrategy = "document",
request: Optional[HttpRequest] = None,
*response_args: Any,
**response_kwargs: Any,
@ -94,7 +96,7 @@
kwargs: Optional[Mapping] = None,
slots: Optional[Mapping] = None,
escape_slots_content: bool = True,
type: RenderType = "document",
deps_strategy: DependenciesStrategy = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
**response_kwargs: Any,
@ -153,6 +155,20 @@
{% component "profile" name="John" job="Developer" / %}
```
- The second argument to `render_dependencies()` is now `strategy` instead of `type`.
Before:
```py
render_dependencies(content, type="document")
```
After:
```py
render_dependencies(content, strategy="document")
```
#### 🚨📢 Deprecation
- `get_context_data()` is now deprecated. Use `get_template_data()` instead.
@ -162,10 +178,56 @@
Since `get_context_data()` is widely used, it will remain available until v2.
- The `type` kwarg in `Component.render()` and `Component.render_to_response()` is now deprecated. Use `deps_strategy` instead. The `type` kwarg will be removed in v1.
Before:
```py
Calendar.render_to_response(type="fragment")
```
After:
```py
Calendar.render_to_response(deps_strategy="fragment")
```
- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1.
#### Feat
- New method to render template variables - `get_template_data()`
`get_template_data()` behaves the same way as `get_context_data()`, but has
a different function signature to accept also slots and context.
```py
class Button(Component):
def get_template_data(self, args, kwargs, slots, context):
return {
"val1": args[0],
"val2": kwargs["field"],
}
```
If you define `Component.Args`, `Component.Kwargs`, `Component.Slots`, then
the `args`, `kwargs`, `slots` arguments will be instances of these classes:
```py
class Button(Component):
class Args(NamedTuple):
field1: str
class Kwargs(NamedTuple):
field2: int
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
return {
"val1": args.field1,
"val2": kwargs.field2,
}
```
- Input validation is now part of the render process.
When you specify the input types (such as `Component.Args`, `Component.Kwargs`, etc),
@ -176,6 +238,43 @@
Read more on [Typing and validation](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/)
- Render emails or other non-browser HTML with new "dependencies render strategies"
When rendering a component with `Component.render()` or `Component.render_to_response()`,
the `deps_strategy` kwarg (previously `type`) now accepts a new options `"simple"`, `"prepend"`, or `"append"`.
```py
Calendar.render_to_response(
request=request,
kwargs={
"date": request.GET.get("date", ""),
},
deps_strategy="append",
)
```
Comparison of dependencies render strategies:
- `"document"`
- 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"`
- A lightweight HTML fragment to be inserted into a document with AJAX.
- Ignores placeholders and any `<head>` / `<body>` tags.
- No JS / CSS included.
- `"simple"`
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
- No extra script loaded.
- `"prepend"`
- Insert JS / CSS before the rendered HTML.
- Ignores placeholders and any `<head>` / `<body>` tags.
- No extra script loaded.
- `"append"`
- Insert JS / CSS after the rendered HTML.
- Ignores placeholders and any `<head>` / `<body>` tags.
- No extra script loaded.
## v0.139.1
#### Fix

View file

@ -1,5 +1,5 @@
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, or vanilla JavaScript.
When you define a component that has extra JS or CSS, and you use django-components
to render the fragment, django-components will:
@ -22,15 +22,17 @@ 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" for rendering JS and CSS.
Two of them are used to enable HTML fragments - "document" and "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
@ -42,7 +44,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 +57,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 +77,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",
)
```
@ -143,8 +145,8 @@ class Frag(Component):
def get(self, request):
return self.component.render_to_response(
request=request,
# IMPORTANT: Don't forget `type="fragment"`
type="fragment",
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
)
template = """
@ -230,8 +232,8 @@ class Frag(Component):
def get(self, request):
return self.component.render_to_response(
request=request,
# IMPORTANT: Don't forget `type="fragment"`
type="fragment",
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
)
# NOTE: We wrap the actual fragment in a template tag with x-if="false" to prevent it
@ -329,8 +331,8 @@ class Frag(Component):
def get(self, request):
return self.component.render_to_response(
request=request,
# IMPORTANT: Don't forget `type="fragment"`
type="fragment",
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
)
template = """

View file

@ -1,4 +1,4 @@
### JS and CSS output locations
## JS and CSS output locations
If:
@ -220,3 +220,203 @@ with [`Component.render()`](#TODO) and inserting them into larger structures.
- 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).
## 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.
[`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 set at the root of a component render tree, which is why it is not available for
the [`{% component %}`](../../../reference/template_tags#component) tag.
!!! info
The `deps_strategy` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
There are five 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.
### `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.

View file

@ -145,7 +145,7 @@ Here is how the HTML is post-processed:
</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 [render type](../rendering_components/#render-types) (document, fragment, or inline).
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)

View file

@ -244,7 +244,7 @@ Button.render(
- `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)
- `type` - Type of rendering (default: `"document"`)
- `deps_strategy` - Dependencies rendering strategy (default: `"document"`)
- `request` - HTTP request object, used for context processors (optional)
- `escape_slots_content` - Whether to HTML-escape slot content (default: `True`)
- `render_dependencies` - Whether to process JS and CSS dependencies (default: `True`)
@ -336,102 +336,66 @@ response = MyComponent.render_to_response()
assert isinstance(response, MyHttpResponse)
```
## Render types
## Dependencies rendering
The rendered HTML may be used in different contexts (browser, email, etc).
If your components use JS and CSS scripts, you need to handle them differently.
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 `type` parameter, which controls this behavior.
accept a `deps_strategy` parameter, which controls where and how the JS / CSS are inserted into the HTML.
The `type` 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.
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 five 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.
!!! info
The `type` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
Learn more about [Rendering JS / CSS](../../advanced/rendering_js_css).
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:
There are three render types:
### `document`
`type="document"` is the default. Use this if you are rendering a whole page, or if no other option suits better.
```python
html = Button.render(type="document")
```
When you render a component tree with the `"document"` type, 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.
With this setting, the JS and CSS is set up to avoid any delays for end users:
- Components' primary JS and CSS scripts ([`Component.js`](../../../reference/api/#django_components.Component.js)
and [`Component.css`](../../../reference/api/#django_components.Component.css)) are inlined into the rendered HTML.
```html
<script>
console.log("Hello from Button!");
</script>
<style>
.button {
background-color: blue;
}
</style>
```py
rendered = Calendar.render_to_response(
request=request,
kwargs={
"date": request.GET.get("date", ""),
},
deps_strategy="append",
)
```
- Components' secondary JS and CSS scripts ([`Component.Media`](../../../reference/api/#django_components.Component.Media))
are inserted into the rendered HTML as links.
Renders something like this:
```html
<link rel="stylesheet" href="https://example.com/styles.css" />
<script src="https://example.com/script.js"></script>
<!-- Calendar component -->
<div class="calendar">
...
</div>
<!-- Appended JS / CSS -->
<script src="..."></script>
<link href="..."></link>
```
- A JS script is injected to manage component dependencies, enabling lazy loading of JS and CSS
for HTML fragments.
!!! info
This render type 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`
`type="fragment"` is used when rendering a piece of HTML that will be inserted into a page
that has already been rendered with the `"document"` type:
```python
fragment = MyComponent.render(type="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.
The fragment type:
- Does not include the dependency manager script (assumes it's already loaded)
- Does not inline JS or CSS directly in the HTML
- Includes a special JSON `<script>` tag that tells the dependency manager what JS and CSS to load
- The dependency manager will fetch only scripts that haven't been loaded yet
This is intended for dynamic content that's loaded after the initial page load, such as with [HTMX](https://htmx.org/) or similar.
## 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.

View file

@ -67,6 +67,10 @@
options:
show_if_no_docstring: true
::: django_components.DependenciesStrategy
options:
show_if_no_docstring: true
::: django_components.Empty
options:
show_if_no_docstring: true

View file

@ -20,7 +20,7 @@ Import as
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1066" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1069" target="_blank">See source code</a>
@ -43,7 +43,7 @@ If you insert this tag multiple times, ALL CSS links will be duplicately inserte
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1088" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1091" target="_blank">See source code</a>
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L2619" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L2794" target="_blank">See source code</a>
@ -75,84 +75,48 @@ Renders one of the components that was previously registered with
[`@register()`](./api.md#django_components.register)
decorator.
**Args:**
The `{% component %}` tag takes:
- `name` (str, required): Registered name of the component to render
- All other args and kwargs are defined based on the component itself.
If you defined a component `"my_table"`
```python
from django_component import Component, register
@register("my_table")
class MyTable(Component):
template = """
<table>
<thead>
{% for header in headers %}
<th>{{ header }}</th>
{% endfor %}
</thead>
<tbody>
{% for row in rows %}
<tr>
{% for cell in row %}
<td>{{ cell }}</td>
{% endfor %}
</tr>
{% endfor %}
<tbody>
</table>
"""
def get_context_data(self, rows: List, headers: List):
return {
"rows": rows,
"headers": headers,
}
```
Then you can render this component by referring to `MyTable` via its
registered name `"my_table"`:
- Component's registered name as the first positional argument,
- Followed by any number of positional and keyword arguments.
```django
{% component "my_table" rows=rows headers=headers ... / %}
{% load component_tags %}
<div>
{% component "button" name="John" job="Developer" / %}
</div>
```
### Component input
The component name must be a string literal.
Positional and keyword arguments can be literals or template variables.
The component name must be a single- or double-quotes string and must
be either:
- The first positional argument after `component`:
```django
{% component "my_table" rows=rows headers=headers ... / %}
```
- Passed as kwarg `name`:
```django
{% component rows=rows headers=headers name="my_table" ... / %}
```
### Inserting into slots
### Inserting slot fills
If the component defined any [slots](../concepts/fundamentals/slots.md), you can
pass in the content to be placed inside those slots by inserting [`{% fill %}`](#fill) tags,
directly within the `{% component %}` tag:
"fill" these slots by placing the [`{% fill %}`](#fill) tags within the `{% component %}` tag:
```django
{% component "my_table" rows=rows headers=headers ... / %}
{% component "my_table" rows=rows headers=headers %}
{% fill "pagination" %}
< 1 | 2 | 3 >
{% endfill %}
{% endcomponent %}
```
You can even nest [`{% fill %}`](#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 %}
```
### Isolating components
By default, components behave similarly to Django's
@ -166,6 +130,36 @@ can access only the data that was explicitly passed to it:
{% component "name" positional_arg keyword_arg=value ... only %}
```
Alternatively, you can set all components to be isolated by default, by setting
[`context_behavior`](../settings#django_components.app_settings.ComponentsSettings.context_behavior)
to `"isolated"` in your settings:
```python
# settings.py
COMPONENTS = {
"context_behavior": "isolated",
}
```
### 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](../../concepts/advanced/tag_formatters)
in the settings:
```python
# settings.py
COMPONENTS = {
"tag_formatter": "django_components.component_shorthand_formatter",
}
```
## fill
```django

View file

@ -25,6 +25,7 @@ class Calendar(Component):
kwargs={
"date": request.GET.get("date", ""),
},
deps_strategy="append",
)
@ -52,4 +53,5 @@ class CalendarRelative(Component):
kwargs={
"date": request.GET.get("date", ""),
},
deps_strategy="append",
)

View file

@ -107,7 +107,7 @@ class FragmentBaseHtmx(Component):
class FragJs(Component):
class View:
def get(self, request):
return FragJs.render_to_response(request=request, type="fragment")
return FragJs.render_to_response(request=request, deps_strategy="fragment")
template: types.django_html = """
<div class="frag">
@ -131,7 +131,7 @@ class FragJs(Component):
class FragAlpine(Component):
class View:
def get(self, request):
return FragAlpine.render_to_response(request=request, type="fragment")
return FragAlpine.render_to_response(request=request, deps_strategy="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.

View file

@ -25,4 +25,5 @@ class CalendarNested(Component):
kwargs={
"date": request.GET.get("date", ""),
},
deps_strategy="append",
)

View file

@ -34,7 +34,7 @@ from django_components.component_registry import (
all_registries,
)
from django_components.components import DynamicComponent
from django_components.dependencies import render_dependencies
from django_components.dependencies import DependenciesStrategy, render_dependencies
from django_components.extension import (
ComponentExtension,
OnComponentRegisteredContext,
@ -100,6 +100,7 @@ __all__ = [
"component_shorthand_formatter",
"ContextBehavior",
"Default",
"DependenciesStrategy",
"DynamicComponent",
"Empty",
"format_attributes",

View file

@ -39,7 +39,7 @@ from django_components.component_registry import registry as registry_
from django_components.constants import COMP_ID_PREFIX
from django_components.context import _COMPONENT_CONTEXT_KEY, make_isolated_context_copy
from django_components.dependencies import (
RenderType,
DependenciesStrategy,
cache_component_css,
cache_component_css_vars,
cache_component_js,
@ -181,7 +181,10 @@ class ComponentInput:
args: List
kwargs: Dict
slots: Dict[SlotName, Slot]
type: RenderType
deps_strategy: DependenciesStrategy
# TODO_v1 - Remove, superseded by `deps_strategy`
type: DependenciesStrategy
"""Deprecated alias for `deps_strategy`."""
render_dependencies: bool
@ -1693,7 +1696,7 @@ class Component(metaclass=ComponentMeta):
[`Context`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context)
object that should be used to render the component
- And other kwargs passed to [`Component.render()`](../api/#django_components.Component.render)
like `type` and `render_dependencies`
like `deps_strategy`
Read more on [Component inputs](../../concepts/fundamentals/render_api/#component-inputs).
@ -1998,7 +2001,9 @@ class Component(metaclass=ComponentMeta):
kwargs: Optional[Any] = None,
slots: Optional[Any] = None,
escape_slots_content: bool = True,
type: RenderType = "document",
deps_strategy: DependenciesStrategy = "document",
# TODO_v1 - Remove, superseded by `deps_strategy`
type: Optional[DependenciesStrategy] = None,
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
**response_kwargs: Any,
@ -2060,6 +2065,8 @@ class Component(metaclass=ComponentMeta):
context=context,
slots=slots,
escape_slots_content=escape_slots_content,
deps_strategy=deps_strategy,
# TODO_v1 - Remove, superseded by `deps_strategy`
type=type,
render_dependencies=render_dependencies,
request=request,
@ -2074,7 +2081,9 @@ class Component(metaclass=ComponentMeta):
kwargs: Optional[Any] = None,
slots: Optional[Any] = None,
escape_slots_content: bool = True,
type: RenderType = "document",
deps_strategy: DependenciesStrategy = "document",
# TODO_v1 - Remove, superseded by `deps_strategy`
type: Optional[DependenciesStrategy] = None,
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
) -> str:
@ -2187,37 +2196,27 @@ class Component(metaclass=ComponentMeta):
- 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.
- `type` - Optional. Configure how to handle JS and CSS dependencies. Read more about
[Render types](../../concepts/fundamentals/rendering_components#render-types).
- `deps_strategy` - Optional. Configure how to handle JS and CSS dependencies. Read more about
[Dependencies rendering](../../concepts/fundamentals/rendering_components#dependencies-rendering).
Options:
There are five strategies:
- `"document"` (default) - Use this if you are rendering a whole page, or if no other option suits better.
If it is possible to insert JS and/or CSS into the rendered HTML, then:
- JS and CSS from [`Component.js`](../api/#django_components.Component.js)
and [`Component.css`](../api/#django_components.Component.css) are inlined into the rendered HTML.
- JS and CSS from [`Component.Media`](../api/#django_components.Component.Media) are inserted
into the rendered HTML only as links.
- Extra JS script to manage component dependencies is inserted into the HTML.
- `"fragment"` - Use this if you plan to insert this HTML into a page that was rendered as `"document"`.
- No JS / CSS is inserted. Instead, a JSON `<script>` is inserted. This JSON
tells the dependency manager to load the component's JS and CSS dependencies.
- No extra scripts are inserted.
- `"inline"` - Use this for non-browser use cases like emails, or when you don't want to use
django-component's dependency manager.
This is the same as `"document"`, except no extra scripts are inserted:
- JS and CSS from [`Component.js`](../api/#django_components.Component.js)
and [`Component.css`](../api/#django_components.Component.css) are inlined into the rendered HTML.
- JS and CSS from [`Component.Media`](../api/#django_components.Component.Media) are inserted
into the rendered HTML only as links.
- No extra scripts are inserted.
- [`"document"`](../../concepts/advanced/rendering_js_css#document) (default)
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
- Inserts extra script to allow `fragment` types to work.
- Assumes the HTML will be rendered in a JS-enabled browser.
- [`"fragment"`](../../concepts/advanced/rendering_js_css#fragment)
- A lightweight HTML fragment to be inserted into a document with AJAX.
- No JS / CSS included.
- [`"simple"`](../../concepts/advanced/rendering_js_css#simple)
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
- No extra script loaded.
- [`"prepend"`](../../concepts/advanced/rendering_js_css#prepend)
- Insert JS / CSS before the rendered HTML.
- No extra script loaded.
- [`"append"`](../../concepts/advanced/rendering_js_css#append)
- Insert JS / CSS after the rendered HTML.
- No extra script loaded.
- `request` - Optional. HTTPRequest object. Pass a request object directly to the component to apply
[context processors](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Context.update).
@ -2292,8 +2291,17 @@ class Component(metaclass=ComponentMeta):
else:
comp = cls()
# TODO_v1 - Remove, superseded by `deps_strategy`
if type is not None:
if deps_strategy != "document":
raise ValueError(
"Component.render() received both `type` and `deps_strategy` arguments. "
"Only one should be given. The `type` argument is deprecated. Use `deps_strategy` instead."
)
deps_strategy = type
return comp._render_with_error_trace(
context, args, kwargs, slots, escape_slots_content, type, render_dependencies, request
context, args, kwargs, slots, escape_slots_content, deps_strategy, render_dependencies, request
)
# This is the internal entrypoint for the render function
@ -2304,18 +2312,15 @@ class Component(metaclass=ComponentMeta):
kwargs: Optional[Any] = None,
slots: Optional[Any] = None,
escape_slots_content: bool = True,
type: RenderType = "document",
deps_strategy: DependenciesStrategy = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
) -> str:
# Modify the error to display full component path (incl. slots)
with component_error_message([self.name]):
try:
return self._render_impl(
context, args, kwargs, slots, escape_slots_content, type, render_dependencies, request
)
except Exception as err:
raise err from None
return self._render_impl(
context, args, kwargs, slots, escape_slots_content, deps_strategy, render_dependencies, request
)
def _render_impl(
self,
@ -2324,7 +2329,7 @@ class Component(metaclass=ComponentMeta):
kwargs: Optional[Any] = None,
slots: Optional[Any] = None,
escape_slots_content: bool = True,
type: RenderType = "document",
deps_strategy: DependenciesStrategy = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
) -> str:
@ -2379,7 +2384,9 @@ class Component(metaclass=ComponentMeta):
args=args_list,
kwargs=kwargs_dict,
slots=slots_dict,
type=type,
deps_strategy=deps_strategy,
# TODO_v1 - Remove, superseded by `deps_strategy`
type=deps_strategy,
render_dependencies=render_dependencies,
),
is_filled=None,
@ -2570,7 +2577,7 @@ class Component(metaclass=ComponentMeta):
# all inserted HTML comments into <script> and <link> tags (if render_dependencies=True)
def on_html_rendered(html: str) -> str:
if render_dependencies:
html = _render_dependencies(html, type)
html = _render_dependencies(html, deps_strategy)
return html
trace_component_msg(

View file

@ -118,12 +118,13 @@ class DynamicComponent(Component):
"kwargs": kwargs,
}
# TODO: Replace combination of `on_render_before()` + `template` with single `on_render()`
# NOTE: The inner component is rendered in `on_render_before`, so that the `Context` object
# is already configured as if the inner component was rendered inside the template.
# E.g. the `_COMPONENT_CONTEXT_KEY` is set, which means that the child component
# will know that it's a child of this component.
def on_render_before(self, context: Context, template: Template) -> Context:
comp_class = context["comp_class"]
comp_class: type[Component] = context["comp_class"]
args = context["args"]
kwargs = context["kwargs"]
@ -140,7 +141,7 @@ class DynamicComponent(Component):
# NOTE: Since we're accessing slots as `self.input.slots`, the content of slot functions
# was already escaped (if set so).
escape_slots_content=False,
type=self.input.type,
deps_strategy=self.input.deps_strategy,
render_dependencies=self.input.render_dependencies,
)

View file

@ -42,7 +42,14 @@ if TYPE_CHECKING:
ScriptType = Literal["css", "js"]
RenderType = Literal["document", "fragment"]
DependenciesStrategy = Literal["document", "fragment", "simple", "prepend", "append"]
"""
Type for the available strategies for rendering JS and CSS dependencies.
Read more about the [dependencies strategies](../../concepts/advanced/rendering_js_css).
"""
DEPS_STRATEGIES = ("document", "fragment", "simple", "prepend", "append")
#########################################################
@ -365,7 +372,7 @@ PLACEHOLDER_REGEX = re.compile(
)
def render_dependencies(content: TContent, type: RenderType = "document") -> TContent:
def render_dependencies(content: TContent, strategy: DependenciesStrategy = "document") -> TContent:
"""
Given a string that contains parts that were rendered by components,
this function inserts all used JS and CSS.
@ -404,8 +411,8 @@ def render_dependencies(content: TContent, type: RenderType = "document") -> TCo
return HttpResponse(processed_html)
```
"""
if type not in ("document", "fragment"):
raise ValueError(f"Invalid type '{type}'")
if strategy not in DEPS_STRATEGIES:
raise ValueError(f"Invalid strategy '{strategy}'")
is_safestring = isinstance(content, SafeString)
@ -414,17 +421,17 @@ def render_dependencies(content: TContent, type: RenderType = "document") -> TCo
else:
content_ = cast(bytes, content)
content_, js_dependencies, css_dependencies = _process_dep_declarations(content_, type)
content_, js_dependencies, css_dependencies = _process_dep_declarations(content_, strategy)
# Replace the placeholders with the actual content
# If type == `document`, we insert the JS and CSS directly into the HTML,
# If strategy in (`document`, 'simple'), we insert the JS and CSS directly into the HTML,
# where the placeholders were.
# If type == `fragment`, we let the client-side manager load the JS and CSS,
# If strategy == `fragment`, we let the client-side manager load the JS and CSS,
# and remove the placeholders.
did_find_js_placeholder = False
did_find_css_placeholder = False
css_replacement = css_dependencies if type == "document" else b""
js_replacement = js_dependencies if type == "document" else b""
css_replacement = css_dependencies if strategy in ("document", "simple") else b""
js_replacement = js_dependencies if strategy in ("document", "simple") else b""
def on_replace_match(match: "re.Match[bytes]") -> bytes:
nonlocal did_find_css_placeholder
@ -445,10 +452,10 @@ def render_dependencies(content: TContent, type: RenderType = "document") -> TCo
content_ = PLACEHOLDER_REGEX.sub(on_replace_match, content_)
# By default, if user didn't specify any `{% component_dependencies %}`,
# By default ("document") and for "simple" strategy, if user didn't specify any `{% component_dependencies %}`,
# then try to insert the JS scripts at the end of <body> and CSS sheets at the end
# of <head>
if type == "document" and (not did_find_js_placeholder or not did_find_css_placeholder):
# of <head>.
if strategy in ("document", "simple") and (not did_find_js_placeholder or not did_find_css_placeholder):
maybe_transformed = _insert_js_css_to_default_locations(
content_.decode(),
css_content=None if did_find_css_placeholder else css_dependencies.decode(),
@ -459,8 +466,13 @@ def render_dependencies(content: TContent, type: RenderType = "document") -> TCo
content_ = maybe_transformed.encode()
# In case of a fragment, we only append the JS (actually JSON) to trigger the call of dependency-manager
if type == "fragment":
elif strategy == "fragment":
content_ += js_dependencies
# For prepend / append, we insert the JS and CSS before / after the content
elif strategy == "prepend":
content_ = js_dependencies + css_dependencies + content_
elif strategy == "append":
content_ = content_ + js_dependencies + css_dependencies
# Return the same type as we were given
output = content_.decode() if isinstance(content, str) else content_
@ -493,7 +505,7 @@ _render_dependencies = render_dependencies
# will be fetched and executed only once.
# 6. And lastly, we generate a JS script that will load / mark as loaded the JS and CSS
# as categorized in previous step.
def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes, bytes, bytes]:
def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) -> Tuple[bytes, bytes, bytes]:
"""
Process a textual content that may include metadata on rendered components.
The metadata has format like this
@ -513,11 +525,11 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
content = COMPONENT_COMMENT_REGEX.sub(on_replace_match, content)
# NOTE: Python's set does NOT preserve order
# NOTE: Python's set does NOT preserve order, so both set and list are needed
seen_comp_hashes: Set[str] = set()
comp_hashes: List[str] = []
# Used for passing Python vars to JS/CSS
inputs_data: List[Tuple[str, ScriptType, Optional[str]]] = []
variables_data: List[Tuple[str, ScriptType, Optional[str]]] = []
comp_data: List[Tuple[str, ScriptType, Optional[str]]] = []
# Process individual parts. Each part is like a CSV row of `name,id,js,css`.
@ -530,8 +542,8 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
raise RuntimeError("Malformed dependencies data")
comp_cls_id: str = part_match.group("comp_cls_id").decode("utf-8")
js_input_hash: Optional[str] = part_match.group("js").decode("utf-8") or None
css_input_hash: Optional[str] = part_match.group("css").decode("utf-8") or None
js_variables_hash: Optional[str] = part_match.group("js").decode("utf-8") or None
css_variables_hash: Optional[str] = part_match.group("css").decode("utf-8") or None
if comp_cls_id in seen_comp_hashes:
continue
@ -545,28 +557,28 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
# Schedule to load the `<script>` / `<link>` tags for the JS / CSS variables.
# Skip if no variables are defined.
if js_input_hash is not None:
inputs_data.append((comp_cls_id, "js", js_input_hash))
if css_input_hash is not None:
inputs_data.append((comp_cls_id, "css", css_input_hash))
if js_variables_hash is not None:
variables_data.append((comp_cls_id, "js", js_variables_hash))
if css_variables_hash is not None:
variables_data.append((comp_cls_id, "css", css_variables_hash))
(
to_load_input_js_urls,
to_load_input_css_urls,
inlined_input_js_tags,
inlined_input_css_tags,
loaded_input_js_urls,
loaded_input_css_urls,
) = _prepare_tags_and_urls(inputs_data, type)
js_variables_urls_to_load,
css_variables_urls_to_load,
js_variables_tags,
css_variables_tags,
js_variables_urls_loaded,
css_variables_urls_loaded,
) = _prepare_tags_and_urls(variables_data, strategy)
(
to_load_component_js_urls,
to_load_component_css_urls,
inlined_component_js_tags,
inlined_component_css_tags,
loaded_component_js_urls,
loaded_component_css_urls,
) = _prepare_tags_and_urls(comp_data, type)
component_js_urls_to_load,
component_css_urls_to_load,
component_js_tags,
component_css_tags,
component_js_urls_loaded,
component_css_urls_loaded,
) = _prepare_tags_and_urls(comp_data, strategy)
def get_component_media(comp_cls_id: str) -> Media:
from django_components.component import get_component_by_class_id
@ -581,20 +593,20 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
*[get_component_media(comp_cls_id) for comp_cls_id in comp_hashes],
# All the inlined scripts that we plan to fetch / load
Media(
js=[*to_load_component_js_urls, *to_load_input_js_urls],
css={"all": [*to_load_component_css_urls, *to_load_input_css_urls]},
js=[*component_js_urls_to_load, *js_variables_urls_to_load],
css={"all": [*component_css_urls_to_load, *css_variables_urls_to_load]},
),
]
# Once we have ALL JS and CSS URLs that we want to fetch, we can convert them to
# <script> and <link> tags. Note that this is done by the user-provided Media classes.
# fmt: off
to_load_css_tags = [
media_css_tags = [
tag
for media in all_medias if media is not None
for tag in media.render_css()
]
to_load_js_tags = [
media_js_tags = [
tag
for media in all_medias if media is not None
for tag in media.render_js()
@ -604,41 +616,45 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
# Postprocess all <script> and <link> tags to 1) dedupe, and 2) extract URLs.
# For the deduplication, if multiple components link to the same JS/CSS, but they
# render the <script> or <link> tag differently, we go with the first tag that we come across.
to_load_css_tags, to_load_css_urls = _postprocess_media_tags("css", to_load_css_tags)
to_load_js_tags, to_load_js_urls = _postprocess_media_tags("js", to_load_js_tags)
media_css_tags, media_css_urls = _postprocess_media_tags("css", media_css_tags)
media_js_tags, media_js_urls = _postprocess_media_tags("js", media_js_tags)
loaded_css_urls = sorted(
[
*loaded_component_css_urls,
*loaded_input_css_urls,
# NOTE: When rendering a document, the initial CSS is inserted directly into the HTML
# to avoid a flash of unstyled content. In the dependency manager, we only mark those
# scripts as loaded.
*(to_load_css_urls if type == "document" else []),
*component_css_urls_loaded,
*css_variables_urls_loaded,
# NOTE: When rendering a "document", the initial CSS is inserted directly into the HTML
# to avoid a flash of unstyled content. In such case, the "CSS to load" is actually already
# loaded, so we have to mark those scripts as loaded in the dependency manager.
*(media_css_urls if strategy == "document" else []),
]
)
loaded_js_urls = sorted(
[
*loaded_component_js_urls,
*loaded_input_js_urls,
# NOTE: When rendering a document, the initial JS is inserted directly into the HTML
# so the scripts are executed at proper order. In the dependency manager, we only mark those
# scripts as loaded.
*(to_load_js_urls if type == "document" else []),
*component_js_urls_loaded,
*js_variables_urls_loaded,
# NOTE: When rendering a "document", the initial JS is inserted directly into the HTML
# so the scripts are executed at proper order. In such case, the "JS to load" is actually already
# loaded, so we have to mark those scripts as loaded in the dependency manager.
*(media_js_urls if strategy == "document" else []),
]
)
exec_script = _gen_exec_script(
to_load_js_tags=to_load_js_tags if type == "fragment" else [],
to_load_css_tags=to_load_css_tags if type == "fragment" else [],
loaded_js_urls=loaded_js_urls,
loaded_css_urls=loaded_css_urls,
)
# NOTE: No exec script for the "simple" mode, as that one is NOT using the dependency manager
if strategy in ("document", "fragment"):
exec_script = _gen_exec_script(
to_load_js_tags=media_js_tags if strategy == "fragment" else [],
to_load_css_tags=media_css_tags if strategy == "fragment" else [],
loaded_js_urls=loaded_js_urls,
loaded_css_urls=loaded_css_urls,
)
else:
exec_script = None
# Core scripts without which the rest wouldn't work
core_script_tags = Media(
# NOTE: When rendering a document, the initial JS is inserted directly into the HTML
js=[static("django_components/django_components.min.js")] if type == "document" else [],
js=[static("django_components/django_components.min.js")] if strategy == "document" else [],
).render_js()
final_script_tags = "".join(
@ -649,14 +665,14 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
# Loads JS from `Media.js` and `Component.js` if fragment
*([exec_script] if exec_script else []),
# JS from `Media.js`
# NOTE: When rendering a document, the initial JS is inserted directly into the HTML
# so the scripts are executed at proper order. In the dependency manager, we only mark those
# scripts as loaded.
*(to_load_js_tags if type == "document" else []),
# NOTE: When strategy in ("document", "simple", "prepend", "append"), the initial JS is inserted
# directly into the HTML so the scripts are executed at proper order. In the dependency manager,
# we only mark those scripts as loaded.
*(media_js_tags if strategy in ("document", "simple", "prepend", "append") else []),
# JS variables
*[tag for tag in inlined_input_js_tags],
*[tag for tag in js_variables_tags],
# JS from `Component.js` (if not fragment)
*[tag for tag in inlined_component_js_tags],
*[tag for tag in component_js_tags],
]
)
@ -665,13 +681,13 @@ def _process_dep_declarations(content: bytes, type: RenderType) -> Tuple[bytes,
# CSS by us
# <NONE>
# CSS from `Component.css` (if not fragment)
*[tag for tag in inlined_component_css_tags],
*[tag for tag in component_css_tags],
# CSS variables
*[tag for tag in inlined_input_css_tags],
*[tag for tag in css_variables_tags],
# CSS from `Media.css` (plus from `Component.css` if fragment)
# NOTE: Similarly to JS, the initial CSS is loaded outside of the dependency
# manager, and only marked as loaded, to avoid a flash of unstyled content.
*[tag for tag in to_load_css_tags],
*[tag for tag in media_css_tags],
]
)
@ -726,18 +742,21 @@ def _postprocess_media_tags(
def _prepare_tags_and_urls(
data: List[Tuple[str, ScriptType, Optional[str]]],
type: RenderType,
strategy: DependenciesStrategy,
) -> Tuple[List[str], List[str], List[str], List[str], List[str], List[str]]:
from django_components.component import get_component_by_class_id
to_load_js_urls: List[str] = []
to_load_css_urls: List[str] = []
# JS / CSS that we should insert into the HTML
inlined_js_tags: List[str] = []
inlined_css_tags: List[str] = []
# JS / CSS that the client-side dependency managers should load
to_load_js_urls: List[str] = []
to_load_css_urls: List[str] = []
# JS / CSS that we want to mark as loaded in the dependency manager
loaded_js_urls: List[str] = []
loaded_css_urls: List[str] = []
# When `type="document"`, we insert the actual <script> and <style> tags into the HTML.
# When `strategy="document"`, we insert the actual <script> and <style> tags into the HTML.
# But even in that case we still need to call `Components.manager.markScriptLoaded`,
# so the client knows NOT to fetch them again.
# So in that case we populate both `inlined` and `loaded` lists
@ -748,14 +767,19 @@ def _prepare_tags_and_urls(
# which means that we are NOT going to load / inline it again.
comp_cls = get_component_by_class_id(comp_cls_id)
if type == "document":
# When strategy is "document", "simple", "prepend", or "append", we insert the actual <script> and
# <style> tags into the HTML.
#
# But in case of strategy == "document" we still need to call `Components.manager.markScriptLoaded`,
# so the client knows NOT to fetch the scripts again.
# So in that case we populate both `inlined` and `loaded` lists
if strategy == "document":
# NOTE: Skip fetching of inlined JS/CSS if it's not defined or empty for given component
#
# NOTE: If `input_hash` is `None`, then we get the component's JS/CSS
# (e.g. `/components/cache/table.js`).
# And if `input_hash` is given, we get the component's JS/CSS variables
# (e.g. `/components/cache/table.0ab2c3.js`).
if script_type == "js" and is_nonempty_str(comp_cls.js):
# NOTE: If `input_hash` is `None`, then we get the component's JS/CSS
# (e.g. `/components/cache/table.js`).
# And if `input_hash` is given, we get the component's JS/CSS variables
# (e.g. `/components/cache/table.0ab2c3.js`).
inlined_js_tags.append(get_script_tag("js", comp_cls, input_hash))
loaded_js_urls.append(get_script_url("js", comp_cls, input_hash))
@ -763,9 +787,16 @@ def _prepare_tags_and_urls(
inlined_css_tags.append(get_script_tag("css", comp_cls, input_hash))
loaded_css_urls.append(get_script_url("css", comp_cls, input_hash))
# When NOT a document (AKA is a fragment), then scripts are NOT inserted into
# the HTML, and instead we fetch and load them all via our JS dependency manager.
else:
elif strategy in ("simple", "prepend", "append"):
if script_type == "js" and is_nonempty_str(comp_cls.js):
inlined_js_tags.append(get_script_tag("js", comp_cls, input_hash))
if script_type == "css" and is_nonempty_str(comp_cls.css):
inlined_css_tags.append(get_script_tag("css", comp_cls, input_hash))
# When a fragment, then scripts are NOT inserted into the HTML,
# and instead we fetch and load them all via our JS dependency manager.
elif strategy == "fragment":
if script_type == "js" and is_nonempty_str(comp_cls.js):
to_load_js_urls.append(get_script_url("js", comp_cls, input_hash))
@ -1012,7 +1043,7 @@ class ComponentDependencyMiddleware:
if not isinstance(response, StreamingHttpResponse) and response.get("Content-Type", "").startswith(
"text/html"
):
response.content = render_dependencies(response.content, type="document")
response.content = render_dependencies(response.content, strategy="document")
return response

View file

@ -250,9 +250,9 @@ def fragment_base_htmx_view(request):
def fragment_view(request):
fragment_type = request.GET["frag"]
if fragment_type == "comp":
return FragComp.render_to_response(type="fragment")
return FragComp.render_to_response(deps_strategy="fragment")
elif fragment_type == "media":
return FragMedia.render_to_response(type="fragment")
return FragMedia.render_to_response(deps_strategy="fragment")
else:
raise ValueError("Invalid fragment type")

View file

@ -49,6 +49,24 @@ class SimpleComponent(Component):
js = "script.js"
@djc_test
class TestDependenciesLegacy:
# TODO_v1 - Remove
def test_render_with_type_arg(self):
rendered = SimpleComponent.render(kwargs={"variable": "foo"}, type="append")
# Dependency manager script NOT present
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
# Check that it contains inlined JS and CSS, and Media.css
assert rendered.strip() == (
'Variable: <strong data-djc-id-ca1bc3e="">foo</strong>\n'
' <script src="script.js"></script><script>console.log("xyz");</script><style>.xyz {\n'
" color: red;\n"
' }</style><link href="style.css" media="all" rel="stylesheet">'
)
@djc_test
class TestRenderDependencies:
def test_standalone_render_dependencies(self):
@ -61,7 +79,7 @@ class TestRenderDependencies:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered_raw: str = template.render(Context({}))
# Placeholders
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
@ -194,103 +212,6 @@ class TestRenderDependencies:
assert rendered.count("<link") == 1
assert rendered.count("<style") == 1
def test_inserts_styles_and_script_to_default_places_if_not_overriden(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
<!DOCTYPE html>
<html>
<head></head>
<body>
{% component "test" variable="foo" / %}
</body>
</html>
"""
rendered_raw = Template(template_str).render(Context({}))
rendered = render_dependencies(rendered_raw)
assert rendered.count("<script") == 4
assert rendered.count("<style") == 1
assert rendered.count("<link") == 1
assert rendered.count("_RENDERED") == 0
assertInHTML(
"""
<head>
<style>.xyz { color: red; }</style>
<link href="style.css" media="all" rel="stylesheet">
</head>
""",
rendered,
count=1,
)
body_re = re.compile(r"<body>(.*?)</body>", re.DOTALL)
rendered_body = body_re.search(rendered).group(1) # type: ignore[union-attr]
assertInHTML(
"""<script src="django_components/django_components.min.js">""",
rendered_body,
count=1,
)
assertInHTML(
'<script>console.log("xyz");</script>',
rendered_body,
count=1,
)
def test_does_not_insert_styles_and_script_to_default_places_if_overriden(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
<!DOCTYPE html>
<html>
<head>
{% component_js_dependencies %}
</head>
<body>
{% component "test" variable="foo" / %}
{% component_css_dependencies %}
</body>
</html>
"""
rendered_raw = Template(template_str).render(Context({}))
rendered = render_dependencies(rendered_raw)
assert rendered.count("<script") == 4
assert rendered.count("<style") == 1
assert rendered.count("<link") == 1
assert rendered.count("_RENDERED") == 0
assertInHTML(
"""
<body>
Variable: <strong data-djc-id-ca1bc41>foo</strong>
<style>.xyz { color: red; }</style>
<link href="style.css" media="all" rel="stylesheet">
</body>
""",
rendered,
count=1,
)
head_re = re.compile(r"<head>(.*?)</head>", re.DOTALL)
rendered_head = head_re.search(rendered).group(1) # type: ignore[union-attr]
assertInHTML(
"""<script src="django_components/django_components.min.js">""",
rendered_head,
count=1,
)
assertInHTML(
'<script>console.log("xyz");</script>',
rendered_head,
count=1,
)
# NOTE: Some HTML parser libraries like selectolax or lxml try to "correct" the given HTML.
# We want to avoid this behavior, so user gets the exact same HTML back.
def test_does_not_try_to_add_close_tags(self):
@ -301,7 +222,7 @@ class TestRenderDependencies:
"""
rendered_raw = Template(template_str).render(Context({"formset": [1]}))
rendered = render_dependencies(rendered_raw, type="fragment")
rendered = render_dependencies(rendered_raw, strategy="fragment")
assertHTMLEqual(rendered, "<thead>")
@ -336,7 +257,7 @@ class TestRenderDependencies:
"""
rendered_raw = Template(template_str).render(Context({"formset": [1]}))
rendered = render_dependencies(rendered_raw, type="fragment")
rendered = render_dependencies(rendered_raw, strategy="fragment")
expected = """
<table class="table-auto border-collapse divide-y divide-x divide-slate-300 w-full">
@ -399,7 +320,7 @@ class TestRenderDependencies:
"""
rendered_raw = Template(template_str).render(Context({"formset": [1]}))
rendered = render_dependencies(rendered_raw, type="fragment")
rendered = render_dependencies(rendered_raw, strategy="fragment")
# Base64 encodings:
# `PGxpbmsgaHJlZj0ic3R5bGUuY3NzIiBtZWRpYT0iYWxsIiByZWw9InN0eWxlc2hlZXQiPg==` -> `<link href="style.css" media="all" rel="stylesheet">` # noqa: E501
@ -472,6 +393,581 @@ class TestRenderDependencies:
ComponentWithScript.render(kwargs={"variable": "foo"})
@djc_test
class TestDependenciesStrategyDocument:
def test_inserts_styles_and_script_to_default_places_if_not_overriden(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
<!DOCTYPE html>
<html>
<head></head>
<body>
{% component "test" variable="foo" / %}
</body>
</html>
"""
rendered_raw = Template(template_str).render(Context({}))
rendered = render_dependencies(rendered_raw, strategy="document")
assert rendered.count("<script") == 4
assert rendered.count("<style") == 1
assert rendered.count("<link") == 1
assert rendered.count("_RENDERED") == 0
assertInHTML(
"""
<head>
<style>.xyz { color: red; }</style>
<link href="style.css" media="all" rel="stylesheet">
</head>
""",
rendered,
count=1,
)
body_re = re.compile(r"<body>(.*?)</body>", re.DOTALL)
rendered_body = body_re.search(rendered).group(1) # type: ignore[union-attr]
assertInHTML(
"""<script src="django_components/django_components.min.js">""",
rendered_body,
count=1,
)
assertInHTML(
'<script>console.log("xyz");</script>',
rendered_body,
count=1,
)
def test_does_not_insert_styles_and_script_to_default_places_if_overriden(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
<!DOCTYPE html>
<html>
<head>
{% component_js_dependencies %}
</head>
<body>
{% component "test" variable="foo" / %}
{% component_css_dependencies %}
</body>
</html>
"""
rendered_raw = Template(template_str).render(Context({}))
rendered = render_dependencies(rendered_raw, strategy="document")
assert rendered.count("<script") == 4
assert rendered.count("<style") == 1
assert rendered.count("<link") == 1
assert rendered.count("_RENDERED") == 0
assertInHTML(
"""
<body>
Variable: <strong data-djc-id-ca1bc41>foo</strong>
<style>.xyz { color: red; }</style>
<link href="style.css" media="all" rel="stylesheet">
</body>
""",
rendered,
count=1,
)
head_re = re.compile(r"<head>(.*?)</head>", re.DOTALL)
rendered_head = head_re.search(rendered).group(1) # type: ignore[union-attr]
assertInHTML(
"""<script src="django_components/django_components.min.js">""",
rendered_head,
count=1,
)
assertInHTML(
'<script>console.log("xyz");</script>',
rendered_head,
count=1,
)
@djc_test
class TestDependenciesStrategySimple:
def test_single_component(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
# Placeholders
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
assert rendered_raw.count('<script name="JS_PLACEHOLDER"></script>') == 1
assert rendered_raw.count("<script") == 1
assert rendered_raw.count("<style") == 0
assert rendered_raw.count("<link") == 1
assert rendered_raw.count("_RENDERED") == 1
rendered = render_dependencies(rendered_raw, strategy="simple")
# Dependency manager script NOT present
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
# Check that it contains inlined JS and CSS, and Media.css
assert rendered.strip() == (
'<script src="script.js"></script><script>console.log("xyz");</script>\n'
" <style>.xyz {\n"
" color: red;\n"
' }</style><link href="style.css" media="all" rel="stylesheet">\n'
" \n"
' Variable: <strong data-djc-id-ca1bc41="">foo</strong>'
)
def test_multiple_components_dependencies(self):
class SimpleComponentNested(Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% component "inner" variable=variable / %}
{% slot "default" default / %}
</div>
"""
css: types.css = """
.my-class {
color: red;
}
"""
js: types.js = """
console.log("Hello");
"""
def get_context_data(self, variable):
return {}
class Media:
css = ["style.css", "style2.css"]
js = "script2.js"
class OtherComponent(Component):
template: types.django_html = """
XYZ: <strong>{{ variable }}</strong>
"""
css: types.css = """
.xyz {
color: red;
}
"""
js: types.js = """
console.log("xyz");
"""
def get_context_data(self, variable):
return {}
class Media:
css = "xyz1.css"
js = "xyz1.js"
registry.register(name="inner", component=SimpleComponent)
registry.register(name="outer", component=SimpleComponentNested)
registry.register(name="other", component=OtherComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
{% component 'outer' variable='variable' %}
{% component 'other' variable='variable_inner' / %}
{% endcomponent %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
rendered = render_dependencies(rendered_raw, strategy="simple")
# Dependency manager script NOT present
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
assert rendered.count("<script") == 6 # 3 Component.js and 3 Media.js
assert rendered.count("<link") == 3 # Media.css
assert rendered.count("<style") == 3 # Component.css
# Components' inlined CSS
# NOTE: Each of these should be present only ONCE!
assertInHTML(
"""
<style>.my-class { color: red; }</style>
<style>.xyz { color: red; }</style>
""",
rendered,
count=1,
)
# Components' Media.css
# Order:
# - "style.css", "style2.css" (from SimpleComponentNested)
# - "style.css" (from SimpleComponent inside SimpleComponentNested)
# - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested)
assertInHTML(
"""
<link href="style.css" media="all" rel="stylesheet">
<link href="style2.css" media="all" rel="stylesheet">
<link href="xyz1.css" media="all" rel="stylesheet">
""",
rendered,
count=1,
)
# Components' Media.js followed by inlined JS
# Order:
# - "script2.js" (from SimpleComponentNested)
# - "script.js" (from SimpleComponent inside SimpleComponentNested)
# - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested)
assertInHTML(
"""
<script src="script2.js"></script>
<script src="script.js"></script>
<script src="xyz1.js"></script>
<script>console.log("Hello");</script>
<script>console.log("xyz");</script>
""",
rendered,
count=1,
)
# Check that there's no payload like with "document" or "fragment" modes
assert "application/json" not in rendered
@djc_test
class TestDependenciesStrategyPrepend:
def test_single_component(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
# Placeholders
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
assert rendered_raw.count('<script name="JS_PLACEHOLDER"></script>') == 1
assert rendered_raw.count("<script") == 1
assert rendered_raw.count("<style") == 0
assert rendered_raw.count("<link") == 1
assert rendered_raw.count("_RENDERED") == 1
rendered = render_dependencies(rendered_raw, strategy="prepend")
# Dependency manager script NOT present
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
# Check that it contains inlined JS and CSS, and Media.css
assert rendered.strip() == (
'<script src="script.js"></script><script>console.log("xyz");</script><style>.xyz {\n'
" color: red;\n"
' }</style><link href="style.css" media="all" rel="stylesheet">\n'
" \n"
" \n"
" \n"
" \n"
' Variable: <strong data-djc-id-ca1bc41="">foo</strong>'
)
def test_multiple_components_dependencies(self):
class SimpleComponentNested(Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% component "inner" variable=variable / %}
{% slot "default" default / %}
</div>
"""
css: types.css = """
.my-class {
color: red;
}
"""
js: types.js = """
console.log("Hello");
"""
def get_context_data(self, variable):
return {}
class Media:
css = ["style.css", "style2.css"]
js = "script2.js"
class OtherComponent(Component):
template: types.django_html = """
XYZ: <strong>{{ variable }}</strong>
"""
css: types.css = """
.xyz {
color: red;
}
"""
js: types.js = """
console.log("xyz");
"""
def get_context_data(self, variable):
return {}
class Media:
css = "xyz1.css"
js = "xyz1.js"
registry.register(name="inner", component=SimpleComponent)
registry.register(name="outer", component=SimpleComponentNested)
registry.register(name="other", component=OtherComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
{% component 'outer' variable='variable' %}
{% component 'other' variable='variable_inner' / %}
{% endcomponent %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
rendered = render_dependencies(rendered_raw, strategy="prepend")
# Dependency manager script NOT present
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
assert rendered.count("<script") == 6 # 3 Component.js and 3 Media.js
assert rendered.count("<link") == 3 # Media.css
assert rendered.count("<style") == 3 # Component.css
# Components' inlined CSS
# NOTE: Each of these should be present only ONCE!
assertInHTML(
"""
<style>.my-class { color: red; }</style>
<style>.xyz { color: red; }</style>
""",
rendered,
count=1,
)
# Components' Media.css
# Order:
# - "style.css", "style2.css" (from SimpleComponentNested)
# - "style.css" (from SimpleComponent inside SimpleComponentNested)
# - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested)
assertInHTML(
"""
<link href="style.css" media="all" rel="stylesheet">
<link href="style2.css" media="all" rel="stylesheet">
<link href="xyz1.css" media="all" rel="stylesheet">
""",
rendered,
count=1,
)
# Components' Media.js followed by inlined JS
# Order:
# - "script2.js" (from SimpleComponentNested)
# - "script.js" (from SimpleComponent inside SimpleComponentNested)
# - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested)
assertInHTML(
"""
<script src="script2.js"></script>
<script src="script.js"></script>
<script src="xyz1.js"></script>
<script>console.log("Hello");</script>
<script>console.log("xyz");</script>
""",
rendered,
count=1,
)
# Check that there's no payload like with "document" or "fragment" modes
assert "application/json" not in rendered
@djc_test
class TestDependenciesStrategyAppend:
def test_single_component(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
# Placeholders
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
assert rendered_raw.count('<script name="JS_PLACEHOLDER"></script>') == 1
assert rendered_raw.count("<script") == 1
assert rendered_raw.count("<style") == 0
assert rendered_raw.count("<link") == 1
assert rendered_raw.count("_RENDERED") == 1
rendered = render_dependencies(rendered_raw, strategy="append")
# Dependency manager script NOT present
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
# Check that it contains inlined JS and CSS, and Media.css
assert rendered.strip() == (
'Variable: <strong data-djc-id-ca1bc41="">foo</strong>\n'
" \n"
' <script src="script.js"></script><script>console.log("xyz");</script><style>.xyz {\n'
" color: red;\n"
' }</style><link href="style.css" media="all" rel="stylesheet">'
)
def test_multiple_components_dependencies(self):
class SimpleComponentNested(Component):
template: types.django_html = """
{% load component_tags %}
<div>
{% component "inner" variable=variable / %}
{% slot "default" default / %}
</div>
"""
css: types.css = """
.my-class {
color: red;
}
"""
js: types.js = """
console.log("Hello");
"""
def get_context_data(self, variable):
return {}
class Media:
css = ["style.css", "style2.css"]
js = "script2.js"
class OtherComponent(Component):
template: types.django_html = """
XYZ: <strong>{{ variable }}</strong>
"""
css: types.css = """
.xyz {
color: red;
}
"""
js: types.js = """
console.log("xyz");
"""
def get_context_data(self, variable):
return {}
class Media:
css = "xyz1.css"
js = "xyz1.js"
registry.register(name="inner", component=SimpleComponent)
registry.register(name="outer", component=SimpleComponentNested)
registry.register(name="other", component=OtherComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component_js_dependencies %}
{% component_css_dependencies %}
{% component 'outer' variable='variable' %}
{% component 'other' variable='variable_inner' / %}
{% endcomponent %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
rendered = render_dependencies(rendered_raw, strategy="append")
# Dependency manager script NOT present
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
assert rendered.count("<script") == 6 # 3 Component.js and 3 Media.js
assert rendered.count("<link") == 3 # Media.css
assert rendered.count("<style") == 3 # Component.css
# Components' inlined CSS
# NOTE: Each of these should be present only ONCE!
assertInHTML(
"""
<style>.my-class { color: red; }</style>
<style>.xyz { color: red; }</style>
""",
rendered,
count=1,
)
# Components' Media.css
# Order:
# - "style.css", "style2.css" (from SimpleComponentNested)
# - "style.css" (from SimpleComponent inside SimpleComponentNested)
# - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested)
assertInHTML(
"""
<link href="style.css" media="all" rel="stylesheet">
<link href="style2.css" media="all" rel="stylesheet">
<link href="xyz1.css" media="all" rel="stylesheet">
""",
rendered,
count=1,
)
# Components' Media.js followed by inlined JS
# Order:
# - "script2.js" (from SimpleComponentNested)
# - "script.js" (from SimpleComponent inside SimpleComponentNested)
# - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested)
assertInHTML(
"""
<script src="script2.js"></script>
<script src="script.js"></script>
<script src="xyz1.js"></script>
<script>console.log("Hello");</script>
<script>console.log("xyz");</script>
""",
rendered,
count=1,
)
# Check that there's no payload like with "document" or "fragment" modes
assert "application/json" not in rendered
@djc_test
class TestMiddleware:
def test_middleware_response_without_content_type(self):