From 7f74e792534b76b1d0b87da27d7d71054102c671 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 8 Mar 2024 14:03:22 -0500 Subject: [PATCH] add ability to reload JavaScript on page transitions with enhanced navigation --- Oqtane.Client/UI/SiteRouter.razor | 1 + Oqtane.Server/Components/App.razor | 21 +++-- .../wwwroot/Oqtane.Server.lib.module.js | 90 +++++++++++++++++++ Oqtane.Shared/Models/Resource.cs | 5 ++ 4 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 316a10c93..4a606397f 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -580,6 +580,7 @@ ES6Module = resource.ES6Module, Content = resource.Content, RenderMode = resource.RenderMode, + Reload = resource.Reload, Level = level, Namespace = name }); diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index be303c8d9..4d35946cd 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -526,12 +526,20 @@ { if (!string.IsNullOrEmpty(resource.Url)) { - var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; - return ""; // src at end of element due to enhanced navigation patch algorithm + if (!resource.Reload) + { + var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; + return ""; // src at end of element due to enhanced navigation patch algorithm + } + else + { + // use custom element which can execute script on every page transition + return ""; + } } else { @@ -684,6 +692,7 @@ ES6Module = resource.ES6Module, Content = resource.Content, RenderMode = resource.RenderMode, + Reload = resource.Reload, Level = level, Namespace = name }); diff --git a/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js b/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js new file mode 100644 index 000000000..508f42a02 --- /dev/null +++ b/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js @@ -0,0 +1,90 @@ +const pageScriptInfoBySrc = new Map(); + +function registerPageScriptElement(src) { + if (!src) { + throw new Error('Must provide a non-empty value for the "src" attribute.'); + } + + let pageScriptInfo = pageScriptInfoBySrc.get(src); + + if (pageScriptInfo) { + pageScriptInfo.referenceCount++; + } else { + pageScriptInfo = { referenceCount: 1, module: null }; + pageScriptInfoBySrc.set(src, pageScriptInfo); + initializePageScriptModule(src, pageScriptInfo); + } +} + +function unregisterPageScriptElement(src) { + if (!src) { + return; + } + + const pageScriptInfo = pageScriptInfoBySrc.get(src); + if (!pageScriptInfo) { + return; + } + + pageScriptInfo.referenceCount--; +} + +async function initializePageScriptModule(src, pageScriptInfo) { + // If the path is relative, normalize it by by making it an absolute URL + // with document's the base HREF. + if (src.startsWith("./")) { + src = new URL(src.substr(2), document.baseURI).toString(); + } + + const module = await import(src); + + if (pageScriptInfo.referenceCount <= 0) { + // All page-script elements with the same 'src' were + // unregistered while we were loading the module. + return; + } + + pageScriptInfo.module = module; + module.onLoad?.(); + module.onUpdate?.(); +} + +function onEnhancedLoad() { + // Start by invoking 'onDispose' on any modules that are no longer referenced. + for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) { + if (referenceCount <= 0) { + module?.onDispose?.(); + pageScriptInfoBySrc.delete(src); + } + } + + // Then invoke 'onUpdate' on the remaining modules. + for (const { module } of pageScriptInfoBySrc.values()) { + module?.onUpdate?.(); + } +} + +export function afterWebStarted(blazor) { + customElements.define('page-script', class extends HTMLElement { + static observedAttributes = ['src']; + + // We use attributeChangedCallback instead of connectedCallback + // because a page-script element might get reused between enhanced + // navigations. + attributeChangedCallback(name, oldValue, newValue) { + if (name !== 'src') { + return; + } + + this.src = newValue; + unregisterPageScriptElement(oldValue); + registerPageScriptElement(newValue); + } + + disconnectedCallback() { + unregisterPageScriptElement(this.src); + } + }); + + blazor.addEventListener('enhancedload', onEnhancedLoad); +} \ No newline at end of file diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index 13680a0d5..8092256bb 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -67,6 +67,11 @@ public string Url /// public string RenderMode { get; set; } + /// + /// Indicates that a script should be reloaded on every page transition - not applicable to Stylesheets + /// + public bool Reload { get; set; } + /// /// The namespace of the component that declared the resource - only used in SiteRouter ///