Skip to content

Commit d4321fc

Browse files
authored
OTP Input (#1238)
* OTP Input - Initial setup * 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. * 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. * 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. * 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. * 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.
1 parent 8854d08 commit d4321fc

File tree

56 files changed

+714
-242
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+714
-242
lines changed

BlazorBootstrap.Demo.RCL/Components/Layout/DemosMainLayout.razor.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ internal override IEnumerable<NavItem> GetNavItems()
2323
new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Demos_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" },
2424
new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Demos_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" },
2525
new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Demos_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" },
26-
new (){ Id = "406", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" },
27-
new (){ Id = "407", Text = "Radio Input", Href = DemoRouteConstants.Demos_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" },
28-
new (){ Id = "408", Text = "Range Input", Href = DemoRouteConstants.Demos_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" },
26+
new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Demos_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" },
27+
new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Demos_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" },
28+
new (){ Id = "408", Text = "Radio Input", Href = DemoRouteConstants.Demos_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" },
29+
new (){ Id = "409", Text = "Range Input", Href = DemoRouteConstants.Demos_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" },
2930
//new (){ Id = "404", Text = "Select Input", Href = DemoRouteConstants.Demos_URL_SelectInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" },
30-
new (){ Id = "409", Text = "Switch", Href = DemoRouteConstants.Demos_URL_Switch, IconName = IconName.ToggleOn, ParentId = "4" },
31-
new (){ Id = "410", Text = "Text Input", Href = DemoRouteConstants.Demos_URL_TextInput, IconName = IconName.InputCursorText, ParentId = "4" },
32-
new (){ Id = "411", Text = "Text Area Input", Href = DemoRouteConstants.Demos_URL_TextAreaInput, IconName = IconName.InputCursorText, ParentId = "4" },
33-
new (){ Id = "412", Text = "Time Input", Href = DemoRouteConstants.Demos_URL_TimeInput, IconName = IconName.ClockFill, ParentId = "4" },
31+
new (){ Id = "410", Text = "Switch", Href = DemoRouteConstants.Demos_URL_Switch, IconName = IconName.ToggleOn, ParentId = "4" },
32+
new (){ Id = "411", Text = "Text Input", Href = DemoRouteConstants.Demos_URL_TextInput, IconName = IconName.InputCursorText, ParentId = "4" },
33+
new (){ Id = "412", Text = "Text Area Input", Href = DemoRouteConstants.Demos_URL_TextAreaInput, IconName = IconName.InputCursorText, ParentId = "4" },
34+
new (){ Id = "413", Text = "Time Input", Href = DemoRouteConstants.Demos_URL_TimeInput, IconName = IconName.ClockFill, ParentId = "4" },
3435

3536
new (){ Id = "5", Text = "Components", IconName = IconName.GearFill, IconColor = IconColor.Danger },
3637
new (){ Id = "500", Text = "Accordion", Href = DemoRouteConstants.Demos_URL_Accordion, IconName = IconName.ChevronBarExpand, ParentId = "5" },

BlazorBootstrap.Demo.RCL/Components/Layout/DocsMainLayout.razor.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ internal override IEnumerable<NavItem> GetNavItems()
2222
new (){ Id = "403", Text = "Date Input", Href = DemoRouteConstants.Docs_URL_DateInput, IconName = IconName.CalendarDate, ParentId = "4" },
2323
new (){ Id = "404", Text = "Enum Input", Href = DemoRouteConstants.Docs_URL_EnumInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" },
2424
new (){ Id = "405", Text = "Number Input", Href = DemoRouteConstants.Docs_URL_NumberInput, IconName = IconName.InputCursor, ParentId = "4" },
25-
new (){ Id = "406", Text = "Password Input", Href = DemoRouteConstants.Docs_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" },
26-
new (){ Id = "407", Text = "Radio Input", Href = DemoRouteConstants.Docs_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" },
27-
new (){ Id = "408", Text = "Range Input", Href = DemoRouteConstants.Docs_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" },
25+
new (){ Id = "406", Text = "OTP Input", Href = DemoRouteConstants.Docs_URL_OTPInput, IconName = IconName.Asterisk, ParentId = "4" },
26+
new (){ Id = "407", Text = "Password Input", Href = DemoRouteConstants.Docs_URL_PasswordInput, IconName = IconName.EyeSlashFill, ParentId = "4" },
27+
new (){ Id = "408", Text = "Radio Input", Href = DemoRouteConstants.Docs_URL_RadioInput, IconName = IconName.RecordCircle, ParentId = "4" },
28+
new (){ Id = "409", Text = "Range Input", Href = DemoRouteConstants.Docs_URL_RangeInput, IconName = IconName.Sliders, ParentId = "4" },
2829
//new (){ Id = "404", Text = "Select Input", Href = DemoRouteConstants.Docs_URL_SelectInput, IconName = IconName.MenuButtonWideFill, ParentId = "4" },
29-
new (){ Id = "409", Text = "Switch", Href = DemoRouteConstants.Docs_URL_Switch, IconName = IconName.ToggleOn, ParentId = "4" },
30-
new (){ Id = "410", Text = "Text Input", Href = DemoRouteConstants.Docs_URL_TextInput, IconName = IconName.InputCursorText, ParentId = "4" },
31-
new (){ Id = "411", Text = "Text Area Input", Href = DemoRouteConstants.Docs_URL_TextAreaInput, IconName = IconName.InputCursorText, ParentId = "4" },
32-
new (){ Id = "412", Text = "Time Input", Href = DemoRouteConstants.Docs_URL_TimeInput, IconName = IconName.ClockFill, ParentId = "4" },
30+
new (){ Id = "410", Text = "Switch", Href = DemoRouteConstants.Docs_URL_Switch, IconName = IconName.ToggleOn, ParentId = "4" },
31+
new (){ Id = "411", Text = "Text Input", Href = DemoRouteConstants.Docs_URL_TextInput, IconName = IconName.InputCursorText, ParentId = "4" },
32+
new (){ Id = "412", Text = "Text Area Input", Href = DemoRouteConstants.Docs_URL_TextAreaInput, IconName = IconName.InputCursorText, ParentId = "4" },
33+
new (){ Id = "413", Text = "Time Input", Href = DemoRouteConstants.Docs_URL_TimeInput, IconName = IconName.ClockFill, ParentId = "4" },
3334

3435
new (){ Id = "5", Text = "Components", IconName = IconName.GearFill, IconColor = IconColor.Danger },
3536
new (){ Id = "500", Text = "Accordion", Href = DemoRouteConstants.Docs_URL_Accordion, IconName = IconName.ChevronBarExpand, ParentId = "5" },
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
@page "/otp-input"
2+
@attribute [Route(pageUrl)]
3+
@layout DemosMainLayout
4+
5+
<DemosPageHeadSection PageUrl="@pageUrl"
6+
PageTitle="@pageTitle"
7+
PageDescription="@pageDescription"
8+
MetaTitle="@metaTitle"
9+
MetaDescription="@metaDescription"
10+
ImageUrl="@imageUrl" />
11+
12+
<DocsLink Href="@DemoRouteConstants.Docs_URL_OTPInput" />
13+
14+
<Section Class="p-0" Size="HeadingSize.H3" Name="How it works" PageUrl="@pageUrl" Link="how-it-works">
15+
<div class="mb-3">
16+
The <strong>OTPInput</strong> component provides a user-friendly interface for entering one-time passwords (OTP), commonly used for authentication and verification flows.
17+
<br /><br />
18+
<strong>How to use:</strong>
19+
<div class="content mb-2">
20+
<ol>
21+
<li>Add the <code>OTPInput</code> component to your page.</li>
22+
<li>Handle the <code>OnOTPChanged</code> event to capture the OTP value as the user types.</li>
23+
<li>Handle the <code>OnOTPCompleted</code> event to respond when the user has entered the complete OTP.</li>
24+
<li>Bind the entered OTP to a variable for display or further processing as needed.</li>
25+
</ol>
26+
</div>
27+
This demo illustrates the basic usage of the OTPInput component, including event handling for OTP entry and completion.
28+
</div>
29+
<Demo Type="typeof(OTPInput_Demo_01_How_it_works)" Tabs="true"/>
30+
</Section>
31+
32+
<Section Class="p-0" Size="HeadingSize.H3" Name="Length" PageUrl="@pageUrl" Link="length">
33+
<div class="mb-3">
34+
The <strong>OTPInput</strong> component allows you to specify the required OTP length, adapting the number of input fields accordingly.
35+
<br /><br />
36+
<strong>How to use:</strong>
37+
<div class="content mb-2">
38+
<ol>
39+
<li>Set the <code>Length</code> parameter to define how many digits or characters the OTP should have (e.g., <code>Length="5"</code>).</li>
40+
<li>Handle the <code>OnOTPChanged</code> and <code>OnOTPCompleted</code> events as shown in the demo to process the OTP input.</li>
41+
<li>Display or use the entered OTP value as needed in your application.</li>
42+
</ol>
43+
</div>
44+
This demo demonstrates how to configure the OTPInput component for a custom OTP length and handle user input accordingly.
45+
</div>
46+
<Demo Type="typeof(OTPInput_Demo_02_Length)" Tabs="true" />
47+
</Section>
48+
49+
@code {
50+
private const string componentName = nameof(OTPInput);
51+
private const string pageUrl = DemoRouteConstants.Demos_URL_OTPInput;
52+
private const string pageTitle = componentName;
53+
private const string pageDescription = $"The <code>{componentName}</code> 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.";
54+
private const string metaTitle = $"Blazor {componentName} Component";
55+
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.";
56+
private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_OTPInput;
57+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<OTPInput OnOTPChanged="HandleOtpChanged"
2+
OnOTPCompleted="HandleOtpCompleted" />
3+
4+
<div class="mt-3">Entered OTP: @enteredOTP</div>
5+
6+
@code {
7+
private string? enteredOTP = null;
8+
9+
private void HandleOtpChanged(string otp)
10+
{
11+
enteredOTP = otp;
12+
}
13+
14+
private void HandleOtpCompleted(string otp)
15+
{
16+
Console.WriteLine($"OTP Completed: {otp}");
17+
enteredOTP = otp;
18+
}
19+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<OTPInput Length="5"
2+
OnOTPChanged="HandleOtpChanged"
3+
OnOTPCompleted="HandleOtpCompleted" />
4+
5+
<div class="mt-3">Entered OTP: @enteredOTP</div>
6+
7+
@code {
8+
private string? enteredOTP = null;
9+
10+
private void HandleOtpChanged(string otp)
11+
{
12+
enteredOTP = otp;
13+
}
14+
15+
private void HandleOtpCompleted(string otp)
16+
{
17+
Console.WriteLine($"OTP Completed: {otp}");
18+
enteredOTP = otp;
19+
}
20+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@attribute [Route(pageUrl)]
2+
@layout DocsMainLayout
3+
4+
<DocsPageHeadSection PageUrl="@pageUrl"
5+
PageTitle="@pageTitle"
6+
PageDescription="@pageDescription"
7+
MetaTitle="@metaTitle"
8+
MetaDescription="@metaDescription"
9+
ImageUrl="@imageUrl" />
10+
11+
<DemoLink Href="@DemoRouteConstants.Demos_URL_OTPInput" />
12+
13+
<Section Class="p-0" Size="HeadingSize.H3" Name="Screenshot" PageUrl="@pageUrl" Link="screenshot">
14+
<img src="@imageUrl" class="img-fluid" alt="@metaTitle" />
15+
</Section>
16+
17+
<Section Class="p-0" Size="HeadingSize.H3" Name="Parameters" PageUrl="@pageUrl" Link="parameters">
18+
<DocxTable TItem="OTPInput" DocType="DocType.Parameters" />
19+
</Section>
20+
21+
<Section Class="p-0" Size="HeadingSize.H3" Name="Methods" PageUrl="@pageUrl" Link="methods">
22+
<DocxTable TItem="OTPInput" DocType="DocType.Methods" />
23+
</Section>
24+
25+
<Section Class="p-0" Size="HeadingSize.H3" Name="Events" PageUrl="@pageUrl" Link="events">
26+
<DocxTable TItem="OTPInput" DocType="DocType.Events" />
27+
</Section>
28+
29+
@code {
30+
private const string componentName = nameof(OTPInput);
31+
private const string pageUrl = DemoRouteConstants.Docs_URL_OTPInput;
32+
private const string pageTitle = componentName;
33+
private const string pageDescription = $"This documentation provides a comprehensive reference for the <code>{componentName}</code> component, guiding you through its configuration options.";
34+
private const string metaTitle = $"Blazor {componentName} Component";
35+
private const string metaDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options.";
36+
private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_NumberInput;
37+
}

BlazorBootstrap.Demo.RCL/Components/Pages/Home/Index.razor

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@
155155
<h4 class="mb-0 fs-5 fw-semibold"><Icon Name="IconName.InputCursor" class="me-2" /> Number Input</h4>
156156
</a>
157157
</div>
158+
<div class="col-sm-4 mb-2">
159+
<a class="d-block pe-lg-4 text-decoration-none lh-sm" href="@DemoRouteConstants.Demos_URL_OTPInput">
160+
<h4 class="mb-0 fs-5 fw-semibold"><Icon Name="IconName.Asterisk" class="me-2" /> OTP Input <Badge Color="BadgeColor.Danger">New</Badge></h4>
161+
</a>
162+
</div>
158163
<div class="col-sm-4 mb-2">
159164
<a class="d-block pe-lg-4 text-decoration-none lh-sm" href="@DemoRouteConstants.Demos_URL_PasswordInput">
160165
<h4 class="mb-0 fs-5 fw-semibold"><Icon Name="IconName.EyeSlashFill" class="me-2" /> Password Input</h4>
@@ -314,6 +319,11 @@
314319
<h4 class="mb-0 fs-5 fw-semibold"><Icon Name="IconName.InputCursor" class="me-2" /> Number Input</h4>
315320
</a>
316321
</div>
322+
<div class="col-sm-4 mb-2">
323+
<a class="d-block pe-lg-4 text-decoration-none lh-sm" href="@DemoRouteConstants.Demos_URL_OTPInput">
324+
<h4 class="mb-0 fs-5 fw-semibold"><Icon Name="IconName.Asterisk" class="me-2" /> OTP Input <Badge Color="BadgeColor.Danger">New</Badge></h4>
325+
</a>
326+
</div>
317327
<div class="col-sm-4 mb-2">
318328
<a class="d-block pe-lg-4 text-decoration-none lh-sm" href="@DemoRouteConstants.Demos_URL_PasswordInput">
319329
<h4 class="mb-0 fs-5 fw-semibold"><Icon Name="IconName.EyeSlashFill" class="me-2" /> Password Input</h4>
@@ -519,7 +529,7 @@
519529

520530
protected override void OnInitialized()
521531
{
522-
version = $"v{Configuration["version"]}"; // example: v0.6.1
532+
version = $"v{Configuration["version"]}"; // example: v4.0.1
523533
releaseShortDescription = Configuration["release:short_description"]!;
524534

525535
base.OnInitialized();

BlazorBootstrap.Demo.RCL/Components/Shared/Demo.razor.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public partial class Demo : BlazorBootstrapComponentBase
2424
protected override async Task OnAfterRenderAsync(bool firstRender)
2525
{
2626
if (firstRender)
27-
await JS.InvokeVoidAsync("highlightCode");
27+
await SafeInvokeVoidAsync("highlightCode");
2828

2929
await base.OnAfterRenderAsync(firstRender);
3030
}
@@ -97,16 +97,14 @@ public void ResetCopyStatusJS()
9797
StateHasChanged();
9898
}
9999

100-
private async Task CopyToClipboardAsync() => await JS.InvokeVoidAsync("copyToClipboard", snippet, objRef);
100+
private async Task CopyToClipboardAsync() => await SafeInvokeVoidAsync("copyToClipboard", snippet, objRef);
101101

102102
#endregion
103103

104104
#region Properties, Indexers
105105

106106
protected override string? ClassNames => BuildClassNames(Class, ("bd-example-snippet bd-code-snippet", true));
107107

108-
[Inject] protected IJSRuntime JS { get; set; } = default!;
109-
110108
[Parameter] public LanguageCode LanguageCode { get; set; } = LanguageCode.Razor;
111109

112110
[Parameter] public bool ShowCodeOnly { get; set; }

BlazorBootstrap.Demo.RCL/Components/Shared/Snippet.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
namespace BlazorBootstrap.Demo.RCL;
22

3-
public class Snippet : ComponentBase
3+
public class Snippet : BlazorBootstrapComponentBase
44
{
55
#region Members
66

77
private string? snippet;
8+
private bool isJsRuntimeAvailable = true;
89

910
#endregion
1011

@@ -31,7 +32,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
3132
protected override async Task OnAfterRenderAsync(bool firstRender)
3233
{
3334
if (firstRender)
34-
await JS.InvokeVoidAsync("highlightCode");
35+
await SafeInvokeVoidAsync("highlightCode");
3536

3637
await base.OnAfterRenderAsync(firstRender);
3738
}
@@ -69,8 +70,6 @@ protected override async Task OnParametersSetAsync()
6970

7071
#region Properties
7172

72-
[Inject] protected IJSRuntime JS { get; set; } = null!;
73-
7473
[Parameter] public LanguageCode LanguageCode { get; set; } = LanguageCode.Razor;
7574

7675
[Parameter] public string? FilePath { get; set; }

BlazorBootstrap.Demo.RCL/Constants/DemoRouteConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public static class DemoRouteConstants
3232
public const string Demos_URL_DateInput = Demos_URL_Forms_Prefix + "/date-input";
3333
public const string Demos_URL_EnumInput = Demos_URL_Forms_Prefix + "/enum-input";
3434
public const string Demos_URL_NumberInput = Demos_URL_Forms_Prefix + "/number-input";
35+
public const string Demos_URL_OTPInput = Demos_URL_Forms_Prefix + "/otp-input";
3536
public const string Demos_URL_PasswordInput = Demos_URL_Forms_Prefix + "/password-input";
3637
public const string Demos_URL_RadioInput = Demos_URL_Forms_Prefix + "/radio-input";
3738
public const string Demos_URL_RangeInput = Demos_URL_Forms_Prefix + "/range-input";
@@ -143,6 +144,7 @@ public static class DemoRouteConstants
143144
public const string Docs_URL_DateInput = Docs_URL_Forms_Prefix + "/date-input";
144145
public const string Docs_URL_EnumInput = Docs_URL_Forms_Prefix + "/enum-input";
145146
public const string Docs_URL_NumberInput = Docs_URL_Forms_Prefix + "/number-input";
147+
public const string Docs_URL_OTPInput = Docs_URL_Forms_Prefix + "/otp-input";
146148
public const string Docs_URL_PasswordInput = Docs_URL_Forms_Prefix + "/password-input";
147149
public const string Docs_URL_RadioInput = Docs_URL_Forms_Prefix + "/radio-input";
148150
public const string Docs_URL_RangeInput = Docs_URL_Forms_Prefix + "/range-input";

0 commit comments

Comments
 (0)