refactor: remove middleware, add strategy "raw", and call render_deps() from within Template.render() (#1166)

* refactor: remove middleware, add strategy "raw", and call render_deps() from within Template.render()

* refactor: fix formatting

* refactor: fix benchmark tests

* refactor: avoid processing deps if rendered HTML contains no components

* refactor: remove comments

* refactor: rename "raw" to "ignore"
This commit is contained in:
Juro Oravec 2025-05-06 21:36:41 +02:00 committed by GitHub
parent 1049c08324
commit 6253042e9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 771 additions and 657 deletions

View file

@ -26,6 +26,44 @@
See [Migrating from generics to class attributes](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/#migrating-from-generics-to-class-attributes) for more info.
- The middleware `ComponentDependencyMiddleware` was removed as it is no longer needed.
The middleware served one purpose - to render the JS and CSS dependencies of components
when you rendered templates with `Template.render()` or `django.shortcuts.render()` and those templates contained `{% component %}` tags.
Now, the JS and CSS dependencies of components are automatically rendered,
even when you render Templates with `Template.render()` or `django.shortcuts.render()`.
- NOTE: If you rendered HTML with `Component.render()` or `Component.render_to_response()`, the JS and CSS were already rendered.
To disable this behavior, set the `DJC_DEPS_STRATEGY` context key to `"ignore"`
when rendering the template:
```py
# With `Template.render()`:
template = Template(template_str)
rendered = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
# Or with django.shortcuts.render():
from django.shortcuts import render
rendered = render(
request,
"my_template.html",
context={"DJC_DEPS_STRATEGY": "ignore"},
)
```
In fact, you can set the `DJC_DEPS_STRATEGY` context key to any of the strategies:
- `"document"`
- `"fragment"`
- `"simple"`
- `"prepend"`
- `"append"`
- `"ignore"`
See [Dependencies rendering](https://django-components.github.io/django-components/0.140/concepts/advanced/rendering_js_css/) for more info.
- The interface of the not-yet-released `get_js_data()` and `get_css_data()` methods has changed to
match `get_template_data()`.
@ -192,6 +230,20 @@
Calendar.render_to_response(deps_strategy="fragment")
```
- The `render_dependencies` kwarg in `Component.render()` and `Component.render_to_response()` is now deprecated. Use `deps_strategy="ignore"` instead. The `render_dependencies` kwarg will be removed in v1.
Before:
```py
Calendar.render_to_response(render_dependencies=False)
```
After:
```py
Calendar.render_to_response(deps_strategy="ignore")
```
- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1.
- In the `on_component_data()` extension hook, the `context_data` field of the context object was superseded by `template_data`.
@ -258,10 +310,15 @@
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"
- Render emails or other non-browser HTML with new "dependencies 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"`.
the `deps_strategy` kwarg (previously `type`) now accepts additional options:
- `"simple"`
- `"prepend"`
- `"append"`
- `"ignore"`
```py
Calendar.render_to_response(
@ -294,6 +351,11 @@
- Insert JS / CSS after the rendered HTML.
- Ignores placeholders and any `<head>` / `<body>` tags.
- No extra script loaded.
- `"ignore"`
- Rendered HTML is left as-is. You can still process it with a different strategy later with `render_dependencies()`.
- Used for inserting rendered HTML into other components.
See [Dependencies rendering](https://django-components.github.io/django-components/0.140/concepts/advanced/rendering_js_css/) for more info.
- `get_component_url()` now optionally accepts `query` and `fragment` arguments.

View file

@ -319,7 +319,7 @@ Read more about [HTML attributes](https://django-components.github.io/django-com
### HTML fragment support
`django-components` makes integration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as HTML fragments:
`django-components` makes integration with HTMX, AlpineJS or jQuery easy by allowing components to be rendered as [HTML fragments](https://django-components.github.io/django-components/latest/concepts/advanced/html_fragments/):
- Components's JS and CSS files are loaded automatically when the fragment is inserted into the DOM.

View file

@ -1,8 +1,7 @@
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.
whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.
When you define a component that has extra JS or CSS, and you use django-components
to render the fragment, django-components will:
If the fragment component has any JS or CSS, django-components will:
- Automatically load the associated JS and CSS
- Ensure that JS is loaded and executed only once even if the fragment is inserted multiple times
@ -24,9 +23,11 @@ to render the fragment, django-components will:
## Document and fragment strategies
Components support different "strategies" for rendering JS and CSS.
Components support different ["strategies"](../../advanced/rendering_js_css#dependencies-strategies)
for rendering JS and CSS.
Two of them are used to enable HTML fragments - "document" and "fragment".
Two of them are used to enable HTML fragments - ["document"](../../advanced/rendering_js_css#document)
and ["fragment"](../../advanced/rendering_js_css#fragment).
What's the difference?
@ -36,7 +37,7 @@ Document strategy assumes that the rendered components will be embedded into the
of the initial page load. This means that:
- The JS and CSS is embedded into the HTML as `<script>` and `<style>` tags
(see [JS and CSS output locations](./rendering_js_css.md#js-and-css-output-locations))
(see [Default JS / CSS locations](./rendering_js_css.md#default-js-css-locations))
- Django-components injects a JS script for managing JS and CSS assets
A component is rendered as a "document" when:
@ -103,15 +104,12 @@ Then navigate to these URLs:
### 1. Define document HTML
This is the HTML into which a fragment will be loaded using HTMX.
```djc_py title="[root]/components/demo.py"
from django_components import Component, types
# HTML into which a fragment will be loaded using HTMX
class MyPage(Component):
Class View:
def get(self, request):
return self.component.render_to_response(request=request)
template = """
{% load component_tags %}
<!DOCTYPE html>
@ -135,20 +133,20 @@ class MyPage(Component):
</body>
</html>
"""
class View:
def get(self, request):
return self.component.render_to_response(request=request)
```
### 2. Define fragment HTML
The fragment to be inserted into the document.
IMPORTANT: Don't forget to set `deps_strategy="fragment"`
```djc_py title="[root]/components/demo.py"
class Frag(Component):
class View:
def get(self, request):
return self.component.render_to_response(
request=request,
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
)
template = """
<div class="frag">
123
@ -165,6 +163,14 @@ class Frag(Component):
background: blue;
}
"""
class View:
def get(self, request):
return self.component.render_to_response(
request=request,
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
)
```
### 3. Create view and URLs
@ -184,15 +190,12 @@ urlpatterns = [
### 1. Define document HTML
This is the HTML into which a fragment will be loaded using AlpineJS.
```djc_py title="[root]/components/demo.py"
from django_components import Component, types
# HTML into which a fragment will be loaded using AlpineJS
class MyPage(Component):
class View:
def get(self, request):
return self.component.render_to_response(request=request)
template = """
{% load component_tags %}
<!DOCTYPE html>
@ -222,20 +225,20 @@ class MyPage(Component):
</body>
</html>
"""
class View:
def get(self, request):
return self.component.render_to_response(request=request)
```
### 2. Define fragment HTML
The fragment to be inserted into the document.
IMPORTANT: Don't forget to set `deps_strategy="fragment"`
```djc_py title="[root]/components/demo.py"
class Frag(Component):
class View:
def get(self, request):
return self.component.render_to_response(
request=request,
# 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
# from being rendered until we have registered the component with AlpineJS.
template = """
@ -265,6 +268,14 @@ class Frag(Component):
background: blue;
}
"""
class View:
def get(self, request):
return self.component.render_to_response(
request=request,
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
)
```
### 3. Create view and URLs
@ -284,15 +295,12 @@ urlpatterns = [
### 1. Define document HTML
This is the HTML into which a fragment will be loaded using vanilla JS.
```djc_py title="[root]/components/demo.py"
from django_components import Component, types
# HTML into which a fragment will be loaded using JS
class MyPage(Component):
class View:
def get(self, request):
return self.component.render_to_response(request=request)
template = """
{% load component_tags %}
<!DOCTYPE html>
@ -321,20 +329,20 @@ class MyPage(Component):
</body>
</html>
"""
class View:
def get(self, request):
return self.component.render_to_response(request=request)
```
### 2. Define fragment HTML
The fragment to be inserted into the document.
IMPORTANT: Don't forget to set `deps_strategy="fragment"`
```djc_py title="[root]/components/demo.py"
class Frag(Component):
class View:
def get(self, request):
return self.component.render_to_response(
request=request,
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
)
template = """
<div class="frag">
123
@ -351,6 +359,14 @@ class Frag(Component):
background: blue;
}
"""
class View:
def get(self, request):
return self.component.render_to_response(
request=request,
# IMPORTANT: Don't forget `deps_strategy="fragment"`
deps_strategy="fragment",
)
```
### 3. Create view and URLs

View file

@ -1,15 +1,20 @@
## JS and CSS output locations
## Introduction
If:
Components consist of 3 parts - HTML, JS and CSS.
1. Your components use JS and CSS via any of:
- [`Component.css`](#TODO)
- [`Component.js`](#TODO)
- [`Component.Media.css`](#TODO)
- [`Component.Media.js`](#TODO)
2. And you use the [`ComponentDependencyMiddleware`](#TODO) middleware
Handling of HTML is straightforward - it is rendered as is, and inserted where
the [`{% component %}`](../../../reference/template_tags#component) tag is.
Then, by default, the components' JS and CSS will be automatically inserted into the HTML:
However, handling of JS and CSS is more complex:
- JS and CSS is are inserted elsewhere in the HTML. As a best practice, JS is placed in the `<body>` HTML tag, and CSS in the `<head>`.
- Multiple components may use the same JS and CSS files. We don't want to load the same files multiple times.
- Fetching of JS and CSS may block the page, so the JS / CSS should be embedded in the HTML.
- Components inserted as HTML fragments need different handling for JS and CSS.
## Default JS / CSS locations
If your components use JS and CSS then, by default, the JS and CSS will be automatically inserted into the HTML:
- CSS styles will be inserted at the end of the `<head>`
- JS scripts will be inserted at the end of the `<body>`
@ -17,8 +22,8 @@ Then, by default, the components' JS and CSS will be automatically inserted into
If you want to place the dependencies elsewhere in the HTML, you can override
the locations by inserting following Django template tags:
- [`{% component_js_dependencies %}`](#TODO) - Set new location(s) for JS scripts
- [`{% component_css_dependencies %}`](#TODO) - Set new location(s) for CSS styles
- [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies) - Set new location(s) for JS scripts
- [`{% component_css_dependencies %}`](../../../reference/template_tags#component_css_dependencies) - Set new location(s) for CSS styles
So if you have a component with JS and CSS:
@ -31,6 +36,7 @@ class MyButton(Component):
Click me!
</button>
"""
js: types.js = """
for (const btnEl of document.querySelectorAll(".my-button")) {
btnEl.addEventListener("click", () => {
@ -38,6 +44,7 @@ class MyButton(Component):
});
}
"""
css: types.css """
.my-button {
background: green;
@ -49,11 +56,13 @@ class MyButton(Component):
css = ["/extra/style.css"]
```
Then the JS from `MyButton.js` and `MyButton.Media.js` will be rendered at the default place,
or in [`{% component_js_dependencies %}`](#TODO).
Then:
And the CSS from `MyButton.css` and `MyButton.Media.css` will be rendered at the default place,
or in [`{% component_css_dependencies %}`](#TODO).
- JS from `MyButton.js` and `MyButton.Media.js` will be rendered at the default place (`<body>`),
or in [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies).
- CSS from `MyButton.css` and `MyButton.Media.css` will be rendered at the default place (`<head>`),
or in [`{% component_css_dependencies %}`](../../../reference/template_tags#component_css_dependencies).
And if you don't specify `{% component_dependencies %}` tags, it is the equivalent of:
@ -74,169 +83,62 @@ And if you don't specify `{% component_dependencies %}` tags, it is the equivale
</html>
```
### Setting up the middleware
!!! warning
[`ComponentDependencyMiddleware`](#TODO) is a Django [middleware](https://docs.djangoproject.com/en/5.1/topics/http/middleware/)
designed to manage and inject CSS / JS dependencies of rendered components dynamically.
It ensures that only the necessary stylesheets and scripts are loaded
in your HTML responses, based on the components used in your Django templates.
If the rendered HTML does NOT contain neither `{% component_dependencies %}` template tags,
nor `<head>` and `<body>` HTML tags, then the JS and CSS will NOT be inserted!
To set it up, add the middleware to your [`MIDDLEWARE`](https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-MIDDLEWARE)
in `settings.py`:
```python
MIDDLEWARE = [
# ... other middleware classes ...
'django_components.middleware.ComponentDependencyMiddleware'
# ... other middleware classes ...
]
```
### `render_dependencies` and rendering JS / CSS without the middleware
For most scenarios, using the [`ComponentDependencyMiddleware`](#TODO) middleware will be just fine.
However, this section is for you if you want to:
- Render HTML that will NOT be sent as a server response
- Insert pre-rendered HTML into another component
- Render HTML fragments (partials)
Every time there is an HTML string that has parts which were rendered using components,
and any of those components has JS / CSS, then this HTML string MUST be processed with [`render_dependencies()`](#TODO).
It is actually [`render_dependencies()`](#TODO) that finds all used components in the HTML string,
and inserts the component's JS and CSS into `{% component_dependencies %}` tags, or at the default locations.
#### Render JS / CSS without the middleware
The truth is that the [`ComponentDependencyMiddleware`](#TODO) middleware just calls [`render_dependencies()`](#TODO),
passing in the HTML content. So if you render a template that contained [`{% component %}`](#TODO) tags,
you MUST pass the result through [`render_dependencies()`](#TODO). And the middleware is just one of the options.
Here is how you can achieve the same, without the middleware, using [`render_dependencies()`](#TODO):
```python
from django.template.base import Template
from django.template.context import Context
from django_component import render_dependencies
template = Template("""
{% load component_tags %}
<!doctype html>
<html>
<head>
<title>MyPage</title>
</head>
<body>
<main>
{% component "my_button" %}
Click me!
{% endcomponent %}
</main>
</body>
</html>
""")
rendered = template.render(Context())
rendered = render_dependencies(rendered)
```
Same applies if you render a template using Django's [`django.shortcuts.render`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render):
```python
from django.shortcuts import render
def my_view(request):
rendered = render(request, "pages/home.html")
rendered = render_dependencies(rendered)
return rendered
```
Alternatively, when you render HTML with [`Component.render()`](#TODO)
or [`Component.render_to_response()`](#TODO),
these, by default, call [`render_dependencies()`](#TODO) for you, so you don't have to:
```python
from django_components import Component
class MyButton(Component):
...
# No need to call `render_dependencies()`
rendered = MyButton.render()
```
#### Inserting pre-rendered HTML into another component
In previous section we've shown that [`render_dependencies()`](#TODO) does NOT need to be called
when you render a component via [`Component.render()`](#TODO).
API of django_components makes it possible to compose components in a "React-like" way,
where we pre-render a piece of HTML and then insert it into a larger structure.
To do this, you must add [`render_dependencies=False`](#TODO) to the nested components:
```python
card_actions = CardActions.render(
kwargs={"editable": editable},
render_dependencies=False,
)
card = Card.render(
slots={"actions": card_actions},
render_dependencies=False,
)
page = MyPage.render(
slots={"card": card},
)
```
Why is `render_dependencies=False` required?
This is a technical limitation of the current implementation.
As mentioned earlier, each time we call [`Component.render()`](#TODO),
we also call [`render_dependencies()`](#TODO).
However, there is a problem here - When we call [`render_dependencies()`](#TODO)
inside [`CardActions.render()`](#TODO),
we extract and REMOVE the info on components' JS and CSS from the HTML. But the template
of `CardActions` contains no `{% component_depedencies %}` tags, and nor `<head>` nor `<body>` HTML tags.
So the component's JS and CSS will NOT be inserted, and will be lost.
To work around this, you must set [`render_dependencies=False`](#TODO) when rendering pieces of HTML
with [`Component.render()`](#TODO) and inserting them into larger structures.
#### Summary
1. Every time you render HTML that contained components, you have to call [`render_dependencies()`](#TODO)
on the rendered output.
2. There are several ways to call [`render_dependencies()`](#TODO):
- Using the [`ComponentDependencyMiddleware`](#TODO) middleware
- Rendering the HTML by calling [`Component.render()`](#TODO) with `render_dependencies=True` (default)
- Rendering the HTML by calling [`Component.render_to_response()`](#TODO) (always renders dependencies)
- Directly passing rendered HTML to [`render_dependencies()`](#TODO)
3. If you pre-render one component to pass it into another, the pre-rendered component must be rendered with
[`render_dependencies=False`](#TODO).
To force the JS and CSS to be inserted, use the [`"append"`](#append) or [`"prepend"`](#prepend)
strategies.
## Dependencies strategies
The rendered HTML may be used in different contexts (browser, email, etc).
If your components use JS and CSS scripts, you may need to handle them differently.
The different ways for handling JS / CSS are called **"dependencies strategies"**.
[`render()`](../../../reference/api/#django_components.Component.render) and [`render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
accept a `deps_strategy` parameter, which controls where and how the JS / CSS are inserted into the HTML.
```python
main_page = MainPage.render(deps_strategy="document")
fragment = MyComponent.render_to_response(deps_strategy="fragment")
```
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.
When you use Django's [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
or [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render) to render templates,
you can't directly set the `deps_strategy` parameter.
In this case, you can set the `deps_strategy` with the `DJC_DEPS_STRATEGY` context variable.
```python
from django.template.context import Context
from django.shortcuts import render
ctx = Context({"DJC_DEPS_STRATEGY": "fragment"})
fragment = render(request, "my_component.html", ctx=ctx)
```
!!! info
The `deps_strategy` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
There are five dependencies strategies:
!!! note "Why is `deps_strategy` required?"
This is a technical limitation of the current implementation.
When a component is rendered, django-components embeds metadata about the component's JS and CSS into the HTML.
This way we can compose components together, and know which JS / CSS dependencies are needed.
As the last step of rendering, django-components extracts this metadata and uses a selected strategy
to insert the JS / CSS into the HTML.
There are six dependencies strategies:
- [`document`](../../advanced/rendering_js_css#document) (default)
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
@ -254,6 +156,10 @@ There are five dependencies strategies:
- [`append`](../../advanced/rendering_js_css#append)
- Insert JS / CSS after the rendered HTML.
- No extra script loaded.
- [`ignore`](../../advanced/rendering_js_css#ignore)
- HTML is left as-is. You can still process it with a different strategy later with
[`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
- Used for inserting rendered HTML into other components.
### `document`
@ -290,7 +196,7 @@ the page in the browser:
</script>
<style>
.button {
background-color: blue;
background-color: blue;
}
</style>
```
@ -420,3 +326,106 @@ JS and CSS is **always** inserted after the rendered content.
**Included scripts:**
Same as for the [`"simple"`](#simple) strategy.
### `ignore`
`deps_strategy="ignore"` is used when you do NOT want to process JS and CSS of the rendered HTML.
```python
html = MyComponent.render(deps_strategy="ignore")
```
The rendered HTML is left as-is. You can still process it with a different strategy later with `render_dependencies()`.
This is useful when you want to insert rendered HTML into another component.
```python
html = MyComponent.render(deps_strategy="ignore")
html = AnotherComponent.render(slots={"content": html})
```
## Manually rendering JS / CSS
When rendering templates or components, django-components covers all the traditional ways how components
or templates can be rendered:
- [`Component.render()`](../../../reference/api/#django_components.Component.render)
- [`Component.render_to_response()`](../../../reference/api/#django_components.Component.render_to_response)
- [`Template.render()`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Template.render)
- [`django.shortcuts.render()`](https://docs.djangoproject.com/en/5.1/topics/http/shortcuts/#render)
This way you don't need to manually handle rendering of JS / CSS.
However, for advanced or low-level use cases, you may need to control when to render JS / CSS.
In such case you can directly pass rendered HTML to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
This function will extract all used components in the HTML string, and insert the components' JS and CSS
based on given strategy.
!!! info
The truth is that all the methods listed above call [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies)
internally.
**Example:**
To see how [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies) works,
let's render a template with a component.
We will render it twice:
- First time, we let `template.render()` handle the rendering.
- Second time, we prevent `template.render()` from inserting the component's JS and CSS with `deps_strategy="ignore"`.
Instead, we pass the "unprocessed" HTML to `render_dependencies()` ourselves to insert the component's JS and CSS.
```python
from django.template.base import Template
from django.template.context import Context
from django_components import render_dependencies
template = Template("""
{% load component_tags %}
<!doctype html>
<html>
<head>
<title>MyPage</title>
</head>
<body>
<main>
{% component "my_button" %}
Click me!
{% endcomponent %}
</main>
</body>
</html>
""")
rendered = template.render(Context({}))
rendered2_raw = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
rendered2 = render_dependencies(rendered2_raw)
assert rendered == rendered2
```
Same applies to other strategies and other methods of rendering:
```python
raw_html = MyComponent.render(deps_strategy="ignore")
html = render_dependencies(raw_html, deps_strategy="document")
html2 = MyComponent.render(deps_strategy="document")
assert html == html2
```
## HTML fragments
Django-components provides a seamless integration with HTML fragments with AJAX ([HTML over the wire](https://hotwired.dev/)),
whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.
This is achieved by the combination of the [`"document"`](#document) and [`"fragment"`](#fragment) strategies.
Read more about [HTML fragments](../../advanced/html_fragments).

View file

@ -247,7 +247,6 @@ Button.render(
- `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`)
All arguments are optional. If not provided, they default to empty values or sensible defaults.
@ -347,7 +346,7 @@ The `deps_strategy` parameter is ultimately passed to [`render_dependencies()`](
Learn more about [Rendering JS / CSS](../../advanced/rendering_js_css).
There are five dependencies rendering strategies:
There are six dependencies rendering strategies:
- [`document`](../../advanced/rendering_js_css#document) (default)
- Smartly inserts JS / CSS into placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) or into `<head>` and `<body>` tags.
@ -368,6 +367,10 @@ There are five dependencies rendering strategies:
- Insert JS / CSS after the rendered HTML.
- Ignores the placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) and any `<head>`/`<body>` HTML tags.
- No extra script loaded.
- [`ignore`](../../advanced/rendering_js_css#ignore)
- HTML is left as-is. You can still process it with a different strategy later with
[`render_dependencies()`](../../../reference/api/#django_components.render_dependencies).
- Used for inserting rendered HTML into other components.
!!! info
@ -434,7 +437,7 @@ class Button(Component):
icon = Icon.render(
context=context,
args=["icon-name"],
render_dependencies=False,
deps_strategy="ignore",
)
# Update context with icon
with context.update({"icon": icon}):
@ -515,9 +518,10 @@ Button.render(
)
```
## Nesting components
## Components as input
You can nest components by rendering one component and using its output as input to another:
django_components makes it possible to compose components in a "React-like" way,
where you can render one component and use its output as input to another component:
```python
from django.utils.safestring import mark_safe
@ -525,7 +529,7 @@ from django.utils.safestring import mark_safe
# Render the inner component
inner_html = InnerComponent.render(
kwargs={"some_data": "value"},
render_dependencies=False, # Important for nesting!
deps_strategy="ignore", # Important for nesting!
)
# Use inner component's output in the outer component
@ -536,9 +540,10 @@ outer_html = OuterComponent.render(
)
```
The key here is setting `render_dependencies=False` for the inner component. This prevents duplicate dependencies when the outer component is rendered.
The key here is setting [`deps_strategy="ignore"`](../../advanced/rendering_js_css#ignore) for the inner component. This prevents duplicate
rendering of JS / CSS dependencies when the outer component is rendered.
When `render_dependencies=False`:
When `deps_strategy="ignore"`:
- No JS or CSS dependencies will be added to the output HTML
- The component's content is rendered as-is
@ -582,7 +587,7 @@ DynamicComponent.render(
},
slots={
"pagination": PaginationComponent.render(
render_dependencies=False,
deps_strategy="ignore",
),
},
)
@ -611,3 +616,13 @@ After which you will be able to use the dynamic component with the new name:
{% endfill %}
{% endcomponent %}
```
## HTML fragments
Django-components provides a seamless integration with HTML fragments with AJAX ([HTML over the wire](https://hotwired.dev/)),
whether you're using jQuery, HTMX, AlpineJS, vanilla JavaScript, or other.
This is achieved by the combination of the [`"document"`](../../advanced/rendering_js_css#document)
and [`"fragment"`](../../advanced/rendering_js_css#fragment) dependencies rendering strategies.
Read more about [HTML fragments](../../advanced/html_fragments) and [Rendering JS / CSS](../../advanced/rendering_js_css).

View file

@ -52,7 +52,8 @@ Be sure to prefix your rules with unique CSS class like `calendar`, so the CSS d
This CSS will be inserted into the page as an inlined `<style>` tag, at the position defined by
[`{% component_css_dependencies %}`](../../reference/template_tags#component_css_dependencies),
or at the end of the inside the `<head>` tag (See [JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations)).
or at the end of the inside the `<head>` tag
(See [Default JS / CSS locations](../../concepts/advanced/rendering_js_css#default-js-css-locations)).
So in your HTML, you may see something like this:
@ -103,7 +104,7 @@ This makes all variables defined only be defined inside this component and not a
Similarly to CSS, JS will be inserted into the page as an inlined `<script>` tag, at the position defined by
[`{% component_js_dependencies %}`](../../reference/template_tags#component_js_dependencies),
or at the end of the inside the `<body>` tag (See [JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations)).
or at the end of the inside the `<body>` tag (See [Default JS / CSS locations](../../concepts/advanced/rendering_js_css#default-js-css-locations)).
So in your HTML, you may see something like this:

View file

@ -159,7 +159,8 @@ and keeping your CSS and Javascript in the static directory.
Remember that you can use
[`{% component_js_dependencies %}`](../../reference/template_tags#component_js_dependencies)
and [`{% component_css_dependencies %}`](../../reference/template_tags#component_css_dependencies)
to change where the `<script>` and `<style>` tags will be rendered (See [JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations)).
to change where the `<script>` and `<style>` tags will be rendered
(See [Default JS / CSS locations](../../concepts/advanced/rendering_js_css#default-js-css-locations)).
!!! info

View file

@ -156,15 +156,17 @@ This is how we achieve that:
There's multiple ways to achieve this:
- The approach recommended to the users is to use the `ComponentDependencyMiddleware` middleware, which scans all outgoing HTML, and post-processes the `<!-- _RENDERED -->` comments.
- If users are using `Component.render()` or `Component.render_to_response()`, these post-process the `<!-- _RENDERED -->` comments by default.
- NOTE: Users are able to opt out of the post-processing by setting `render_dependencies=False`.
- NOTE: Users are able to opt out of the post-processing by setting `deps_strategy="ignore"`.
- For advanced use cases, users may use `render_dependencies()` directly. This is the function that both `ComponentDependencyMiddleware` and `Component.render()` call internally.
- If one renders a Template directly, the `<!-- _RENDERED -->` will be processed too. We achieve this by
modifying Django's `Template.render()` method.
`render_dependencies()`, whether called directly, via middleware or other way, does the following:
- For advanced use cases, users may use `render_dependencies()` directly. This is the function that
`Component.render()` calls internally.
`render_dependencies()`, whether called directly, or other way, does the following:
1. Find all `<!-- _RENDERED -->` comments, and for each comment:

View file

@ -1,12 +1,15 @@
### Basic installation
1. Install `django_components` into your environment:
```bash
pip install django_components
```
2. Load `django_components` into Django by adding it into `INSTALLED_APPS` in in your settings file:
2. Load `django_components` into Django by adding it into `INSTALLED_APPS` in your settings file:
```python
# app/settings.py
INSTALLED_APPS = [
...,
'django_components',
@ -16,43 +19,13 @@
3. `BASE_DIR` setting is required. Ensure that it is defined:
```python
# app/settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
```
4. _Optional._ Set [`COMPONENTS.dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
and/or [`COMPONENTS.app_dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
so django_components knows where to find component HTML, JS and CSS files:
If [`COMPONENTS.dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
is omitted, django-components will by default look for a top-level `/components` directory,
`{BASE_DIR}/components`.
```python
from django_components import ComponentsSettings
COMPONENTS = ComponentsSettings(
dirs=[
...,
Path(BASE_DIR) / "components",
],
)
```
In addition to [`COMPONENTS.dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs),
django_components will also load components from app-level directories, such as `my-app/components/`.
The directories within apps are configured with
[`COMPONENTS.app_dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs),
and the default is `[app]/components`.
!!! note
The input to [`COMPONENTS.dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
is the same as for `STATICFILES_DIRS`, and the paths must be full paths.
[See Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs).
5. Next, modify `TEMPLATES` section of settings.py as follows:
4. Next, modify `TEMPLATES` section of `settings.py` as follows:
- _Remove `'APP_DIRS': True,`_
- NOTE: Instead of `APP_DIRS: True`, we will use
@ -67,6 +40,7 @@
{
...,
'OPTIONS': {
...,
'loaders':[(
'django.template.loaders.cached.Loader', [
# Default Django loader
@ -82,6 +56,20 @@
]
```
5. Add django-component's URL paths to your `urlpatterns`:
Django components already prefixes all URLs with `components/`. So when you are
adding the URLs to `urlpatterns`, you can use an empty string as the first argument:
```python
from django.urls import include, path
urlpatterns = [
...
path("", include("django_components.urls")),
]
```
## Adding support for JS and CSS
If you want to use JS or CSS with components, you will need to:
@ -100,38 +88,13 @@ If you want to use JS or CSS with components, you will need to:
]
```
2. Add [`ComponentDependencyMiddleware`](../reference/middlewares.md#django_components.dependencies.ComponentDependencyMiddleware)
to `MIDDLEWARE` setting.
The middleware searches the outgoing HTML for all components that were rendered
to generate the HTML, and adds the JS and CSS associated with those components.
```python
MIDDLEWARE = [
...
"django_components.middleware.ComponentDependencyMiddleware",
]
```
Read more in [Rendering JS/CSS dependencies](../concepts/advanced/rendering_js_css.md).
3. Add django-component's URL paths to your `urlpatterns`:
```python
from django.urls import include, path
urlpatterns = [
...
path("", include("django_components.urls")),
]
```
4. _Optional._ If you want to change where the JS and CSS is rendered, use
2. _Optional._ If you want to change where the JS and CSS is rendered, use
[`{% component_js_dependencies %}`](../reference/template_tags.md#component_css_dependencies)
and [`{% component_css_dependencies %}`](../reference/template_tags.md#component_js_dependencies).
By default, the JS `<script>` and CSS `<link>` tags are automatically inserted
into the HTML (See [JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations)).
into the HTML (See [Default JS / CSS locations](../../concepts/advanced/rendering_js_css/#default-js-css-locations)).
```django
<!doctype html>
@ -147,7 +110,7 @@ If you want to use JS or CSS with components, you will need to:
</html>
```
5. _Optional._ By default, components' JS and CSS files are cached in memory.
3. _Optional._ By default, components' JS and CSS files are cached in memory.
If you want to change the cache backend, set the [`COMPONENTS.cache`](../reference/settings.md#django_components.app_settings.ComponentsSettings.cache) setting.
@ -156,13 +119,15 @@ If you want to use JS or CSS with components, you will need to:
## Optional
### Builtin template tags
To avoid loading the app in each template using `{% load component_tags %}`, you can add the tag as a 'builtin' in settings.py
To avoid loading the app in each template using `{% load component_tags %}`, you can add the tag as a 'builtin' in `settings.py`:
```python
TEMPLATES = [
{
...,
'OPTIONS': {
...,
'builtins': [
'django_components.templatetags.component_tags',
]
@ -171,6 +136,48 @@ TEMPLATES = [
]
```
### Component directories
django-components needs to know where to search for component HTML, JS and CSS files.
There are two ways to configure the component directories:
- [`COMPONENTS.dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs) sets global component directories.
- [`COMPONENTS.app_dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.app_dirs) sets app-level component directories.
By default, django-components will look for a top-level `/components` directory,
`{BASE_DIR}/components`, equivalent to:
```python
from django_components import ComponentsSettings
COMPONENTS = ComponentsSettings(
dirs=[
...,
Path(BASE_DIR) / "components",
],
)
```
For app-level directories, the default is `[app]/components`, equivalent to:
```python
from django_components import ComponentsSettings
COMPONENTS = ComponentsSettings(
app_dirs=[
...,
"components",
],
)
```
!!! note
The input to [`COMPONENTS.dirs`](../reference/settings.md#django_components.app_settings.ComponentsSettings.dirs)
is the same as for `STATICFILES_DIRS`, and the paths must be full paths.
[See Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs).
---
Now you're all set! Read on to find out how to build your first component.

View file

@ -7,7 +7,6 @@ nav:
- Extension commands: extension_commands.md
- Extension hooks: extension_hooks.md
- Extension URLs: extension_urls.md
- Middlewares: middlewares.md
- Settings: settings.md
- Signals: signals.md
- Tag formatters: tag_formatters.md

View file

@ -54,9 +54,7 @@ python manage.py components ext run <extension> <command>
## `components create`
```txt
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
[--dry-run]
name
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] name
```
@ -238,7 +236,7 @@ List all extensions.
- `--columns COLUMNS`
- Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`.
- `-s`, `--simple`
- Only show table data, without headers. Use this option for generating machine- readable output.
- Only show table data, without headers. Use this option for generating machine-readable output.
@ -401,7 +399,7 @@ List all components created in this project.
- `--columns COLUMNS`
- Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`.
- `-s`, `--simple`
- Only show table data, without headers. Use this option for generating machine- readable output.
- Only show table data, without headers. Use this option for generating machine-readable output.
@ -463,9 +461,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
## `upgradecomponent`
```txt
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
[--skip-checks]
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
[--force-color] [--skip-checks]
```
@ -509,10 +506,8 @@ Deprecated. Use `components upgrade` instead.
## `startcomponent`
```txt
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
[--skip-checks]
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] [--version] [-v {0,1,2,3}]
[--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
name
```

View file

@ -1,16 +0,0 @@
<!-- Autogenerated by reference.py -->
# Middlewares
::: django_components.dependencies.ComponentDependencyMiddleware
options:
inherited_members: false
show_root_heading: true
show_signature: false
separate_signature: false
show_symbol_type_heading: false
show_symbol_type_toc: false
show_if_no_docstring: true
show_labels: false

View file

@ -20,7 +20,7 @@ Import as
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1069" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1024" target="_blank">See source code</a>
@ -30,7 +30,7 @@ Generally, this should be inserted into the `<head>` tag of the HTML.
If the generated HTML does NOT contain any `{% component_css_dependencies %}` tags, CSS links
are by default inserted into the `<head>` tag of the HTML. (See
[JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations))
[Default JS / CSS locations](../../concepts/advanced/rendering_js_css/#default-js-css-locations))
Note that there should be only one `{% component_css_dependencies %}` for the whole HTML document.
If you insert this tag multiple times, ALL CSS links will be duplicately inserted into ALL these places.
@ -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#L1091" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1046" target="_blank">See source code</a>
@ -53,7 +53,7 @@ Generally, this should be inserted at the end of the `<body>` tag of the HTML.
If the generated HTML does NOT contain any `{% component_js_dependencies %}` tags, JS scripts
are by default inserted at the end of the `<body>` tag of the HTML. (See
[JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations))
[Default JS / CSS locations](../../concepts/advanced/rendering_js_css/#default-js-css-locations))
Note that there should be only one `{% component_js_dependencies %}` for the whole HTML document.
If you insert this tag multiple times, ALL JS scripts will be duplicately inserted into ALL these places.
@ -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#L2791" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L2785" target="_blank">See source code</a>

View file

@ -1,6 +1,6 @@
"""
Generate reference for all the different kinds of public API that we expose,
like regular Python imports, middleware, template tags, settings, Django URLs, etc.
like regular Python imports, template tags, settings, Django URLs, etc.
All pages are generated inside `docs/reference/`.
@ -338,49 +338,6 @@ def _gen_default_settings_section(app_settings_filepath: str) -> str:
)
def gen_reference_middlewares():
"""
Generate documentation for all available middleware of django-components,
as listed in module `django_components.middleware`.
"""
module = import_module("django_components.middleware")
preface = "<!-- Autogenerated by reference.py -->\n\n"
preface += (root / "docs/templates/reference_middlewares.md").read_text()
out_file = root / "docs/reference/middlewares.md"
out_file.parent.mkdir(parents=True, exist_ok=True)
with out_file.open("w", encoding="utf-8") as f:
f.write(preface + "\n\n")
for name, obj in inspect.getmembers(module):
if not inspect.isclass(obj):
continue
class_name = get_import_path(obj)
# For each entry, generate a mkdocstrings entry, e.g.
# ```
# ::: django_components.middleware.ComponentDependencyMiddleware
# options:
# ...
# ```
f.write(
f"::: {class_name}\n"
f" options:\n"
f" inherited_members: false\n"
f" show_root_heading: true\n"
f" show_signature: false\n"
f" separate_signature: false\n"
f" show_symbol_type_heading: false\n"
f" show_symbol_type_toc: false\n"
f" show_if_no_docstring: true\n"
f" show_labels: false\n"
)
f.write("\n")
def gen_reference_tagformatters():
"""
Generate documentation for all pre-defined TagFormatters included
@ -1145,7 +1102,6 @@ def gen_reference():
gen_reference_api()
gen_reference_exceptions()
gen_reference_components()
gen_reference_middlewares()
gen_reference_settings()
gen_reference_tagformatters()
gen_reference_urls()

View file

@ -1 +0,0 @@
# Middlewares

View file

@ -46,7 +46,6 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"django_components.middleware.ComponentDependencyMiddleware",
]
ROOT_URLCONF = "sampleproject.urls"

View file

@ -1,7 +1,6 @@
"""Main package for Django Components."""
# Public API
# NOTE: Middleware is exposed via django_components.middleware
# NOTE: Some of the documentation is generated based on these exports
# isort: off
from django_components.app_settings import ContextBehavior, ComponentsSettings

View file

@ -185,7 +185,9 @@ class ComponentInput:
# TODO_v1 - Remove, superseded by `deps_strategy`
type: DependenciesStrategy
"""Deprecated alias for `deps_strategy`."""
# TODO_v1 - Remove, superseded by `deps_strategy`
render_dependencies: bool
"""Deprecated. Instead use `deps_strategy="ignore"`."""
@dataclass()
@ -1999,6 +2001,7 @@ class Component(metaclass=ComponentMeta):
deps_strategy: DependenciesStrategy = "document",
# TODO_v1 - Remove, superseded by `deps_strategy`
type: Optional[DependenciesStrategy] = None,
# TODO_v1 - Remove, superseded by `deps_strategy="ignore"`
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
**response_kwargs: Any,
@ -2063,6 +2066,7 @@ class Component(metaclass=ComponentMeta):
deps_strategy=deps_strategy,
# TODO_v1 - Remove, superseded by `deps_strategy`
type=type,
# TODO_v1 - Remove, superseded by `deps_strategy`
render_dependencies=render_dependencies,
request=request,
)
@ -2079,6 +2083,7 @@ class Component(metaclass=ComponentMeta):
deps_strategy: DependenciesStrategy = "document",
# TODO_v1 - Remove, superseded by `deps_strategy`
type: Optional[DependenciesStrategy] = None,
# TODO_v1 - Remove, superseded by `deps_strategy="ignore"`
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
) -> str:
@ -2176,7 +2181,7 @@ class Component(metaclass=ComponentMeta):
icon = Icon.render(
context=context,
args=["icon-name"],
render_dependencies=False,
deps_strategy="ignore",
)
# Update context with icon
with context.update({"icon": icon}):
@ -2194,7 +2199,7 @@ class Component(metaclass=ComponentMeta):
- `deps_strategy` - Optional. Configure how to handle JS and CSS dependencies. Read more about
[Dependencies rendering](../../concepts/fundamentals/rendering_components#dependencies-rendering).
There are five strategies:
There are six strategies:
- [`"document"`](../../concepts/advanced/rendering_js_css#document) (default)
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
@ -2212,6 +2217,10 @@ class Component(metaclass=ComponentMeta):
- [`"append"`](../../concepts/advanced/rendering_js_css#append)
- Insert JS / CSS after the rendered HTML.
- No extra script loaded.
- [`"ignore"`](../../concepts/advanced/rendering_js_css#ignore)
- HTML is left as-is. You can still process it with a different strategy later with
[`render_dependencies()`](../api/#django_components.render_dependencies).
- Used for inserting rendered HTML into other components.
- `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).
@ -2222,22 +2231,6 @@ class Component(metaclass=ComponentMeta):
[`escape`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#std-templatefilter-escape).
Defaults to `True`.
- `render_dependencies` - Optional. Whether the output should be processed to finalize JS and CSS dependencies.
Defaults to `True`.
Set this to `False` if you want to insert the resulting HTML into another component:
```py
html = Button.render(
render_dependencies=False,
)
# Insert the resulting HTML into another component
MyOtherComponent.render(
content=html,
)
```
**Type hints:**
`Component.render()` is NOT typed. To add type hints, you can wrap the inputs
@ -2295,8 +2288,12 @@ class Component(metaclass=ComponentMeta):
)
deps_strategy = type
# TODO_v1 - Remove, superseded by `deps_strategy="ignore"`
if not render_dependencies:
deps_strategy = "ignore"
return comp._render_with_error_trace(
context, args, kwargs, slots, escape_slots_content, deps_strategy, render_dependencies, request
context, args, kwargs, slots, escape_slots_content, deps_strategy, request
)
# This is the internal entrypoint for the render function
@ -2308,14 +2305,11 @@ class Component(metaclass=ComponentMeta):
slots: Optional[Any] = None,
escape_slots_content: bool = True,
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]):
return self._render_impl(
context, args, kwargs, slots, escape_slots_content, deps_strategy, render_dependencies, request
)
return self._render_impl(context, args, kwargs, slots, escape_slots_content, deps_strategy, request)
def _render_impl(
self,
@ -2325,7 +2319,6 @@ class Component(metaclass=ComponentMeta):
slots: Optional[Any] = None,
escape_slots_content: bool = True,
deps_strategy: DependenciesStrategy = "document",
render_dependencies: bool = True,
request: Optional[HttpRequest] = None,
) -> str:
# Allow to pass down Request object via context.
@ -2382,7 +2375,8 @@ class Component(metaclass=ComponentMeta):
deps_strategy=deps_strategy,
# TODO_v1 - Remove, superseded by `deps_strategy`
type=deps_strategy,
render_dependencies=render_dependencies,
# TODO_v1 - Remove, superseded by `deps_strategy`
render_dependencies=deps_strategy != "ignore",
),
is_filled=None,
request=request,
@ -2571,10 +2565,9 @@ class Component(metaclass=ComponentMeta):
post_render_callbacks[render_id] = on_component_rendered
# This is triggered after a full component tree was rendered, we resolve
# all inserted HTML comments into <script> and <link> tags (if render_dependencies=True)
# all inserted HTML comments into <script> and <link> tags.
def on_html_rendered(html: str) -> str:
if render_dependencies:
html = _render_dependencies(html, deps_strategy)
html = _render_dependencies(html, deps_strategy)
return html
trace_component_msg(
@ -2971,7 +2964,7 @@ class ComponentNode(BaseNode):
slots=slot_fills,
# NOTE: When we render components inside the template via template tags,
# do NOT render deps, because this may be decided by outer component
render_dependencies=False,
deps_strategy="ignore",
)
return output

View file

@ -59,7 +59,7 @@ class DynamicComponent(Component):
},
slots={
"pagination": PaginationComponent.render(
render_dependencies=False,
deps_strategy="ignore",
),
},
)
@ -143,7 +143,6 @@ class DynamicComponent(Component):
# was already escaped (if set so).
escape_slots_content=False,
deps_strategy=self.input.deps_strategy,
render_dependencies=self.input.render_dependencies,
)
context["output"] = output

View file

@ -11,6 +11,7 @@ from django.template import Context
from django_components.util.misc import get_last_index
_COMPONENT_CONTEXT_KEY = "_DJC_COMPONENT_CTX"
_STRATEGY_CONTEXT_KEY = "DJC_DEPS_STRATEGY"
_INJECT_CONTEXT_KEY_PREFIX = "_DJC_INJECT__"

View file

@ -6,7 +6,6 @@ import re
from hashlib import md5
from typing import (
TYPE_CHECKING,
Callable,
Dict,
List,
Literal,
@ -21,14 +20,11 @@ from typing import (
cast,
)
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
from django.forms import Media
from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed, HttpResponseNotFound, StreamingHttpResponse
from django.http.response import HttpResponseBase
from django.http import HttpRequest, HttpResponse, HttpResponseNotAllowed, HttpResponseNotFound
from django.template import Context, TemplateSyntaxError
from django.templatetags.static import static
from django.urls import path, reverse
from django.utils.decorators import sync_and_async_middleware
from django.utils.safestring import SafeString, mark_safe
from djc_core_html_parser import set_html_attributes
@ -42,14 +38,14 @@ if TYPE_CHECKING:
ScriptType = Literal["css", "js"]
DependenciesStrategy = Literal["document", "fragment", "simple", "prepend", "append"]
DependenciesStrategy = Literal["document", "fragment", "simple", "prepend", "append", "ignore"]
"""
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")
DEPS_STRATEGIES = ("document", "fragment", "simple", "prepend", "append", "ignore")
#########################################################
@ -316,7 +312,7 @@ def insert_component_dependencies_comment(
) -> SafeString:
"""
Given some textual content, prepend it with a short string that
will be used by the ComponentDependencyMiddleware to collect all
will be used by the `render_dependencies()` function to collect all
declared JS / CSS scripts.
"""
data = f"{component_cls.class_id},{component_id},{js_input_hash or ''},{css_input_hash or ''}"
@ -413,6 +409,8 @@ def render_dependencies(content: TContent, strategy: DependenciesStrategy = "doc
"""
if strategy not in DEPS_STRATEGIES:
raise ValueError(f"Invalid strategy '{strategy}'")
elif strategy == "ignore":
return content
is_safestring = isinstance(content, SafeString)
@ -1006,50 +1004,7 @@ urlpatterns = [
#########################################################
# 5. Middleware that automatically applies the dependency-
# aggregating logic on all HTML responses.
#########################################################
@sync_and_async_middleware
class ComponentDependencyMiddleware:
"""
Middleware that inserts CSS/JS dependencies for all rendered
components at points marked with template tags.
"""
def __init__(self, get_response: "Callable[[HttpRequest], HttpResponse]") -> None:
self._get_response = get_response
# NOTE: Required to work with async
if iscoroutinefunction(self._get_response):
markcoroutinefunction(self)
def __call__(self, request: HttpRequest) -> HttpResponseBase:
if iscoroutinefunction(self):
return self.__acall__(request)
response = self._get_response(request)
response = self._process_response(response)
return response
# NOTE: Required to work with async
async def __acall__(self, request: HttpRequest) -> HttpResponseBase:
response = await self._get_response(request)
response = self._process_response(response)
return response
def _process_response(self, response: HttpResponse) -> HttpResponse:
if not isinstance(response, StreamingHttpResponse) and response.get("Content-Type", "").startswith(
"text/html"
):
response.content = render_dependencies(response.content, strategy="document")
return response
#########################################################
# 6. Template tags
# 5. Template tags
#########################################################
@ -1075,7 +1030,7 @@ class ComponentCssDependenciesNode(BaseNode):
If the generated HTML does NOT contain any `{% component_css_dependencies %}` tags, CSS links
are by default inserted into the `<head>` tag of the HTML. (See
[JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations))
[Default JS / CSS locations](../../concepts/advanced/rendering_js_css/#default-js-css-locations))
Note that there should be only one `{% component_css_dependencies %}` for the whole HTML document.
If you insert this tag multiple times, ALL CSS links will be duplicately inserted into ALL these places.
@ -1097,7 +1052,7 @@ class ComponentJsDependenciesNode(BaseNode):
If the generated HTML does NOT contain any `{% component_js_dependencies %}` tags, JS scripts
are by default inserted at the end of the `<body>` tag of the HTML. (See
[JS and CSS output locations](../../concepts/advanced/rendering_js_css/#js-and-css-output-locations))
[Default JS / CSS locations](../../concepts/advanced/rendering_js_css/#default-js-css-locations))
Note that there should be only one `{% component_js_dependencies %}` for the whole HTML document.
If you insert this tag multiple times, ALL JS scripts will be duplicately inserted into ALL these places.

View file

@ -1,4 +0,0 @@
# These middlewares are part of public API
from django_components.dependencies import ComponentDependencyMiddleware
__all__ = ["ComponentDependencyMiddleware"]

View file

@ -1102,7 +1102,11 @@ def _nodelist_to_slot_render_func(
trace_component_msg("RENDER_NODELIST", component_name, component_id=None, slot_name=slot_name)
rendered = template.render(ctx)
# We wrap the slot nodelist in Template. However, we also override Django's `Template.render()`
# to call `render_dependencies()` on the results. So we need to set the strategy to `ignore`
# so that the dependencies are processed only once the whole component tree is rendered.
with ctx.push({"DJC_DEPS_STRATEGY": "ignore"}):
rendered = template.render(ctx)
# After the rendering is done, remove the `extra_context` from the context stack
ctx.dicts.pop(index_of_last_component_layer)

View file

@ -3,6 +3,8 @@ from typing import Any, Type
from django.template import Context, NodeList, Template
from django.template.base import Parser
from django_components.context import _COMPONENT_CONTEXT_KEY, _STRATEGY_CONTEXT_KEY
from django_components.dependencies import COMPONENT_COMMENT_REGEX, render_dependencies
from django_components.util.template_parser import parse_template
@ -81,11 +83,9 @@ def monkeypatch_template_render(template_cls: Type[Template]) -> None:
# Do not patch if done so already. This helps us avoid RecursionError
return
# NOTE: This implementation is based on Django v5.1.3)
def _template_render(self: Template, context: Context, *args: Any, **kwargs: Any) -> str:
"Display stage -- can be called many times"
# ---------------- ORIGINAL (Django v5.1.3) ----------------
# with context.render_context.push_state(self):
# ---------------- OUR CHANGES START ----------------
# We parametrized `isolated_context`, which was `True` in the original method.
if not hasattr(self, "_djc_is_component_nested"):
isolated_context = True
@ -95,13 +95,44 @@ def monkeypatch_template_render(template_cls: Type[Template]) -> None:
isolated_context = not self._djc_is_component_nested
with context.render_context.push_state(self, isolated_context=isolated_context):
# ---------------- OUR CHANGES END ----------------
if context.template is None:
with context.bind_template(self):
context.template_name = self.name
return self._render(context, *args, **kwargs)
result: str = self._render(context, *args, **kwargs)
else:
return self._render(context, *args, **kwargs)
result = self._render(context, *args, **kwargs)
# If the key is present, that means this Template is rendered as part of `Component.render()`
# or `{% component %}`. In that case the parent component will take care of rendering the
# dependencies, so we don't need to do that here.
if _COMPONENT_CONTEXT_KEY in context:
return result
# NOTE: Only process dependencies if the rendered result contains AT LEAST ONE rendered component.
# This has two reasons:
# 1. To keep the behavior consistent with the previous implementation, when `Template.render()`
# didn't call `render_dependencies()`.
# 2. To avoid unnecessary processing which otherwise has a considerable perf overhead.
# See https://github.com/django-components/django-components/pull/1166#issuecomment-2850899765
if not COMPONENT_COMMENT_REGEX.search(result.encode("utf-8")):
return result
# Allow users to configure the `deps_strategy` kwarg of `render_dependencies()`, even if
# they render a Template directly with `Template.render()` or Django's `django.shortcuts.render()`.
#
# Example:
# ```
# result = render_dependencies(
# result,
# Context({ "DJC_DEPS_STRATEGY": "fragment" }),
# )
# ```
if _STRATEGY_CONTEXT_KEY in context and context[_STRATEGY_CONTEXT_KEY] is not None:
strategy = context[_STRATEGY_CONTEXT_KEY]
result = render_dependencies(result, strategy)
else:
result = render_dependencies(result)
return result
template_cls.render = _template_render

View file

@ -2,7 +2,7 @@ from django.http import HttpResponse
from django.template import Context, Template
from testserver.components import FragComp, FragMedia
from django_components import render_dependencies, types
from django_components import types
def single_component_view(request):
@ -23,8 +23,7 @@ def single_component_view(request):
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({}))
return HttpResponse(rendered)
@ -47,8 +46,7 @@ def multiple_components_view(request):
</html>
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({}))
return HttpResponse(rendered)
@ -72,8 +70,7 @@ def check_js_order_in_js_view(request):
</html>
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({}))
return HttpResponse(rendered)
@ -97,8 +94,7 @@ def check_js_order_in_media_view(request):
</html>
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({}))
return HttpResponse(rendered)
@ -122,8 +118,7 @@ def check_js_order_vars_not_available_before_view(request):
</html>
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({}))
return HttpResponse(rendered)
@ -163,14 +158,13 @@ def fragment_base_js_view(request):
template = Template(template_str)
frag = request.GET["frag"]
rendered_raw = template.render(
rendered = template.render(
Context(
{
"frag": frag,
}
)
)
rendered = render_dependencies(rendered_raw)
return HttpResponse(rendered)
@ -211,8 +205,7 @@ def fragment_base_alpine_view(request):
template = Template(template_str)
frag = request.GET["frag"]
rendered_raw = template.render(Context({"frag": frag}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({"frag": frag}))
return HttpResponse(rendered)
@ -242,8 +235,7 @@ def fragment_base_htmx_view(request):
template = Template(template_str)
frag = request.GET["frag"]
rendered_raw = template.render(Context({"frag": frag}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({"frag": frag}))
return HttpResponse(rendered)
@ -273,8 +265,7 @@ def alpine_in_head_view(request):
</html>
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({}))
return HttpResponse(rendered)
@ -294,8 +285,7 @@ def alpine_in_body_view(request):
</html>
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({}))
return HttpResponse(rendered)
@ -316,8 +306,7 @@ def alpine_in_body_view_2(request):
</html>
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({}))
return HttpResponse(rendered)
@ -338,6 +327,5 @@ def alpine_in_body_vars_not_available_before_view(request):
</html>
"""
template = Template(template_str)
rendered_raw = template.render(Context({}))
rendered = render_dependencies(rendered_raw)
rendered = template.render(Context({}))
return HttpResponse(rendered)

View file

@ -70,7 +70,7 @@ if not settings.configured:
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},
MIDDLEWARE=["django_components.middleware.ComponentDependencyMiddleware"],
MIDDLEWARE=[],
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",

View file

@ -41,7 +41,7 @@ if not settings.configured:
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},
MIDDLEWARE=["django_components.middleware.ComponentDependencyMiddleware"],
MIDDLEWARE=[],
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",

View file

@ -70,7 +70,7 @@ if not settings.configured:
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},
MIDDLEWARE=["django_components.middleware.ComponentDependencyMiddleware"],
MIDDLEWARE=[],
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",
@ -2455,6 +2455,7 @@ class ProjectLayoutTabbed(Component):
"stroke_width": 2,
"color": "text-gray-400 hover:text-gray-500",
},
# deps_strategy="ignore",
render_dependencies=False,
),
),
@ -4490,6 +4491,7 @@ class Tabs(Component):
"header_attrs": context["header_attrs"],
"content_attrs": context["content_attrs"],
},
# deps_strategy="ignore",
render_dependencies=False,
)
@ -5135,6 +5137,7 @@ class ProjectUsers(Component):
"project_id": project_id,
"role_id": role['id'],
},
# deps_strategy="ignore",
render_dependencies=False,
)
else:

View file

@ -41,7 +41,7 @@ if not settings.configured:
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},
MIDDLEWARE=["django_components.middleware.ComponentDependencyMiddleware"],
MIDDLEWARE=[],
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",

View file

@ -872,7 +872,7 @@ class TestComponentRender:
{% endblock %}
"""
rendered = SimpleComponent.render(render_dependencies=False)
rendered = SimpleComponent.render(deps_strategy="ignore")
assertHTMLEqual(
rendered,
"""

View file

@ -79,7 +79,7 @@ class TestMainMedia:
assert ".html-css-only {\n color: blue;\n}" in TestComponent.css # type: ignore[operator]
assert 'console.log("JS file");' in TestComponent.js # type: ignore[operator]
rendered_raw = Template(
rendered = Template(
"""
{% load component_tags %}
{% component_js_dependencies %}
@ -87,7 +87,6 @@ class TestMainMedia:
{% component "test" variable="test" / %}
"""
).render(Context())
rendered = render_dependencies(rendered_raw)
assertInHTML(
"""
@ -143,7 +142,7 @@ class TestMainMedia:
assert ".html-css-only {\n color: blue;\n}" in TestComponent.css # type: ignore[operator]
assert 'console.log("HTML and JS only");' in TestComponent.js # type: ignore[operator]
rendered_raw = Template(
rendered = Template(
"""
{% load component_tags %}
{% component_js_dependencies %}
@ -151,7 +150,6 @@ class TestMainMedia:
{% component "test" variable="test" / %}
"""
).render(Context())
rendered = render_dependencies(rendered_raw)
assert 'Variable: <strong data-djc-id-ca1bc41="">test</strong>' in rendered
assertInHTML(

View file

@ -6,19 +6,15 @@ For checking the OUTPUT of the dependencies, see `test_dependency_rendering.py`.
"""
import re
from unittest.mock import Mock
import pytest
from django.http import HttpResponseNotModified
from django.template import Context, Template
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, registry, render_dependencies, types
from django_components.components.dynamic import DynamicComponent
from django_components.middleware import ComponentDependencyMiddleware
from django_components.testing import djc_test
from .testutils import create_and_process_template_response, setup_test_config
from .testutils import setup_test_config
setup_test_config({"autodiscover": False})
@ -69,7 +65,8 @@ class TestDependenciesLegacy:
@djc_test
class TestRenderDependencies:
def test_standalone_render_dependencies(self):
# Check that `render_dependencies()` works when called directly
def test_render_dependencies(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
@ -79,7 +76,8 @@ class TestRenderDependencies:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
# NOTE: `"ignore"` is a special value that means "do not render dependencies"
rendered_raw: str = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
# Placeholders
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
@ -100,7 +98,8 @@ class TestRenderDependencies:
assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css
def test_middleware_renders_dependencies(self):
# Check that instead of `render_dependencies()`, we can simply call `Template.render()`
def test_template_render(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
@ -110,19 +109,64 @@ class TestRenderDependencies:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template, use_middleware=True)
rendered = template.render(Context({}))
# Dependency manager script
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
assertInHTML("<style>.xyz { color: red; }</style>", rendered, count=1) # Inlined CSS
assertInHTML('<script>console.log("xyz");</script>', rendered, count=1) # Inlined JS
assertInHTML(
"<style>.xyz { color: red; }</style>",
rendered,
count=1,
) # Inlined CSS
assertInHTML(
'<script>console.log("xyz");</script>', rendered, count=1
) # Inlined JS
assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css
assert rendered.count("<link") == 1
assert rendered.count("<style") == 1
def test_component_render_renders_dependencies(self):
# Check that we can change the dependencies strategy via `DJC_DEPS_STRATEGY` context key
def test_template_render_deps_strategy(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: str = template.render(Context({"DJC_DEPS_STRATEGY": "append"}))
# Dependency manager script NOT included
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
assertInHTML(
"<style>.xyz { color: red; }</style>",
rendered,
count=1,
) # Inlined CSS
assertInHTML(
'<script>console.log("xyz");</script>', rendered, count=1
) # Inlined JS
assertInHTML('<link href="style.css" media="all" rel="stylesheet">', rendered, count=1) # Media.css
assert rendered.count("<link") == 1
assert rendered.count("<style") == 1
# Check that the order is correct (dependencies are appended)
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">'
)
# Check that `Component.render()` renders dependencies
def test_component_render(self):
class SimpleComponentWithDeps(SimpleComponent):
template: types.django_html = (
"""
@ -149,7 +193,7 @@ class TestRenderDependencies:
assert rendered.count("<link") == 1
assert rendered.count("<style") == 1
def test_component_render_renders_dependencies_opt_out(self):
def test_component_render_opt_out(self):
class SimpleComponentWithDeps(SimpleComponent):
template: types.django_html = (
"""
@ -164,7 +208,7 @@ class TestRenderDependencies:
rendered_raw = SimpleComponentWithDeps.render(
kwargs={"variable": "foo"},
render_dependencies=False,
deps_strategy="ignore",
)
assert rendered_raw.count("<script") == 1
@ -184,7 +228,8 @@ class TestRenderDependencies:
count=0,
) # Inlined JS
def test_component_render_to_response_renders_dependencies(self):
# Check that `Component.render_to_response()` renders dependencies
def test_component_render_to_response(self):
class SimpleComponentWithDeps(SimpleComponent):
template: types.django_html = (
"""
@ -212,6 +257,103 @@ 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({"DJC_DEPS_STRATEGY": "ignore"}))
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({"DJC_DEPS_STRATEGY": "ignore"}))
rendered: str = 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):
@ -221,7 +363,7 @@ class TestRenderDependencies:
<thead>
"""
rendered_raw = Template(template_str).render(Context({"formset": [1]}))
rendered_raw = Template(template_str).render(Context({"formset": [1], "DJC_DEPS_STRATEGY": "ignore"}))
rendered = render_dependencies(rendered_raw, strategy="fragment")
assertHTMLEqual(rendered, "<thead>")
@ -256,7 +398,7 @@ class TestRenderDependencies:
</table>
"""
rendered_raw = Template(template_str).render(Context({"formset": [1]}))
rendered_raw = Template(template_str).render(Context({"formset": [1], "DJC_DEPS_STRATEGY": "ignore"}))
rendered = render_dependencies(rendered_raw, strategy="fragment")
expected = """
@ -319,7 +461,7 @@ class TestRenderDependencies:
</table>
"""
rendered_raw = Template(template_str).render(Context({"formset": [1]}))
rendered_raw = Template(template_str).render(Context({"formset": [1], "DJC_DEPS_STRATEGY": "ignore"}))
rendered = render_dependencies(rendered_raw, strategy="fragment")
# Base64 encodings:
@ -408,7 +550,7 @@ class TestDependenciesStrategyDocument:
</body>
</html>
"""
rendered_raw = Template(template_str).render(Context({}))
rendered_raw = Template(template_str).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
rendered = render_dependencies(rendered_raw, strategy="document")
assert rendered.count("<script") == 4
@ -457,7 +599,7 @@ class TestDependenciesStrategyDocument:
</body>
</html>
"""
rendered_raw = Template(template_str).render(Context({}))
rendered_raw = Template(template_str).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
rendered = render_dependencies(rendered_raw, strategy="document")
assert rendered.count("<script") == 4
@ -505,7 +647,7 @@ class TestDependenciesStrategySimple:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
rendered_raw: str = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
# Placeholders
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
@ -593,7 +735,7 @@ class TestDependenciesStrategySimple:
{% endcomponent %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
rendered_raw: str = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
rendered = render_dependencies(rendered_raw, strategy="simple")
@ -663,7 +805,7 @@ class TestDependenciesStrategyPrepend:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
rendered_raw: str = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
# Placeholders
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
@ -753,7 +895,7 @@ class TestDependenciesStrategyPrepend:
{% endcomponent %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
rendered_raw: str = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
rendered = render_dependencies(rendered_raw, strategy="prepend")
@ -823,7 +965,7 @@ class TestDependenciesStrategyAppend:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
rendered_raw: str = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
# Placeholders
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
@ -910,7 +1052,7 @@ class TestDependenciesStrategyAppend:
{% endcomponent %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({}))
rendered_raw: str = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
rendered = render_dependencies(rendered_raw, strategy="append")
@ -969,58 +1111,32 @@ class TestDependenciesStrategyAppend:
@djc_test
class TestMiddleware:
def test_middleware_response_without_content_type(self):
response = HttpResponseNotModified()
middleware = ComponentDependencyMiddleware(get_response=lambda _: response)
request = Mock()
assert response == middleware(request=request)
def test_middleware_response_with_components_with_slash_dash_and_underscore(self):
registry.register("dynamic", DynamicComponent)
registry.register("test-component", component=SimpleComponent)
registry.register("test/component", component=SimpleComponent)
registry.register("test_component", component=SimpleComponent)
class TestDependenciesStrategyRaw:
def test_single_component(self):
registry.register(name="test", component=SimpleComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component_css_dependencies %}
{% component_js_dependencies %}
{% component "dynamic" is=component_name variable='value' / %}
{% component_css_dependencies %}
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered_raw: str = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
def assert_dependencies(content: str):
# Dependency manager script (empty)
assertInHTML('<script src="django_components/django_components.min.js"></script>', content, count=1)
# Placeholders
assert rendered_raw.count('<link name="CSS_PLACEHOLDER">') == 1
assert rendered_raw.count('<script name="JS_PLACEHOLDER"></script>') == 1
# Inlined JS
assertInHTML('<script>console.log("xyz");</script>', content, count=1)
# Inlined CSS
assertInHTML("<style>.xyz { color: red; }</style>", content, count=1)
# Media.css
assertInHTML('<link href="style.css" media="all" rel="stylesheet">', content, count=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
rendered1 = create_and_process_template_response(
template,
context=Context({"component_name": "test-component"}),
# Check that it contains inlined JS and CSS, and Media.css
assert rendered_raw.strip() == (
'<script name="JS_PLACEHOLDER"></script>\n'
' <link name="CSS_PLACEHOLDER">\n'
' <!-- _RENDERED SimpleComponent_311097,ca1bc41,, -->\n'
' Variable: <strong data-djc-id-ca1bc41="">foo</strong>'
)
assert_dependencies(rendered1)
assert rendered1.count('Variable: <strong data-djc-id-ca1bc42="" data-djc-id-ca1bc41="">value</strong>') == 1
rendered2 = create_and_process_template_response(
template,
context=Context({"component_name": "test-component"}),
)
assert_dependencies(rendered2)
assert rendered2.count('Variable: <strong data-djc-id-ca1bc44="" data-djc-id-ca1bc43="">value</strong>') == 1
rendered3 = create_and_process_template_response(
template,
context=Context({"component_name": "test_component"}),
)
assert_dependencies(rendered3)
assert rendered3.count('Variable: <strong data-djc-id-ca1bc46="" data-djc-id-ca1bc45="">value</strong>') == 1

View file

@ -11,7 +11,7 @@ from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, registry, types
from django_components.testing import djc_test
from .testutils import create_and_process_template_response, setup_test_config
from .testutils import setup_test_config
setup_test_config({"autodiscover": False})
@ -120,13 +120,13 @@ class TestDependencyRendering:
{% component_css_dependencies %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered: str = template.render(Context({}))
# Dependency manager script
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
assert rendered.count("<script") == 1 # 1 boilerplate script
assert rendered.count("<link") == 0 # No CSS
assert rendered.count("<script") == 1 # 1 placeholder script
assert rendered.count("<link") == 1 # 1 placeholder link
assert rendered.count("<style") == 0
assert "loadedJsUrls" not in rendered
@ -134,6 +134,11 @@ class TestDependencyRendering:
assert "toLoadJsTags" not in rendered
assert "toLoadCssTags" not in rendered
assert rendered.strip() == (
'<script name="JS_PLACEHOLDER"></script>\n'
' <link name="CSS_PLACEHOLDER">'
)
def test_no_js_dependencies_when_no_components_used(self):
registry.register(name="test", component=SimpleComponent)
@ -141,12 +146,12 @@ class TestDependencyRendering:
{% load component_tags %}{% component_js_dependencies %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
# Dependency manager script
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
assert rendered.count("<script") == 1 # 1 boilerplate script
assert rendered.count("<script") == 1 # 1 placeholder script
assert rendered.count("<link") == 0 # No CSS
assert rendered.count("<style") == 0
@ -155,6 +160,8 @@ class TestDependencyRendering:
assert "toLoadJsTags" not in rendered
assert "toLoadCssTags" not in rendered
assert rendered.strip() == '<script name="JS_PLACEHOLDER"></script>'
def test_no_css_dependencies_when_no_components_used(self):
registry.register(name="test", component=SimpleComponent)
@ -162,12 +169,14 @@ class TestDependencyRendering:
{% load component_tags %}{% component_css_dependencies %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered: str = template.render(Context({}))
assert rendered.count("<script") == 0 # No JS
assert rendered.count("<link") == 0 # No CSS
assert rendered.count("<link") == 1 # 1 placeholder link
assert rendered.count("<style") == 0
assert rendered.strip() == '<link name="CSS_PLACEHOLDER">'
def test_single_component_dependencies(self):
registry.register(name="test", component=SimpleComponent)
@ -178,7 +187,7 @@ class TestDependencyRendering:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
# Dependency manager script
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
@ -213,7 +222,7 @@ class TestDependencyRendering:
{% component 'te-s/t' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
# Dependency manager script
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
@ -248,7 +257,7 @@ class TestDependencyRendering:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
assert "_RENDERED" not in rendered
@ -260,7 +269,7 @@ class TestDependencyRendering:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
# Dependency manager script - NOT present
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
@ -279,7 +288,7 @@ class TestDependencyRendering:
{% component 'test' variable='foo' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
# Dependency manager script
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
@ -315,7 +324,7 @@ class TestDependencyRendering:
{% component 'test' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
# Dependency manager script
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
@ -372,13 +381,13 @@ class TestDependencyRendering:
{% component_css_dependencies %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered: str = template.render(Context({}))
# Dependency manager script
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=1)
assertInHTML('<script src="django_components/django_components.min.js"></script>', rendered, count=0)
assert rendered.count("<script") == 1 # 1 boilerplate script
assert rendered.count("<link") == 0 # No CSS
assert rendered.count("<script") == 1 # 1 placeholder script
assert rendered.count("<link") == 1 # 1 placeholder link
assert rendered.count("<style") == 0
assert "loadedJsUrls" not in rendered
@ -386,6 +395,11 @@ class TestDependencyRendering:
assert "toLoadJsTags" not in rendered
assert "toLoadCssTags" not in rendered
assert rendered.strip() == (
'<script name="JS_PLACEHOLDER"></script>\n'
' <link name="CSS_PLACEHOLDER">'
)
def test_multiple_components_dependencies(self):
registry.register(name="inner", component=SimpleComponent)
registry.register(name="outer", component=SimpleComponentNested)
@ -400,7 +414,7 @@ class TestDependencyRendering:
{% endcomponent %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered: str = template.render(Context({}))
# Dependency manager script
# NOTE: Should be present only ONCE!
@ -499,7 +513,7 @@ class TestDependencyRendering:
{% component 'test' variable='variable' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
assert "_RENDERED" not in rendered
def test_adds_component_id_html_attr_single(self):
@ -510,7 +524,7 @@ class TestDependencyRendering:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
assertHTMLEqual(rendered, "Variable: <strong data-djc-id-ca1bc3f>foo</strong>")
@ -529,7 +543,7 @@ class TestDependencyRendering:
{% component 'test' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
assertHTMLEqual(
rendered,
@ -565,7 +579,7 @@ class TestDependencyRendering:
{% component 'outer' variable='foo' / %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(template)
rendered = template.render(Context({}))
assertHTMLEqual(
rendered,
@ -605,10 +619,7 @@ class TestDependencyRendering:
{% endfor %}
"""
template = Template(template_str)
rendered = create_and_process_template_response(
template,
context=Context({"lst": range(3)}),
)
rendered = template.render(Context({"lst": range(3)}))
assertHTMLEqual(
rendered,

View file

@ -229,9 +229,10 @@ class TestDynamicExpr:
)
template = Template(template_str)
rendered = template.render(
rendered: str = template.render(
Context(
{
"DJC_DEPS_STRATEGY": "ignore",
"var_a": 3,
"is_active": True,
"list": [{"a": 1}, {"a": 2}],
@ -297,9 +298,10 @@ class TestDynamicExpr:
)
template = Template(template_str)
rendered = template.render(
rendered: str = template.render(
Context(
{
"DJC_DEPS_STRATEGY": "ignore",
"var_a": 3,
"is_active": True,
"list": [{"a": 1}, {"a": 2}],

View file

@ -59,7 +59,7 @@ class TestExtendsCompat:
{% endcomponent %}
{% endblock %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
@ -109,7 +109,7 @@ class TestExtendsCompat:
{% endcomponent %}
{% endblock %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
@ -176,7 +176,7 @@ class TestExtendsCompat:
{% endblock %}
"""
template = Template(template_str)
rendered = template.render(Context())
rendered = template.render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
@ -242,7 +242,7 @@ class TestExtendsCompat:
{% endcomponent %}
{% endblock %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
@ -295,7 +295,7 @@ class TestExtendsCompat:
</body>
</html>
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
@ -343,7 +343,7 @@ class TestExtendsCompat:
</body>
</html>
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
@ -395,7 +395,7 @@ class TestExtendsCompat:
{% endcomponent %}
{% endblock %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
@ -440,7 +440,7 @@ class TestExtendsCompat:
{% component "extended_component" / %}
{% endblock %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
@ -458,7 +458,7 @@ class TestExtendsCompat:
assertHTMLEqual(rendered, expected)
# second rendering after cache built
rendered_2 = Template(template).render(Context())
rendered_2 = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected_2 = expected.replace("data-djc-id-ca1bc3f", "data-djc-id-ca1bc41")
assertHTMLEqual(rendered_2, expected_2)
@ -480,7 +480,7 @@ class TestExtendsCompat:
{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc40 lang="en">
@ -513,7 +513,7 @@ class TestExtendsCompat:
{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc40 lang="en">
@ -544,7 +544,7 @@ class TestExtendsCompat:
{% endcomponent %}
{% endblock %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html lang="en">
@ -575,7 +575,7 @@ class TestExtendsCompat:
</div>
{% endblock %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html lang="en">
@ -604,7 +604,7 @@ class TestExtendsCompat:
{% load component_tags %}
{% component "block_in_component_parent" %}{% endcomponent %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc3f lang="en">
@ -644,7 +644,7 @@ class TestExtendsCompat:
wow
{% endblock %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc40 lang="en">
@ -674,7 +674,7 @@ class TestExtendsCompat:
{% load component_tags %}
{% component "slot_inside_block" %}{% endcomponent %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc3f lang="en">
@ -710,7 +710,7 @@ class TestExtendsCompat:
{% load component_tags %}
{% component "slot_inside_block" %}{% endcomponent %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc3f lang="en">
@ -746,7 +746,7 @@ class TestExtendsCompat:
{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc40 lang="en">
@ -792,7 +792,7 @@ class TestExtendsCompat:
{% endfill %}
{% endcomponent %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc41 lang="en">
@ -832,7 +832,7 @@ class TestExtendsCompat:
{% endcomponent %}
{% endblock %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html lang="en">
@ -857,7 +857,7 @@ class TestExtendsCompat:
{% load component_tags %}
{% component "relative_file_component_using_template_file" %}{% endcomponent %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc3f="" lang="en">
@ -880,7 +880,7 @@ class TestExtendsCompat:
{% load component_tags %}
{% component "relative_file_component_using_get_template_name" %}{% endcomponent %}
"""
rendered = Template(template).render(Context())
rendered = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected = """
<!DOCTYPE html>
<html data-djc-id-ca1bc3f="" lang="en">

View file

@ -311,7 +311,7 @@ class TestComponentSlot:
"""
self.template = Template(template_str)
nested_ctx = Context()
nested_ctx = Context({"DJC_DEPS_STRATEGY": "ignore"})
# Check that the component can access vars across different context layers
nested_ctx.push({"some": "var"})
nested_ctx.push({"name": "carl"})
@ -470,7 +470,7 @@ class TestComponentSlot:
slots={
"main": "MAIN",
},
render_dependencies=False,
deps_strategy="ignore",
)
# 2. Specify the non-required slot by the "default" name
@ -479,7 +479,7 @@ class TestComponentSlot:
slots={
"default": "MAIN",
},
render_dependencies=False,
deps_strategy="ignore",
)
assertHTMLEqual(rendered1, "<div data-djc-id-ca1bc3e><div>MAIN</div></div>")
@ -491,7 +491,7 @@ class TestComponentSlot:
slots={
"main": "MAIN",
},
render_dependencies=False,
deps_strategy="ignore",
)
assertHTMLEqual(rendered3, "<div data-djc-id-ca1bc42><main>MAIN</main><div>MAIN</div></div>")
@ -506,7 +506,7 @@ class TestComponentSlot:
slots={
"default": "MAIN",
},
render_dependencies=False,
deps_strategy="ignore",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)

View file

@ -1,13 +1,8 @@
from pathlib import Path
from typing import Dict, Optional
from unittest.mock import Mock
import django
from django.conf import settings
from django.template import Context
from django.template.response import TemplateResponse
from django_components.middleware import ComponentDependencyMiddleware
# Common use case in our tests is to check that the component works in both
# "django" and "isolated" context behaviors. If you need only that, pass this
@ -22,28 +17,6 @@ PARAMETRIZE_CONTEXT_BEHAVIOR = (
)
# Create middleware instance
response_stash = None
middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash)
request = Mock()
mock_template = Mock()
def create_and_process_template_response(template, context=None, use_middleware=True):
context = context if context is not None else Context({})
mock_template.render = lambda context, _: template.render(context)
response = TemplateResponse(request, mock_template, context)
if use_middleware:
response.render()
global response_stash
response_stash = response
response = middleware(request)
else:
response.render()
return response.content.decode("utf-8")
def setup_test_config(
components: Optional[Dict] = None,
extra_settings: Optional[Dict] = None,
@ -72,7 +45,7 @@ def setup_test_config(
"template_cache_size": 128,
**(components or {}),
},
"MIDDLEWARE": ["django_components.middleware.ComponentDependencyMiddleware"],
"MIDDLEWARE": [],
"DATABASES": {
"default": {
"ENGINE": "django.db.backends.sqlite3",