mirror of
https://github.com/django-components/django-components.git
synced 2025-09-01 03:37:20 +00:00
feat: render fragments without document strategy (#1339)
This commit is contained in:
parent
aa14e3698d
commit
c72fed8255
11 changed files with 217 additions and 26 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
@ -1,5 +1,20 @@
|
|||
# Release notes
|
||||
|
||||
## v0.141.3
|
||||
|
||||
#### Feat
|
||||
|
||||
- You no longer need to render the whole page with the `document` strategy to use HTML fragments.
|
||||
|
||||
Previously, if you wanted to insert rendered components as HTML fragments, you had to ensure
|
||||
that the HTML document it was being inserted into was rendered with the `document` strategy.
|
||||
|
||||
Now, when you render components with `fragment` strategy, they know how to fetch their own JS / CSS dependencies.
|
||||
|
||||
#### Fix
|
||||
|
||||
- Fix compatibility with django-template-partials ([#1322](https://github.com/django-components/django-components/issues/1322))
|
||||
|
||||
## v0.141.2
|
||||
|
||||
#### Fix
|
||||
|
|
|
@ -142,11 +142,12 @@ There are six dependencies strategies:
|
|||
|
||||
- [`document`](../../advanced/rendering_js_css#document) (default)
|
||||
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||
- Inserts extra script to allow `fragment` strategy to work.
|
||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
||||
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||
- Inserts extra script for managing fragments.
|
||||
- [`fragment`](../../advanced/rendering_js_css#fragment)
|
||||
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
||||
- No JS / CSS included.
|
||||
- Fragment will fetch its own JS / CSS dependencies when inserted into the page.
|
||||
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||
- [`simple`](../../advanced/rendering_js_css#simple)
|
||||
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||
- No extra script loaded.
|
||||
|
@ -212,10 +213,6 @@ the page in the browser:
|
|||
- 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.
|
||||
|
@ -228,8 +225,7 @@ the page in the browser:
|
|||
|
||||
### `fragment`
|
||||
|
||||
`deps_strategy="fragment"` is used when rendering a piece of HTML that will be inserted into a page
|
||||
that has already been rendered with the [`"document"`](#document) strategy:
|
||||
`deps_strategy="fragment"` is used when rendering a piece of HTML that will be inserted into a page:
|
||||
|
||||
```python
|
||||
fragment = MyComponent.render(deps_strategy="fragment")
|
||||
|
@ -426,6 +422,6 @@ assert html == html2
|
|||
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.
|
||||
This is achieved by the [`"fragment"`](#fragment) strategy.
|
||||
|
||||
Read more about [HTML fragments](../../advanced/html_fragments).
|
||||
|
|
|
@ -349,12 +349,12 @@ 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.
|
||||
- Inserts extra script to allow `fragment` components to work.
|
||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
||||
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||
- Inserts extra script for managing fragments.
|
||||
- [`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.
|
||||
- Fragment will fetch its own JS / CSS dependencies when inserted into the page.
|
||||
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||
- [`simple`](../../advanced/rendering_js_css#simple)
|
||||
- Smartly insert JS / CSS into placeholders ([`{% component_js_dependencies %}`](../../../reference/template_tags#component_js_dependencies)) or into `<head>` and `<body>` tags.
|
||||
- No extra script loaded.
|
||||
|
|
|
@ -2697,11 +2697,12 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
- [`"document"`](../../concepts/advanced/rendering_js_css#document) (default)
|
||||
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||
- Inserts extra script to allow `fragment` types to work.
|
||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
||||
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||
- Inserts extra script for managing fragments.
|
||||
- [`"fragment"`](../../concepts/advanced/rendering_js_css#fragment)
|
||||
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
||||
- No JS / CSS included.
|
||||
- Fragment will fetch its own JS / CSS dependencies when inserted into the page.
|
||||
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||
- [`"simple"`](../../concepts/advanced/rendering_js_css#simple)
|
||||
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||
- No extra script loaded.
|
||||
|
@ -3186,11 +3187,12 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
- [`"document"`](../../concepts/advanced/rendering_js_css#document) (default)
|
||||
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||
- Inserts extra script to allow `fragment` types to work.
|
||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
||||
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||
- Inserts extra script for managing fragments.
|
||||
- [`"fragment"`](../../concepts/advanced/rendering_js_css#fragment)
|
||||
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
||||
- No JS / CSS included.
|
||||
- Fragment will fetch its own JS / CSS dependencies when inserted into the page.
|
||||
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||
- [`"simple"`](../../concepts/advanced/rendering_js_css#simple)
|
||||
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||
- No extra script loaded.
|
||||
|
|
|
@ -476,6 +476,28 @@ def render_dependencies(content: TContent, strategy: DependenciesStrategy = "doc
|
|||
_render_dependencies = render_dependencies
|
||||
|
||||
|
||||
def _pre_loader_js() -> str:
|
||||
"""
|
||||
This script checks if our dependency manager script is already loaded on the page,
|
||||
and loads the manager if not yet.
|
||||
|
||||
This script is included with every "fragment", so that the "fragments" can be rendered
|
||||
even on pages that weren't rendered with the "document" strategy.
|
||||
"""
|
||||
manager_url = static("django_components/django_components.min.js")
|
||||
return f"""
|
||||
(() => {{
|
||||
if (!globalThis.Components) {{
|
||||
const s = document.createElement('script');
|
||||
s.src = "{manager_url}";
|
||||
document.head.appendChild(s);
|
||||
}}
|
||||
// Remove this loader script
|
||||
if (document.currentScript) document.currentScript.remove();
|
||||
}})();
|
||||
"""
|
||||
|
||||
|
||||
# Overview of this function:
|
||||
# 1. We extract all HTML comments like `<!-- _RENDERED table_10bac31,1234-->`.
|
||||
# 2. We look up the corresponding component classes
|
||||
|
@ -647,6 +669,20 @@ def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) ->
|
|||
js=[static("django_components/django_components.min.js")] if strategy == "document" else [],
|
||||
).render_js()
|
||||
|
||||
# Core scripts without which the rest wouldn't work
|
||||
core_script_tags = []
|
||||
if strategy == "document":
|
||||
# For full documents, load manager as a normal external <script src="...">
|
||||
core_script_tags = Media(js=[static("django_components/django_components.min.js")]).render_js()
|
||||
elif strategy == "fragment":
|
||||
# For fragments, inline a script that conditionally injects the dependency manager
|
||||
# if it's not already loaded.
|
||||
#
|
||||
# TODO: Eventually we want to parametrize how the `<script>` tag is rendered
|
||||
# (e.g. to use `type="module"`, `defer`, or csp nonce) based on which component
|
||||
# it was defined in.
|
||||
core_script_tags = [mark_safe(f"<script>{_pre_loader_js()}</script>")]
|
||||
|
||||
final_script_tags = "".join(
|
||||
[
|
||||
# JS by us
|
||||
|
|
|
@ -1 +1 @@
|
|||
(()=>{var x=o=>new DOMParser().parseFromString(o,"text/html").documentElement.textContent,E=Array.isArray,m=o=>typeof o=="function",H=o=>o!==null&&typeof o=="object",S=o=>(H(o)||m(o))&&m(o.then)&&m(o.catch);function N(o,i){try{return i?o.apply(null,i):o()}catch(s){L(s)}}function g(o,i){if(m(o)){let s=N(o,i);return s&&S(s)&&s.catch(c=>{L(c)}),[s]}if(E(o)){let s=[];for(let c=0;c<o.length;c++)s.push(g(o[c],i));return s}else console.warn(`[Components] Invalid value type passed to callWithAsyncErrorHandling(): ${typeof o}`)}function L(o){console.error(o)}var M=o=>{let i=new MutationObserver(s=>{for(let c of s)c.type==="childList"&&c.addedNodes.forEach(d=>{d.nodeName==="SCRIPT"&&d.hasAttribute("data-djc")&&o(d)})});return i.observe(document,{childList:!0,subtree:!0}),i};var y=()=>{let o=new Set,i=new Set,s={},c={},d=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("script");if(!e)throw Error("[Components] Failed to extract <script> tag. Make sure that the string contains <script><\/script> and is a valid HTML");return e},F=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("link");if(!e)throw Error("[Components] Failed to extract <link> tag. Make sure that the string contains <link></link> and is a valid HTML");return e},T=t=>{let e=document.createElement(t.tagName);e.innerHTML=t.innerHTML;for(let r of t.attributes)e.setAttributeNode(r.cloneNode());return e},f=t=>{let e=d(t),r=e.getAttribute("src");if(!r||C("js",r))return;p("js",r);let a=T(e),l=e.getAttribute("async")!=null||e.getAttribute("defer")!=null||e.getAttribute("type")==="module";a.async=l;let u=new Promise((n,b)=>{a.onload=()=>{n()},globalThis.document.body.append(a)});return{el:a,promise:u}},h=t=>{let e=F(t),r=e.getAttribute("href");if(!r||C("css",r))return;let a=T(e);return globalThis.document.head.append(a),p("css",r),{el:a,promise:Promise.resolve()}},p=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] markScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);(t==="js"?o:i).add(e)},C=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] isScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);return(t==="js"?o:i).has(e)},w=(t,e)=>{s[t]=e},j=(t,e,r)=>{let a=`${t}:${e}`;c[a]=r},A=(t,e,r)=>{let a=s[t];if(!a)throw Error(`[Components] '${t}': No component registered for that name`);let l=Array.from(document.querySelectorAll(`[data-djc-id-${e}]`));if(!l.length)throw Error(`[Components] '${t}': No elements with component ID '${e}' found`);let u=`${t}:${r}`,n=c[u];if(!n)throw Error(`[Components] '${t}': Cannot find input for hash '${r}'`);let b=n(),v={name:t,id:e,els:l},[P]=g(a,[b,v]);return P},k=async t=>{let e=t.loadedCssUrls.map(n=>atob(n)),r=t.loadedJsUrls.map(n=>atob(n)),a=t.toLoadCssTags.map(n=>atob(n)),l=t.toLoadJsTags.map(n=>atob(n));e.forEach(n=>p("css",n)),r.forEach(n=>p("js",n)),Promise.all(a.map(n=>h(n))).catch(console.error);let u=Promise.all(l.map(n=>f(n))).catch(console.error)};return M(t=>{let e=JSON.parse(t.text);k(e)}),{callComponent:A,registerComponent:w,registerComponentData:j,loadJs:f,loadCss:h,markScriptLoaded:p}};var $={manager:y(),createComponentsManager:y,unescapeJs:x};globalThis.Components=$;})();
|
||||
(()=>{var x=o=>new DOMParser().parseFromString(o,"text/html").documentElement.textContent,E=Array.isArray,m=o=>typeof o=="function",N=o=>o!==null&&typeof o=="object",L=o=>(N(o)||m(o))&&m(o.then)&&m(o.catch);function $(o,i){try{return i?o.apply(null,i):o()}catch(s){M(s)}}function g(o,i){if(m(o)){let s=$(o,i);return s&&L(s)&&s.catch(c=>{M(c)}),[s]}if(E(o)){let s=[];for(let c=0;c<o.length;c++)s.push(g(o[c],i));return s}else console.warn(`[Components] Invalid value type passed to callWithAsyncErrorHandling(): ${typeof o}`)}function M(o){console.error(o)}var j=o=>{let i=new MutationObserver(s=>{for(let c of s)c.type==="childList"&&c.addedNodes.forEach(p=>{p.nodeName==="SCRIPT"&&p.hasAttribute("data-djc")&&o(p)})});return i.observe(document,{childList:!0,subtree:!0}),i};var y=()=>{let o=new Set,i=new Set,s={},c={},p=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("script");if(!e)throw Error("[Components] Failed to extract <script> tag. Make sure that the string contains <script><\/script> and is a valid HTML");return e},F=t=>{let e=new DOMParser().parseFromString(t,"text/html").querySelector("link");if(!e)throw Error("[Components] Failed to extract <link> tag. Make sure that the string contains <link></link> and is a valid HTML");return e},T=t=>{let e=document.createElement(t.tagName);e.innerHTML=t.innerHTML;for(let r of t.attributes)e.setAttributeNode(r.cloneNode());return e},f=t=>{let e=p(t),r=e.getAttribute("src");if(!r||C("js",r))return;d("js",r);let a=T(e),l=e.getAttribute("async")!=null||e.getAttribute("defer")!=null||e.getAttribute("type")==="module";a.async=l;let u=new Promise((n,b)=>{a.onload=()=>{n()},globalThis.document.body.append(a)});return{el:a,promise:u}},h=t=>{let e=F(t),r=e.getAttribute("href");if(!r||C("css",r))return;let a=T(e);return globalThis.document.head.append(a),d("css",r),{el:a,promise:Promise.resolve()}},d=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] markScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);(t==="js"?o:i).add(e)},C=(t,e)=>{if(t!=="js"&&t!=="css")throw Error(`[Components] isScriptLoaded received invalid script type '${t}'. Must be one of 'js', 'css'`);return(t==="js"?o:i).has(e)},w=(t,e)=>{s[t]=e},A=(t,e,r)=>{let a=`${t}:${e}`;c[a]=r},H=(t,e,r)=>{let a=s[t];if(!a)throw Error(`[Components] '${t}': No component registered for that name`);let l=Array.from(document.querySelectorAll(`[data-djc-id-${e}]`));if(!l.length)throw Error(`[Components] '${t}': No elements with component ID '${e}' found`);let u=`${t}:${r}`,n=c[u];if(!n)throw Error(`[Components] '${t}': Cannot find input for hash '${r}'`);let b=n(),v={name:t,id:e,els:l},[P]=g(a,[b,v]);return P},k=async t=>{let e=t.loadedCssUrls.map(n=>atob(n)),r=t.loadedJsUrls.map(n=>atob(n)),a=t.toLoadCssTags.map(n=>atob(n)),l=t.toLoadJsTags.map(n=>atob(n));e.forEach(n=>d("css",n)),r.forEach(n=>d("js",n)),Promise.all(a.map(n=>h(n))).catch(console.error);let u=Promise.all(l.map(n=>f(n))).catch(console.error)},S=t=>{let e=JSON.parse(t.text);k(e)};return j(S),document.querySelectorAll("script[data-djc]").forEach(S),{callComponent:H,registerComponent:w,registerComponentData:A,loadJs:f,loadCss:h,markScriptLoaded:d}};var J={manager:y(),createComponentsManager:y,unescapeJs:x};globalThis.Components=J;})();
|
||||
|
|
|
@ -258,11 +258,17 @@ export const createComponentsManager = () => {
|
|||
.catch(console.error);
|
||||
};
|
||||
|
||||
// Initialise the MutationObserver that watches for `<script>` tags with `data-djc` attribute
|
||||
observeScriptTag((script) => {
|
||||
const onDjcScript = (script: HTMLScriptElement) => {
|
||||
const data = JSON.parse(script.text);
|
||||
_loadComponentScripts(data);
|
||||
});
|
||||
};
|
||||
|
||||
// Initialise the MutationObserver that watches for `<script>` tags with `data-djc` attribute
|
||||
observeScriptTag(onDjcScript);
|
||||
|
||||
// Also consider any embedded scripts at the moment the file is loaded.
|
||||
const existingScripts = document.querySelectorAll<HTMLScriptElement>('script[data-djc]');
|
||||
existingScripts.forEach(onDjcScript);
|
||||
|
||||
return {
|
||||
callComponent,
|
||||
|
|
|
@ -10,6 +10,7 @@ from testserver.views import (
|
|||
check_js_order_vars_not_available_before_view,
|
||||
fragment_base_alpine_view,
|
||||
fragment_base_htmx_view,
|
||||
fragment_base_htmx_view__raw,
|
||||
fragment_base_js_view,
|
||||
fragment_view,
|
||||
multiple_components_view,
|
||||
|
@ -28,6 +29,7 @@ urlpatterns = [
|
|||
path("js-order/invalid", check_js_order_vars_not_available_before_view),
|
||||
path("fragment/base/alpine", fragment_base_alpine_view),
|
||||
path("fragment/base/htmx", fragment_base_htmx_view),
|
||||
path("fragment/base/htmx_raw", fragment_base_htmx_view__raw),
|
||||
path("fragment/base/js", fragment_base_js_view),
|
||||
path("fragment/frag", fragment_view),
|
||||
path("alpine/head", alpine_in_head_view),
|
||||
|
|
|
@ -146,7 +146,15 @@ def fragment_base_js_view(request):
|
|||
.then(response => response.text())
|
||||
.then(html => {
|
||||
console.log({ fragment: html })
|
||||
document.querySelector('#target').outerHTML = html;
|
||||
const target = document.querySelector('#target');
|
||||
const a = new DOMParser().parseFromString(html, "text/html");
|
||||
target.replaceWith(...a.body.childNodes);
|
||||
for (const script of a.querySelectorAll('script')) {
|
||||
const newScript = document.createElement('script');
|
||||
newScript.textContent = script.textContent;
|
||||
newScript.async = false;
|
||||
document.body.appendChild(newScript);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -239,6 +247,38 @@ def fragment_base_htmx_view(request):
|
|||
return HttpResponse(rendered)
|
||||
|
||||
|
||||
# HTML into which a fragment will be loaded using HTMX
|
||||
# This variant doesn't include the component manager script, so that we can test
|
||||
# that the fragment can be rendered even when the page wasn't rendered with the "document" strategy.
|
||||
def fragment_base_htmx_view__raw(request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% component_css_dependencies %}
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
</head>
|
||||
<body>
|
||||
{% component 'inner' variable='foo' / %}
|
||||
|
||||
<div id="target">OLD</div>
|
||||
|
||||
<button id="loader" hx-get="/fragment/frag?frag={{ frag }}" hx-swap="outerHTML" hx-target="#target">
|
||||
Click me!
|
||||
</button>
|
||||
|
||||
{% component_js_dependencies %}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
template = Template(template_str)
|
||||
|
||||
frag = request.GET["frag"]
|
||||
rendered = template.render(Context({"frag": frag, "DJC_DEPS_STRATEGY": "ignore"}))
|
||||
return HttpResponse(rendered)
|
||||
|
||||
|
||||
def fragment_view(request):
|
||||
fragment_type = request.GET["frag"]
|
||||
if fragment_type == "comp":
|
||||
|
|
|
@ -366,7 +366,24 @@ class TestRenderDependencies:
|
|||
rendered_raw = Template(template_str).render(Context({"formset": [1], "DJC_DEPS_STRATEGY": "ignore"}))
|
||||
rendered = render_dependencies(rendered_raw, strategy="fragment")
|
||||
|
||||
assertHTMLEqual(rendered, "<thead>")
|
||||
# NOTE: Fragments adds a script to optionally load the component manager script.
|
||||
assertHTMLEqual(
|
||||
rendered,
|
||||
"""
|
||||
<thead>
|
||||
<script>
|
||||
(() => {
|
||||
if (!globalThis.Components) {
|
||||
const s = document.createElement('script');
|
||||
s.src = "django_components/django_components.min.js";
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
// Remove this loader script
|
||||
if (document.currentScript) document.currentScript.remove();
|
||||
})();
|
||||
</script>
|
||||
""",
|
||||
)
|
||||
|
||||
def test_does_not_modify_html_when_no_component_used(self):
|
||||
registry.register(name="test", component=SimpleComponent)
|
||||
|
@ -401,6 +418,7 @@ class TestRenderDependencies:
|
|||
rendered_raw = Template(template_str).render(Context({"formset": [1], "DJC_DEPS_STRATEGY": "ignore"}))
|
||||
rendered = render_dependencies(rendered_raw, strategy="fragment")
|
||||
|
||||
# NOTE: Fragments adds a script to optionally load the component manager script.
|
||||
expected = """
|
||||
<table class="table-auto border-collapse divide-y divide-x divide-slate-300 w-full">
|
||||
<!-- Table head -->
|
||||
|
@ -422,6 +440,17 @@ class TestRenderDependencies:
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
(() => {
|
||||
if (!globalThis.Components) {
|
||||
const s = document.createElement('script');
|
||||
s.src = "django_components/django_components.min.js";
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
// Remove this loader script
|
||||
if (document.currentScript) document.currentScript.remove();
|
||||
})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
assertHTMLEqual(expected, rendered)
|
||||
|
@ -469,6 +498,8 @@ class TestRenderDependencies:
|
|||
# `PGxpbmsgaHJlZj0iL2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50XzMxMTA5Ny5jc3MiIG1lZGlhPSJhbGwiIHJlbD0ic3R5bGVzaGVldCI+` -> `<link href="/components/cache/SimpleComponent_311097.css" media="all" rel="stylesheet">` # noqa: E501
|
||||
# `PHNjcmlwdCBzcmM9InNjcmlwdC5qcyI+PC9zY3JpcHQ+` -> `<script src="script.js"></script>`
|
||||
# `PHNjcmlwdCBzcmM9Ii9jb21wb25lbnRzL2NhY2hlL1NpbXBsZUNvbXBvbmVudF8zMTEwOTcuanMiPjwvc2NyaXB0Pg==` -> `<script src="/components/cache/SimpleComponent_311097.js"></script>` # noqa: E501
|
||||
#
|
||||
# NOTE: Fragments adds a script to optionally load the component manager script.
|
||||
expected = """
|
||||
<table class="table-auto border-collapse divide-y divide-x divide-slate-300 w-full">
|
||||
<!-- Table head -->
|
||||
|
@ -491,6 +522,17 @@ class TestRenderDependencies:
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
(() => {
|
||||
if (!globalThis.Components) {
|
||||
const s = document.createElement('script');
|
||||
s.src = "django_components/django_components.min.js";
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
// Remove this loader script
|
||||
if (document.currentScript) document.currentScript.remove();
|
||||
})();
|
||||
</script>
|
||||
<script type="application/json" data-djc>
|
||||
{"loadedCssUrls": [],
|
||||
"loadedJsUrls": [],
|
||||
|
|
|
@ -539,6 +539,58 @@ class TestE2eDependencyRendering:
|
|||
|
||||
await page.close()
|
||||
|
||||
# Fragment where the page wasn't rendered with the "document" strategy
|
||||
@with_playwright
|
||||
async def test_fragment_without_document(self):
|
||||
page: Page = await self.browser.new_page() # type: ignore[attr-defined]
|
||||
await page.goto(f"{TEST_SERVER_URL}/fragment/base/htmx_raw?frag=comp")
|
||||
|
||||
test_before_js: types.js = """() => {
|
||||
const targetEl = document.querySelector("#target");
|
||||
const targetHtml = targetEl ? targetEl.outerHTML : null;
|
||||
const fragEl = document.querySelector(".frag");
|
||||
const fragHtml = fragEl ? fragEl.outerHTML : null;
|
||||
|
||||
return { targetHtml, fragHtml };
|
||||
}"""
|
||||
|
||||
data_before = await page.evaluate(test_before_js)
|
||||
|
||||
assert data_before["targetHtml"] == '<div id="target">OLD</div>'
|
||||
assert data_before["fragHtml"] is None
|
||||
|
||||
# Clicking button should load and insert the fragment
|
||||
await page.locator("button").click()
|
||||
|
||||
# Wait until both JS and CSS are loaded
|
||||
await page.locator(".frag").wait_for(state="visible")
|
||||
await page.wait_for_function(
|
||||
"() => document.head.innerHTML.includes('<link href=\"/components/cache/FragComp_')"
|
||||
)
|
||||
await page.wait_for_timeout(100) # NOTE: For CI we need to wait a bit longer
|
||||
|
||||
test_js: types.js = """() => {
|
||||
const targetEl = document.querySelector("#target");
|
||||
const targetHtml = targetEl ? targetEl.outerHTML : null;
|
||||
const fragEl = document.querySelector(".frag");
|
||||
const fragHtml = fragEl ? fragEl.outerHTML : null;
|
||||
|
||||
// Get the stylings defined via CSS
|
||||
const fragBg = fragEl ? globalThis.getComputedStyle(fragEl).getPropertyValue('background') : null;
|
||||
|
||||
return { targetHtml, fragHtml, fragBg };
|
||||
}"""
|
||||
|
||||
data = await page.evaluate(test_js)
|
||||
|
||||
assert data["targetHtml"] is None
|
||||
assert re.compile(
|
||||
r'<div class="frag" data-djc-id-\w{7}="">\s*' r"123\s*" r'<span id="frag-text">xxx</span>\s*' r"</div>"
|
||||
).search(data["fragHtml"]) is not None
|
||||
assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue'
|
||||
|
||||
await page.close()
|
||||
|
||||
@with_playwright
|
||||
async def test_alpine__head(self):
|
||||
single_comp_url = TEST_SERVER_URL + "/alpine/head"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue