mirror of
https://github.com/django-components/django-components.git
synced 2025-08-03 22:08:17 +00:00
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:
parent
e74e1241ac
commit
bf7a204e92
16 changed files with 1210 additions and 408 deletions
105
CHANGELOG.md
105
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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 = """
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -25,4 +25,5 @@ class CalendarNested(Component):
|
|||
kwargs={
|
||||
"date": request.GET.get("date", ""),
|
||||
},
|
||||
deps_strategy="append",
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue