12

It is recommended to load JS libraries in the _Host.cshtml and not in the layout (nor in component where is not allowed)

But doing so, how is it possible to load some script for one page and some for another ? eg. I want the googlemaps stuff or the datatable stuff only in the pages where I use them, not in all (waste of space/memory/load times).

Thanks

SandroRiz
  • 903
  • 4
  • 10
  • 18
  • 3
    Blazor is a SPA: __Single__ Page App. So there only is one page and you need to bring everything. _Host is the right place. – H H Nov 21 '19 at 14:27
  • But maybe you can do some dynamic loading with JS Interop, not my area. – H H Nov 21 '19 at 14:28
  • "Blazor is a SPA: Single Page App" So what... Angular is also an SPA. "So there only is one page and you need to bring everything" What you say ??? – enet Nov 21 '19 at 14:47
  • 1
    @SandroRiz, please be patient. It's coming soon. And it's going to be implemented, I was told, like Angular. – enet Nov 21 '19 at 14:50

3 Answers3

16

As discussed in the comments, Blazor is an SPA so any loaded script is available on Blazor pages since it's the same page.

However, you don't have to list them all in _Host.cshtml, and indeed you probably want to only load a specific script when it's needed (e.g. not all users use a particular page/component where the script is required).

It is possible to load scripts dynamically using JS Interop. I created the following scriptLoader.js library and included this in _Host.cshtml:

// loadScript: returns a promise that completes when the script loads
window.loadScript = function (scriptPath) {
    // check list - if already loaded we can ignore
    if (loaded[scriptPath]) {
        console.log(scriptPath + " already loaded");
        // return 'empty' promise
        return new this.Promise(function (resolve, reject) {
            resolve();
        });
    }

    return new Promise(function (resolve, reject) {
        // create JS library script element
        var script = document.createElement("script");
        script.src = scriptPath;
        script.type = "text/javascript";
        console.log(scriptPath + " created");

        // flag as loading/loaded
        loaded[scriptPath] = true;

        // if the script returns okay, return resolve
        script.onload = function () {
            console.log(scriptPath + " loaded ok");
            resolve(scriptPath);
        };

        // if it fails, return reject
        script.onerror = function () {
            console.log(scriptPath + " load failed");
            reject(scriptPath);
        }

        // scripts will load at end of body
        document["body"].appendChild(script);
    });
}
// store list of what scripts we've loaded
loaded = [];

This creates a script element and appends to the body element of the document. It returns a promise since the script will load asynchronously, so you need await in the C# code.

The loaded array is there to avoid re-loading the script again. Any script, once loaded, stays loaded unless the user refreshes the page. So the load only occurs once.

On a page/component where I need to ensure a library is loaded, I will need to inject the IJSruntime...

@inject IJSRuntime jsRuntime

And then call it..

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // invoke script loader
        Console.WriteLine("Loading jQuery");
        await jsRuntime.InvokeVoidAsync("loadScript", "https://code.jquery.com/jquery-3.4.1.js");
        await jsRuntime.InvokeVoidAsync("loadScript", "myJQueryTest.js");

        Console.WriteLine("Invoking jQuery");

        await jsRuntime.InvokeVoidAsync("setH1", "Hello world!");

        Console.WriteLine("Invoked JQuery");

        await base.OnAfterRenderAsync(firstRender);
    }

The myJQueryTest.js is simple:

window.setH1 = function (message) {
    $('h1').text(message);
}

Demo repo created: https://github.com/conficient/BlazorDynamicScriptLoad

Quango
  • 12,338
  • 6
  • 48
  • 83
  • I should add this is a very rough-and-ready demo - better error handling would be a good idea – Quango Nov 21 '19 at 17:17
  • 2
    Another note - I tested this with a complex library that also loads its own scripts, and this failed, so it may not work in all cases. For simple use cases it should be fine. – Quango Nov 22 '19 at 15:33
  • Do you have tryed to refresh the page, where your dynamicall script load appears? My very similar code does work good aslong i do not refresh the page, when i do - the whole app doesen't react anymore correctly. I'm looking forward to use ur code, but could not get it to live atm – Nicwin Mar 05 '20 at 15:23
  • I'd check the log/console for errors, you should not have to refresh the page – Quango Mar 06 '20 at 15:30
  • Hey, i spend some time to it too - will provide Details later but here my Findlings: yes, it work without the reload as already Said. A typicall User will lose Trust when i Tell him ‚no Need to refresh‘.... i know its still a previou. So but! another Second Page with Same Tech does Not work when i visitied First Page Already – Nicwin Mar 08 '20 at 09:55
  • Excellent solution! But fails when used for multiple instance of same component, see [here](https://stackoverflow.com/q/74415181/9971404) and [here](https://github.com/conficient/BlazorDynamicScriptLoad/issues/2#issuecomment-1312470304) – lonix Nov 12 '22 at 17:43
  • This will fail if using it multiple times - e.g. for a component that could be included multiple times - as there is a race condition where the components don't wait for the script to load. See [here](https://stackoverflow.com/a/74426379/9971404) for a way to avoid that. – lonix Nov 16 '22 at 01:32
4

There's one more way to do that using exported functions in JavaScript

wwwroot/js/script.js

export async function someFunction(parameter1, paramater2) {
        ...
  }

Pages/CallJavaScript.razor

@page "/call-js"
@inject IJSRuntime JS

@code{
  private IJSObjectReference myScript;

  protected override async Task OnAfterRenderAsync(bool firstRender)
  {
    if (firstRender)
    {
      myScript = await JS.InvokeAsync<IJSObjectReference>("import", "/js/script.js");
    }
  }

  private async Task CallJavaScriptMethod()
  {
    myScript.InvokeVoidAsync("someFunction", "param1", "param2");
  }

}
Aid Death
  • 41
  • 3
0

This work even if coming back from a page trough an href and not from a Navigationmanager redirect (with forcing reload to true). The CallJavascriptMethod can be used even in the "firstrender" section if you have to apply some js to dom elements on page load.

MaxDNet
  • 108
  • 4