diff --git a/src/js/src/components.ts b/src/js/src/components.ts index 712a24c5..95c89875 100644 --- a/src/js/src/components.ts +++ b/src/js/src/components.ts @@ -63,31 +63,47 @@ export function DjangoForm({ return null; } -export function OnlyOnceJS({ jsPath, autoRemove }: OnlyOnceProps): null { +export function LoadOnlyOnce({ + path, + nodeName, + autoRemove, +}: OnlyOnceProps): null { React.useEffect(() => { - // Check if the script element already exists - let el = document.head.querySelector( - "script.reactpy-staticfile[src='" + jsPath + "']", - ); + // Check if the element already exists + let el: null | HTMLElement = null; + if (nodeName === "script") { + el = document.head.querySelector( + "script.reactpy-staticfile[src='" + path + "']", + ); + } else if (nodeName === "link") { + el = document.head.querySelector( + "link.reactpy-staticfile[href='" + path + "']", + ); + } else { + throw new Error("Invalid nodeName provided to LoadOnlyOnce"); + } - // Create a new script element, if needed + // Create a new element, if needed if (el === null) { - el = document.createElement("script"); + el = document.createElement(nodeName); el.className = "reactpy-staticfile"; - if (jsPath) { - el.setAttribute("src", jsPath); + if (nodeName === "script") { + el.setAttribute("src", path); + } else if (nodeName === "link") { + el.setAttribute("href", path); + el.setAttribute("rel", "stylesheet"); } document.head.appendChild(el); } - // If requested, auto remove the script when it is no longer needed + // If requested, auto remove the element when it is no longer needed if (autoRemove) { // Keep track of the number of ReactPy components that are dependent on this script let count = Number(el.getAttribute("data-count")); count += 1; el.setAttribute("data-count", count.toString()); - // Remove the script element when the last dependent component is unmounted + // Remove the element when the last dependent component is unmounted return () => { count -= 1; if (count === 0) { diff --git a/src/js/src/index.ts b/src/js/src/index.ts index d3b08a4c..cb365f55 100644 --- a/src/js/src/index.ts +++ b/src/js/src/index.ts @@ -1,2 +1,2 @@ -export { OnlyOnceJS, DjangoForm, bind } from "./components"; +export { LoadOnlyOnce, DjangoForm, bind } from "./components"; export { mountComponent } from "./mount"; diff --git a/src/js/src/types.ts b/src/js/src/types.ts index e12ccb0f..0e826dc0 100644 --- a/src/js/src/types.ts +++ b/src/js/src/types.ts @@ -25,6 +25,7 @@ export interface DjangoFormProps { } export interface OnlyOnceProps { - jsPath: string; + path: string; + nodeName: "script" | "link"; autoRemove: boolean; } diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index 8cdf5c78..51986cce 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -27,12 +27,6 @@ from reactpy_django.types import AsyncFormEvent, SyncFormEvent, ViewToComponentConstructor, ViewToIframeConstructor -DjangoJS = web.export( - web.module_from_file("reactpy-django", file=Path(__file__).parent / "static" / "reactpy_django" / "client.js"), - ("OnlyOnceJS"), -) - - def view_to_component( view: Callable | View | str, transforms: Sequence[Callable[[VdomDict], Any]] = (), @@ -92,7 +86,9 @@ def constructor( return constructor -def django_css(static_path: str, key: Key | None = None) -> ComponentType: +def django_css( + static_path: str, only_once: bool = False, auto_remove: bool = False, key: Key | None = None +) -> ComponentType: """Fetches a CSS static file for use within ReactPy. This allows for deferred CSS loading. Args: @@ -102,11 +98,11 @@ def django_css(static_path: str, key: Key | None = None) -> ComponentType: immediate siblings """ - return _django_css(static_path=static_path, key=key) + return _django_css(static_path=static_path, only_once=only_once, auto_remove=auto_remove, key=key) def django_js( - static_path: str, only_once: bool = False, only_once_auto_remove: bool = False, key: Key | None = None + static_path: str, only_once: bool = False, auto_remove: bool = False, key: Key | None = None ) -> ComponentType: """Fetches a JS static file for use within ReactPy. This allows for deferred JS loading. @@ -117,9 +113,7 @@ def django_js( immediate siblings """ - return _django_js( - static_path=static_path, only_once=only_once, only_once_auto_remove=only_once_auto_remove, key=key - ) + return _django_js(static_path=static_path, only_once=only_once, auto_remove=auto_remove, key=key) def django_form( @@ -285,13 +279,30 @@ def _view_to_iframe( @component -def _django_css(static_path: str): +def _django_css(static_path: str, only_once: bool, auto_remove: bool): + if only_once: + return LoadOnlyOnce({ + "path": static(static_path), + "nodeName": "link", + "autoRemove": auto_remove, + }) + return html.style(cached_static_file(static_path)) @component -def _django_js(static_path: str, only_once: bool, only_once_auto_remove: bool): +def _django_js(static_path: str, only_once: bool, auto_remove: bool): if only_once: - return DjangoJS({"jsPath": static(static_path), "autoRemove": only_once_auto_remove}) + return LoadOnlyOnce({ + "path": static(static_path), + "nodeName": "script", + "autoRemove": auto_remove, + }) return html.script(cached_static_file(static_path)) + + +LoadOnlyOnce = web.export( + web.module_from_file("reactpy-django", file=Path(__file__).parent / "static" / "reactpy_django" / "client.js"), + ("LoadOnlyOnce"), +)