[Feature Request] JS module adapter for Blazor
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
Description
### Is there an existing issue for this?
- [X] I have searched the existing issues
### Is your feature request related to a problem? Please describe the problem.
It would be helpful to have an easy way to import JS modules - for any given `fooModule.js` to have an adapter `FooModule.cs`.
I wrote some code for that, and then I heard @SteveSandersonMS [say](https://youtube.com/clip/UgkxNVp2yunt8GAZ2ZmdDduJ_DNWRS7jOJTj) he'd like something like that in v8, and he also published [his code](https://github.com/SteveSandersonMS/AudioBrowser/blob/dd7a03d93b4de5e97efb333f1120792ee48c70ba/MediaFilesAPI/Util/JSModule.cs). Mine is very similar, below.
*I'm opening this tracking issue so the idea doesn't get lost.* It would be a nice addition.
### Describe the solution you'd like
I've linked Steve's code above. Mine's below.
`JsModule.cs`:
````cs
public abstract class JsModule : IAsyncDisposable
{
private bool _isDisposed = false;
// From `dotnet new razorclasslib` template. It's nice because it
// avoids the loading hit when the user does not create a need for
// the JS module.
private readonly Lazy> _jsModuleProvider;
protected JsModule(IJSRuntime jsRuntime, string? modulePath = null)
{
if (jsRuntime == null) throw new ArgumentNullException(nameof(jsRuntime), "Argument was null.");
if (modulePath != null && string.IsNullOrWhiteSpace(modulePath)) throw new ArgumentException("Argument was empty or whitespace.", nameof(modulePath));
var path = modulePath ?? GetPathOfJsModule();
_jsModuleProvider = new(
() => jsRuntime.InvokeAsync("import", path)
.AsTask()
);
}
private string GetPathOfJsModule()
{
var nameAssembly = this.GetType().Assembly.GetName().Name; // e.g. "MyCompany.MyProject"
var nameType = this.GetType().Name; // e.g. "MyJsModule"
var nameScript = Camelise(nameType) + ".js"; // => "myJsModule.js"
var path = $"./_content/{nameAssembly}/{nameScript}"; // => "./_content/MyCompany.MyProject/myJsModule.js"
Debug.Assert(nameAssembly != null);
return path;
}
private string Camelise(string input) => input[0].ToString().ToLowerInvariant() + input[1..];
protected async ValueTask InvokeVoidAsync(string jsFunctionName, params object[]? args)
{
if (string.IsNullOrWhiteSpace(jsFunctionName)) throw new ArgumentNullException(nameof(jsFunctionName));
var module = await _jsModuleProvider.Value;
await module.InvokeVoidAsync(Camelise(jsFunctionName), args);
}
protected async ValueTask InvokeAsync(string jsFunctionName, params object[]? args)
{
if (string.IsNullOrWhiteSpace(jsFunctionName)) throw new ArgumentNullException(nameof(jsFunctionName));
var module = await _jsModuleProvider.Value;
return await module.InvokeAsync(Camelise(jsFunctionName), args);
}
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
GC.SuppressFinalize(this);
}
protected virtual async ValueTask DisposeAsyncCore() {
if (_isDisposed) return;
if (_jsModuleProvider.IsValueCreated)
{
var module = await _jsModuleProvider.Value;
await module.DisposeAsync().ConfigureAwait(false);
}
_isDisposed = true;
}
}
````
- I added the async dispose pattern
- To make it easy for users, I inferred the module file's path, to match that of the c# class; many people do it that way, e.g. `MyProject/Foo/Bar/SomeModule.cs` and its corresponding `MyProject/wwwroot/Foo/Bar/someModule.js`
- But user can supply a custom path (into ctor) to override that assumption
- The js module is assumed to be camel cased, as is the convention for JS
- The JS object reference is lazy, as is done in the `dotnet new razorclasslib` template; this avoids an unnecessary load when the user doesn't use that module
- I wanted to use `[CallerMemberName] string jsFunctionName = ""` to further simplify callsites, but couldn't find a way to do that because of the `params` array
`MyJsModule.cs`:
````cs
public sealed class MyJsModule : JsModule
{
public MyJsModule(IJSRuntime jsRuntime) : base(jsRuntime /*, custom js file path here if needed*/) { }
public async ValueTask Foo(string arg)
{
if (string.IsNullOrWhiteSpace(arg)) throw new ArgumentNullException(nameof(arg));
await InvokeVoidAsync(nameof(Foo), arg);
}
public async ValueTask Bar(string arg1, string arg2) {
if (string.IsNullOrWhiteSpace(arg1)) throw new ArgumentNullException(nameof(arg1));
if (string.IsNullOrWhiteSpace(arg2)) throw new ArgumentNullException(nameof(arg2));
await InvokeVoidAsync(nameof(Bar), arg1, arg2);
}
}
````
- I use `nameof(MethodName)` to ease refactoring/renaming
This works nicely, and greatly simplifies subclasses.
### Additional context
No.
Contributor guide
Assessment
This issue has not been assessed yet.