mirror of
https://github.com/django-components/django-components.git
synced 2025-08-10 01:08:00 +00:00
fix: Fix bug where JS and CSS were missing when {% component %}
tag was inside {% include %}
tag (#1300)
* fix: Fix bug where JS and CSS were missing when `{% component %}` tag was inside `{% include %}` tag * refactor: fix mypy error
This commit is contained in:
parent
672811b8b4
commit
81c0d419b4
10 changed files with 170 additions and 12 deletions
|
@ -1,5 +1,11 @@
|
|||
# Release notes
|
||||
|
||||
## v0.141.2
|
||||
|
||||
#### Fix
|
||||
|
||||
- Fix bug where JS and CSS were missing when `{% component %}` tag was inside `{% include %}` tag ([#1296](https://github.com/django-components/django-components/issues/1296))
|
||||
|
||||
## v0.141.1
|
||||
|
||||
#### Fix
|
||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "django_components"
|
||||
version = "0.141.1"
|
||||
version = "0.141.2"
|
||||
requires-python = ">=3.8, <4.0"
|
||||
description = "A way to create simple reusable template components in Django."
|
||||
keywords = ["django", "components", "css", "js", "html"]
|
||||
|
|
|
@ -4,6 +4,7 @@ from typing import Any
|
|||
|
||||
from django.apps import AppConfig
|
||||
from django.template import Template
|
||||
from django.template.loader_tags import IncludeNode
|
||||
from django.utils.autoreload import file_changed, trigger_reload
|
||||
|
||||
|
||||
|
@ -18,12 +19,13 @@ class ComponentsConfig(AppConfig):
|
|||
from django_components.component_registry import registry
|
||||
from django_components.components.dynamic import DynamicComponent
|
||||
from django_components.extension import extensions
|
||||
from django_components.util.django_monkeypatch import monkeypatch_template_cls
|
||||
from django_components.util.django_monkeypatch import monkeypatch_include_node, monkeypatch_template_cls
|
||||
|
||||
# NOTE: This monkeypatch is applied here, before Django processes any requests.
|
||||
# To make django-components work with django-debug-toolbar-template-profiler
|
||||
# See https://github.com/django-components/django-components/discussions/819
|
||||
monkeypatch_template_cls(Template)
|
||||
monkeypatch_include_node(IncludeNode)
|
||||
|
||||
# Import modules set in `COMPONENTS.libraries` setting
|
||||
import_libraries()
|
||||
|
|
|
@ -2277,7 +2277,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
deps_strategy = cast(DependenciesStrategy, default(deps_strategy, "document"))
|
||||
|
||||
self.id = default(id, _gen_component_id, factory=True)
|
||||
self.id = default(id, _gen_component_id, factory=True) # type: ignore[arg-type]
|
||||
self.name = _get_component_name(self.__class__, registered_name)
|
||||
self.registered_name: Optional[str] = registered_name
|
||||
self.args = default(args, [])
|
||||
|
|
|
@ -8,7 +8,7 @@ from django.template import Context, Origin, Template
|
|||
from django.template.loader import get_template as django_get_template
|
||||
|
||||
from django_components.cache import get_template_cache
|
||||
from django_components.util.django_monkeypatch import is_template_cls_patched
|
||||
from django_components.util.django_monkeypatch import is_cls_patched
|
||||
from django_components.util.loader import get_component_dirs
|
||||
from django_components.util.logger import trace_component_msg
|
||||
from django_components.util.misc import get_import_path, get_module_info
|
||||
|
@ -98,7 +98,7 @@ def prepare_component_template(
|
|||
yield template
|
||||
return
|
||||
|
||||
if not is_template_cls_patched(template):
|
||||
if not is_cls_patched(template):
|
||||
raise RuntimeError(
|
||||
"Django-components received a Template instance which was not patched."
|
||||
"If you are using Django's Template class, check if you added django-components"
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from typing import Any, Optional, Type
|
||||
|
||||
from django.template import Context, NodeList, Template
|
||||
from django.template.base import Origin, Parser
|
||||
from django.template.base import Node, Origin, Parser
|
||||
from django.template.loader_tags import IncludeNode
|
||||
|
||||
from django_components.context import _COMPONENT_CONTEXT_KEY, _STRATEGY_CONTEXT_KEY, COMPONENT_IS_NESTED_KEY
|
||||
from django_components.dependencies import COMPONENT_COMMENT_REGEX, render_dependencies
|
||||
|
@ -11,7 +12,7 @@ from django_components.util.template_parser import parse_template
|
|||
|
||||
# In some cases we can't work around Django's design, and need to patch the template class.
|
||||
def monkeypatch_template_cls(template_cls: Type[Template]) -> None:
|
||||
if is_template_cls_patched(template_cls):
|
||||
if is_cls_patched(template_cls):
|
||||
return
|
||||
|
||||
monkeypatch_template_init(template_cls)
|
||||
|
@ -150,7 +151,7 @@ def monkeypatch_template_render(template_cls: Type[Template]) -> None:
|
|||
# have passed the value to `monkeypatch_template_render` directly. However, we intentionally
|
||||
# did NOT do that, so the monkey-patched method is more robust, and can be e.g. copied
|
||||
# to other.
|
||||
if is_template_cls_patched(template_cls):
|
||||
if is_cls_patched(template_cls):
|
||||
# Do not patch if done so already. This helps us avoid RecursionError
|
||||
return
|
||||
|
||||
|
@ -210,5 +211,37 @@ def monkeypatch_template_render(template_cls: Type[Template]) -> None:
|
|||
template_cls.render = _template_render
|
||||
|
||||
|
||||
def is_template_cls_patched(template_cls: Type[Template]) -> bool:
|
||||
return getattr(template_cls, "_djc_patched", False)
|
||||
def monkeypatch_include_node(include_node_cls: Type[Node]) -> None:
|
||||
if is_cls_patched(include_node_cls):
|
||||
return
|
||||
|
||||
monkeypatch_include_render(include_node_cls)
|
||||
include_node_cls._djc_patched = True
|
||||
|
||||
|
||||
def monkeypatch_include_render(include_node_cls: Type[Node]) -> None:
|
||||
# Modify `IncludeNode.render()` (what renders `{% include %}` tag) so that the included
|
||||
# template does NOT render the JS/CSS by itself.
|
||||
#
|
||||
# Instead, we want the parent template
|
||||
# (which contains the `{% component %}` tag) to decide whether to render the JS/CSS.
|
||||
#
|
||||
# We achieve this by setting `DJC_DEPS_STRATEGY` to `ignore` in the context.
|
||||
#
|
||||
# Fix for https://github.com/django-components/django-components/issues/1296
|
||||
if is_cls_patched(include_node_cls):
|
||||
# Do not patch if done so already. This helps us avoid RecursionError
|
||||
return
|
||||
|
||||
orig_include_render = include_node_cls.render
|
||||
|
||||
# NOTE: This implementation is based on Django v5.1.3)
|
||||
def _include_render(self: IncludeNode, context: Context, *args: Any, **kwargs: Any) -> str:
|
||||
with context.update({_STRATEGY_CONTEXT_KEY: "ignore"}):
|
||||
return orig_include_render(self, context, *args, **kwargs)
|
||||
|
||||
include_node_cls.render = _include_render
|
||||
|
||||
|
||||
def is_cls_patched(cls: Type[Any]) -> bool:
|
||||
return getattr(cls, "_djc_patched", False)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% include "test_cached_component_inside_include_sub.html" %}
|
||||
{% include "component_inside_include_sub.html" %}
|
||||
{% block content %}
|
||||
THIS_IS_IN_BASE_TEMPLATE_SO_SHOULD_BE_OVERRIDDEN
|
||||
{% endblock %}
|
|
@ -219,7 +219,7 @@ class TestComponentCache:
|
|||
|
||||
template = Template(
|
||||
"""
|
||||
{% extends "test_cached_component_inside_include_base.html" %}
|
||||
{% extends "component_inside_include_base.html" %}
|
||||
{% block content %}
|
||||
THIS_IS_IN_ACTUAL_TEMPLATE_SO_SHOULD_NOT_BE_OVERRIDDEN
|
||||
{% endblock %}
|
||||
|
|
|
@ -25,6 +25,17 @@ def gen_blocked_and_slotted_component():
|
|||
return BlockedAndSlottedComponent
|
||||
|
||||
|
||||
def gen_component_inside_include():
|
||||
class ComponentInsideInclude(Component):
|
||||
template: types.django_html = """<div>Hello</div>"""
|
||||
|
||||
class Media:
|
||||
css = "style.css"
|
||||
js = "script.js"
|
||||
|
||||
return ComponentInsideInclude
|
||||
|
||||
|
||||
#######################
|
||||
# TESTS
|
||||
#######################
|
||||
|
@ -536,6 +547,112 @@ class TestExtendsCompat:
|
|||
"""
|
||||
assertHTMLEqual(rendered, expected)
|
||||
|
||||
# In this case, `{% include %}` is NOT nested inside a `{% component %}` tag.
|
||||
# We need to ensure that the component inside the `{% include %}` is rendered as if with deps_strategy="ignore",
|
||||
# so the parent template decides how to render the JS/CSS.
|
||||
# See https://github.com/django-components/django-components/issues/1296
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_component_with_media_inside_include(self, components_settings):
|
||||
registry.register("test_component", gen_component_inside_include())
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<body>
|
||||
<outer>
|
||||
{% include "component_inside_include_sub.html" %}
|
||||
</outer>
|
||||
</body>
|
||||
"""
|
||||
|
||||
rendered_raw = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
|
||||
expected_raw = """
|
||||
<body>
|
||||
<outer>
|
||||
<div data-djc-id-ca1bc3f>Hello</div>
|
||||
</outer>
|
||||
</body>
|
||||
"""
|
||||
assertHTMLEqual(rendered_raw, expected_raw)
|
||||
|
||||
template_obj = Template(template)
|
||||
context = Context()
|
||||
rendered = template_obj.render(context)
|
||||
|
||||
# NOTE: It's important that the <script> tags are rendered outside of <div> and <outer> tags,
|
||||
# because that tells us that the JS/CSS is rendered by the parent template, not the component
|
||||
# inside the include.
|
||||
expected = """
|
||||
<body>
|
||||
<outer>
|
||||
<div data-djc-id-ca1bc41>Hello</div>
|
||||
</outer>
|
||||
<script src="django_components/django_components.min.js"></script>
|
||||
<script type="application/json" data-djc>{"loadedCssUrls": ["c3R5bGUuY3Nz"],
|
||||
"loadedJsUrls": ["c2NyaXB0Lmpz"],
|
||||
"toLoadCssTags": [],
|
||||
"toLoadJsTags": []}</script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
"""
|
||||
assertHTMLEqual(rendered, expected)
|
||||
|
||||
# In this case, because `{% include %}` is rendered inside a `{% component %}` tag,
|
||||
# then the component inside the `{% include %}` knows it's inside another component.
|
||||
# So it's always rendered as if with deps_strategy="ignore".
|
||||
# See https://github.com/django-components/django-components/issues/1296
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_component_with_media_inside_include_inside_component(self, components_settings):
|
||||
registry.register("test_component", gen_component_inside_include())
|
||||
|
||||
@register("component_inside_include")
|
||||
class CompInsideIncludeComponent(Component):
|
||||
template: types.django_html = """
|
||||
<body>
|
||||
<outer>
|
||||
{% include "component_inside_include_sub.html" %}
|
||||
</outer>
|
||||
</body>
|
||||
"""
|
||||
|
||||
template: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<html>
|
||||
{% component "component_inside_include" / %}
|
||||
</html>
|
||||
"""
|
||||
|
||||
rendered_raw = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
|
||||
expected_raw = """
|
||||
<html>
|
||||
<body data-djc-id-ca1bc3f>
|
||||
<outer>
|
||||
<div data-djc-id-ca1bc41>Hello</div>
|
||||
</outer>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
assertHTMLEqual(rendered_raw, expected_raw)
|
||||
|
||||
template_obj = Template(template)
|
||||
context = Context()
|
||||
rendered = template_obj.render(context)
|
||||
expected = """
|
||||
<html>
|
||||
<body data-djc-id-ca1bc43>
|
||||
<outer>
|
||||
<div data-djc-id-ca1bc45>Hello</div>
|
||||
</outer>
|
||||
<script src="django_components/django_components.min.js"></script>
|
||||
<script type="application/json" data-djc>{"loadedCssUrls": ["c3R5bGUuY3Nz"],
|
||||
"loadedJsUrls": ["c2NyaXB0Lmpz"],
|
||||
"toLoadCssTags": [],
|
||||
"toLoadJsTags": []}</script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
assertHTMLEqual(rendered, expected)
|
||||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_component_inside_block(self, components_settings):
|
||||
registry.register("slotted_component", gen_slotted_component())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue