Skip to content

Commit

Permalink
Improve JavaScript interop pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
jsakamoto committed Apr 3, 2024
1 parent 10c86b1 commit e5b2856
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 122 deletions.
10 changes: 3 additions & 7 deletions BlazingStory/Components/BlazingStoryApp.razor
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@

private string _PreferesColorScheme = "light";

private int _PreferesColorSchemeChangeSubscribeId = 0;
private IJSObjectReference? _PreferesColorSchemeChangeSubscriber;

private readonly BlazingStoryOptions _Options = new();

Expand Down Expand Up @@ -208,7 +208,7 @@

await this.UpdatePreferesColorSchemeAsync();

this._PreferesColorSchemeChangeSubscribeId = await this._JSModule.InvokeAsync<int>("subscribePreferesColorSchemeChanged", this._RefThis, nameof(OnPreferesColorSchemeChanged));
this._PreferesColorSchemeChangeSubscriber = await this._JSModule.InvokeAsync<IJSObjectReference>("subscribePreferesColorSchemeChanged", this._RefThis, nameof(OnPreferesColorSchemeChanged));

// Init Level 0 -> Default Background is visible & Color Scheme Container is hidden and transparent.
Expand Down Expand Up @@ -265,11 +265,7 @@

public async ValueTask DisposeAsync()
{
if (this._PreferesColorSchemeChangeSubscribeId != 0)
{
await this._JSModule.InvokeVoidIfConnectedAsync("unsubscribePreferesColorSchemeChanged", this._PreferesColorSchemeChangeSubscribeId);
this._PreferesColorSchemeChangeSubscribeId = 0;
}
await this._PreferesColorSchemeChangeSubscriber.DisposeIfConnectedAsync("dispose");
this._RefThis.Dispose();
await _JSModule.DisposeAsync();
}
Expand Down
17 changes: 2 additions & 15 deletions BlazingStory/Components/BlazingStoryApp.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,8 @@ export const ensureAllFontsAndStylesAreLoaded = async () => {
};
const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
export const getPrefersColorScheme = () => darkModeMediaQuery.matches ? "dark" : "light";
let subscriberIndex = 0;
const subscribers = new Map();
export const subscribePreferesColorSchemeChanged = (dotnetObjRef, methodName) => {
const subscriber = (e) => {
dotnetObjRef.invokeMethodAsync(methodName, getPrefersColorScheme());
};
const subscriber = (e) => { dotnetObjRef.invokeMethodAsync(methodName, getPrefersColorScheme()); };
darkModeMediaQuery.addEventListener("change", subscriber);
subscriberIndex++;
subscribers.set(subscriberIndex, subscriber);
return subscriberIndex;
};
export const unsubscribePreferesColorSchemeChanged = (subscriberIndex) => {
const subscriber = subscribers.get(subscriberIndex);
if (typeof (subscriber) === "undefined")
return;
darkModeMediaQuery.removeEventListener("change", subscriber);
subscribers.delete(subscriberIndex);
return ({ dispose: () => darkModeMediaQuery.removeEventListener("change", subscriber) });
};
23 changes: 4 additions & 19 deletions BlazingStory/Components/BlazingStoryApp.razor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DotNetObjectReference } from "../Scripts/types";
import { DotNetObjectReference, IDisposable } from "../Scripts/types";

export const ensureAllFontsAndStylesAreLoaded = async () => {

Expand Down Expand Up @@ -26,23 +26,8 @@ const darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

export const getPrefersColorScheme = (): string => darkModeMediaQuery.matches ? "dark" : "light";

let subscriberIndex = 0;
const subscribers = new Map<number, (e: MediaQueryListEvent) => void>();

export const subscribePreferesColorSchemeChanged = (dotnetObjRef: DotNetObjectReference, methodName: string): number => {
const subscriber = (e: MediaQueryListEvent) => {
dotnetObjRef.invokeMethodAsync(methodName, getPrefersColorScheme());
};
export const subscribePreferesColorSchemeChanged = (dotnetObjRef: DotNetObjectReference, methodName: string): IDisposable => {
const subscriber = (e: MediaQueryListEvent) => { dotnetObjRef.invokeMethodAsync(methodName, getPrefersColorScheme()); };
darkModeMediaQuery.addEventListener("change", subscriber);

subscriberIndex++;
subscribers.set(subscriberIndex, subscriber);
return subscriberIndex;
}

export const unsubscribePreferesColorSchemeChanged = (subscriberIndex: number): void => {
const subscriber = subscribers.get(subscriberIndex);
if (typeof (subscriber) === "undefined") return;
darkModeMediaQuery.removeEventListener("change", subscriber);
subscribers.delete(subscriberIndex);
return ({ dispose: () => darkModeMediaQuery.removeEventListener("change", subscriber) });
}
4 changes: 1 addition & 3 deletions BlazingStory/Internals/Components/Menus/PopupMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,7 @@
this._HotKeysContext?.Dispose();
this._HotKeysContext = null;

if (this._EventSubscriber == null) return;
await this._EventSubscriber.InvokeVoidIfConnectedAsync("dispose");
await this._EventSubscriber.DisposeIfConnectedAsync();
await this._EventSubscriber.DisposeIfConnectedAsync("dispose");
this._EventSubscriber = null;
}

Expand Down
3 changes: 1 addition & 2 deletions BlazingStory/Internals/Components/Preview/PreviewFrame.razor
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,7 @@

public async ValueTask DisposeAsync()
{
await _EventMonitorSubscriber.InvokeVoidIfConnectedAsync("dispose");
await _EventMonitorSubscriber.DisposeIfConnectedAsync();
await _EventMonitorSubscriber.DisposeIfConnectedAsync("dispose");
await _JSModule.DisposeAsync();
_ThisRef?.Dispose();
}
Expand Down
40 changes: 37 additions & 3 deletions BlazingStory/Internals/Extensions/IJSExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,52 @@
using Microsoft.JSInterop;
using BlazingStory.Internals.Utils;
using Microsoft.JSInterop;

namespace BlazingStory.Internals.Extensions;

internal static class IJSExtensions
{
private const string _DefaultBasePath = "./_content/BlazingStory/";

/// <summary>
/// Import a JavScript module from the specified path.
/// </summary>
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> instance.</param>
/// <param name="modulePath">The path to the JavaScript module to import, like "./Component.razor.js".</param>
/// <returns></returns>
public static async ValueTask<IJSObjectReference> ImportAsync(this IJSRuntime jsRuntime, string modulePath)
{
var updateToken = UriParameterKit.GetUpdateToken(jsRuntime);
return await jsRuntime.InvokeAsync<IJSObjectReference>("import", _DefaultBasePath + modulePath + updateToken);
}

/// <summary>
/// Invoke a JavaScript function with the specified identifier.<br/>
/// This method will not throw an exception if the <see cref="IJSObjectReference"/> is disconnected.
/// </summary>
/// <param name="value">The <see cref="IJSObjectReference"/> instance.</param>
/// <param name="identifier">An identifier for the function to invoke.</param>
/// <param name="args">JSON-serializable arguments.</param>
public static async ValueTask InvokeVoidIfConnectedAsync(this IJSObjectReference? value, string identifier, params object?[]? args)
{
try { if (value != null) await value.InvokeVoidAsync(identifier, args); }
catch (JSDisconnectedException) { }
}

public static async ValueTask DisposeIfConnectedAsync(this IJSObjectReference? value)
/// <summary>
/// Dispose this <see cref="IJSObjectReference"/> object.<br/>
/// If <paramref name="methodToCallBeforeDispose"/> is not null, it will be invoked before disposing the object.<br/>
/// This method will not throw an exception if the <see cref="IJSObjectReference"/> is disconnected.
/// </summary>
/// <param name="value">The <see cref="IJSObjectReference"/> instance.</param>
/// <param name="methodToCallBeforeDispose">If specified, this method will be invoked before disposing the object.</param>
public static async ValueTask DisposeIfConnectedAsync(this IJSObjectReference? value, string? methodToCallBeforeDispose = null)
{
try { if (value != null) await value.DisposeAsync(); }
if (value == null) return;
try
{
if (methodToCallBeforeDispose != null) await value.InvokeVoidAsync(methodToCallBeforeDispose);
await value.DisposeAsync();
}
catch (JSDisconnectedException) { }
}
}
20 changes: 8 additions & 12 deletions BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@
"opacity:1 !important;" +
"transition:none !important;";

private readonly DotNetObjectReference<MeasureLayer> _This;
private DotNetObjectReference<MeasureLayer>? _This;

private readonly JSModule _JSModule;
private IJSObjectReference? _EventMonitorSubscriber;

public struct DOMRect
{
Expand Down Expand Up @@ -129,16 +129,13 @@

private string _ContentSizeText = "";

public MeasureLayer()
{
this._This = DotNetObjectReference.Create(this);
this._JSModule = new(() => this.JSRuntime, "Internals/Pages/Canvas/MeasureLayer.razor.js");
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
await this._JSModule.InvokeVoidAsync("attach", this._This);

await using var module = await this.JSRuntime.ImportAsync("Internals/Pages/Canvas/MeasureLayer.razor.js");
this._This = DotNetObjectReference.Create(this);
this._EventMonitorSubscriber = await module.InvokeAsync<IJSObjectReference>("subscribeTargetElementChanged", this._This);
}

[JSInvokable(nameof(TargetElementChanged))]
Expand Down Expand Up @@ -315,8 +312,7 @@

public async ValueTask DisposeAsync()
{
await this._JSModule.InvokeVoidIfConnectedAsync("detach");
await this._JSModule.DisposeAsync();
this._This.Dispose();
await this._EventMonitorSubscriber.DisposeIfConnectedAsync("dispose");
this._This?.Dispose();
}
}
34 changes: 14 additions & 20 deletions BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,35 @@ const targetEvents = [
"pointerleave",
"scroll"
];
let lastHoveredElement = null;
let attachedOwner = null;
const pxToNumber = (px) => parseInt(px.replace("px", ""), 10);
const getSpacingSize = (style, prefix) => {
const [top, left, bottom, right] = ["Top", "Left", "Bottom", "Right"].map(sufix => pxToNumber(style[prefix + sufix]));
return { top, left, bottom, right };
};
const handler = (ev) => {
if (attachedOwner === null)
return;
const handler = (context, ev) => {
let hoveredElement = (ev instanceof MouseEvent) && document.elementFromPoint(ev.clientX, ev.clientY);
let measurement = null;
if (lastHoveredElement !== null && hoveredElement === null) {
lastHoveredElement = null;
if (context.lastHoveredElement !== null && hoveredElement === null) {
context.lastHoveredElement = null;
}
else if (hoveredElement !== null && lastHoveredElement !== hoveredElement) {
lastHoveredElement = hoveredElement === false ? lastHoveredElement : hoveredElement;
if (lastHoveredElement !== null) {
const computedStyle = window.getComputedStyle(lastHoveredElement);
else if (hoveredElement !== null && context.lastHoveredElement !== hoveredElement) {
context.lastHoveredElement = hoveredElement === false ? context.lastHoveredElement : hoveredElement;
if (context.lastHoveredElement !== null) {
const computedStyle = window.getComputedStyle(context.lastHoveredElement);
measurement = {
boundary: lastHoveredElement.getBoundingClientRect(),
boundary: context.lastHoveredElement.getBoundingClientRect(),
padding: getSpacingSize(computedStyle, "padding"),
margin: getSpacingSize(computedStyle, "margin"),
};
}
}
else
return;
attachedOwner.invokeMethodAsync("TargetElementChanged", measurement);
};
export const attach = (owner) => {
attachedOwner = owner;
targetEvents.forEach(eventName => doc.addEventListener(eventName, handler));
context.owner.invokeMethodAsync("TargetElementChanged", measurement);
};
export const detach = () => {
attachedOwner = null;
targetEvents.forEach(eventName => doc.removeEventListener(eventName, handler));
export const subscribeTargetElementChanged = (owner) => {
const context = { owner, lastHoveredElement: null };
const h = (ev) => handler(context, ev);
targetEvents.forEach(eventName => doc.addEventListener(eventName, h));
return ({ dispose: () => { targetEvents.forEach(eventName => doc.removeEventListener(eventName, h)); } });
};
41 changes: 19 additions & 22 deletions BlazingStory/Internals/Pages/Canvas/MeasureLayer.razor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DotNetObjectReference } from "../../../Scripts/types";
import { DotNetObjectReference, IDisposable } from "../../../Scripts/types";

type SpacingSize = {
top: number;
Expand All @@ -21,9 +21,10 @@ const targetEvents = [
"scroll"
] as const;

let lastHoveredElement: HTMLElement | null = null;

let attachedOwner: DotNetObjectReference | null = null;
type Context = {
owner: DotNetObjectReference,
lastHoveredElement: HTMLElement | null
};

const pxToNumber = (px: string) => parseInt(px.replace("px", ""), 10);

Expand All @@ -32,37 +33,33 @@ const getSpacingSize = (style: CSSStyleDeclaration, prefix: "margin" | "padding"
return { top, left, bottom, right };
}

const handler = (ev: MouseEvent | Event) => {
if (attachedOwner === null) return;
const handler = (context: Context, ev: MouseEvent | Event) => {

let hoveredElement = (ev instanceof MouseEvent) && document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement | null;

let measurement: Measurement | null = null;
if (lastHoveredElement !== null && hoveredElement === null) {
lastHoveredElement = null;
if (context.lastHoveredElement !== null && hoveredElement === null) {
context.lastHoveredElement = null;
}
else if (hoveredElement !== null && lastHoveredElement !== hoveredElement) {
lastHoveredElement = hoveredElement === false ? lastHoveredElement : hoveredElement;
if (lastHoveredElement !== null) {
const computedStyle = window.getComputedStyle(lastHoveredElement);
else if (hoveredElement !== null && context.lastHoveredElement !== hoveredElement) {
context.lastHoveredElement = hoveredElement === false ? context.lastHoveredElement : hoveredElement;
if (context.lastHoveredElement !== null) {
const computedStyle = window.getComputedStyle(context.lastHoveredElement);
measurement = {
boundary: lastHoveredElement.getBoundingClientRect(),
boundary: context.lastHoveredElement.getBoundingClientRect(),
padding: getSpacingSize(computedStyle, "padding"),
margin: getSpacingSize(computedStyle, "margin"),
};
}
}
else return;

attachedOwner.invokeMethodAsync("TargetElementChanged", measurement);
context.owner.invokeMethodAsync("TargetElementChanged", measurement);
}

export const attach = (owner: DotNetObjectReference) => {
attachedOwner = owner;
targetEvents.forEach(eventName => doc.addEventListener(eventName, handler));
export const subscribeTargetElementChanged = (owner: DotNetObjectReference): IDisposable => {
const context = { owner, lastHoveredElement: null as (HTMLElement | null)};
const h = (ev: MouseEvent | Event) => handler(context, ev);
targetEvents.forEach(eventName => doc.addEventListener(eventName, h));
return ({ dispose: () => { targetEvents.forEach(eventName => doc.removeEventListener(eventName, h)); } });
}

export const detach = () => {
attachedOwner = null;
targetEvents.forEach(eventName => doc.removeEventListener(eventName, handler));
}
16 changes: 2 additions & 14 deletions BlazingStory/Internals/Pages/IFrame.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
@page "/iframe.html"
@layout NullLayout
@implements IAsyncDisposable
@inject IJSRuntime JSRuntime

<IdQueryRouter>
Expand All @@ -9,21 +8,10 @@

@code
{
private readonly JSModule _JSModule;

public IFrame()
{
this._JSModule = new(() => this.JSRuntime, "Internals/Pages/IFrame.razor.js");
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
await this._JSModule.InvokeVoidAsync("initializeCanvasFrame");
}

public async ValueTask DisposeAsync()
{
await this._JSModule.DisposeAsync();
await using var module = await this.JSRuntime.ImportAsync("Internals/Pages/IFrame.razor.js");
await module.InvokeVoidAsync("initializeCanvasFrame");
}
}
6 changes: 1 addition & 5 deletions BlazingStory/Internals/Services/JSModule.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using BlazingStory.Internals.Extensions;
using BlazingStory.Internals.Utils;
using Microsoft.JSInterop;

namespace BlazingStory.Internals.Services;
Expand All @@ -12,8 +11,6 @@ internal class JSModule : IAsyncDisposable

private readonly string _ModulePath;

private const string _DefaultBasePath = "./_content/BlazingStory/";

internal JSModule(Func<IJSRuntime> jSRuntime, string modulePath)
{
this._GetJSRuntime = jSRuntime;
Expand All @@ -25,8 +22,7 @@ private async ValueTask<IJSObjectReference> GetModuleAsync()
if (this._Module == null)
{
var jsRuntime = this._GetJSRuntime();
var updateToken = UriParameterKit.GetUpdateToken(jsRuntime);
this._Module = await jsRuntime.InvokeAsync<IJSObjectReference>("import", _DefaultBasePath + this._ModulePath + updateToken);
this._Module = await jsRuntime.ImportAsync(this._ModulePath);
}
return this._Module;
}
Expand Down

0 comments on commit e5b2856

Please sign in to comment.