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
|
# 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
|
## v0.141.2
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
|
@ -142,11 +142,12 @@ There are six dependencies strategies:
|
||||||
|
|
||||||
- [`document`](../../advanced/rendering_js_css#document) (default)
|
- [`document`](../../advanced/rendering_js_css#document) (default)
|
||||||
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||||
- Inserts extra script to allow `fragment` strategy to work.
|
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
- Inserts extra script for managing fragments.
|
||||||
- [`fragment`](../../advanced/rendering_js_css#fragment)
|
- [`fragment`](../../advanced/rendering_js_css#fragment)
|
||||||
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
- 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)
|
- [`simple`](../../advanced/rendering_js_css#simple)
|
||||||
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||||
- No extra script loaded.
|
- 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
|
- A JS script is injected to manage component dependencies, enabling lazy loading of JS and CSS
|
||||||
for HTML fragments.
|
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"
|
!!! 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.
|
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`
|
### `fragment`
|
||||||
|
|
||||||
`deps_strategy="fragment"` is used when rendering a piece of HTML that will be inserted into a page
|
`deps_strategy="fragment"` is used when rendering a piece of HTML that will be inserted into a page:
|
||||||
that has already been rendered with the [`"document"`](#document) strategy:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
fragment = MyComponent.render(deps_strategy="fragment")
|
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/)),
|
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.
|
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).
|
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)
|
- [`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.
|
- 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.
|
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
- Inserts extra script for managing fragments.
|
||||||
- [`fragment`](../../advanced/rendering_js_css#fragment)
|
- [`fragment`](../../advanced/rendering_js_css#fragment)
|
||||||
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
||||||
- Assumes the page was already rendered with `"document"` strategy.
|
- Fragment will fetch its own JS / CSS dependencies when inserted into the page.
|
||||||
- No JS / CSS included.
|
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||||
- [`simple`](../../advanced/rendering_js_css#simple)
|
- [`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.
|
- 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.
|
- No extra script loaded.
|
||||||
|
|
|
@ -2697,11 +2697,12 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
- [`"document"`](../../concepts/advanced/rendering_js_css#document) (default)
|
- [`"document"`](../../concepts/advanced/rendering_js_css#document) (default)
|
||||||
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||||
- Inserts extra script to allow `fragment` types to work.
|
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
- Inserts extra script for managing fragments.
|
||||||
- [`"fragment"`](../../concepts/advanced/rendering_js_css#fragment)
|
- [`"fragment"`](../../concepts/advanced/rendering_js_css#fragment)
|
||||||
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
- 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)
|
- [`"simple"`](../../concepts/advanced/rendering_js_css#simple)
|
||||||
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||||
- No extra script loaded.
|
- No extra script loaded.
|
||||||
|
@ -3186,11 +3187,12 @@ class Component(metaclass=ComponentMeta):
|
||||||
|
|
||||||
- [`"document"`](../../concepts/advanced/rendering_js_css#document) (default)
|
- [`"document"`](../../concepts/advanced/rendering_js_css#document) (default)
|
||||||
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
- Smartly inserts JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||||
- Inserts extra script to allow `fragment` types to work.
|
- Requires the HTML to be rendered in a JS-enabled browser.
|
||||||
- Assumes the HTML will be rendered in a JS-enabled browser.
|
- Inserts extra script for managing fragments.
|
||||||
- [`"fragment"`](../../concepts/advanced/rendering_js_css#fragment)
|
- [`"fragment"`](../../concepts/advanced/rendering_js_css#fragment)
|
||||||
- A lightweight HTML fragment to be inserted into a document with AJAX.
|
- 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)
|
- [`"simple"`](../../concepts/advanced/rendering_js_css#simple)
|
||||||
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
- Smartly insert JS / CSS into placeholders or into `<head>` and `<body>` tags.
|
||||||
- No extra script loaded.
|
- No extra script loaded.
|
||||||
|
|
|
@ -476,6 +476,28 @@ def render_dependencies(content: TContent, strategy: DependenciesStrategy = "doc
|
||||||
_render_dependencies = render_dependencies
|
_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:
|
# Overview of this function:
|
||||||
# 1. We extract all HTML comments like `<!-- _RENDERED table_10bac31,1234-->`.
|
# 1. We extract all HTML comments like `<!-- _RENDERED table_10bac31,1234-->`.
|
||||||
# 2. We look up the corresponding component classes
|
# 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 [],
|
js=[static("django_components/django_components.min.js")] if strategy == "document" else [],
|
||||||
).render_js()
|
).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(
|
final_script_tags = "".join(
|
||||||
[
|
[
|
||||||
# JS by us
|
# 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);
|
.catch(console.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialise the MutationObserver that watches for `<script>` tags with `data-djc` attribute
|
const onDjcScript = (script: HTMLScriptElement) => {
|
||||||
observeScriptTag((script) => {
|
|
||||||
const data = JSON.parse(script.text);
|
const data = JSON.parse(script.text);
|
||||||
_loadComponentScripts(data);
|
_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 {
|
return {
|
||||||
callComponent,
|
callComponent,
|
||||||
|
|
|
@ -10,6 +10,7 @@ from testserver.views import (
|
||||||
check_js_order_vars_not_available_before_view,
|
check_js_order_vars_not_available_before_view,
|
||||||
fragment_base_alpine_view,
|
fragment_base_alpine_view,
|
||||||
fragment_base_htmx_view,
|
fragment_base_htmx_view,
|
||||||
|
fragment_base_htmx_view__raw,
|
||||||
fragment_base_js_view,
|
fragment_base_js_view,
|
||||||
fragment_view,
|
fragment_view,
|
||||||
multiple_components_view,
|
multiple_components_view,
|
||||||
|
@ -28,6 +29,7 @@ urlpatterns = [
|
||||||
path("js-order/invalid", check_js_order_vars_not_available_before_view),
|
path("js-order/invalid", check_js_order_vars_not_available_before_view),
|
||||||
path("fragment/base/alpine", fragment_base_alpine_view),
|
path("fragment/base/alpine", fragment_base_alpine_view),
|
||||||
path("fragment/base/htmx", fragment_base_htmx_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/base/js", fragment_base_js_view),
|
||||||
path("fragment/frag", fragment_view),
|
path("fragment/frag", fragment_view),
|
||||||
path("alpine/head", alpine_in_head_view),
|
path("alpine/head", alpine_in_head_view),
|
||||||
|
|
|
@ -146,7 +146,15 @@ def fragment_base_js_view(request):
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(html => {
|
.then(html => {
|
||||||
console.log({ fragment: 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>
|
</script>
|
||||||
|
@ -239,6 +247,38 @@ def fragment_base_htmx_view(request):
|
||||||
return HttpResponse(rendered)
|
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):
|
def fragment_view(request):
|
||||||
fragment_type = request.GET["frag"]
|
fragment_type = request.GET["frag"]
|
||||||
if fragment_type == "comp":
|
if fragment_type == "comp":
|
||||||
|
|
|
@ -366,7 +366,24 @@ class TestRenderDependencies:
|
||||||
rendered_raw = Template(template_str).render(Context({"formset": [1], "DJC_DEPS_STRATEGY": "ignore"}))
|
rendered_raw = Template(template_str).render(Context({"formset": [1], "DJC_DEPS_STRATEGY": "ignore"}))
|
||||||
rendered = render_dependencies(rendered_raw, strategy="fragment")
|
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):
|
def test_does_not_modify_html_when_no_component_used(self):
|
||||||
registry.register(name="test", component=SimpleComponent)
|
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_raw = Template(template_str).render(Context({"formset": [1], "DJC_DEPS_STRATEGY": "ignore"}))
|
||||||
rendered = render_dependencies(rendered_raw, strategy="fragment")
|
rendered = render_dependencies(rendered_raw, strategy="fragment")
|
||||||
|
|
||||||
|
# NOTE: Fragments adds a script to optionally load the component manager script.
|
||||||
expected = """
|
expected = """
|
||||||
<table class="table-auto border-collapse divide-y divide-x divide-slate-300 w-full">
|
<table class="table-auto border-collapse divide-y divide-x divide-slate-300 w-full">
|
||||||
<!-- Table head -->
|
<!-- Table head -->
|
||||||
|
@ -422,6 +440,17 @@ class TestRenderDependencies:
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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)
|
assertHTMLEqual(expected, rendered)
|
||||||
|
@ -469,6 +498,8 @@ class TestRenderDependencies:
|
||||||
# `PGxpbmsgaHJlZj0iL2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50XzMxMTA5Ny5jc3MiIG1lZGlhPSJhbGwiIHJlbD0ic3R5bGVzaGVldCI+` -> `<link href="/components/cache/SimpleComponent_311097.css" media="all" rel="stylesheet">` # noqa: E501
|
# `PGxpbmsgaHJlZj0iL2NvbXBvbmVudHMvY2FjaGUvU2ltcGxlQ29tcG9uZW50XzMxMTA5Ny5jc3MiIG1lZGlhPSJhbGwiIHJlbD0ic3R5bGVzaGVldCI+` -> `<link href="/components/cache/SimpleComponent_311097.css" media="all" rel="stylesheet">` # noqa: E501
|
||||||
# `PHNjcmlwdCBzcmM9InNjcmlwdC5qcyI+PC9zY3JpcHQ+` -> `<script src="script.js"></script>`
|
# `PHNjcmlwdCBzcmM9InNjcmlwdC5qcyI+PC9zY3JpcHQ+` -> `<script src="script.js"></script>`
|
||||||
# `PHNjcmlwdCBzcmM9Ii9jb21wb25lbnRzL2NhY2hlL1NpbXBsZUNvbXBvbmVudF8zMTEwOTcuanMiPjwvc2NyaXB0Pg==` -> `<script src="/components/cache/SimpleComponent_311097.js"></script>` # noqa: E501
|
# `PHNjcmlwdCBzcmM9Ii9jb21wb25lbnRzL2NhY2hlL1NpbXBsZUNvbXBvbmVudF8zMTEwOTcuanMiPjwvc2NyaXB0Pg==` -> `<script src="/components/cache/SimpleComponent_311097.js"></script>` # noqa: E501
|
||||||
|
#
|
||||||
|
# NOTE: Fragments adds a script to optionally load the component manager script.
|
||||||
expected = """
|
expected = """
|
||||||
<table class="table-auto border-collapse divide-y divide-x divide-slate-300 w-full">
|
<table class="table-auto border-collapse divide-y divide-x divide-slate-300 w-full">
|
||||||
<!-- Table head -->
|
<!-- Table head -->
|
||||||
|
@ -491,6 +522,17 @@ class TestRenderDependencies:
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
<script type="application/json" data-djc>
|
||||||
{"loadedCssUrls": [],
|
{"loadedCssUrls": [],
|
||||||
"loadedJsUrls": [],
|
"loadedJsUrls": [],
|
||||||
|
|
|
@ -539,6 +539,58 @@ class TestE2eDependencyRendering:
|
||||||
|
|
||||||
await page.close()
|
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
|
@with_playwright
|
||||||
async def test_alpine__head(self):
|
async def test_alpine__head(self):
|
||||||
single_comp_url = TEST_SERVER_URL + "/alpine/head"
|
single_comp_url = TEST_SERVER_URL + "/alpine/head"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue