Closed iJungleboy closed 1 month ago
I noticed that OnInitializedAsync was not always called when the Site is set to Interactive. The setting of the module then has no influence on this. If the Site setting is set to Static, OnInitializedAsync will be called every time.
Maybe you can test by setting the Site (UI Component Settins) to Static.
If this is for js files, you can make your js script to have below functions:
export function onLoad() {
...
}
export function onUpdate() {
...
}
export function onDispose() {
...
}
then register your js file with below format:
new Resource { ResourceType = ResourceType.Script, Url = "[Js File URL]", Location = ResourceLocation.Body, Reload = true, RenderMode = RenderModes.Static }
in this case your code will be executed every time navigate to the page.
@zyhfish that's good for code that must run every time, but not for external files that should be loaded - especially larger code.
@iJungleboy it is not clear if you are saying that the JavaScript references are not being added to the DOM, or if they are not being executed (ie. calling onload), or both.
The fact that Blazor does not call onload when navigating from page to page has been well documented previously - it is due to Blazor's "enhanced navigation" which I covered in the "building beautiful websites" webinar I presented 2 months ago (based on the Arsha theme). In scenarios where you need JavaScript to execute on navigation you need to hook the onupdated event.
Can you please provide more information on your specific scenario.
@sbwalker thx for looking into this. The module has
public override List<Resource> Resources => new()
{
new Resource { ResourceType = ResourceType.Script, Url = $"Modules/{OqtConstants.PackageName}/Module.js" }
};
If it's on the initially loaded page - eg. ctrl-F5 - it's available.
If the user starts on another page, and hen navigates to the page with the module, it's not.
This is using SSR.
If we need to call some event to make it happen, no problem.
Note btw that we also needed some trickery to even detect if the page is being loaded "initial" or on a subsequent "magic navigation" effect (to try to implement a workaround in that scenario). We figured it out, but I think we're reading some special HTTP headers for this.
If it turns out that other modules need this regularly, I also recommend having some official property to detect this scenario.
@iJungleboy you did not actually answer my question. I was asking if you viewed the source code for the page in your browser dev tools and verified if the link element reference to Module.js is in the page head or not?
@sbwalker I don't have a running copy, but as far as I remember it's not even added to the DOM.
@tvatavuk could you confirm / explain? The question was:
Resources
with module.js
module.js
then appear in the source - but not load - or is in not even in the source?Thanks to everyone for helping!
To test and understand the new rendering modes and their behaviors, we are using the Oqtane 5.1.2 source code (master). Our setup includes Windows 11, Visual Studio 2022 v17.10.3, and latest .NET 8.0 SDK with all the latest updates.
We've created several sub-portals for different tests. Specifically, to test Static SSR behaviors with the default Oqtane site template and theme, we created a new sub-site. The home page is accessible at http://localhost:44357/ss10 and it is defult one, without custom module. On the page http://localhost:44357/ss10/t1, we developed a new custom Oqtane module, ToSic.Module.Test512, using the default Oqtane module template.
Here are our observations:
Initial Page Visit: Yes, the first page visited did not contain the module.
Navigation to Module Page: Yes, after home we are navigating to a page with the module.
I will provide more details in the next comment.
The solution using Reload = true
is working, but it has some limitations for our specific cases.
new Resource { ResourceType = ResourceType.Script, Url = ModulePath() + "Module.js", Reload = true }
As @iJungleboy mentioned, the page-script
magic doesn't work with some external .js resources, as illustrated in this example:
<script integrity="sha256-0af2VbC4vmPsa8OLBAKBmLoyuKq4bBlKK2KOgMWayio=" crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/@fancyapps/ui@4.0.31/dist/fancybox.umd.js"></script>
To better support different cases of loading resources in Static SSR, we created a simple service to distinguish between the first full page load on Static SSR and subsequent page loads that occur with Blazor enhanced navigation.
Here is the helper service implementation:
namespace ToSic.Sxc.Oqt.Shared.Interfaces
{
/// <summary>
/// Service for providing rendering information based on the current HTTP context and module render mode.
/// </summary>
public interface IRenderInfoService
{
/// <summary>
/// Checks if the module render mode is static SSR (Server-Side Rendering).
/// </summary>
/// <param name="renderMode">The module render mode.</param>
/// <returns>True if the render mode is static SSR, otherwise false.</returns>
bool IsStaticSsr(string renderMode);
/// <summary>
/// Checks if Blazor Enhanced Navigation is enabled for static SSR.
/// </summary>
/// <param name="renderMode">The module render mode.</param>
/// <returns>True if Blazor Enhanced Navigation is enabled, otherwise false.</returns>
/// <remarks>
/// Blazor Enhanced Navigation is a feature in .NET 8 that allows for progressively-enhanced navigation in multi-page apps.
/// This is enabled when the app loads `blazor.web.js` and does not use an interactive Router. It works by intercepting
/// navigation within the base href URI space, loading content via `fetch` requests, and syncing the DOM.
/// </remarks>
bool IsBlazorEnhancedNav(string renderMode);
/// <summary>
/// Checks if the SSR Framing response header exists, indicating a fetch page request in Blazor Enhanced Navigation.
/// </summary>
/// <param name="renderMode">The module render mode.</param>
/// <returns>
/// True if the SSR Framing response header exists (indicating a fetch page request during Blazor Enhanced Navigation).
/// False if it is a standard full page load in the browser.
/// </returns>
/// <remarks>
/// This helps distinguish between a full page load (initial load) and a fetch page request (subsequent navigation within the app).
/// This behavior occurs when Blazor Enhanced Navigation is in use for static SSR.
/// </remarks>
bool IsSsrFraming(string renderMode);
}
}
using Microsoft.AspNetCore.Http;
using Oqtane.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using ToSic.Sxc.Oqt.Shared.Interfaces;
namespace ToSic.Sxc.Oqt.Server.Services
{
/// <inheritdoc />
public class RenderInfoService(IHttpContextAccessor httpContextAccessor) : IRenderInfoService
{
private readonly List<string> _enhancedNavValues = ["allow", "on"];
private const string BlazorEnhancedNav = "blazor-enhanced-nav";
private const string SsrFraming = "ssr-framing";
/// <inheritdoc />
public bool IsStaticSsr(string renderMode = RenderModes.Interactive)
=> string.Equals($"{renderMode}", RenderModes.Static, StringComparison.OrdinalIgnoreCase);
/// <inheritdoc />
public bool IsBlazorEnhancedNav(string renderMode)
{
if (!IsStaticSsr(renderMode))
return false;
var context = httpContextAccessor.HttpContext;
if (context != null && context.Response.Headers.TryGetValue(BlazorEnhancedNav, out var enhancedNav))
return _enhancedNavValues.Contains($"{enhancedNav}", StringComparer.OrdinalIgnoreCase);
return false;
}
/// <inheritdoc />
public bool IsSsrFraming(string renderMode)
{
if (!IsBlazorEnhancedNav(renderMode))
return false;
var context = httpContextAccessor.HttpContext;
return context != null && context.Response.Headers.TryGetValue(SsrFraming, out _);
}
}
}
using ToSic.Sxc.Oqt.Shared.Interfaces;
namespace ToSic.Sxc.Oqt.Client.Services
{
/// <inheritdoc />
public class RenderInfoService : IRenderInfoService
{
/// <inheritdoc />
public bool IsStaticSsr(string renderMode) => false;
/// <inheritdoc />
public bool IsBlazorEnhancedNav(string renderMode) => false;
/// <inheritdoc />
public bool IsSsrFraming(string renderMode) => false;
}
}
This service helps distinguish between a full page load and a fetch page request during Blazor Enhanced Navigation. This way, you can handle resource loading more appropriately based on the context of the page load.
To ensure loading of external .js resource like one in example we used solution based on turnOn that was invented by @iJungleboy.
Example of external resource that we loaded with turnOn.
<script integrity="sha256-0af2VbC4vmPsa8OLBAKBmLoyuKq4bBlKK2KOgMWayio=" crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/@fancyapps/ui@4.0.31/dist/fancybox.umd.js"></script>
More info about turnOn: https://docs.2sxc.org/js-code/turn-on/index.html
just to clarify what @tvatavuk said - turnOn doesn't ensure that the JS is loaded into the browser, it's job is to determine when all the requirements of a script are given, and then start some code.
A bit like a browser onLoad, but in a way that can handle late-added scripts, complicated dependencies, and CSP safe since it's configuration only, meaning that it can be used even in very strict CSP scenarios. CSP = Client Security Policy. because it can receive parameters as JSON in the source code - which doesn't require CSP fingerprinting.
@tvatavuk thank you for confirming that Oqtane is injecting the reference to the JavaScript resource into the page DOM - this means that Oqtane is behaving properly. The fact that the JavaScript was not being executed is not an Oqtane issue - it is related to Blazor SSR and how Enhanced Navigation behaves in .NET 8. There is plenty of information available online about this behavior (ie. https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/static-server-rendering?view=aspnetcore-8.0). As explained in the Building Beautiful Websites webinar, Oqtane is utilizing a solution created by Mackinnon Buck from the Blazor Dev Team which enables JavaScript navigation callbacks (https://github.com/MackinnonBuck/blazor-page-script). If this solution does not work for you, you are free to use your own custom solution.
@tvatavuk you mentioned that the page-script
magic doesn't work with some external .js resources, as illustrated in this example:
<script integrity="sha256-0af2VbC4vmPsa8OLBAKBmLoyuKq4bBlKK2KOgMWayio=" crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/@fancyapps/ui@4.0.31/dist/fancybox.umd.js"></script>
I assume this is because the page-script does not support integrity or crossorigin attributes. Would it make sense for me to reach out to Mackinnon Buck to provide this feedback and explore a potential solution? A lot of developers are using the page-script project as it is the official example provided by Microsoft, so it would benefit the community to address this scenario.
Page-script (https://github.com/MackinnonBuck/blazor-page-script) already provides the ability to distinguish between the first full page load on Static SSR and subsequent page loads that occur with Blazor enhanced navigation. It exposes different events for these purposes - onLoad and onUpdate. It would be useful to familiarize yourself with the capabilities as I would assume the page-script approach will eventually become the "standard" approach in Blazor for dealing with JavaScript in SSR Enhanced Navigation.
Thank you for bringing up the issue with the page-script
magic and external JavaScript resources. Let me clarify the situation.
The problem arises because the external JavaScript module we're using, https://cdn.jsdelivr.net/npm/@fancyapps/ui@4.0.31/dist/fancybox.umd.js, is in the classic UMD (Universal Module Definition) format, whereas page-script
magic relies on the standard dynamic import, which expects modules in the ESM (ECMAScript module) format. Due to this format discrepancy, the page-script
magic does not work as expected.
A potential solution would be to use an external JavaScript module in the ESM format, such as https://cdn.jsdelivr.net/npm/@fancyapps/ui@4.0.31/dist/fancybox.esm.js. However, this requires changes to the existing code in production, which might not be feasible immediately.
Regarding the inclusion of additional script attributes like integrity
and crossorigin
, more investigation is necessary. These attributes enhance security of the external scripts in browser. However, their interaction with the dynamic import()
functionality, which already has specific handling for CORS, needs to be understood better. If @iJungleboy or anyone, has more experience or insights on this matter, suggestions would be greatly appreciated.
Thanks for explaining Page-script (https://github.com/MackinnonBuck/blazor-page-script) and its ability to distinguish between the first full page load on Static SSR and subsequent page loads that occur with Blazor enhanced navigation. Yes, I will familiarize myself with these capabilities and find how to use them in 2sxc. In our case we needed info about Static SSR and enhanced navigation during razor rendering that happens on server to provide correct list of resources (js, css) to browser.
@sbwalker thanks for your help and support 🙏🏼
@tvatavuk I reached out to Mackinnon Buck and he explained that that page-script custom element can include additional attributes - he created an example here:
https://github.com/MackinnonBuck/blazor-page-script/commit/44ad6833b8375a9eaa9b0fa9ace70e024b823f2d
The most important part is that the attribute rel="modulepreload" needs to be included so that the dynamic module imports will respect the attributes set on the link element:
https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/modulepreload
Oqtane will need to be modified to include the additional attributes - and then it will be able to support remote resources.
One other thing I want to explore is if it is really necessary to force developers to include the Reload = True property in their Resources declaration. Instead, the framework could determine that the site is using Static rendering and use the page-script approach by default for Script resources. This would automatically wire up the onLoad, onUpdate, onDispose events if they exist - and if a JavaScript library does not utilize them, it should not cause any issues. This would simplify the experience for developers as they would not need to worry about setting an extra property which is only applicable for a specific render mode.
One other thing I want to explore is if it is really necessary to force developers to include the Reload = True property in their Resources declaration. Instead, the framework could determine that the site is using Static rendering and use the page-script approach by default for Script resources. This would automatically wire up the onLoad, onUpdate, onDispose events if they exist - and if a JavaScript library does not utilize them, it should not cause any issues. This would simplify the experience for developers as they would not need to worry about setting an extra property which is only applicable for a specific render mode.
This would actually be a really nice addition for the developer, as I was also getting a bit confused with javascript in static ssr (see #4038). Although you would still need to implement the onLoad, onUpdate and onDispose events when required, so the problem does not totally disappear.
Absolutely, I agree! 🎉 Simplifying the developer experience by having the framework automatically determine the need for Reload = True
based on the rendering mode is a great idea. 🚀 This enhancement will undoubtedly reduce the complexity of custom module development in Oqtane. Thank you 👏
After some more research I think we really need to clarify what Reload = true
should mean.
@sbwalker I'll also document it, as soon as it's clear to me 😉
Basically my interpretation is that Reload = true
should mean
This would still pose the question:
Because I assume that's what Reload = true
was originally meant to say.
ATM we're forced to use it to ensure loading of our scripts when soft-navigating to a page with the module on it - which is probably not the real intent of this.
I assume that a module can expect it's resources to always be loaded by Oqtane - no matter how the page was accessed.
...which in turn means that the behavior in 5.1.2 where we must provide Reload=true
is a bug.
But since it's not fully clear what the official meaning Reload=true
is, it's hard to create a bug report.
@sbwalker I would appreciate clarification on this, so I can document it - and then it would also be clear if this is a bug or something developers must be educated about.
The Reload parameter was discussed in March in this thread: https://github.com/oqtane/oqtane.framework/issues/4038 when it was originally introduced.
The Reload parameter is only applicable when a module component is using Static Render Mode and it is only applicable to JavaScript resources. In this scenario, Blazor is using Enhanced Navigation which means that when you navigate to a new page, a full page refresh is not occurring - Blazor is patching the DOM based on the differences in the output (ie. it is behaving like a SPA). This means that <script>
references are not refreshed in the DOM - which means they will not be reloaded by the browser. The Reload parameter instructs the framework that a JavaScript resource needs to be reloaded on page navigations.
@iJungleboy just to clarify so that I am able to reproduce the behavior you are describing:
<script>
element for the Resource is NOT added to the page DOMcorrect?
Almost correct, but not quite. the last line:
when you say "load" you mean that the Githubissues.
Githubissues is a development platform for aggregating issues.
Oqtane Info
Version - 5.1.2 Render Mode - Static Database - SQL Server
Describe the bug
A module can describe resources to load - JS/CSS. The idea is that these are automatically added to the page by Oqtane.
But on our tests, it only appears to work if the first full load happens on the page with the module.
Steps To Reproduce
So in our tests, this works:
...but this fails:
Resources seem to not load.
Anything else?
The module is configured to have
RenderMode.Static
.@tvatavuk just fyi