From bf7a204e92ba1d774e82a11934572a015ef16f9c Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Fri, 2 May 2025 15:07:16 +0200 Subject: [PATCH] feat: add "simple", "prepend", and "append" render types (#1156) * feat: add "simple", "prepend", and "append" render types * refactor: explicitly set strategy for "document" in tests --- CHANGELOG.md | 105 ++- docs/concepts/advanced/html_fragments.md | 38 +- docs/concepts/advanced/rendering_js_css.md | 202 ++++- .../fundamentals/html_js_css_files.md | 2 +- .../fundamentals/rendering_components.md | 130 ++-- docs/reference/api.md | 4 + docs/reference/template_tags.md | 124 ++-- sampleproject/components/calendar/calendar.py | 2 + sampleproject/components/fragment.py | 4 +- .../components/nested/calendar/calendar.py | 1 + src/django_components/__init__.py | 3 +- src/django_components/component.py | 97 +-- src/django_components/components/dynamic.py | 5 +- src/django_components/dependencies.py | 199 ++--- tests/e2e/testserver/testserver/views.py | 4 +- tests/test_dependencies.py | 698 +++++++++++++++--- 16 files changed, 1210 insertions(+), 408 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f200a33..aae6950f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,12 +29,14 @@ - The interface of the not-yet-released `get_js_data()` and `get_css_data()` methods has changed to match `get_template_data()`. + Before: + ```py def get_js_data(self, *args, **kwargs): def get_css_data(self, *args, **kwargs): ``` - to: + After: ```py def get_js_data(self, args, kwargs, slots, context): @@ -78,7 +80,7 @@ escape_slots_content: bool = True, args: Optional[ArgsType] = None, kwargs: Optional[KwargsType] = None, - type: RenderType = "document", + deps_strategy: DependenciesStrategy = "document", request: Optional[HttpRequest] = None, *response_args: Any, **response_kwargs: Any, @@ -94,7 +96,7 @@ kwargs: Optional[Mapping] = None, slots: Optional[Mapping] = None, escape_slots_content: bool = True, - type: RenderType = "document", + deps_strategy: DependenciesStrategy = "document", render_dependencies: bool = True, request: Optional[HttpRequest] = None, **response_kwargs: Any, @@ -153,6 +155,20 @@ {% component "profile" name="John" job="Developer" / %} ``` +- The second argument to `render_dependencies()` is now `strategy` instead of `type`. + + Before: + + ```py + render_dependencies(content, type="document") + ``` + + After: + + ```py + render_dependencies(content, strategy="document") + ``` + #### 🚨📢 Deprecation - `get_context_data()` is now deprecated. Use `get_template_data()` instead. @@ -162,10 +178,56 @@ Since `get_context_data()` is widely used, it will remain available until v2. +- The `type` kwarg in `Component.render()` and `Component.render_to_response()` is now deprecated. Use `deps_strategy` instead. The `type` kwarg will be removed in v1. + + Before: + + ```py + Calendar.render_to_response(type="fragment") + ``` + + After: + + ```py + Calendar.render_to_response(deps_strategy="fragment") + ``` + - `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1. #### Feat +- New method to render template variables - `get_template_data()` + + `get_template_data()` behaves the same way as `get_context_data()`, but has + a different function signature to accept also slots and context. + + ```py + class Button(Component): + def get_template_data(self, args, kwargs, slots, context): + return { + "val1": args[0], + "val2": kwargs["field"], + } + ``` + + If you define `Component.Args`, `Component.Kwargs`, `Component.Slots`, then + the `args`, `kwargs`, `slots` arguments will be instances of these classes: + + ```py + class Button(Component): + class Args(NamedTuple): + field1: str + + class Kwargs(NamedTuple): + field2: int + + def get_template_data(self, args: Args, kwargs: Kwargs, slots, context): + return { + "val1": args.field1, + "val2": kwargs.field2, + } + ``` + - Input validation is now part of the render process. When you specify the input types (such as `Component.Args`, `Component.Kwargs`, etc), @@ -176,6 +238,43 @@ Read more on [Typing and validation](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/) +- Render emails or other non-browser HTML with new "dependencies render strategies" + + When rendering a component with `Component.render()` or `Component.render_to_response()`, + the `deps_strategy` kwarg (previously `type`) now accepts a new options `"simple"`, `"prepend"`, or `"append"`. + + ```py + Calendar.render_to_response( + request=request, + kwargs={ + "date": request.GET.get("date", ""), + }, + deps_strategy="append", + ) + ``` + + Comparison of dependencies render strategies: + + - `"document"` + - Smartly inserts JS / CSS into placeholders or into `` and `` tags. + - Inserts extra script to allow `fragment` strategy to work. + - Assumes the HTML will be rendered in a JS-enabled browser. + - `"fragment"` + - A lightweight HTML fragment to be inserted into a document with AJAX. + - Ignores placeholders and any `` / `` tags. + - No JS / CSS included. + - `"simple"` + - Smartly insert JS / CSS into placeholders or into `` and `` tags. + - No extra script loaded. + - `"prepend"` + - Insert JS / CSS before the rendered HTML. + - Ignores placeholders and any `` / `` tags. + - No extra script loaded. + - `"append"` + - Insert JS / CSS after the rendered HTML. + - Ignores placeholders and any `` / `` tags. + - No extra script loaded. + ## v0.139.1 #### Fix diff --git a/docs/concepts/advanced/html_fragments.md b/docs/concepts/advanced/html_fragments.md index b4e8e1ff..dd776461 100644 --- a/docs/concepts/advanced/html_fragments.md +++ b/docs/concepts/advanced/html_fragments.md @@ -1,5 +1,5 @@ -Django-components provides a seamless integration with HTML fragments ([HTML over the wire](https://hotwired.dev/)), -whether you're using HTMX, AlpineJS, or vanilla JavaScript. +Django-components provides a seamless integration with HTML fragments with AJAX ([HTML over the wire](https://hotwired.dev/)), +whether you're using jQuery, HTMX, AlpineJS, or vanilla JavaScript. When you define a component that has extra JS or CSS, and you use django-components to render the fragment, django-components will: @@ -22,15 +22,17 @@ to render the fragment, django-components will: 4. A library like HTMX, AlpineJS, or custom function inserts the new HTML into the correct place. -## Document and fragment types +## Document and fragment strategies -Components support two modes of rendering - As a "document" or as a "fragment". +Components support different "strategies" for rendering JS and CSS. + +Two of them are used to enable HTML fragments - "document" and "fragment". What's the difference? -### Document mode +### Document strategy -Document mode assumes that the rendered components will be embedded into the HTML +Document strategy assumes that the rendered components will be embedded into the HTML of the initial page load. This means that: - The JS and CSS is embedded into the HTML as ` + + ``` + +- Components' secondary JS and CSS scripts + ([`Component.Media`](../../../reference/api/#django_components.Component.Media)) - inserted as links: + + ```html + + + ``` + +- A JS script is injected to manage component dependencies, enabling lazy loading of JS and CSS + for HTML fragments. + +!!! info + + This strategy is required for fragments to work properly, as it sets up the dependency manager that fragments rely on. + +!!! note "How the dependency manager works" + + The dependency manager is a JS script that keeps track of all the JS and CSS dependencies that have already been loaded. + + When a fragment is inserted into the page, it will also insert a JSON ` + + ``` + +- Components' secondary JS and CSS scripts + ([`Component.Media`](../../../reference/api/#django_components.Component.Media)) - inserted as links: + + ```html + + + ``` + +- No extra scripts are inserted. + +### `prepend` + +This is the same as [`"simple"`](#simple), but placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies) and HTML tags `` and `` are all ignored. The JS and CSS are **always** inserted **before** the rendered content. + +```python +html = MyComponent.render(deps_strategy="prepend") +``` + +**Location:** + +JS and CSS is **always** inserted before the rendered content. + +**Included scripts:** + +Same as for the [`"simple"`](#simple) strategy. + +### `append` + +This is the same as [`"simple"`](#simple), but placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies) and HTML tags `` and `` are all ignored. The JS and CSS are **always** inserted **after** the rendered content. + +```python +html = MyComponent.render(deps_strategy="append") +``` + +**Location:** + +JS and CSS is **always** inserted after the rendered content. + +**Included scripts:** + +Same as for the [`"simple"`](#simple) strategy. diff --git a/docs/concepts/fundamentals/html_js_css_files.md b/docs/concepts/fundamentals/html_js_css_files.md index af49357a..69a8d5e3 100644 --- a/docs/concepts/fundamentals/html_js_css_files.md +++ b/docs/concepts/fundamentals/html_js_css_files.md @@ -145,7 +145,7 @@ Here is how the HTML is post-processed: ``` -3. **Insert JS and CSS**: After the HTML is rendered, Django Components handles inserting JS and CSS dependencies into the page based on the [render type](../rendering_components/#render-types) (document, fragment, or inline). +3. **Insert JS and CSS**: After the HTML is rendered, Django Components handles inserting JS and CSS dependencies into the page based on the [dependencies rendering strategy](../rendering_components/#dependencies-rendering) (document, fragment, or inline). For example, if your component contains the [`{% component_js_dependencies %}`](../../reference/template_tags.md#component_js_dependencies) diff --git a/docs/concepts/fundamentals/rendering_components.md b/docs/concepts/fundamentals/rendering_components.md index e9446f94..bed2709f 100644 --- a/docs/concepts/fundamentals/rendering_components.md +++ b/docs/concepts/fundamentals/rendering_components.md @@ -244,7 +244,7 @@ Button.render( - `kwargs` - Keyword arguments to pass to the component (as a dictionary) - `slots` - Slot content to pass to the component (as a dictionary) - `context` - Django context for rendering (can be a dictionary or a `Context` object) -- `type` - Type of rendering (default: `"document"`) +- `deps_strategy` - Dependencies rendering strategy (default: `"document"`) - `request` - HTTP request object, used for context processors (optional) - `escape_slots_content` - Whether to HTML-escape slot content (default: `True`) - `render_dependencies` - Whether to process JS and CSS dependencies (default: `True`) @@ -336,102 +336,66 @@ response = MyComponent.render_to_response() assert isinstance(response, MyHttpResponse) ``` -## Render types +## Dependencies rendering -The rendered HTML may be used in different contexts (browser, email, etc). -If your components use JS and CSS scripts, you need to handle them differently. +The rendered HTML may be used in different contexts (browser, email, etc), and each may need different handling of JS and CSS scripts. [`render()`](../../../reference/api/#django_components.Component.render) and [`render_to_response()`](../../../reference/api/#django_components.Component.render_to_response) -accept a `type` parameter, which controls this behavior. +accept a `deps_strategy` parameter, which controls where and how the JS / CSS are inserted into the HTML. -The `type` parameter is set at the root of a component render tree, which is why it is not available for -the [`{% component %}`](../../../reference/template_tags#component) tag. +The `deps_strategy` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies). + +Learn more about [Rendering JS / CSS](../../advanced/rendering_js_css). + +There are five dependencies rendering strategies: + +- [`document`](../../advanced/rendering_js_css#document) (default) + - Smartly inserts JS / CSS into placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) or into `` and `` tags. + - Inserts extra script to allow `fragment` components to work. + - Assumes the HTML will be rendered in a JS-enabled browser. +- [`fragment`](../../advanced/rendering_js_css#fragment) + - A lightweight HTML fragment to be inserted into a document with AJAX. + - Assumes the page was already rendered with `"document"` strategy. + - No JS / CSS included. +- [`simple`](../../advanced/rendering_js_css#simple) + - Smartly insert JS / CSS into placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) or into `` and `` tags. + - No extra script loaded. +- [`prepend`](../../advanced/rendering_js_css#prepend) + - Insert JS / CSS before the rendered HTML. + - Ignores the placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) and any ``/`` HTML tags. + - No extra script loaded. +- [`append`](../../advanced/rendering_js_css#append) + - Insert JS / CSS after the rendered HTML. + - Ignores the placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) and any ``/`` HTML tags. + - No extra script loaded. !!! info - The `type` parameter is ultimately passed to [`render_dependencies()`](../../../reference/api/#django_components.render_dependencies). - Learn more about [Rendering JS / CSS](../../advanced/rendering_js_css). + You can use the `"prepend"` and `"append"` strategies to force to output JS / CSS for components + that don't have neither the placeholders like [`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies), nor any ``/`` HTML tags: -There are three render types: - -### `document` - -`type="document"` is the default. Use this if you are rendering a whole page, or if no other option suits better. - -```python -html = Button.render(type="document") -``` - -When you render a component tree with the `"document"` type, it is expected that: - -- The HTML will be rendered at page load. -- The HTML will be inserted into a page / browser where JS can be executed. - -With this setting, the JS and CSS is set up to avoid any delays for end users: - -- Components' primary JS and CSS scripts ([`Component.js`](../../../reference/api/#django_components.Component.js) - and [`Component.css`](../../../reference/api/#django_components.Component.css)) are inlined into the rendered HTML. - - ```html - - + ```py + rendered = Calendar.render_to_response( + request=request, + kwargs={ + "date": request.GET.get("date", ""), + }, + deps_strategy="append", + ) ``` -- Components' secondary JS and CSS scripts ([`Component.Media`](../../../reference/api/#django_components.Component.Media)) - are inserted into the rendered HTML as links. + Renders something like this: ```html - - + +
+ ... +
+ + + ``` -- A JS script is injected to manage component dependencies, enabling lazy loading of JS and CSS - for HTML fragments. - -!!! info - - This render type is required for fragments to work properly, as it sets up the dependency manager that fragments rely on. - -!!! note "How the dependency manager works" - - The dependency manager is a JS script that keeps track of all the JS and CSS dependencies that have already been loaded. - - When a fragment is inserted into the page, it will also insert a JSON `', rendered, count=0) + + # Check that it contains inlined JS and CSS, and Media.css + assert rendered.strip() == ( + 'Variable: foo\n' + ' ' + ) + + @djc_test class TestRenderDependencies: def test_standalone_render_dependencies(self): @@ -61,7 +79,7 @@ class TestRenderDependencies: {% component 'test' variable='foo' / %} """ template = Template(template_str) - rendered_raw = template.render(Context({})) + rendered_raw: str = template.render(Context({})) # Placeholders assert rendered_raw.count('') == 1 @@ -194,103 +212,6 @@ class TestRenderDependencies: assert rendered.count(" - - - - {% component "test" variable="foo" / %} - - - """ - rendered_raw = Template(template_str).render(Context({})) - rendered = render_dependencies(rendered_raw) - - assert rendered.count(" - - - - """, - rendered, - count=1, - ) - - body_re = re.compile(r"(.*?)", re.DOTALL) - rendered_body = body_re.search(rendered).group(1) # type: ignore[union-attr] - - assertInHTML( - """', - 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 %} - - - - {% component_js_dependencies %} - - - {% component "test" variable="foo" / %} - {% component_css_dependencies %} - - - """ - rendered_raw = Template(template_str).render(Context({})) - rendered = render_dependencies(rendered_raw) - - assert rendered.count(" - Variable: foo - - - - - """, - rendered, - count=1, - ) - - head_re = re.compile(r"(.*?)", re.DOTALL) - rendered_head = head_re.search(rendered).group(1) # type: ignore[union-attr] - - assertInHTML( - """', - rendered_head, - count=1, - ) - # NOTE: Some HTML parser libraries like selectolax or lxml try to "correct" the given HTML. # We want to avoid this behavior, so user gets the exact same HTML back. def test_does_not_try_to_add_close_tags(self): @@ -301,7 +222,7 @@ class TestRenderDependencies: """ rendered_raw = Template(template_str).render(Context({"formset": [1]})) - rendered = render_dependencies(rendered_raw, type="fragment") + rendered = render_dependencies(rendered_raw, strategy="fragment") assertHTMLEqual(rendered, "") @@ -336,7 +257,7 @@ class TestRenderDependencies: """ rendered_raw = Template(template_str).render(Context({"formset": [1]})) - rendered = render_dependencies(rendered_raw, type="fragment") + rendered = render_dependencies(rendered_raw, strategy="fragment") expected = """ @@ -399,7 +320,7 @@ class TestRenderDependencies: """ rendered_raw = Template(template_str).render(Context({"formset": [1]})) - rendered = render_dependencies(rendered_raw, type="fragment") + rendered = render_dependencies(rendered_raw, strategy="fragment") # Base64 encodings: # `PGxpbmsgaHJlZj0ic3R5bGUuY3NzIiBtZWRpYT0iYWxsIiByZWw9InN0eWxlc2hlZXQiPg==` -> `` # noqa: E501 @@ -472,6 +393,581 @@ class TestRenderDependencies: ComponentWithScript.render(kwargs={"variable": "foo"}) +@djc_test +class TestDependenciesStrategyDocument: + def test_inserts_styles_and_script_to_default_places_if_not_overriden(self): + registry.register(name="test", component=SimpleComponent) + + template_str: types.django_html = """ + {% load component_tags %} + + + + + {% component "test" variable="foo" / %} + + + """ + rendered_raw = Template(template_str).render(Context({})) + rendered = render_dependencies(rendered_raw, strategy="document") + + assert rendered.count(" + + + + """, + rendered, + count=1, + ) + + body_re = re.compile(r"(.*?)", re.DOTALL) + rendered_body = body_re.search(rendered).group(1) # type: ignore[union-attr] + + assertInHTML( + """', + 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 %} + + + + {% component_js_dependencies %} + + + {% component "test" variable="foo" / %} + {% component_css_dependencies %} + + + """ + rendered_raw = Template(template_str).render(Context({})) + rendered = render_dependencies(rendered_raw, strategy="document") + + assert rendered.count(" + Variable: foo + + + + + """, + rendered, + count=1, + ) + + head_re = re.compile(r"(.*?)", re.DOTALL) + rendered_head = head_re.search(rendered).group(1) # type: ignore[union-attr] + + assertInHTML( + """', + rendered_head, + count=1, + ) + + +@djc_test +class TestDependenciesStrategySimple: + def test_single_component(self): + registry.register(name="test", component=SimpleComponent) + + template_str: types.django_html = """ + {% load component_tags %} + {% component_js_dependencies %} + {% component_css_dependencies %} + {% component 'test' variable='foo' / %} + """ + template = Template(template_str) + rendered_raw: str = template.render(Context({})) + + # Placeholders + assert rendered_raw.count('') == 1 + assert rendered_raw.count('') == 1 + + assert rendered_raw.count("', rendered, count=0) + + # Check that it contains inlined JS and CSS, and Media.css + assert rendered.strip() == ( + '\n' + " \n' + " \n" + ' Variable: foo' + ) + + def test_multiple_components_dependencies(self): + class SimpleComponentNested(Component): + template: types.django_html = """ + {% load component_tags %} +
+ {% component "inner" variable=variable / %} + {% slot "default" default / %} +
+ """ + + css: types.css = """ + .my-class { + color: red; + } + """ + + js: types.js = """ + console.log("Hello"); + """ + + def get_context_data(self, variable): + return {} + + class Media: + css = ["style.css", "style2.css"] + js = "script2.js" + + class OtherComponent(Component): + template: types.django_html = """ + XYZ: {{ variable }} + """ + + css: types.css = """ + .xyz { + color: red; + } + """ + + js: types.js = """ + console.log("xyz"); + """ + + def get_context_data(self, variable): + return {} + + class Media: + css = "xyz1.css" + js = "xyz1.js" + + registry.register(name="inner", component=SimpleComponent) + registry.register(name="outer", component=SimpleComponentNested) + registry.register(name="other", component=OtherComponent) + + template_str: types.django_html = """ + {% load component_tags %} + {% component_js_dependencies %} + {% component_css_dependencies %} + {% component 'outer' variable='variable' %} + {% component 'other' variable='variable_inner' / %} + {% endcomponent %} + """ + template = Template(template_str) + rendered_raw: str = template.render(Context({})) + + rendered = render_dependencies(rendered_raw, strategy="simple") + + # Dependency manager script NOT present + assertInHTML('', rendered, count=0) + + assert rendered.count(".my-class { color: red; } + + """, + rendered, + count=1, + ) + + # Components' Media.css + # Order: + # - "style.css", "style2.css" (from SimpleComponentNested) + # - "style.css" (from SimpleComponent inside SimpleComponentNested) + # - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested) + assertInHTML( + """ + + + + """, + rendered, + count=1, + ) + + # Components' Media.js followed by inlined JS + # Order: + # - "script2.js" (from SimpleComponentNested) + # - "script.js" (from SimpleComponent inside SimpleComponentNested) + # - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested) + assertInHTML( + """ + + + + + + """, + rendered, + count=1, + ) + + # Check that there's no payload like with "document" or "fragment" modes + assert "application/json" not in rendered + + +@djc_test +class TestDependenciesStrategyPrepend: + def test_single_component(self): + registry.register(name="test", component=SimpleComponent) + + template_str: types.django_html = """ + {% load component_tags %} + {% component_js_dependencies %} + {% component_css_dependencies %} + {% component 'test' variable='foo' / %} + """ + template = Template(template_str) + rendered_raw: str = template.render(Context({})) + + # Placeholders + assert rendered_raw.count('') == 1 + assert rendered_raw.count('') == 1 + + assert rendered_raw.count("', rendered, count=0) + + # Check that it contains inlined JS and CSS, and Media.css + assert rendered.strip() == ( + '\n' + " \n" + " \n" + " \n" + " \n" + ' Variable: foo' + ) + + def test_multiple_components_dependencies(self): + class SimpleComponentNested(Component): + template: types.django_html = """ + {% load component_tags %} +
+ {% component "inner" variable=variable / %} + {% slot "default" default / %} +
+ """ + + css: types.css = """ + .my-class { + color: red; + } + """ + + js: types.js = """ + console.log("Hello"); + """ + + def get_context_data(self, variable): + return {} + + class Media: + css = ["style.css", "style2.css"] + js = "script2.js" + + class OtherComponent(Component): + template: types.django_html = """ + XYZ: {{ variable }} + """ + + css: types.css = """ + .xyz { + color: red; + } + """ + + js: types.js = """ + console.log("xyz"); + """ + + def get_context_data(self, variable): + return {} + + class Media: + css = "xyz1.css" + js = "xyz1.js" + + registry.register(name="inner", component=SimpleComponent) + registry.register(name="outer", component=SimpleComponentNested) + registry.register(name="other", component=OtherComponent) + + template_str: types.django_html = """ + {% load component_tags %} + {% component_js_dependencies %} + {% component_css_dependencies %} + {% component 'outer' variable='variable' %} + {% component 'other' variable='variable_inner' / %} + {% endcomponent %} + """ + template = Template(template_str) + rendered_raw: str = template.render(Context({})) + + rendered = render_dependencies(rendered_raw, strategy="prepend") + + # Dependency manager script NOT present + assertInHTML('', rendered, count=0) + + assert rendered.count(".my-class { color: red; } + + """, + rendered, + count=1, + ) + + # Components' Media.css + # Order: + # - "style.css", "style2.css" (from SimpleComponentNested) + # - "style.css" (from SimpleComponent inside SimpleComponentNested) + # - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested) + assertInHTML( + """ + + + + """, + rendered, + count=1, + ) + + # Components' Media.js followed by inlined JS + # Order: + # - "script2.js" (from SimpleComponentNested) + # - "script.js" (from SimpleComponent inside SimpleComponentNested) + # - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested) + assertInHTML( + """ + + + + + + """, + rendered, + count=1, + ) + + # Check that there's no payload like with "document" or "fragment" modes + assert "application/json" not in rendered + + +@djc_test +class TestDependenciesStrategyAppend: + def test_single_component(self): + registry.register(name="test", component=SimpleComponent) + + template_str: types.django_html = """ + {% load component_tags %} + {% component_js_dependencies %} + {% component_css_dependencies %} + {% component 'test' variable='foo' / %} + """ + template = Template(template_str) + rendered_raw: str = template.render(Context({})) + + # Placeholders + assert rendered_raw.count('') == 1 + assert rendered_raw.count('') == 1 + + assert rendered_raw.count("', rendered, count=0) + + # Check that it contains inlined JS and CSS, and Media.css + assert rendered.strip() == ( + 'Variable: foo\n' + " \n" + ' ' + ) + + def test_multiple_components_dependencies(self): + class SimpleComponentNested(Component): + template: types.django_html = """ + {% load component_tags %} +
+ {% component "inner" variable=variable / %} + {% slot "default" default / %} +
+ """ + + css: types.css = """ + .my-class { + color: red; + } + """ + + js: types.js = """ + console.log("Hello"); + """ + + def get_context_data(self, variable): + return {} + + class Media: + css = ["style.css", "style2.css"] + js = "script2.js" + + class OtherComponent(Component): + template: types.django_html = """ + XYZ: {{ variable }} + """ + + css: types.css = """ + .xyz { + color: red; + } + """ + + js: types.js = """ + console.log("xyz"); + """ + + def get_context_data(self, variable): + return {} + + class Media: + css = "xyz1.css" + js = "xyz1.js" + + registry.register(name="inner", component=SimpleComponent) + registry.register(name="outer", component=SimpleComponentNested) + registry.register(name="other", component=OtherComponent) + + template_str: types.django_html = """ + {% load component_tags %} + {% component_js_dependencies %} + {% component_css_dependencies %} + {% component 'outer' variable='variable' %} + {% component 'other' variable='variable_inner' / %} + {% endcomponent %} + """ + template = Template(template_str) + rendered_raw: str = template.render(Context({})) + + rendered = render_dependencies(rendered_raw, strategy="append") + + # Dependency manager script NOT present + assertInHTML('', rendered, count=0) + + assert rendered.count(".my-class { color: red; } + + """, + rendered, + count=1, + ) + + # Components' Media.css + # Order: + # - "style.css", "style2.css" (from SimpleComponentNested) + # - "style.css" (from SimpleComponent inside SimpleComponentNested) + # - "xyz1.css" (from OtherComponent inserted into SimpleComponentNested) + assertInHTML( + """ + + + + """, + rendered, + count=1, + ) + + # Components' Media.js followed by inlined JS + # Order: + # - "script2.js" (from SimpleComponentNested) + # - "script.js" (from SimpleComponent inside SimpleComponentNested) + # - "xyz1.js" (from OtherComponent inserted into SimpleComponentNested) + assertInHTML( + """ + + + + + + """, + rendered, + count=1, + ) + + # Check that there's no payload like with "document" or "fragment" modes + assert "application/json" not in rendered + + @djc_test class TestMiddleware: def test_middleware_response_without_content_type(self):