feat: render fragments without document strategy (#1339)

This commit is contained in:
Juro Oravec 2025-08-14 11:33:31 +02:00 committed by GitHub
parent aa14e3698d
commit c72fed8255
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 217 additions and 26 deletions

View file

@ -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.

View file

@ -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

View file

@ -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;})();