From cdc2a0dc1b75622c0c53cd44813095927eda2cdf Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Mon, 5 Jan 2026 22:56:27 +0530 Subject: [PATCH 1/6] OTP Input - Initial setup --- .../Layout/DemosMainLayout.razor.cs | 15 +- .../Components/Layout/DocsMainLayout.razor.cs | 15 +- .../Form/OTPInput/OTPInputDocumentation.razor | 57 +++++ .../OTPInput_Demo_01_How_it_works.razor | 19 ++ .../OTPInput/OTPInput_Demo_02_Length.razor | 20 ++ .../OTPInput_Doc_01_Documentation.razor | 37 ++++ .../Components/Pages/Home/Index.razor | 12 +- .../Constants/DemoRouteConstants.cs | 2 + .../Constants/DemoScreenshotSrcConstants.cs | 1 + .../Components/Form/OTPInput/OTPInput.razor | 20 ++ .../Form/OTPInput/OTPInput.razor.cs | 196 ++++++++++++++++++ blazorbootstrap/Constants/BootstrapClass.cs | 4 + blazorbootstrap/Utils/JsInteropUtils.cs | 12 ++ blazorbootstrap/wwwroot/blazor.bootstrap.js | 10 +- 14 files changed, 403 insertions(+), 17 deletions(-) create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInputDocumentation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInput_Demo_01_How_it_works.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInput_Demo_02_Length.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/OTPInput/OTPInput_Doc_01_Documentation.razor create mode 100644 blazorbootstrap/Components/Form/OTPInput/OTPInput.razor create mode 100644 blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs create mode 100644 blazorbootstrap/Utils/JsInteropUtils.cs diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs index ff436eebf..e6d066b2f 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs @@ -23,14 +23,15 @@ internal override IEnumerable GetNavItems() new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Demos_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" }, new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Demos_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Demos_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" }, - new (){ Id = "406", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, - new (){ Id = "407", Text = "Radio Input", Href = DemoRouteConstants.Demos_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" }, - new (){ Id = "408", Text = "Range Input", Href = DemoRouteConstants.Demos_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" }, + new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Demos_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" }, + new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, + new (){ Id = "408", Text = "Radio Input", Href = DemoRouteConstants.Demos_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" }, + new (){ Id = "409", Text = "Range Input", Href = DemoRouteConstants.Demos_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" }, //new (){ Id = "404", Text = "Select Input", Href = DemoRouteConstants.Demos_URL_SelectInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, - new (){ Id = "409", Text = "Switch", Href = DemoRouteConstants.Demos_URL_Switch, IconName = IconName.ToggleOn, ParentId = "4" }, - new (){ Id = "410", Text = "Text Input", Href = DemoRouteConstants.Demos_URL_TextInput, IconName = IconName.InputCursorText, ParentId = "4" }, - new (){ Id = "411", Text = "Text Area Input", Href = DemoRouteConstants.Demos_URL_TextAreaInput, IconName = IconName.InputCursorText, ParentId = "4" }, - new (){ Id = "412", Text = "Time Input", Href = DemoRouteConstants.Demos_URL_TimeInput, IconName = IconName.ClockFill, ParentId = "4" }, + new (){ Id = "410", Text = "Switch", Href = DemoRouteConstants.Demos_URL_Switch, IconName = IconName.ToggleOn, ParentId = "4" }, + new (){ Id = "411", Text = "Text Input", Href = DemoRouteConstants.Demos_URL_TextInput, IconName = IconName.InputCursorText, ParentId = "4" }, + new (){ Id = "412", Text = "Text Area Input", Href = DemoRouteConstants.Demos_URL_TextAreaInput, IconName = IconName.InputCursorText, ParentId = "4" }, + new (){ Id = "413", Text = "Time Input", Href = DemoRouteConstants.Demos_URL_TimeInput, IconName = IconName.ClockFill, ParentId = "4" }, new (){ Id = "5", Text = "Components", IconName = IconName.GearFill, IconColor = IconColor.Danger }, new (){ Id = "500", Text = "Accordion", Href = DemoRouteConstants.Demos_URL_Accordion, IconName = IconName.ChevronBarExpand, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs index c1b5e241b..d05756698 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs @@ -22,14 +22,15 @@ internal override IEnumerable GetNavItems() new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Docs_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" }, new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Docs_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Docs_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" }, - new (){ Id = "406", Text = "Password Input", Href = DemoRouteConstants.Docs_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, - new (){ Id = "407", Text = "Radio Input", Href = DemoRouteConstants.Docs_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" }, - new (){ Id = "408", Text = "Range Input", Href = DemoRouteConstants.Docs_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" }, + new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Docs_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" }, + new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Docs_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" }, + new (){ Id = "408", Text = "Radio Input", Href = DemoRouteConstants.Docs_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" }, + new (){ Id = "409", Text = "Range Input", Href = DemoRouteConstants.Docs_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" }, //new (){ Id = "404", Text = "Select Input", Href = DemoRouteConstants.Docs_URL_SelectInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" }, - new (){ Id = "409", Text = "Switch", Href = DemoRouteConstants.Docs_URL_Switch, IconName = IconName.ToggleOn, ParentId = "4" }, - new (){ Id = "410", Text = "Text Input", Href = DemoRouteConstants.Docs_URL_TextInput, IconName = IconName.InputCursorText, ParentId = "4" }, - new (){ Id = "411", Text = "Text Area Input", Href = DemoRouteConstants.Docs_URL_TextAreaInput, IconName = IconName.InputCursorText, ParentId = "4" }, - new (){ Id = "412", Text = "Time Input", Href = DemoRouteConstants.Docs_URL_TimeInput, IconName = IconName.ClockFill, ParentId = "4" }, + new (){ Id = "410", Text = "Switch", Href = DemoRouteConstants.Docs_URL_Switch, IconName = IconName.ToggleOn, ParentId = "4" }, + new (){ Id = "411", Text = "Text Input", Href = DemoRouteConstants.Docs_URL_TextInput, IconName = IconName.InputCursorText, ParentId = "4" }, + new (){ Id = "412", Text = "Text Area Input", Href = DemoRouteConstants.Docs_URL_TextAreaInput, IconName = IconName.InputCursorText, ParentId = "4" }, + new (){ Id = "413", Text = "Time Input", Href = DemoRouteConstants.Docs_URL_TimeInput, IconName = IconName.ClockFill, ParentId = "4" }, new (){ Id = "5", Text = "Components", IconName = IconName.GearFill, IconColor = IconColor.Danger }, new (){ Id = "500", Text = "Accordion", Href = DemoRouteConstants.Docs_URL_Accordion, IconName = IconName.ChevronBarExpand, ParentId = "5" }, diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInputDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInputDocumentation.razor new file mode 100644 index 000000000..7b5d29bb6 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInputDocumentation.razor @@ -0,0 +1,57 @@ +@page "/otp-input" +@attribute [Route(pageUrl)] +@layout DemosMainLayout + + + + + +
+
+ The OTPInput component provides a user-friendly interface for entering one-time passwords (OTP), commonly used for authentication and verification flows. +

+ How to use: +
+
    +
  1. Add the OTPInput component to your page.
  2. +
  3. Handle the OnOTPChanged event to capture the OTP value as the user types.
  4. +
  5. Handle the OnOTPCompleted event to respond when the user has entered the complete OTP.
  6. +
  7. Bind the entered OTP to a variable for display or further processing as needed.
  8. +
+
+ This demo illustrates the basic usage of the OTPInput component, including event handling for OTP entry and completion. +
+ +
+ +
+
+ The OTPInput component allows you to specify the required OTP length, adapting the number of input fields accordingly. +

+ How to use: +
+
    +
  1. Set the Length parameter to define how many digits or characters the OTP should have (e.g., Length="5").
  2. +
  3. Handle the OnOTPChanged and OnOTPCompleted events as shown in the demo to process the OTP input.
  4. +
  5. Display or use the entered OTP value as needed in your application.
  6. +
+
+ This demo demonstrates how to configure the OTPInput component for a custom OTP length and handle user input accordingly. +
+ +
+ +@code { + private const string componentName = nameof(OTPInput); + private const string pageUrl = DemoRouteConstants.Demos_URL_OTPInput; + private const string pageTitle = componentName; + private const string pageDescription = $"The {componentName} component allows users to enter a one-time password (OTP) in a secure and user-friendly manner. The component is designed to enhance the user experience by providing a visually appealing and functional input field for OTP entry."; + private const string metaTitle = $"Blazor {componentName} Component"; + private const string metaDescription = $"The {componentName} component allows users to enter a one-time password (OTP) in a secure and user-friendly manner. The component is designed to enhance the user experience by providing a visually appealing and functional input field for OTP entry."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_OTPInput; +} diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInput_Demo_01_How_it_works.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInput_Demo_01_How_it_works.razor new file mode 100644 index 000000000..824373cf4 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInput_Demo_01_How_it_works.razor @@ -0,0 +1,19 @@ + + +
Entered OTP: @enteredOTP
+ +@code { + private string? enteredOTP = null; + + private void HandleOtpChanged(string otp) + { + enteredOTP = otp; + } + + private void HandleOtpCompleted(string otp) + { + Console.WriteLine($"OTP Completed: {otp}"); + enteredOTP = otp; + } +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInput_Demo_02_Length.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInput_Demo_02_Length.razor new file mode 100644 index 000000000..7194b3841 --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Form/OTPInput/OTPInput_Demo_02_Length.razor @@ -0,0 +1,20 @@ + + +
Entered OTP: @enteredOTP
+ +@code { + private string? enteredOTP = null; + + private void HandleOtpChanged(string otp) + { + enteredOTP = otp; + } + + private void HandleOtpCompleted(string otp) + { + Console.WriteLine($"OTP Completed: {otp}"); + enteredOTP = otp; + } +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/OTPInput/OTPInput_Doc_01_Documentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/OTPInput/OTPInput_Doc_01_Documentation.razor new file mode 100644 index 000000000..1efb8d2cc --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Form/OTPInput/OTPInput_Doc_01_Documentation.razor @@ -0,0 +1,37 @@ +@attribute [Route(pageUrl)] +@layout DocsMainLayout + + + + + +
+ @metaTitle +
+ +
+ +
+ +
+ +
+ +
+ +
+ +@code { + private const string componentName = nameof(OTPInput); + private const string pageUrl = DemoRouteConstants.Docs_URL_OTPInput; + private const string pageTitle = componentName; + private const string pageDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string metaTitle = $"Blazor {componentName} Component"; + private const string metaDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_NumberInput; +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor index 392b6ecd6..b4a94d512 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor @@ -155,6 +155,11 @@

Number Input

+ +

Password Input

@@ -519,7 +529,7 @@ protected override void OnInitialized() { - version = $"v{Configuration["version"]}"; // example: v0.6.1 + version = $"v{Configuration["version"]}"; // example: v4.0.1 releaseShortDescription = Configuration["release:short_description"]!; base.OnInitialized(); diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs index 594c680fb..e4b1a7138 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs @@ -32,6 +32,7 @@ public static class DemoRouteConstants public const string Demos_URL_DateInput = Demos_URL_Forms_Prefix + "/date-input"; public const string Demos_URL_EnumInput = Demos_URL_Forms_Prefix + "/enum-input"; public const string Demos_URL_NumberInput = Demos_URL_Forms_Prefix + "/number-input"; + public const string Demos_URL_OTPInput = Demos_URL_Forms_Prefix + "/otp-input"; public const string Demos_URL_PasswordInput = Demos_URL_Forms_Prefix + "/password-input"; public const string Demos_URL_RadioInput = Demos_URL_Forms_Prefix + "/radio-input"; public const string Demos_URL_RangeInput = Demos_URL_Forms_Prefix + "/range-input"; @@ -143,6 +144,7 @@ public static class DemoRouteConstants public const string Docs_URL_DateInput = Docs_URL_Forms_Prefix + "/date-input"; public const string Docs_URL_EnumInput = Docs_URL_Forms_Prefix + "/enum-input"; public const string Docs_URL_NumberInput = Docs_URL_Forms_Prefix + "/number-input"; + public const string Docs_URL_OTPInput = Docs_URL_Forms_Prefix + "/otp-input"; public const string Docs_URL_PasswordInput = Docs_URL_Forms_Prefix + "/password-input"; public const string Docs_URL_RadioInput = Docs_URL_Forms_Prefix + "/radio-input"; public const string Docs_URL_RangeInput = Docs_URL_Forms_Prefix + "/range-input"; diff --git a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs index ddfd69234..3db0516fe 100644 --- a/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs +++ b/BlazorBootstrap.Demo.RCL/Constants/DemoScreenshotSrcConstants.cs @@ -24,6 +24,7 @@ public class DemoScreenshotSrcConstants public const string Demos_URL_DateInput = DemoScreenshotSrcPrefix + "date-input.png"; public const string Demos_URL_EnumInput = DemoScreenshotSrcPrefix + "enum-input.png"; // TODO: pending public const string Demos_URL_NumberInput = DemoScreenshotSrcPrefix + "number-input.png"; + public const string Demos_URL_OTPInput = DemoScreenshotSrcPrefix + "otp-input.png"; public const string Demos_URL_PasswordInput = DemoScreenshotSrcPrefix + "password-input.png"; public const string Demos_URL_RadioInput = DemoScreenshotSrcPrefix + "radio-input.png"; public const string Demos_URL_RangeInput = DemoScreenshotSrcPrefix + "range-input.png"; diff --git a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor new file mode 100644 index 000000000..b889a7773 --- /dev/null +++ b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor @@ -0,0 +1,20 @@ +@namespace BlazorBootstrap +@inherits BlazorBootstrapComponentBase + +
+ @for (int i = 0; i < Length; i++) + { + var index = i; + var inputId = GetInputId(index); + + + } +
\ No newline at end of file diff --git a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs new file mode 100644 index 000000000..db998e2ae --- /dev/null +++ b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs @@ -0,0 +1,196 @@ +namespace BlazorBootstrap; + +public partial class OTPInput : BlazorBootstrapComponentBase +{ + #region Fields and Constants + + private string[] otpValues = Array.Empty(); + + #endregion + + #region Methods + + // Auto focus + // Color + + protected override void OnParametersSet() + { + if (otpValues.Length != Length) + { + otpValues = new string[Length]; + Array.Fill(otpValues, string.Empty); + } + } + + /// + /// Clears the OTP input fields. + /// + [AddedVersion("4.0.0")] + [Description("Clears the OTP input fields.")] + public async Task ClearAsync() + { + otpValues = new string[Length]; + Array.Fill(otpValues, string.Empty); + await NotifyChangesAsync(); + + if (Length > 0) + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(0)); + + await InvokeAsync(StateHasChanged); + } + + private string GetInputId(int index) => $"{Id}-otp-input-{index}"; + + private async Task NotifyChangesAsync() + { + var otpValue = string.Join(string.Empty, otpValues); + await OnOTPChanged.InvokeAsync(otpValue); + + if (otpValue.Length == Length && !otpValues.Any(string.IsNullOrWhiteSpace)) + await OnOTPCompleted.InvokeAsync(otpValue); + } + + private async Task OnInput(ChangeEventArgs e, int index) + { + var currentValue = otpValues[index] ?? ""; + var newValue = new string(e.Value?.ToString()?.Where(char.IsDigit)?.ToArray()); + + if (string.IsNullOrEmpty(newValue)) + { + otpValues[index] = string.Empty; + + return; + } + + if (int.TryParse(newValue, out var digit)) + { + otpValues[index] = digit.ToString(); + + if (index < Length - 1) + { + otpValues[index + 1] = string.Empty; + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(index + 1)); + } + } + else + { + otpValues[index] = string.Empty; + } + + // Notify changes + await NotifyChangesAsync(); + } + + private async Task OnKeyUp(KeyboardEventArgs e, int index) + { + // Handle backspace key to clear the current input and focus on the previous one + if (e.Key == "Backspace" && index > 0) + { + otpValues[index] = string.Empty; + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(index - 1)); + + // Notify changes + await NotifyChangesAsync(); + } + + // Handle left arrow key to focus on the previous input + if (e.Key == "ArrowLeft" && index > 0) + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(index - 1)); + + // Handle right arrow key to focus on the next input + if (e.Key == "ArrowRight" && index < Length - 1) + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(index + 1)); + } + + #endregion + + #region Properties, Indexers + + protected override string? ClassNames => + BuildClassNames( + Class, + (BootstrapClass.FormControl, true), + (BootstrapClass.TextCenter, true), + (BootstrapClass.MarginEnd1, true) + ); + + protected override string? StyleNames => + BuildClassNames( + Style, + ("width:40px;", true), + ("height:40px;", true) + ); + + private string? ContainerClassNames => + BuildClassNames( + ContainerCssClass, + (BootstrapClass.Flex, true), + (BootstrapClass.FlexRow, true) + ); + + /// + /// Gets or sets the CSS class for the container element. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the CSS class for the container element.")] + [Parameter] + public string? ContainerCssClass { get; set; } + + /// + /// Gets or sets the CSS style for the container element. + /// + /// Default value is . + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(null)] + [Description("Gets or sets the CSS style for the container element.")] + [Parameter] + public string? ContainerCssStyle { get; set; } + + private string? ContainerStyleNames => + BuildClassNames( + ContainerCssStyle + ); + + /// + /// Gets or sets the OTP input length. + /// + /// Default value is 6. + /// + /// + [AddedVersion("4.0.0")] + [DefaultValue(6)] + [Description("Gets or sets the OTP input length.")] + [Parameter] + public int Length { get; set; } = 6; + + /// + /// This event fires when the OTP input value changes. + /// + [AddedVersion("4.0.0")] + [Description("This event fires when the OTP input value changes.")] + [Parameter] + public EventCallback OnOTPChanged { get; set; } + + // Disabled + // Divider + + /// + /// This event fires when the OTP input is completed. + /// + [AddedVersion("4.0.0")] + [Description("This event fires when the OTP input is completed.")] + [Parameter] + public EventCallback OnOTPCompleted { get; set; } + + #endregion + + // Size + // Style + // Width +} diff --git a/blazorbootstrap/Constants/BootstrapClass.cs b/blazorbootstrap/Constants/BootstrapClass.cs index 7b92fb56a..d8ee08c56 100644 --- a/blazorbootstrap/Constants/BootstrapClass.cs +++ b/blazorbootstrap/Constants/BootstrapClass.cs @@ -103,6 +103,9 @@ public static class BootstrapClass public const string ImageFluid = "img-fluid"; public const string ImageThumbnail = "img-thumbnail"; + public const string MarginEnd1 = "me-1"; + public const string MarginEnd2 = "me-2"; + public const string Modal = "modal"; public const string ModalFade = "fade"; @@ -143,6 +146,7 @@ public static class BootstrapClass public const string TableResponsive = "table-responsive"; public const string TableSticky = "bb-table-sticky"; + public const string TextCenter = "text-center"; public const string TextNoWrap = "text-nowrap"; public const string Toast = "toast"; diff --git a/blazorbootstrap/Utils/JsInteropUtils.cs b/blazorbootstrap/Utils/JsInteropUtils.cs new file mode 100644 index 000000000..e0be42a50 --- /dev/null +++ b/blazorbootstrap/Utils/JsInteropUtils.cs @@ -0,0 +1,12 @@ +namespace BlazorBootstrap; + +public class JsInteropUtils +{ + #region Fields and Constants + + private const string Prefix = "window.blazorBootstrap."; + + public const string FocusElement = Prefix + "focusElement"; + + #endregion +} diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.js b/blazorbootstrap/wwwroot/blazor.bootstrap.js index daefce205..f34ee2af2 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.js +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.js @@ -948,8 +948,11 @@ window.blazorBootstrap = { windowSize: () => window.innerWidth }, // global function - invokeMethodAsync: (callbackEventName, dotNetHelper) => { - dotNetHelper.invokeMethodAsync(callbackEventName); + focusElement(elementId) { + const element = document.getElementById(elementId); + if (element) { + element.focus(); + } }, hasInvalidChars: (input, validChars) => { if (input.length <= 0 || validChars.length <= 0) @@ -963,6 +966,9 @@ window.blazorBootstrap = { return false; }, + invokeMethodAsync: (callbackEventName, dotNetHelper) => { + dotNetHelper.invokeMethodAsync(callbackEventName); + }, scrollToElementBottom: (elementId) => { let el = document.getElementById(elementId); if (el) From f997370cbc371579a49a5c63434c9064acd382b2 Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Wed, 7 Jan 2026 23:15:52 +0530 Subject: [PATCH 2/6] Refactor OTPInput JS interop, add CharExtensions - Refactored OTPInput component to use more specific JS interop functions: replaced FocusElement with FocusInputElement and added SetInputElementValue for programmatically clearing input fields. - Added debug Console.WriteLine statements to OTPInput for easier tracing. - Updated OTP input margin class from MarginEnd1 to MarginEnd2 for improved spacing. - Added a new CharExtensions static class with an IsAlphanumeric extension method for char. --- .../Form/OTPInput/OTPInput.razor.cs | 19 ++++++++++++------- blazorbootstrap/Extensions/CharExtensions.cs | 11 +++++++++++ blazorbootstrap/Utils/JsInteropUtils.cs | 4 +++- blazorbootstrap/wwwroot/blazor.bootstrap.js | 8 +++++++- 4 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 blazorbootstrap/Extensions/CharExtensions.cs diff --git a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs index db998e2ae..cd28e7270 100644 --- a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs +++ b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs @@ -34,7 +34,7 @@ public async Task ClearAsync() await NotifyChangesAsync(); if (Length > 0) - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(0)); + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(0)); await InvokeAsync(StateHasChanged); } @@ -43,7 +43,9 @@ public async Task ClearAsync() private async Task NotifyChangesAsync() { + Console.WriteLine(">> NotifyChangesAsync called"); var otpValue = string.Join(string.Empty, otpValues); + Console.WriteLine($">> otpValue: {otpValue}"); await OnOTPChanged.InvokeAsync(otpValue); if (otpValue.Length == Length && !otpValues.Any(string.IsNullOrWhiteSpace)) @@ -52,13 +54,16 @@ private async Task NotifyChangesAsync() private async Task OnInput(ChangeEventArgs e, int index) { + Console.WriteLine(">> OnInput called"); var currentValue = otpValues[index] ?? ""; var newValue = new string(e.Value?.ToString()?.Where(char.IsDigit)?.ToArray()); + Console.WriteLine($">> newValue: {newValue}"); if (string.IsNullOrEmpty(newValue)) { otpValues[index] = string.Empty; - + await JSRuntime.InvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), string.Empty); + await NotifyChangesAsync(); return; } @@ -69,7 +74,7 @@ private async Task OnInput(ChangeEventArgs e, int index) if (index < Length - 1) { otpValues[index + 1] = string.Empty; - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(index + 1)); + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1)); } } else @@ -87,7 +92,7 @@ private async Task OnKeyUp(KeyboardEventArgs e, int index) if (e.Key == "Backspace" && index > 0) { otpValues[index] = string.Empty; - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(index - 1)); + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1)); // Notify changes await NotifyChangesAsync(); @@ -95,11 +100,11 @@ private async Task OnKeyUp(KeyboardEventArgs e, int index) // Handle left arrow key to focus on the previous input if (e.Key == "ArrowLeft" && index > 0) - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(index - 1)); + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1)); // Handle right arrow key to focus on the next input if (e.Key == "ArrowRight" && index < Length - 1) - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusElement, GetInputId(index + 1)); + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1)); } #endregion @@ -111,7 +116,7 @@ private async Task OnKeyUp(KeyboardEventArgs e, int index) Class, (BootstrapClass.FormControl, true), (BootstrapClass.TextCenter, true), - (BootstrapClass.MarginEnd1, true) + (BootstrapClass.MarginEnd2, true) ); protected override string? StyleNames => diff --git a/blazorbootstrap/Extensions/CharExtensions.cs b/blazorbootstrap/Extensions/CharExtensions.cs new file mode 100644 index 000000000..9aa38342b --- /dev/null +++ b/blazorbootstrap/Extensions/CharExtensions.cs @@ -0,0 +1,11 @@ +namespace BlazorBootstrap; + +public static class CharExtensions +{ + public static bool IsAlphanumeric(this char c) + { + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9'); + } +} diff --git a/blazorbootstrap/Utils/JsInteropUtils.cs b/blazorbootstrap/Utils/JsInteropUtils.cs index e0be42a50..0124b8f4a 100644 --- a/blazorbootstrap/Utils/JsInteropUtils.cs +++ b/blazorbootstrap/Utils/JsInteropUtils.cs @@ -6,7 +6,9 @@ public class JsInteropUtils private const string Prefix = "window.blazorBootstrap."; - public const string FocusElement = Prefix + "focusElement"; + public const string FocusInputElement = Prefix + "focusInputElement"; + + public const string SetInputElementValue = Prefix + "setInputElementValue"; #endregion } diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.js b/blazorbootstrap/wwwroot/blazor.bootstrap.js index f34ee2af2..fa50c95de 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.js +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.js @@ -948,7 +948,7 @@ window.blazorBootstrap = { windowSize: () => window.innerWidth }, // global function - focusElement(elementId) { + focusInputElement(elementId) { const element = document.getElementById(elementId); if (element) { element.focus(); @@ -978,6 +978,12 @@ window.blazorBootstrap = { let el = document.getElementById(elementId); if (el) el.scrollTop = 0; + }, + setInputElementValue: (elementId, value) => { + const element = document.getElementById(elementId); + if (element) { + element.value = value; + } } } From 976ac09f32490670f33ce0c9316458e6f377a0c2 Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Wed, 7 Jan 2026 23:28:59 +0530 Subject: [PATCH 3/6] Improve OTP input sanitization and focus handling Refactored the OnInput method to sanitize input by allowing only digits, handle pasted or fast-typed multiple digits by using the last digit, and clear invalid input both in state and DOM. Updated the input field if the raw value doesn't match the sanitized digit and streamlined focus movement logic. Removed unnecessary else branches and debug statements for cleaner code. --- .../Form/OTPInput/OTPInput.razor.cs | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs index cd28e7270..b8dc8303e 100644 --- a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs +++ b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs @@ -54,35 +54,40 @@ private async Task NotifyChangesAsync() private async Task OnInput(ChangeEventArgs e, int index) { - Console.WriteLine(">> OnInput called"); - var currentValue = otpValues[index] ?? ""; - var newValue = new string(e.Value?.ToString()?.Where(char.IsDigit)?.ToArray()); + var rawValue = e.Value?.ToString(); + var numericValue = new string(rawValue?.Where(char.IsDigit).ToArray()); - Console.WriteLine($">> newValue: {newValue}"); - if (string.IsNullOrEmpty(newValue)) + if (string.IsNullOrEmpty(numericValue)) { otpValues[index] = string.Empty; - await JSRuntime.InvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), string.Empty); + + // Clear the input element if it contained invalid characters + if (!string.IsNullOrEmpty(rawValue)) + { + await JSRuntime.InvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), string.Empty); + } + await NotifyChangesAsync(); return; } - if (int.TryParse(newValue, out var digit)) - { - otpValues[index] = digit.ToString(); + // If multiple digits were entered (e.g. fast typing or paste), use the last one + var digit = numericValue.Length > 1 ? numericValue[^1].ToString() : numericValue; - if (index < Length - 1) - { - otpValues[index + 1] = string.Empty; - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1)); - } + otpValues[index] = digit; + + // Reset the input value on the client side if it doesn't match the sanitized digit + if (rawValue != digit) + { + await JSRuntime.InvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), digit); } - else + + // Move focus to the next input field + if (index < Length - 1) { - otpValues[index] = string.Empty; + await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1)); } - // Notify changes await NotifyChangesAsync(); } From 7f01ef491bf55499001b7ae6d49136dc2697f8bb Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Sat, 31 Jan 2026 18:26:34 +0530 Subject: [PATCH 4/6] Improve OTPInput paste handling and JS interop safety - Allow multi-digit paste by setting maxlength=6 on input fields. - Distribute pasted digits across OTP fields and update focus accordingly. - Add SafeInvokeVoidAsync to safely wrap JS interop calls. - Replace direct JSRuntime.InvokeVoidAsync calls with SafeInvokeVoidAsync to handle component disposal and JS runtime disconnects gracefully. - Remove old logic that only used the last digit on multi-digit input. - Improves robustness and user experience during input and navigation. --- .../Components/Form/OTPInput/OTPInput.razor | 2 +- .../Form/OTPInput/OTPInput.razor.cs | 65 ++++++++++++++++--- 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor index b889a7773..a9958dc7e 100644 --- a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor +++ b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor @@ -11,7 +11,7 @@ id="@inputId" class="@ClassNames" style="@StyleNames" - maxlength="1" + maxlength="6" inputmode="numeric" value="@otpValues[index]" @oninput="(e) => OnInput(e, index)" diff --git a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs index b8dc8303e..34df96941 100644 --- a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs +++ b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs @@ -1,5 +1,7 @@ namespace BlazorBootstrap; +using Microsoft.JSInterop; + public partial class OTPInput : BlazorBootstrapComponentBase { #region Fields and Constants @@ -22,6 +24,23 @@ protected override void OnParametersSet() } } + private async Task SafeInvokeVoidAsync(string identifier, params object?[] args) + { + try + { + await JSRuntime.InvokeVoidAsync(identifier, args); + } + catch (TaskCanceledException) + { + // Component/DOM likely got removed (navigation, conditional render, etc.) + // Treat as benign for focus/value updates. + } + catch (JSDisconnectedException) + { + // JS runtime no longer available (more common on Server, but safe here too). + } + } + /// /// Clears the OTP input fields. /// @@ -34,7 +53,7 @@ public async Task ClearAsync() await NotifyChangesAsync(); if (Length > 0) - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(0)); + await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(0)); await InvokeAsync(StateHasChanged); } @@ -64,28 +83,56 @@ private async Task OnInput(ChangeEventArgs e, int index) // Clear the input element if it contained invalid characters if (!string.IsNullOrEmpty(rawValue)) { - await JSRuntime.InvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), string.Empty); + await SafeInvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), string.Empty); } await NotifyChangesAsync(); return; } - // If multiple digits were entered (e.g. fast typing or paste), use the last one - var digit = numericValue.Length > 1 ? numericValue[^1].ToString() : numericValue; + // If multiple digits were entered (e.g. paste), distribute them across the input fields + if (numericValue.Length > 1) + { + var digits = numericValue.ToCharArray(); + var currentInputLength = digits.Length; + + for (int i = 0; i < currentInputLength; i++) + { + var targetIndex = index + i; + if (targetIndex < Length) + { + otpValues[targetIndex] = digits[i].ToString(); + + // Update the UI value for the current input and subsequent inputs + await SafeInvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(targetIndex), otpValues[targetIndex]); + } + } + + // Move focus to the next input field after the last pasted digit + var nextIndex = index + currentInputLength; + if (nextIndex < Length) + await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(nextIndex)); + else + await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(Length - 1)); + + await NotifyChangesAsync(); + return; + } + + var digit = numericValue; otpValues[index] = digit; // Reset the input value on the client side if it doesn't match the sanitized digit if (rawValue != digit) { - await JSRuntime.InvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), digit); + await SafeInvokeVoidAsync(JsInteropUtils.SetInputElementValue, GetInputId(index), digit); } // Move focus to the next input field if (index < Length - 1) { - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1)); + await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1)); } await NotifyChangesAsync(); @@ -97,7 +144,7 @@ private async Task OnKeyUp(KeyboardEventArgs e, int index) if (e.Key == "Backspace" && index > 0) { otpValues[index] = string.Empty; - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1)); + await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1)); // Notify changes await NotifyChangesAsync(); @@ -105,11 +152,11 @@ private async Task OnKeyUp(KeyboardEventArgs e, int index) // Handle left arrow key to focus on the previous input if (e.Key == "ArrowLeft" && index > 0) - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1)); + await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index - 1)); // Handle right arrow key to focus on the next input if (e.Key == "ArrowRight" && index < Length - 1) - await JSRuntime.InvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1)); + await SafeInvokeVoidAsync(JsInteropUtils.FocusInputElement, GetInputId(index + 1)); } #endregion From a10a6fdd80df1189066374baa9312fec54bac321 Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Sat, 31 Jan 2026 23:57:45 +0530 Subject: [PATCH 5/6] Improve JS interop reliability with SafeInvokeVoidAsync Introduce SafeInvokeVoidAsync in BlazorBootstrapComponentBase to safely handle JSRuntime calls, preventing exceptions on disposal or JS disconnect. Refactor all components to use this method instead of direct JSRuntime.InvokeVoidAsync calls. Add isJsRuntimeAvailable flag to skip future JS calls after disconnect. Enhance Tabs disposal logic and remove redundant SafeInvokeVoidAsync from OTPInput. Add global using for Microsoft.JSInterop. Update PDF JS initialization to check for canvas existence before creating Pdf instance. --- .../Components/Alert/Alert.razor.cs | 6 ++--- .../Components/Carousel/Carousel.razor.cs | 12 ++++----- .../Components/Collapse/Collapse.razor.cs | 10 +++---- .../ConfirmDialog/ConfirmDialog.razor.cs | 4 +-- .../Core/BlazorBootstrapComponentBase.cs | 25 ++++++++++++++++++ .../Components/Dropdown/Dropdown.razor.cs | 21 +++++---------- .../Form/AutoComplete/AutoComplete.razor.cs | 8 +++--- .../Form/CurrencyInput/CurrencyInput.razor.cs | 2 +- .../Form/DateInput/DateInput.razor.cs | 2 +- .../Form/NumberInput/NumberInput.razor.cs | 4 +-- .../Form/OTPInput/OTPInput.razor.cs | 19 -------------- .../Form/RadioInput/RadioInput.razor.cs | 2 +- .../Form/RangeInput/RangeInput.razor.cs | 2 +- .../Form/TimeInput/TimeInput.razor.cs | 2 +- blazorbootstrap/Components/Grid/Grid.razor.cs | 6 ++--- .../Components/Maps/GoogleMap.razor.cs | 8 +++--- .../Components/Modals/Modal.razor.cs | 8 +++--- .../Components/Offcanvas/Offcanvas.razor.cs | 8 +++--- .../PdfViewer/PdfViewerJsInterop.cs | 15 +++++++++-- .../ScriptLoader/ScriptLoader.razor.cs | 2 +- .../Components/Sidebar/Sidebar.razor.cs | 2 +- .../SortableList/SortableListJsInterop.cs | 13 +++++++++- blazorbootstrap/Components/Tabs/Tab.razor.cs | 11 ++------ blazorbootstrap/Components/Tabs/Tabs.razor.cs | 26 +++++++++++++++++-- .../Components/Toasts/SimpleToast.razor.cs | 10 ++++++- .../Components/Toasts/Toasts.razor.cs | 2 +- .../Components/Tooltip/Tooltip.razor.cs | 10 +++---- blazorbootstrap/Usings.cs | 1 + .../wwwroot/blazor.bootstrap.pdf.js | 5 +++- 29 files changed, 147 insertions(+), 99 deletions(-) diff --git a/blazorbootstrap/Components/Alert/Alert.razor.cs b/blazorbootstrap/Components/Alert/Alert.razor.cs index 758db0329..ca1ba48b9 100644 --- a/blazorbootstrap/Components/Alert/Alert.razor.cs +++ b/blazorbootstrap/Components/Alert/Alert.razor.cs @@ -18,7 +18,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) try { if (IsRenderComplete) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.alert.dispose", Id); + await SafeInvokeVoidAsync("window.blazorBootstrap.alert.dispose", Id); } catch (JSDisconnectedException) { @@ -34,7 +34,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.alert.initialize", Id, objRef); + await SafeInvokeVoidAsync("window.blazorBootstrap.alert.initialize", Id, objRef); await base.OnAfterRenderAsync(firstRender); } @@ -57,7 +57,7 @@ protected override async Task OnInitializedAsync() /// [AddedVersion("1.0.0")] [Description("Closes an alert by removing it from the DOM.")] - public async Task CloseAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.alert.close", Id); + public async Task CloseAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.alert.close", Id); #endregion diff --git a/blazorbootstrap/Components/Carousel/Carousel.razor.cs b/blazorbootstrap/Components/Carousel/Carousel.razor.cs index 482f6966d..bbf5fff67 100644 --- a/blazorbootstrap/Components/Carousel/Carousel.razor.cs +++ b/blazorbootstrap/Components/Carousel/Carousel.razor.cs @@ -30,7 +30,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) try { if (IsRenderComplete) - await JSRuntime.InvokeVoidAsync(CarouselInterop.Dispose, Id); + await SafeInvokeVoidAsync(CarouselInterop.Dispose, Id); } catch (JSDisconnectedException) { @@ -48,7 +48,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender) { CarouselOptions options = new() { Interval = Interval, Keyboard = Keyboard, Ride = Autoplay.ToCarouselAutoPlayString(), Touch = Touch }; - await JSRuntime.InvokeVoidAsync(CarouselInterop.Initialize, Id, options, objRef); + await SafeInvokeVoidAsync(CarouselInterop.Initialize, Id, options, objRef); StateHasChanged(); // Required } @@ -87,7 +87,7 @@ public ValueTask ShowItemByIndexAsync(int index) if (!isDefaultActiveCarouselItemSet) isDefaultActiveCarouselItemSet = true; - return JSRuntime.InvokeVoidAsync(CarouselInterop.To, Id, index); + return new(SafeInvokeVoidAsync(CarouselInterop.To, Id, index)); } internal void AddItem(CarouselItem carouselItem) @@ -103,7 +103,7 @@ internal void AddItem(CarouselItem carouselItem) /// [AddedVersion("3.0.0")] [Description("Shows next CarouselItem.")] - public ValueTask PauseCarouselAsync() => JSRuntime.InvokeVoidAsync(CarouselInterop.Pause, Id); + public ValueTask PauseCarouselAsync() => new(SafeInvokeVoidAsync(CarouselInterop.Pause, Id)); /// /// Shows next . @@ -115,7 +115,7 @@ public ValueTask ShowNextItemAsync() var nextIndex = activeIndex + 1; activeIndex = nextIndex > items.Count - 1 ? 0 : nextIndex; - return JSRuntime.InvokeVoidAsync(CarouselInterop.Next, Id); + return new(SafeInvokeVoidAsync(CarouselInterop.Next, Id)); } /// @@ -128,7 +128,7 @@ public ValueTask ShowPreviousItemAsync() var previousIndex = activeIndex - 1; activeIndex = previousIndex < 0 ? items.Count - 1 : previousIndex; - return JSRuntime.InvokeVoidAsync(CarouselInterop.Previous, Id); + return new(SafeInvokeVoidAsync(CarouselInterop.Previous, Id)); } #endregion diff --git a/blazorbootstrap/Components/Collapse/Collapse.razor.cs b/blazorbootstrap/Components/Collapse/Collapse.razor.cs index 8f1855a78..351785540 100644 --- a/blazorbootstrap/Components/Collapse/Collapse.razor.cs +++ b/blazorbootstrap/Components/Collapse/Collapse.razor.cs @@ -18,7 +18,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) try { if (IsRenderComplete) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.collapse.dispose", Id); + await SafeInvokeVoidAsync("window.blazorBootstrap.collapse.dispose", Id); } catch (JSDisconnectedException) { @@ -34,7 +34,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.collapse.initialize", Id, Parent, Toggle, objRef); + await SafeInvokeVoidAsync("window.blazorBootstrap.collapse.initialize", Id, Parent, Toggle, objRef); await base.OnAfterRenderAsync(firstRender); } @@ -63,21 +63,21 @@ protected override async Task OnInitializedAsync() /// [AddedVersion("1.7.0")] [Description("Hides a collapsible element.")] - public async Task HideAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.collapse.hide", Id); + public async Task HideAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.collapse.hide", Id); /// /// Shows a collapsible element. /// [AddedVersion("1.7.0")] [Description("Shows a collapsible element.")] - public async Task ShowAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.collapse.show", Id); + public async Task ShowAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.collapse.show", Id); /// /// Toggles a collapsible element to shown or hidden. /// [AddedVersion("1.7.0")] [Description("Toggles a collapsible element to shown or hidden.")] - public async Task ToggleAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.collapse.toggle", Id); + public async Task ToggleAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.collapse.toggle", Id); #endregion diff --git a/blazorbootstrap/Components/ConfirmDialog/ConfirmDialog.razor.cs b/blazorbootstrap/Components/ConfirmDialog/ConfirmDialog.razor.cs index e97daaf40..9a4a23a9a 100644 --- a/blazorbootstrap/Components/ConfirmDialog/ConfirmDialog.razor.cs +++ b/blazorbootstrap/Components/ConfirmDialog/ConfirmDialog.razor.cs @@ -77,7 +77,7 @@ private void Hide() StateHasChanged(); - Task.Run(() => JSRuntime.InvokeVoidAsync("window.blazorBootstrap.confirmDialog.hide", Id)); + Task.Run(async () => await SafeInvokeVoidAsync("window.blazorBootstrap.confirmDialog.hide", Id)); } private void OnNoClick() @@ -122,7 +122,7 @@ private Task Show(string title, string? message1, string? message2, Type? StateHasChanged(); - Task.Run(() => JSRuntime.InvokeVoidAsync("window.blazorBootstrap.confirmDialog.show", Id)); + Task.Run(async () => await SafeInvokeVoidAsync("window.blazorBootstrap.confirmDialog.show", Id)); return task; } diff --git a/blazorbootstrap/Components/Core/BlazorBootstrapComponentBase.cs b/blazorbootstrap/Components/Core/BlazorBootstrapComponentBase.cs index 87dc2375e..8a238a63e 100644 --- a/blazorbootstrap/Components/Core/BlazorBootstrapComponentBase.cs +++ b/blazorbootstrap/Components/Core/BlazorBootstrapComponentBase.cs @@ -8,6 +8,8 @@ public abstract class BlazorBootstrapComponentBase : ComponentBase, IDisposable, private bool isDisposed; + private bool isJsRuntimeAvailable = true; + internal Queue> queuedTasks = new(); #endregion @@ -30,6 +32,27 @@ protected override void OnInitialized() Id ??= IdUtility.GetNextId(); } + protected async Task SafeInvokeVoidAsync(string identifier, params object?[] args) + { + if (!isJsRuntimeAvailable) + return; + + try + { + await JSRuntime.InvokeVoidAsync(identifier, args); + } + catch (TaskCanceledException) + { + // Component/DOM likely got removed (navigation, conditional render, etc.) + // Treat as benign for focus/value updates; do not mark JS runtime as unavailable. + } + catch (JSDisconnectedException) + { + // JS runtime no longer available (more common on Server, but safe here too). + isJsRuntimeAvailable = false; + } + } + public static string BuildClassNames(params (string? cssClass, bool when)[] cssClassList) { var list = new HashSet(); @@ -189,6 +212,8 @@ protected virtual ValueTask DisposeAsyncCore(bool disposing) protected bool IsRenderComplete { get; private set; } + protected bool IsJsRuntimeAvailable => isJsRuntimeAvailable; + [Inject] protected IJSRuntime JSRuntime { get; set; } = default!; /// diff --git a/blazorbootstrap/Components/Dropdown/Dropdown.razor.cs b/blazorbootstrap/Components/Dropdown/Dropdown.razor.cs index 550b3d549..86074c61d 100644 --- a/blazorbootstrap/Components/Dropdown/Dropdown.razor.cs +++ b/blazorbootstrap/Components/Dropdown/Dropdown.razor.cs @@ -15,15 +15,8 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) { if (disposing) { - try - { - if (IsRenderComplete) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.dropdown.dispose", Id); - } - catch (JSDisconnectedException) - { - // do nothing - } + if (IsRenderComplete) + await SafeInvokeVoidAsync("window.blazorBootstrap.dropdown.dispose", Id); objRef?.Dispose(); } @@ -34,7 +27,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.dropdown.initialize", Id, objRef); + await SafeInvokeVoidAsync("window.blazorBootstrap.dropdown.initialize", Id, objRef); await base.OnAfterRenderAsync(firstRender); } @@ -64,7 +57,7 @@ protected override void OnInitialized() /// Task [AddedVersion("1.10.0")] [Description("Hides the dropdown menu of a given navbar or tabbed navigation.")] - public async Task HideAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.dropdown.hide", Id); + public async Task HideAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.dropdown.hide", Id); /// /// Shows the dropdown menu of a given navbar or tabbed navigation. @@ -72,7 +65,7 @@ protected override void OnInitialized() /// Task [AddedVersion("1.10.0")] [Description("Shows the dropdown menu of a given navbar or tabbed navigation.")] - public async Task ShowAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.dropdown.show", Id); + public async Task ShowAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.dropdown.show", Id); /// /// Toggles the dropdown menu of a given navbar or tabbed navigation. @@ -80,7 +73,7 @@ protected override void OnInitialized() /// Task [AddedVersion("1.10.0")] [Description("Toggles the dropdown menu of a given navbar or tabbed navigation.")] - public async Task ToggleAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.dropdown.toggle", Id); + public async Task ToggleAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.dropdown.toggle", Id); /// /// Updates the position of an element’s dropdown. @@ -88,7 +81,7 @@ protected override void OnInitialized() /// Task [AddedVersion("1.10.0")] [Description("Updates the position of an element’s dropdown.")] - public async Task UpdateAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.dropdown.update", Id); + public async Task UpdateAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.dropdown.update", Id); #endregion diff --git a/blazorbootstrap/Components/Form/AutoComplete/AutoComplete.razor.cs b/blazorbootstrap/Components/Form/AutoComplete/AutoComplete.razor.cs index d98eeca0b..15d8d7d15 100644 --- a/blazorbootstrap/Components/Form/AutoComplete/AutoComplete.razor.cs +++ b/blazorbootstrap/Components/Form/AutoComplete/AutoComplete.razor.cs @@ -34,7 +34,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) try { if (IsRenderComplete) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.autocomplete.dispose", Element); // NOTE: Always pass ElementRef + await SafeInvokeVoidAsync("window.blazorBootstrap.autocomplete.dispose", Element); // NOTE: Always pass ElementRef } catch (JSDisconnectedException) { @@ -50,7 +50,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.autocomplete.initialize", Element, objRef); + await SafeInvokeVoidAsync("window.blazorBootstrap.autocomplete.initialize", Element, objRef); await base.OnAfterRenderAsync(firstRender); } @@ -198,7 +198,7 @@ private async Task HideAsync() if (AdditionalAttributes is not null && AdditionalAttributes.TryGetValue(BootstrapAttributes.DataBootstrapToggle, out _)) AdditionalAttributes.Remove(BootstrapAttributes.DataBootstrapToggle); - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.autocomplete.hide", Element); + await SafeInvokeVoidAsync("window.blazorBootstrap.autocomplete.hide", Element); } private async Task OnInputChangedAsync(ChangeEventArgs args) @@ -280,7 +280,7 @@ private async Task ShowAsync() if (AdditionalAttributes is not null && !AdditionalAttributes.TryGetValue(BootstrapAttributes.DataBootstrapToggle, out _)) AdditionalAttributes.Add(BootstrapAttributes.DataBootstrapToggle, "dropdown"); - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.autocomplete.show", Element); + await SafeInvokeVoidAsync("window.blazorBootstrap.autocomplete.show", Element); } #endregion diff --git a/blazorbootstrap/Components/Form/CurrencyInput/CurrencyInput.razor.cs b/blazorbootstrap/Components/Form/CurrencyInput/CurrencyInput.razor.cs index 99881011d..08d993584 100644 --- a/blazorbootstrap/Components/Form/CurrencyInput/CurrencyInput.razor.cs +++ b/blazorbootstrap/Components/Form/CurrencyInput/CurrencyInput.razor.cs @@ -20,7 +20,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.currencyInput.initialize", Id, isFloatingNumber(), AllowNegativeNumbers, cultureInfo.NumberFormat.CurrencyDecimalSeparator); + await SafeInvokeVoidAsync("window.blazorBootstrap.currencyInput.initialize", Id, isFloatingNumber(), AllowNegativeNumbers, cultureInfo.NumberFormat.CurrencyDecimalSeparator); var currentValue = Value; // object diff --git a/blazorbootstrap/Components/Form/DateInput/DateInput.razor.cs b/blazorbootstrap/Components/Form/DateInput/DateInput.razor.cs index 181d4239b..2c24410c5 100644 --- a/blazorbootstrap/Components/Form/DateInput/DateInput.razor.cs +++ b/blazorbootstrap/Components/Form/DateInput/DateInput.razor.cs @@ -230,7 +230,7 @@ private async Task SetValueAsync(TValue oldValue, object? newValue) formattedValue = GetFormattedValue(Value!); if (oldValue!.Equals(Value)) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.dateInput.setValue", Id, formattedValue); + await SafeInvokeVoidAsync("window.blazorBootstrap.dateInput.setValue", Id, formattedValue); await ValueChanged.InvokeAsync(Value); diff --git a/blazorbootstrap/Components/Form/NumberInput/NumberInput.razor.cs b/blazorbootstrap/Components/Form/NumberInput/NumberInput.razor.cs index 74c2ca9a9..a462d5abe 100644 --- a/blazorbootstrap/Components/Form/NumberInput/NumberInput.razor.cs +++ b/blazorbootstrap/Components/Form/NumberInput/NumberInput.razor.cs @@ -18,7 +18,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.numberInput.initialize", Id, isFloatingNumber(), AllowNegativeNumbers, cultureInfo.NumberFormat.NumberDecimalSeparator); + await SafeInvokeVoidAsync("window.blazorBootstrap.numberInput.initialize", Id, isFloatingNumber(), AllowNegativeNumbers, cultureInfo.NumberFormat.NumberDecimalSeparator); var currentValue = Value; // object @@ -261,7 +261,7 @@ private async Task OnChange(ChangeEventArgs e) Value = value; if (oldValue!.Equals(Value)) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.numberInput.setValue", Id, Value); + await SafeInvokeVoidAsync("window.blazorBootstrap.numberInput.setValue", Id, Value); await ValueChanged.InvokeAsync(Value); diff --git a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs index 34df96941..af0c38744 100644 --- a/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs +++ b/blazorbootstrap/Components/Form/OTPInput/OTPInput.razor.cs @@ -1,7 +1,5 @@ namespace BlazorBootstrap; -using Microsoft.JSInterop; - public partial class OTPInput : BlazorBootstrapComponentBase { #region Fields and Constants @@ -24,23 +22,6 @@ protected override void OnParametersSet() } } - private async Task SafeInvokeVoidAsync(string identifier, params object?[] args) - { - try - { - await JSRuntime.InvokeVoidAsync(identifier, args); - } - catch (TaskCanceledException) - { - // Component/DOM likely got removed (navigation, conditional render, etc.) - // Treat as benign for focus/value updates. - } - catch (JSDisconnectedException) - { - // JS runtime no longer available (more common on Server, but safe here too). - } - } - /// /// Clears the OTP input fields. /// diff --git a/blazorbootstrap/Components/Form/RadioInput/RadioInput.razor.cs b/blazorbootstrap/Components/Form/RadioInput/RadioInput.razor.cs index b0dffa725..4fb9c4fb3 100644 --- a/blazorbootstrap/Components/Form/RadioInput/RadioInput.razor.cs +++ b/blazorbootstrap/Components/Form/RadioInput/RadioInput.razor.cs @@ -16,7 +16,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.radioInput.initialize", Id, Name, objRef); + await SafeInvokeVoidAsync("window.blazorBootstrap.radioInput.initialize", Id, Name, objRef); } await base.OnAfterRenderAsync(firstRender); diff --git a/blazorbootstrap/Components/Form/RangeInput/RangeInput.razor.cs b/blazorbootstrap/Components/Form/RangeInput/RangeInput.razor.cs index f112e4511..d3661c438 100644 --- a/blazorbootstrap/Components/Form/RangeInput/RangeInput.razor.cs +++ b/blazorbootstrap/Components/Form/RangeInput/RangeInput.razor.cs @@ -28,7 +28,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.rangeInput.initialize", Id, objRef); + await SafeInvokeVoidAsync("window.blazorBootstrap.rangeInput.initialize", Id, objRef); var currentValue = Value; // object diff --git a/blazorbootstrap/Components/Form/TimeInput/TimeInput.razor.cs b/blazorbootstrap/Components/Form/TimeInput/TimeInput.razor.cs index ca48872d4..973814c2d 100644 --- a/blazorbootstrap/Components/Form/TimeInput/TimeInput.razor.cs +++ b/blazorbootstrap/Components/Form/TimeInput/TimeInput.razor.cs @@ -202,7 +202,7 @@ private async Task SetValueAsync(TValue oldValue, object? newValue) formattedValue = GetFormattedValue(Value!); if (oldValue!.Equals(Value)) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.timeInput.setValue", Id, formattedValue); + await SafeInvokeVoidAsync("window.blazorBootstrap.timeInput.setValue", Id, formattedValue); await ValueChanged.InvokeAsync(Value); diff --git a/blazorbootstrap/Components/Grid/Grid.razor.cs b/blazorbootstrap/Components/Grid/Grid.razor.cs index 331e101e8..63b3b6cbf 100644 --- a/blazorbootstrap/Components/Grid/Grid.razor.cs +++ b/blazorbootstrap/Components/Grid/Grid.razor.cs @@ -331,7 +331,7 @@ internal async Task SortingChangedAsync(GridColumn column) await RefreshDataAsync(false); } - private async Task CheckOrUnCheckAll() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.grid.checkOrUnCheckAll", $".bb-grid-form-check-{headerCheckboxId} > input.form-check-input", allItemsSelected); + private async Task CheckOrUnCheckAll() => await SafeInvokeVoidAsync("window.blazorBootstrap.grid.checkOrUnCheckAll", $".bb-grid-form-check-{headerCheckboxId} > input.form-check-input", allItemsSelected); /// /// Child selection template. @@ -534,7 +534,7 @@ private async Task OnRowCheckboxChanged(string id, TItem item, ChangeEventArgs a private async Task OnScroll(EventArgs e) { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.grid.scroll", Id); + await SafeInvokeVoidAsync("window.blazorBootstrap.grid.scroll", Id); } private void PrepareCheckboxIds() @@ -617,7 +617,7 @@ private async Task SelectAllItemsInternalAsync(bool selectAll) private Task SetCheckboxStateAsync(string id, CheckboxState checkboxState) { - queuedTasks.Enqueue(async () => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.grid.setSelectAllCheckboxState", id, (int)checkboxState)); + queuedTasks.Enqueue(async () => await SafeInvokeVoidAsync("window.blazorBootstrap.grid.setSelectAllCheckboxState", id, (int)checkboxState)); return Task.CompletedTask; } diff --git a/blazorbootstrap/Components/Maps/GoogleMap.razor.cs b/blazorbootstrap/Components/Maps/GoogleMap.razor.cs index a0688a3c5..0eba255af 100644 --- a/blazorbootstrap/Components/Maps/GoogleMap.razor.cs +++ b/blazorbootstrap/Components/Maps/GoogleMap.razor.cs @@ -26,7 +26,7 @@ protected override async Task OnInitializedAsync() [Description("Adds a marker to the GoogleMap.")] public ValueTask AddMarkerAsync(GoogleMapMarker marker) { - JSRuntime.InvokeVoidAsync("window.blazorBootstrap.googlemaps.addMarker", Id, marker, objRef); + _ = SafeInvokeVoidAsync("window.blazorBootstrap.googlemaps.addMarker", Id, marker, objRef); return ValueTask.CompletedTask; } @@ -46,7 +46,7 @@ public async Task OnMarkerClickJS(GoogleMapMarker marker) [Description("Refreshes the Google Map component.")] public ValueTask RefreshAsync() { - JSRuntime.InvokeVoidAsync("window.blazorBootstrap.googlemaps.initialize", Id, Zoom, Center, Markers, Clickable, objRef); + _ = SafeInvokeVoidAsync("window.blazorBootstrap.googlemaps.initialize", Id, Zoom, Center, Markers, Clickable, objRef); return ValueTask.CompletedTask; } @@ -59,14 +59,14 @@ public ValueTask RefreshAsync() [Description("Updates the markers on the Google Map.")] public ValueTask UpdateMarkersAsync(IEnumerable markers) { - JSRuntime.InvokeVoidAsync("window.blazorBootstrap.googlemaps.updateMarkers", Id, markers, objRef); + _ = SafeInvokeVoidAsync("window.blazorBootstrap.googlemaps.updateMarkers", Id, markers, objRef); return ValueTask.CompletedTask; } private void OnScriptLoad() { - Task.Run(async () => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.googlemaps.initialize", Id, Zoom, Center, Markers, Clickable, objRef)); + Task.Run(() => SafeInvokeVoidAsync("window.blazorBootstrap.googlemaps.initialize", Id, Zoom, Center, Markers, Clickable, objRef)); } #endregion diff --git a/blazorbootstrap/Components/Modals/Modal.razor.cs b/blazorbootstrap/Components/Modals/Modal.razor.cs index c0b6b4991..02a81ed37 100644 --- a/blazorbootstrap/Components/Modals/Modal.razor.cs +++ b/blazorbootstrap/Components/Modals/Modal.razor.cs @@ -32,7 +32,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) try { if (IsRenderComplete) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.modal.dispose", Id); + await SafeInvokeVoidAsync("window.blazorBootstrap.modal.dispose", Id); } catch (JSDisconnectedException) { @@ -51,7 +51,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.modal.initialize", Id, UseStaticBackdrop, CloseOnEscape, objRef); + await SafeInvokeVoidAsync("window.blazorBootstrap.modal.initialize", Id, UseStaticBackdrop, CloseOnEscape, objRef); await base.OnAfterRenderAsync(firstRender); } @@ -95,7 +95,7 @@ public async Task bsHiddenModal() public async Task HideAsync() { isVisible = false; - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.modal.hide", Id); + await SafeInvokeVoidAsync("window.blazorBootstrap.modal.hide", Id); } /// @@ -156,7 +156,7 @@ private async Task ShowAsync(string? title, string? message, Type? type, Diction await InvokeAsync(StateHasChanged); - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.modal.show", Id); + await SafeInvokeVoidAsync("window.blazorBootstrap.modal.show", Id); } #endregion diff --git a/blazorbootstrap/Components/Offcanvas/Offcanvas.razor.cs b/blazorbootstrap/Components/Offcanvas/Offcanvas.razor.cs index a56075b42..f84ebc3c8 100644 --- a/blazorbootstrap/Components/Offcanvas/Offcanvas.razor.cs +++ b/blazorbootstrap/Components/Offcanvas/Offcanvas.razor.cs @@ -24,7 +24,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) try { if (IsRenderComplete) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.offcanvas.dispose", Id); + await SafeInvokeVoidAsync("window.blazorBootstrap.offcanvas.dispose", Id); } catch (JSDisconnectedException) { @@ -40,7 +40,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.offcanvas.initialize", Id, UseStaticBackdrop, CloseOnEscape, IsScrollable, objRef); + await SafeInvokeVoidAsync("window.blazorBootstrap.offcanvas.initialize", Id, UseStaticBackdrop, CloseOnEscape, IsScrollable, objRef); await base.OnAfterRenderAsync(firstRender); } @@ -71,7 +71,7 @@ protected override async Task OnInitializedAsync() /// [AddedVersion("1.0.0")] [Description("Hides an offcanvas.")] - public async Task HideAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.offcanvas.hide", Id); + public async Task HideAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.offcanvas.hide", Id); /// /// Shows an offcanvas. @@ -97,7 +97,7 @@ private async Task ShowAsync(string? title, Type? type, Dictionary("window.blazorBootstrap.sidebar.windowSize"); diff --git a/blazorbootstrap/Components/SortableList/SortableListJsInterop.cs b/blazorbootstrap/Components/SortableList/SortableListJsInterop.cs index dbda19200..3d6bcd08e 100644 --- a/blazorbootstrap/Components/SortableList/SortableListJsInterop.cs +++ b/blazorbootstrap/Components/SortableList/SortableListJsInterop.cs @@ -21,11 +21,22 @@ public SortableListJsInterop(IJSRuntime jsRuntime) public async ValueTask DisposeAsync() { - if (moduleTask.IsValueCreated) + if (!moduleTask.IsValueCreated) + return; + + try { var module = await moduleTask.Value; await module.DisposeAsync(); } + catch (JSDisconnectedException) + { + // Circuit is gone; ignore. + } + catch (TaskCanceledException) + { + // Dispose during teardown; ignore. + } } public async Task InitializeAsync(string elementId, string elementName, string handle, string group, bool allowSorting, object pull, object put, string filter, object objRef) diff --git a/blazorbootstrap/Components/Tabs/Tab.razor.cs b/blazorbootstrap/Components/Tabs/Tab.razor.cs index 80048b65b..3751a9700 100644 --- a/blazorbootstrap/Components/Tabs/Tab.razor.cs +++ b/blazorbootstrap/Components/Tabs/Tab.razor.cs @@ -7,16 +7,9 @@ public partial class Tab : BlazorBootstrapComponentBase /// protected override async ValueTask DisposeAsyncCore(bool disposing) { - if (disposing && IsRenderComplete) + if (disposing && IsRenderComplete && IsJsRuntimeAvailable) { - try - { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.tabs.dispose", Id); - } - catch (JSDisconnectedException) - { - // do nothing - } + await SafeInvokeVoidAsync("window.blazorBootstrap.tabs.dispose", Id); } await base.DisposeAsyncCore(disposing); diff --git a/blazorbootstrap/Components/Tabs/Tabs.razor.cs b/blazorbootstrap/Components/Tabs/Tabs.razor.cs index 573a49a92..2ae387515 100644 --- a/blazorbootstrap/Components/Tabs/Tabs.razor.cs +++ b/blazorbootstrap/Components/Tabs/Tabs.razor.cs @@ -24,7 +24,29 @@ public partial class Tabs : BlazorBootstrapComponentBase protected override async ValueTask DisposeAsyncCore(bool disposing) { if (disposing && tabs is not null) + { + foreach (var tab in tabs) + { + try + { + if (tab is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else if (tab is IDisposable disposable) + { + disposable.Dispose(); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Exception while disposing tab '{tab?.Name}': {ex}"); + } + } + + tabs.Clear(); tabs = null!; + } await base.DisposeAsyncCore(disposing); } @@ -32,7 +54,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.tabs.initialize", Id, objRef); + await SafeInvokeVoidAsync("window.blazorBootstrap.tabs.initialize", Id, objRef); // Set active tab if (firstRender && !isDefaultActiveTabSet) @@ -283,7 +305,7 @@ private async Task ShowTabAsync(Tab tab) if (!isDefaultActiveTabSet) isDefaultActiveTabSet = true; - queuedTasks.Enqueue(async () => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.tabs.show", tab.Id)); + queuedTasks.Enqueue(async () => await SafeInvokeVoidAsync("window.blazorBootstrap.tabs.show", tab.Id)); if (tab?.OnClick.HasDelegate ?? false) await tab.OnClick.InvokeAsync(new TabEventArgs(tab.Name!, tab.Title!)); diff --git a/blazorbootstrap/Components/Toasts/SimpleToast.razor.cs b/blazorbootstrap/Components/Toasts/SimpleToast.razor.cs index 6bce92ce9..50f741324 100644 --- a/blazorbootstrap/Components/Toasts/SimpleToast.razor.cs +++ b/blazorbootstrap/Components/Toasts/SimpleToast.razor.cs @@ -15,7 +15,15 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) { if (disposing) { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.toasts.dispose", Id); + try + { + await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.toasts.dispose", Id); + } + catch (Microsoft.JSInterop.JSDisconnectedException) + { + // Circuit already disconnected; ignore. + } + objRef?.Dispose(); } diff --git a/blazorbootstrap/Components/Toasts/Toasts.razor.cs b/blazorbootstrap/Components/Toasts/Toasts.razor.cs index 7cc1eb7e1..024358339 100644 --- a/blazorbootstrap/Components/Toasts/Toasts.razor.cs +++ b/blazorbootstrap/Components/Toasts/Toasts.razor.cs @@ -72,7 +72,7 @@ private async Task OnToastShownAsync(ToastEventArgs args) { Messages.Remove(message); if (!string.IsNullOrWhiteSpace(message.ElementId)) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.toasts.hide", message.ElementId); + await SafeInvokeVoidAsync("window.blazorBootstrap.toasts.hide", message.ElementId); } } } diff --git a/blazorbootstrap/Components/Tooltip/Tooltip.razor.cs b/blazorbootstrap/Components/Tooltip/Tooltip.razor.cs index fb34e959c..0707015f4 100644 --- a/blazorbootstrap/Components/Tooltip/Tooltip.razor.cs +++ b/blazorbootstrap/Components/Tooltip/Tooltip.razor.cs @@ -21,7 +21,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) try { if (IsRenderComplete) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.tooltip.dispose", Element); + await SafeInvokeVoidAsync("window.blazorBootstrap.tooltip.dispose", Element); } catch (JSDisconnectedException) { @@ -38,7 +38,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.tooltip.initialize", Element); + await SafeInvokeVoidAsync("window.blazorBootstrap.tooltip.initialize", Element); isFirstRenderComplete = true; } @@ -65,14 +65,14 @@ protected override async Task OnParametersSetAsync() title = Title; color = Color; - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.tooltip.dispose", Element); - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.tooltip.update", Element); + await SafeInvokeVoidAsync("window.blazorBootstrap.tooltip.dispose", Element); + await SafeInvokeVoidAsync("window.blazorBootstrap.tooltip.update", Element); } } public async Task ShowAsync() { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.tooltip.show", Element); + await SafeInvokeVoidAsync("window.blazorBootstrap.tooltip.show", Element); } #endregion diff --git a/blazorbootstrap/Usings.cs b/blazorbootstrap/Usings.cs index 734a0407a..30d068b25 100644 --- a/blazorbootstrap/Usings.cs +++ b/blazorbootstrap/Usings.cs @@ -10,3 +10,4 @@ global using System.Linq.Expressions; global using System.Text.Json.Serialization; global using System.Text.RegularExpressions; +global using Microsoft.JSInterop; diff --git a/blazorbootstrap/wwwroot/blazor.bootstrap.pdf.js b/blazorbootstrap/wwwroot/blazor.bootstrap.pdf.js index a920e1782..3769918b8 100644 --- a/blazorbootstrap/wwwroot/blazor.bootstrap.pdf.js +++ b/blazorbootstrap/wwwroot/blazor.bootstrap.pdf.js @@ -195,7 +195,10 @@ pageRotateCcwButton.disabled = this.pagesCount === 0; */ export function initialize(dotNetHelper, elementId, scale, rotation, url, password = null) { - const pdf = new Pdf(elementId); + const canvas = getCanvas(elementId); + if (!canvas) return; + + const pdf = new Pdf(canvas); pdf.scale = scale; pdf.rotation = rotation; From 653d4b75205573bf76dc7c58f1b96adea596c33c Mon Sep 17 00:00:00 2001 From: gvreddy04 Date: Sun, 1 Feb 2026 00:24:06 +0530 Subject: [PATCH 6/6] Refactor JS interop: add JsInteropBase, unify error handling Introduce JsInteropBase to centralize JS module management and safe invocation for Blazor components. Refactor PdfViewerJsInterop, SortableListJsInterop, and ThemeSwitcherJsInterop to inherit from this base class, removing redundant DisposeAsync logic. Replace direct JSRuntime.InvokeVoidAsync calls with SafeInvokeVoidAsync across components (charts, Demo, Snippet, Toasts) to handle disconnects and cancellations gracefully. Remove unused JS property from Demo. Improves robustness and maintainability of JS interop throughout the codebase. --- .../Components/Shared/Demo.razor.cs | 6 +- .../Components/Shared/Snippet.cs | 7 +- .../Components/Charts/BarChart.razor.cs | 10 +-- .../Components/Charts/BlazorBootstrapChart.cs | 20 +++--- .../Components/Charts/DoughnutChart.razor.cs | 10 +-- .../Components/Charts/LineChart.razor.cs | 10 +-- .../Components/Charts/PieChart.razor.cs | 10 +-- .../Components/Charts/PolarAreaChart.razor.cs | 10 +-- .../Components/Charts/RadarChart.razor.cs | 10 +-- .../Components/Charts/ScatterChart.razor.cs | 10 +-- .../Components/Core/JsInteropBase.cs | 65 +++++++++++++++++++ .../PdfViewer/PdfViewerJsInterop.cs | 57 ++++------------ .../SortableList/SortableListJsInterop.cs | 33 +--------- .../ThemeSwitcher/ThemeSwitcherJsInterop.cs | 32 ++------- .../Components/Toasts/SimpleToast.razor.cs | 13 +--- .../Components/Toasts/Toast.razor.cs | 15 ++--- 16 files changed, 140 insertions(+), 178 deletions(-) create mode 100644 blazorbootstrap/Components/Core/JsInteropBase.cs diff --git a/BlazorBootstrap.Demo.RCL/Components/Shared/Demo.razor.cs b/BlazorBootstrap.Demo.RCL/Components/Shared/Demo.razor.cs index d2b282d24..226158eda 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Shared/Demo.razor.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Shared/Demo.razor.cs @@ -24,7 +24,7 @@ public partial class Demo : BlazorBootstrapComponentBase protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - await JS.InvokeVoidAsync("highlightCode"); + await SafeInvokeVoidAsync("highlightCode"); await base.OnAfterRenderAsync(firstRender); } @@ -97,7 +97,7 @@ public void ResetCopyStatusJS() StateHasChanged(); } - private async Task CopyToClipboardAsync() => await JS.InvokeVoidAsync("copyToClipboard", snippet, objRef); + private async Task CopyToClipboardAsync() => await SafeInvokeVoidAsync("copyToClipboard", snippet, objRef); #endregion @@ -105,8 +105,6 @@ public void ResetCopyStatusJS() protected override string? ClassNames => BuildClassNames(Class, ("bd-example-snippet bd-code-snippet", true)); - [Inject] protected IJSRuntime JS { get; set; } = default!; - [Parameter] public LanguageCode LanguageCode { get; set; } = LanguageCode.Razor; [Parameter] public bool ShowCodeOnly { get; set; } diff --git a/BlazorBootstrap.Demo.RCL/Components/Shared/Snippet.cs b/BlazorBootstrap.Demo.RCL/Components/Shared/Snippet.cs index 929ff8d96..e4600d8ec 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Shared/Snippet.cs +++ b/BlazorBootstrap.Demo.RCL/Components/Shared/Snippet.cs @@ -1,10 +1,11 @@ namespace BlazorBootstrap.Demo.RCL; -public class Snippet : ComponentBase +public class Snippet : BlazorBootstrapComponentBase { #region Members private string? snippet; + private bool isJsRuntimeAvailable = true; #endregion @@ -31,7 +32,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) - await JS.InvokeVoidAsync("highlightCode"); + await SafeInvokeVoidAsync("highlightCode"); await base.OnAfterRenderAsync(firstRender); } @@ -69,8 +70,6 @@ protected override async Task OnParametersSetAsync() #region Properties - [Inject] protected IJSRuntime JS { get; set; } = null!; - [Parameter] public LanguageCode LanguageCode { get; set; } = LanguageCode.Razor; [Parameter] public string? FilePath { get; set; } diff --git a/blazorbootstrap/Components/Charts/BarChart.razor.cs b/blazorbootstrap/Components/Charts/BarChart.razor.cs index c48a934f2..26db0541c 100644 --- a/blazorbootstrap/Components/Charts/BarChart.razor.cs +++ b/blazorbootstrap/Components/Charts/BarChart.razor.cs @@ -35,7 +35,7 @@ public override async Task AddDataAsync(ChartData chartData, string d if (data is BarChartDatasetData barChartDatasetData) barChartDataset.Data?.Add(barChartDatasetData.Data as double?); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); return chartData; } @@ -80,7 +80,7 @@ public override async Task AddDataAsync(ChartData chartData, string d barChartDataset.Data?.Add(barChartDatasetData.Data as double?); } - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (BarChartDatasetData)x)); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (BarChartDatasetData)x)); return chartData; } @@ -99,7 +99,7 @@ public override async Task AddDatasetAsync(ChartData chartData, IChar if (chartDataset is BarChartDataset) { chartData.Datasets.Add(chartDataset); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDataset", Id, (BarChartDataset)chartDataset); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDataset", Id, (BarChartDataset)chartDataset); } return chartData; @@ -111,7 +111,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (BarChartOptions)chartOptions, plugins); + await SafeInvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (BarChartOptions)chartOptions, plugins); } } @@ -121,7 +121,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (BarChartOptions)chartOptions); + await SafeInvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (BarChartOptions)chartOptions); } } diff --git a/blazorbootstrap/Components/Charts/BlazorBootstrapChart.cs b/blazorbootstrap/Components/Charts/BlazorBootstrapChart.cs index 0bfa8781d..d68d90ea5 100644 --- a/blazorbootstrap/Components/Charts/BlazorBootstrapChart.cs +++ b/blazorbootstrap/Components/Charts/BlazorBootstrapChart.cs @@ -47,11 +47,11 @@ public virtual async Task InitializeAsync(ChartData chartData, IChartOptions cha var _data = GetChartDataObject(chartData); if (chartType == ChartType.Bar) - await JSRuntime.InvokeVoidAsync("window.blazorChart.bar.initialize", Id, GetChartType(), _data, (BarChartOptions)chartOptions, plugins); + await SafeInvokeVoidAsync("window.blazorChart.bar.initialize", Id, GetChartType(), _data, (BarChartOptions)chartOptions, plugins); else if (chartType == ChartType.Line) - await JSRuntime.InvokeVoidAsync("window.blazorChart.line.initialize", Id, GetChartType(), _data, (LineChartOptions)chartOptions, plugins); + await SafeInvokeVoidAsync("window.blazorChart.line.initialize", Id, GetChartType(), _data, (LineChartOptions)chartOptions, plugins); else - await JSRuntime.InvokeVoidAsync("window.blazorChart.initialize", Id, GetChartType(), _data, chartOptions, plugins); + await SafeInvokeVoidAsync("window.blazorChart.initialize", Id, GetChartType(), _data, chartOptions, plugins); } } @@ -70,7 +70,7 @@ public async Task ResizeAsync(int width, int height, Unit widthUnit = Unit.Px, U { var widthWithUnit = $"width:{width.ToString(CultureInfo.InvariantCulture)}{widthUnit.ToCssString()}"; var heightWithUnit = $"height:{height.ToString(CultureInfo.InvariantCulture)}{heightUnit.ToCssString()}"; - await JSRuntime.InvokeVoidAsync("window.blazorChart.resize", Id, widthWithUnit, heightWithUnit); + await SafeInvokeVoidAsync("window.blazorChart.resize", Id, widthWithUnit, heightWithUnit); } /// @@ -86,11 +86,11 @@ public virtual async Task UpdateAsync(ChartData chartData, IChartOptions chartOp var data = GetChartDataObject(chartData); if (chartType == ChartType.Bar) - await JSRuntime.InvokeVoidAsync("window.blazorChart.bar.update", Id, GetChartType(), data, (BarChartOptions)chartOptions); + await SafeInvokeVoidAsync("window.blazorChart.bar.update", Id, GetChartType(), data, (BarChartOptions)chartOptions); else if (chartType == ChartType.Line) - await JSRuntime.InvokeVoidAsync("window.blazorChart.line.update", Id, GetChartType(), data, (LineChartOptions)chartOptions); + await SafeInvokeVoidAsync("window.blazorChart.line.update", Id, GetChartType(), data, (LineChartOptions)chartOptions); else - await JSRuntime.InvokeVoidAsync("window.blazorChart.update", Id, GetChartType(), data, chartOptions); + await SafeInvokeVoidAsync("window.blazorChart.update", Id, GetChartType(), data, chartOptions); } } @@ -106,11 +106,11 @@ public virtual async Task UpdateValuesAsync(ChartData chartData) var data = GetChartDataObject(chartData); if (chartType == ChartType.Bar) - await JSRuntime.InvokeVoidAsync("window.blazorChart.bar.updateDataValues", Id, data); + await SafeInvokeVoidAsync("window.blazorChart.bar.updateDataValues", Id, data); else if (chartType == ChartType.Line) - await JSRuntime.InvokeVoidAsync("window.blazorChart.line.updateDataValues", Id, data); + await SafeInvokeVoidAsync("window.blazorChart.line.updateDataValues", Id, data); else - await JSRuntime.InvokeVoidAsync("window.blazorChart.updateDataValues", Id, data); + await SafeInvokeVoidAsync("window.blazorChart.updateDataValues", Id, data); } } diff --git a/blazorbootstrap/Components/Charts/DoughnutChart.razor.cs b/blazorbootstrap/Components/Charts/DoughnutChart.razor.cs index 6119d6da7..1e28b6c52 100644 --- a/blazorbootstrap/Components/Charts/DoughnutChart.razor.cs +++ b/blazorbootstrap/Components/Charts/DoughnutChart.razor.cs @@ -38,7 +38,7 @@ public override async Task AddDataAsync(ChartData chartData, string d doughnutChartDataset.BackgroundColor?.Add(doughnutChartDatasetData.BackgroundColor!); } - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); return chartData; } @@ -86,7 +86,7 @@ public override async Task AddDataAsync(ChartData chartData, string d } } - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (DoughnutChartDatasetData)x)); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (DoughnutChartDatasetData)x)); return chartData; } @@ -105,7 +105,7 @@ public override async Task AddDatasetAsync(ChartData chartData, IChar if (chartDataset is DoughnutChartDataset doughnutChartDataset) { chartData.Datasets.Add(doughnutChartDataset); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDataset", Id, doughnutChartDataset); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDataset", Id, doughnutChartDataset); } return chartData; @@ -117,7 +117,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (DoughnutChartOptions)chartOptions, plugins); + await SafeInvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (DoughnutChartOptions)chartOptions, plugins); } } @@ -127,7 +127,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (DoughnutChartOptions)chartOptions); + await SafeInvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (DoughnutChartOptions)chartOptions); } } diff --git a/blazorbootstrap/Components/Charts/LineChart.razor.cs b/blazorbootstrap/Components/Charts/LineChart.razor.cs index 765fa000e..a68334578 100644 --- a/blazorbootstrap/Components/Charts/LineChart.razor.cs +++ b/blazorbootstrap/Components/Charts/LineChart.razor.cs @@ -29,7 +29,7 @@ public override async Task AddDataAsync(ChartData chartData, string d if (data is LineChartDatasetData lineChartDatasetData) lineChartDataset.Data?.Add(lineChartDatasetData.Data as double?); - await JSRuntime.InvokeVoidAsync("window.blazorChart.line.addDatasetData", Id, dataLabel, data); + await SafeInvokeVoidAsync("window.blazorChart.line.addDatasetData", Id, dataLabel, data); return chartData; } @@ -74,7 +74,7 @@ public override async Task AddDataAsync(ChartData chartData, string d lineChartDataset.Data?.Add(lineChartDatasetData.Data as double?); } - await JSRuntime.InvokeVoidAsync("window.blazorChart.line.addDatasetsData", Id, dataLabel, data?.Select(x => (LineChartDatasetData)x)); + await SafeInvokeVoidAsync("window.blazorChart.line.addDatasetsData", Id, dataLabel, data?.Select(x => (LineChartDatasetData)x)); return chartData; } @@ -93,7 +93,7 @@ public override async Task AddDatasetAsync(ChartData chartData, IChar if (chartDataset is LineChartDataset) { chartData.Datasets.Add(chartDataset); - await JSRuntime.InvokeVoidAsync("window.blazorChart.line.addDataset", Id, (LineChartDataset)chartDataset); + await SafeInvokeVoidAsync("window.blazorChart.line.addDataset", Id, (LineChartDataset)chartDataset); } return chartData; @@ -112,7 +112,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync("window.blazorChart.line.initialize", Id, GetChartType(), data, (LineChartOptions)chartOptions, plugins); + await SafeInvokeVoidAsync("window.blazorChart.line.initialize", Id, GetChartType(), data, (LineChartOptions)chartOptions, plugins); } public override async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) @@ -128,7 +128,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync("window.blazorChart.line.update", Id, GetChartType(), data, (LineChartOptions)chartOptions); + await SafeInvokeVoidAsync("window.blazorChart.line.update", Id, GetChartType(), data, (LineChartOptions)chartOptions); } #endregion diff --git a/blazorbootstrap/Components/Charts/PieChart.razor.cs b/blazorbootstrap/Components/Charts/PieChart.razor.cs index 86a4eeb5e..9c9f6d52b 100644 --- a/blazorbootstrap/Components/Charts/PieChart.razor.cs +++ b/blazorbootstrap/Components/Charts/PieChart.razor.cs @@ -38,7 +38,7 @@ public override async Task AddDataAsync(ChartData chartData, string d pieChartDataset.BackgroundColor?.Add(pieChartDatasetData.BackgroundColor!); } - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); return chartData; } @@ -86,7 +86,7 @@ public override async Task AddDataAsync(ChartData chartData, string d } } - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (PieChartDatasetData)x)); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (PieChartDatasetData)x)); return chartData; } @@ -105,7 +105,7 @@ public override async Task AddDatasetAsync(ChartData chartData, IChar if (chartDataset is PieChartDataset pieChartDataset) { chartData.Datasets.Add(pieChartDataset); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDataset", Id, pieChartDataset); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDataset", Id, pieChartDataset); } return chartData; @@ -117,7 +117,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (PieChartOptions)chartOptions, plugins); + await SafeInvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (PieChartOptions)chartOptions, plugins); } } @@ -127,7 +127,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (PieChartOptions)chartOptions); + await SafeInvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (PieChartOptions)chartOptions); } } diff --git a/blazorbootstrap/Components/Charts/PolarAreaChart.razor.cs b/blazorbootstrap/Components/Charts/PolarAreaChart.razor.cs index 615c9b624..993ec1f99 100644 --- a/blazorbootstrap/Components/Charts/PolarAreaChart.razor.cs +++ b/blazorbootstrap/Components/Charts/PolarAreaChart.razor.cs @@ -36,7 +36,7 @@ public override async Task AddDataAsync(ChartData chartData, string d if (data is PolarAreaChartDatasetData barChartDatasetData) barChartDataset.Data?.Add(barChartDatasetData.Data as double?); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); return chartData; } @@ -81,7 +81,7 @@ public override async Task AddDataAsync(ChartData chartData, string d barChartDataset.Data?.Add(barChartDatasetData.Data as double?); } - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (PolarAreaChartDatasetData)x)); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (PolarAreaChartDatasetData)x)); return chartData; } @@ -100,7 +100,7 @@ public override async Task AddDatasetAsync(ChartData chartData, IChar if (chartDataset is PolarAreaChartDataset) { chartData.Datasets.Add(chartDataset); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDataset", Id, (PolarAreaChartDataset)chartDataset); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDataset", Id, (PolarAreaChartDataset)chartDataset); } return chartData; @@ -112,7 +112,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (PolarAreaChartOptions)chartOptions, plugins); + await SafeInvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (PolarAreaChartOptions)chartOptions, plugins); } } @@ -122,7 +122,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (PolarAreaChartOptions)chartOptions); + await SafeInvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (PolarAreaChartOptions)chartOptions); } } diff --git a/blazorbootstrap/Components/Charts/RadarChart.razor.cs b/blazorbootstrap/Components/Charts/RadarChart.razor.cs index 360607416..d727a2e6f 100644 --- a/blazorbootstrap/Components/Charts/RadarChart.razor.cs +++ b/blazorbootstrap/Components/Charts/RadarChart.razor.cs @@ -36,7 +36,7 @@ public override async Task AddDataAsync(ChartData chartData, string d if (data is RadarChartDatasetData radarChartDatasetData) radarChartDataset.Data?.Add(radarChartDatasetData.Data as double?); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); return chartData; } @@ -81,7 +81,7 @@ public override async Task AddDataAsync(ChartData chartData, string d radarChartDataset.Data?.Add(radarChartDatasetData.Data as double?); } - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (RadarChartDatasetData)x)); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (RadarChartDatasetData)x)); return chartData; } @@ -100,7 +100,7 @@ public override async Task AddDatasetAsync(ChartData chartData, IChar if (chartDataset is RadarChartDataset) { chartData.Datasets.Add(chartDataset); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDataset", Id, (RadarChartDataset)chartDataset); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDataset", Id, (RadarChartDataset)chartDataset); } return chartData; @@ -112,7 +112,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (RadarChartOptions)chartOptions, plugins); + await SafeInvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (RadarChartOptions)chartOptions, plugins); } } @@ -122,7 +122,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO { var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (RadarChartOptions)chartOptions); + await SafeInvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (RadarChartOptions)chartOptions); } } diff --git a/blazorbootstrap/Components/Charts/ScatterChart.razor.cs b/blazorbootstrap/Components/Charts/ScatterChart.razor.cs index 02f22c924..df7271d7c 100644 --- a/blazorbootstrap/Components/Charts/ScatterChart.razor.cs +++ b/blazorbootstrap/Components/Charts/ScatterChart.razor.cs @@ -36,7 +36,7 @@ public override async Task AddDataAsync(ChartData chartData, string d if (data is ScatterChartDatasetData scatterChartDatasetData && scatterChartDatasetData.Data is ScatterChartDataPoint scatterChartDataPoint) scatterChartDataset.Data?.Add(scatterChartDataPoint); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetData", Id, dataLabel, data); return chartData; } @@ -81,7 +81,7 @@ public override async Task AddDataAsync(ChartData chartData, string d scatterChartDataset.Data?.Add(scatterChartDataPoint); } - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (ScatterChartDatasetData)x)); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDatasetsData", Id, dataLabel, data?.Select(x => (ScatterChartDatasetData)x)); return chartData; } @@ -100,7 +100,7 @@ public override async Task AddDatasetAsync(ChartData chartData, IChar if (chartDataset is ScatterChartDataset) { chartData.Datasets.Add(chartDataset); - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.addDataset", Id, (ScatterChartDataset)chartDataset); + await SafeInvokeVoidAsync($"{_jsObjectName}.addDataset", Id, (ScatterChartDataset)chartDataset); } return chartData; @@ -119,7 +119,7 @@ public override async Task InitializeAsync(ChartData chartData, IChartOptions ch var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (ScatterChartOptions)chartOptions, plugins); + await SafeInvokeVoidAsync($"{_jsObjectName}.initialize", Id, GetChartType(), data, (ScatterChartOptions)chartOptions, plugins); } public override async Task UpdateAsync(ChartData chartData, IChartOptions chartOptions) @@ -135,7 +135,7 @@ public override async Task UpdateAsync(ChartData chartData, IChartOptions chartO var datasets = chartData.Datasets.OfType(); var data = new { chartData.Labels, Datasets = datasets }; - await JSRuntime.InvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (ScatterChartOptions)chartOptions); + await SafeInvokeVoidAsync($"{_jsObjectName}.update", Id, GetChartType(), data, (ScatterChartOptions)chartOptions); } #endregion diff --git a/blazorbootstrap/Components/Core/JsInteropBase.cs b/blazorbootstrap/Components/Core/JsInteropBase.cs new file mode 100644 index 000000000..9c61e7dcc --- /dev/null +++ b/blazorbootstrap/Components/Core/JsInteropBase.cs @@ -0,0 +1,65 @@ +namespace BlazorBootstrap; + +public abstract class JsInteropBase : IAsyncDisposable +{ + #region Fields and Constants + + private readonly Lazy> moduleTask; + private bool isJsRuntimeAvailable = true; + + #endregion + + #region Constructors + + protected JsInteropBase(IJSRuntime jsRuntime, string modulePath) + { + moduleTask = new Lazy>(() => + jsRuntime.InvokeAsync("import", modulePath).AsTask()); + } + + #endregion + + #region Methods + + public async ValueTask DisposeAsync() + { + if (!moduleTask.IsValueCreated) + return; + + try + { + var module = await moduleTask.Value; + await module.DisposeAsync(); + } + catch (JSDisconnectedException) + { + // Circuit is gone; ignore cleanup. + } + catch (TaskCanceledException) + { + // Circuit is shutting down; ignore cleanup. + } + } + + protected async Task SafeInvokeVoidAsync(string identifier, params object?[] args) + { + if (!isJsRuntimeAvailable) + return; + + try + { + var module = await moduleTask.Value; + await module.InvokeVoidAsync(identifier, args); + } + catch (JSDisconnectedException) + { + isJsRuntimeAvailable = false; + } + catch (TaskCanceledException) + { + // do nothing + } + } + + #endregion +} diff --git a/blazorbootstrap/Components/PdfViewer/PdfViewerJsInterop.cs b/blazorbootstrap/Components/PdfViewer/PdfViewerJsInterop.cs index 1f9a867dc..b049e0502 100644 --- a/blazorbootstrap/Components/PdfViewer/PdfViewerJsInterop.cs +++ b/blazorbootstrap/Components/PdfViewer/PdfViewerJsInterop.cs @@ -1,96 +1,61 @@ namespace BlazorBootstrap; -public class PdfViewerJsInterop : IAsyncDisposable +public class PdfViewerJsInterop : JsInteropBase { - #region Fields and Constants - - private readonly Lazy> moduleTask; - - #endregion - #region Constructors public PdfViewerJsInterop(IJSRuntime jsRuntime) + : base(jsRuntime, "./_content/Blazor.Bootstrap/blazor.bootstrap.pdf.js") { - moduleTask = new Lazy>(() => jsRuntime.InvokeAsync("import", "./_content/Blazor.Bootstrap/blazor.bootstrap.pdf.js").AsTask()); } #endregion #region Methods - public async ValueTask DisposeAsync() - { - if (moduleTask.IsValueCreated) - { - try - { - var module = await moduleTask.Value; - await module.DisposeAsync(); - } - catch (JSDisconnectedException) - { - // Circuit is gone; ignore cleanup. - } - catch (TaskCanceledException) - { - // Circuit is shutting down; ignore cleanup. - } - } - } - public async Task FirstPageAsync(object objRef, string elementId) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("firstPage", objRef, elementId); + await SafeInvokeVoidAsync("firstPage", objRef, elementId); } public async Task GotoPageAsync(object objRef, string elementId, int gotoPageNum) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("gotoPage", objRef, elementId, gotoPageNum); + await SafeInvokeVoidAsync("gotoPage", objRef, elementId, gotoPageNum); } public async Task InitializeAsync(object objRef, string elementId, double scale, double rotation, string url, string password) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("initialize", objRef, elementId, scale, rotation, url, password); + await SafeInvokeVoidAsync("initialize", objRef, elementId, scale, rotation, url, password); } public async Task LastPageAsync(object objRef, string elementId) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("lastPage", objRef, elementId); + await SafeInvokeVoidAsync("lastPage", objRef, elementId); } public async Task NextPageAsync(object objRef, string elementId) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("nextPage", objRef, elementId); + await SafeInvokeVoidAsync("nextPage", objRef, elementId); } public async Task PreviousPageAsync(object objRef, string elementId) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("previousPage", objRef, elementId); + await SafeInvokeVoidAsync("previousPage", objRef, elementId); } public async Task PrintAsync(object objRef, string elementId, string url) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("print", objRef, elementId, url); + await SafeInvokeVoidAsync("print", objRef, elementId, url); } public async Task RotateAsync(object objRef, string elementId, double rotation) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("rotate", objRef, elementId, rotation); + await SafeInvokeVoidAsync("rotate", objRef, elementId, rotation); } public async Task ZoomInOutAsync(object objRef, string elementId, double scale) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("zoomInOut", objRef, elementId, scale); + await SafeInvokeVoidAsync("zoomInOut", objRef, elementId, scale); } #endregion diff --git a/blazorbootstrap/Components/SortableList/SortableListJsInterop.cs b/blazorbootstrap/Components/SortableList/SortableListJsInterop.cs index 3d6bcd08e..bf9c7aef7 100644 --- a/blazorbootstrap/Components/SortableList/SortableListJsInterop.cs +++ b/blazorbootstrap/Components/SortableList/SortableListJsInterop.cs @@ -1,48 +1,21 @@ namespace BlazorBootstrap; -public class SortableListJsInterop : IAsyncDisposable +public class SortableListJsInterop : JsInteropBase { - #region Fields and Constants - - private readonly Lazy> moduleTask; - - #endregion - #region Constructors public SortableListJsInterop(IJSRuntime jsRuntime) + : base(jsRuntime, "./_content/Blazor.Bootstrap/blazor.bootstrap.sortable-list.js") { - moduleTask = new Lazy>(() => jsRuntime.InvokeAsync("import", "./_content/Blazor.Bootstrap/blazor.bootstrap.sortable-list.js").AsTask()); } #endregion #region Methods - public async ValueTask DisposeAsync() - { - if (!moduleTask.IsValueCreated) - return; - - try - { - var module = await moduleTask.Value; - await module.DisposeAsync(); - } - catch (JSDisconnectedException) - { - // Circuit is gone; ignore. - } - catch (TaskCanceledException) - { - // Dispose during teardown; ignore. - } - } - public async Task InitializeAsync(string elementId, string elementName, string handle, string group, bool allowSorting, object pull, object put, string filter, object objRef) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("initialize", elementId, elementName, handle, group, allowSorting, pull, put, filter, objRef); + await SafeInvokeVoidAsync("initialize", elementId, elementName, handle, group, allowSorting, pull, put, filter, objRef); } #endregion diff --git a/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcherJsInterop.cs b/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcherJsInterop.cs index 50f8f9c2d..53ece5576 100644 --- a/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcherJsInterop.cs +++ b/blazorbootstrap/Components/ThemeSwitcher/ThemeSwitcherJsInterop.cs @@ -1,44 +1,21 @@ namespace BlazorBootstrap; -public class ThemeSwitcherJsInterop : IAsyncDisposable +public class ThemeSwitcherJsInterop : JsInteropBase { - #region Fields and Constants - - private readonly Lazy> moduleTask; - - #endregion - #region Constructors public ThemeSwitcherJsInterop(IJSRuntime jsRuntime) + : base(jsRuntime, "./_content/Blazor.Bootstrap/blazor.bootstrap.theme-switcher.js") { - moduleTask = new Lazy>(() => jsRuntime.InvokeAsync("import", "./_content/Blazor.Bootstrap/blazor.bootstrap.theme-switcher.js").AsTask()); } #endregion #region Methods - public async ValueTask DisposeAsync() - { - try - { - if (moduleTask.IsValueCreated) - { - var module = await moduleTask.Value; - await module.DisposeAsync(); - } - } - catch (JSDisconnectedException) - { - // do nothing - } - } - public async Task InitializeAsync(DotNetObjectReference? objRef) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("initializeTheme", objRef); + await SafeInvokeVoidAsync("initializeTheme", objRef); } internal Task SetAutoThemeAsync(DotNetObjectReference? objRef) => SetThemeAsync(objRef, "system"); @@ -49,8 +26,7 @@ public async Task InitializeAsync(DotNetObjectReference? objRef) internal async Task SetThemeAsync(DotNetObjectReference? objRef, string themeName) { - var module = await moduleTask.Value; - await module.InvokeVoidAsync("setTheme", objRef, themeName); + await SafeInvokeVoidAsync("setTheme", objRef, themeName); } #endregion diff --git a/blazorbootstrap/Components/Toasts/SimpleToast.razor.cs b/blazorbootstrap/Components/Toasts/SimpleToast.razor.cs index 50f741324..7b3040332 100644 --- a/blazorbootstrap/Components/Toasts/SimpleToast.razor.cs +++ b/blazorbootstrap/Components/Toasts/SimpleToast.razor.cs @@ -15,14 +15,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) { if (disposing) { - try - { - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.toasts.dispose", Id); - } - catch (Microsoft.JSInterop.JSDisconnectedException) - { - // Circuit already disconnected; ignore. - } + await SafeInvokeVoidAsync("window.blazorBootstrap.toasts.dispose", Id); objRef?.Dispose(); } @@ -60,12 +53,12 @@ protected override async Task OnInitializedAsync() /// /// Hides an element’s toast. /// - public async Task HideAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.toasts.hide", Id); + public async Task HideAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.toasts.hide", Id); /// /// Reveals an element’s toast. /// - public async Task ShowAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.toasts.show", Id, AutoHide, Delay, objRef); + public async Task ShowAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.toasts.show", Id, AutoHide, Delay, objRef); #endregion diff --git a/blazorbootstrap/Components/Toasts/Toast.razor.cs b/blazorbootstrap/Components/Toasts/Toast.razor.cs index 58f33c8cc..0df7a39c8 100644 --- a/blazorbootstrap/Components/Toasts/Toast.razor.cs +++ b/blazorbootstrap/Components/Toasts/Toast.razor.cs @@ -21,15 +21,8 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) { if (disposing) { - try - { - if (IsRenderComplete) - await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.toasts.dispose", Id); - } - catch (JSDisconnectedException) - { - // do nothing - } + if (IsRenderComplete) + await SafeInvokeVoidAsync("window.blazorBootstrap.toasts.dispose", Id); objRef?.Dispose(); } @@ -93,12 +86,12 @@ protected override async Task OnInitializedAsync() /// /// Hides an element’s toast. /// - public async Task HideAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.toasts.hide", Id); + public async Task HideAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.toasts.hide", Id); /// /// Reveals an element’s toast. /// - public async Task ShowAsync() => await JSRuntime.InvokeVoidAsync("window.blazorBootstrap.toasts.show", Id, AutoHide, Delay, objRef); + public async Task ShowAsync() => await SafeInvokeVoidAsync("window.blazorBootstrap.toasts.show", Id, AutoHide, Delay, objRef); private string GetIconClass() => ToastMessage.Type switch