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

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