'Embed JavaScript files inside a .NET class library for Blazor project

I'm working on a Blazor project and I'm trying to move one of the JavaScript files to a class library, I've read the following guides:

  1. https://docs.microsoft.com/en-us/aspnet/core/razor-pages/ui-class?view=aspnetcore-6.0&tabs=visual-studio#consume-content-from-a-referenced-rcl-1
  2. https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-6.0#load-a-script-from-an-external-javascript-file-js
  3. https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0#javascript-isolation-in-javascript-modules

I'm getting the following error:

Browser developer tools

I'm not sure what I'm missing or what I need to do to get this to work so here is the setup I have:

Facts:

  1. I'm using .NET 6.
  2. I'm using Visual Studio for Mac Version 17.0 Preview (17.0 build 8754).
  3. The name of the solution is LinkScreen.
  4. The name of the application project is Client.Web.
  5. The name of the application assembly is LinkScreen.Client.Web.
  6. The name of the application default namespace is LinkScreen.Client.Web.
  7. The hosting model for the application is Web Assembly.
  8. The name of the class library project is Client.BrowserInterop.
  9. The name of the class library assembly is LinkScreen.Client.BrowserInterop.
  10. The name of the class library default namespace is LinkScreen.Client.BrowserInterop.

Project Structur and Code

  1. Inside the class library I have a script file called screen-capture.js under the following directory wwwroot\scripts like so:

enter image description here

The build action for this is set to Content and copy to output directory is set to Do not copy.

  1. The class library is referenced to the application like so:

enter image description here

  1. I have a C# class called ScreenCapture that wraps the JavaScript module that is the screen-capture.js and retuns a reference like so:
public static async ValueTask<ScreenCapture> CreateAsync(
        IJSRuntime jsRuntime,
        ElementReference videoTagReference)
    {
        var jsModuleRef = await jsRuntime.InvokeAsync<IJSInProcessObjectReference>("import", "./_content/LinkScreen.Client.BrowserInterop/screen-capture.js").ConfigureAwait(false);

        return new ScreenCapture(jsModuleRef, videoTagReference);
    }

Previously I had a reference to the screen-capture.js inside index.html like so <script src="_content/LinkScreen.Client.BrowserInterop/scripts/screen-capture.js" type="module"></script> because I thought it was required to use the IJSRuntime methods and then someone on the Blazor channel enlighten me so I removed the reference but I'm still getting the same 404 error now from the wrapper by calling jsRuntime.InvokeAsync.

It's important to note that everything works correctly when the script is inside the wwwroot/scripts of application folder.

  1. My program.cs file is the default for .NET 6 Blazor WebAssembly projects and is like so:
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using LinkScreen.Client.Web;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

await builder.Build().RunAsync();

Things I've tried and it still doesn't work:

  1. Clean the project, delete the bin and obj directories.
  2. Clear the browser's cache as well as disabling it.
  3. I've tried both Safari and Chrome but I get the exact same issue.


Solution 1:[1]

Eventually I solved this by following the steps below:

  1. Created a new Blazor WebAssembly App with the following options: Configure for HTTPS, ASP.NET Core Hosted and Progressive Web App that generates 3 different projects in .NET 6 Client, Shared and Server.
  2. Tested that it works as is and the Server serves the Client.
  3. Created a Razor Class Library (RCL) without checking the "support pages and views! because it generates something different.
  4. Referenced the RCL to the Server project.
  5. Created a page on the Client that calls the component that exists in the RCL.
  6. And finally it worked!

Now, I think that the reason the component as a library didn't work for my old project are as follows:

  • I noticed that the Server didn't serve the Client and the Client was the project that was setup as the Startup project as opposed to the new project that I made.

  • I used a regular Class Library and not RCL in the project because I didn't think that there is a difference except the starter files but apparently there is more to it.

Solution 2:[2]

Are you getting a 404 error? try to modify your script source

<script src="_content/LinkScreen.Client.BrowserInterop/scripts/screen-capture.js" type="module"></script>

to

<script src="./_content/Client.BrowserInterop/scripts/screen-capture.js" type="module"></script>

as the offcial document explains:

The path segment for the current directory (./) is required in order to create the correct static asset path to the JS file. The {PACKAGE ID} placeholder is the library's package ID. The package ID defaults to the project's assembly name if isn't specified in the project file.

in my case ,it could work afer I modified the source: enter image description here

enter image description here

Solution 3:[3]

I was just trying to export my own components to a library and re-discovered the most important Blazor WASM rule :

  • Always clear the Client bin and obj folders.

What worked after copying the folders from the main project to the class library failed after a couple of executions. It seems the bin folder still contained some scripts so the paths that worked in the main project kept working until a full clean removed them.

If the script is stored in wwwroot/scripts and you use :

<script src="./_content/Client.BrowserInterop/scripts/screen-capture.js

Your methods should be available globally.

You should consider using JS isolation with side-by-side component scripts to avoid polluting the JS namespace and cluttering your wwwroot folder.

If your component is names ScreenCapture.razor, create a JS module in the same folder named ScreenCapture.razor.js. You can load this as a module in your component's OnAfterRenderAsync and call its exported methods :

private IJSObjectReference? module;

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if(module ==null)
    {
        var path = "./_content/Client.BrowserInterop/ScreenCapture.razor.js";
        module = await JS.InvokeAsync<IJSObjectReference>("import", path);
    }
}

    private ValueTask SetTextAsync(string text)
    {
        if (module is null)
        {
            return ValueTask.CompletedTask;
        }
        return module.InvokeVoidAsync("setValue", TextBox, text);
    }

Example

I was trying to put the code from this TagSelector component into my own RCL library using JS isolation to keep things tidy. The CSS and JS files are stored in wwwroot directly so normally I'd have to use :

<link rel="stylesheet" href="_content/MW.Blazor.TagSelector/styles.css" />
<script src="_content/MW.Blazor.TagSelector/interop.js"></script>

By renaming the files to TagSelector.razor.css and TagSelector.razor.js though, I was able to keep them together with the Razor file. I only need to use the TagSelector component now. The CSS and JS files are imported automatically.

The JS file was changed to a module :


export function getValue(element) {
    return element.value;
}

export function setValue(element, value) {
    element.value = value;
}

export function blur(element) {
    element.blur();
}

The module is loaded with :

var path = "./_content/MyLibraryName/TagSelector.razor.js";
module = await JS.InvokeAsync<IJSObjectReference>("import", path);

To call the exported setText method, the following wrapper is used:

    private ValueTask SetTextAsync(string text)
    {
        if (module is null)
        {
            return ValueTask.CompletedTask;
        }
        return module.InvokeVoidAsync("setValue", TextBox, text);
    }

Solution 4:[4]

Have you tried adding builder.WebHost.UseStaticWebAssets(); to your Program.cs of the Host Net Core app?

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Panagiotis Kanavos
Solution 4 americanslon