Components
OTP input
A one-time password typed a character to a cell, and the code they spell bound as one string. A character moves on to the next cell, Backspace walks back, and a pasted code fills every cell.
Drawn and behaving as Flux UI's otp input does, measured against its live docs.
OTP input
using System.Globalization;
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class OtpExample : Component
{
private string Code { get; set; } = "";
protected override Component? Render() =>
Div.Class("space-y-3")[
Ui.Otp.Bind(() => Code).Live().Length(6),
Ui.Text.Data("testid", "ui-otp-state")[
Code.Length == 6 ? $"Code complete: {Code}." : $"{Code.Length.ToString(CultureInfo.InvariantCulture)} of 6 entered."
]
];
}0 of 6 entered.
Example usage
A form for verifying a one-time password: the page empties the code to resend one, and the cells follow.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class OtpFormExample : Component
{
private string _code = "";
private string? _verified;
protected override Component? Render() =>
Ui.Card.Class("max-w-sm space-y-8")[
Div.Class("mx-auto max-w-64 space-y-2")[
Ui.Heading.Lg.Class("text-center")["Verify your account"],
Ui.Text.Class("text-center")["Please enter a one-time password from the authenticator app."]
],
Ui.Otp.Value(_code).OnChange(code => { _code = code; }).Length(6).Label("OTP Code").Class("mx-auto"),
Div.Class("space-y-4")[
Ui.Button.Primary.Class("w-full").OnClick(() => { _verified = _code; })["Verify"],
Ui.Button.Class("w-full").Data("testid", "ui-otp-resend").OnClick(() => { _code = ""; })["Resend code"]
],
_verified is null ? null : Ui.Text.Class("text-center")[$"Verifying {_verified}…"]
];
}Please enter a one-time password from the authenticator app.
Autosubmit
OnComplete runs as soon as the last cell is filled: verify, submit or move on from it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class OtpAutoSubmitExample : Component
{
private string? _verified;
private string Code { get; set; } = "";
protected override Component? Render() =>
Div.Class("max-w-sm space-y-8")[
Div.Class("mx-auto max-w-64 space-y-2")[
Ui.Heading.Lg.Class("text-center")["Verify your account"],
Ui.Text.Class("text-center")["Please enter a one-time password from the authenticator app."]
],
Div.Class("space-y-6")[
// Where Flux's submit="auto" submits the form: the handler runs once the last cell is filled.
Ui.Otp.Bind(() => Code).Length(6).OnComplete(code => { _verified = code; }).Class("mx-auto")
],
Ui.Text.Class("text-center").Data("testid", "ui-otp-verified")[
_verified is null ? "Fill every cell to verify." : $"Verifying {_verified}…"
]
];
}Please enter a one-time password from the authenticator app.
Fill every cell to verify.
Alphanumeric
Letters as well as digits, upper-cased in the value. Alpha takes letters alone.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class OtpAlphanumericExample : Component
{
private string LicenseKey { get; set; } = "L49R4";
protected override Component? Render() =>
Div.Class("space-y-3")[
Ui.Otp.Bind(() => LicenseKey).Live().Length(10).Alphanumeric.Label("License key")
.DescriptionTrailing("Enter the license key printed on the installation disc"),
Ui.Text.Data("testid", "ui-otp-license")[$"Key: {LicenseKey}"]
];
}Key: L49R4
Private
Masks each character, as a password field does.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class OtpPrivateExample : Component
{
private string Pin { get; set; } = "";
protected override Component? Render() =>
Ui.Otp.Bind(() => Pin).Length(4).Private().Label("PIN Code");
}Separator
Place the cells yourself to put a separator between them; they are counted, and Length is not needed.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class OtpSeparatorExample : Component
{
private string Code { get; set; } = "";
protected override Component? Render() =>
Ui.Otp.Bind(() => Code)[
Ui.OtpInput,
Ui.OtpInput,
Ui.OtpInput,
Ui.OtpSeparator,
Ui.OtpInput,
Ui.OtpInput,
Ui.OtpInput
];
}—
Group
A group joins its cells into one box.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class OtpGroupExample : Component
{
private string Code { get; set; } = "";
protected override Component? Render() =>
Ui.Otp.Bind(() => Code)[
Ui.OtpGroup[
Ui.OtpInput,
Ui.OtpInput,
Ui.OtpInput,
Ui.OtpInput,
Ui.OtpInput,
Ui.OtpInput
]
];
}Group separator
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class OtpGroupSeparatorExample : Component
{
private string Code { get; set; } = "";
protected override Component? Render() =>
Ui.Otp.Bind(() => Code)[
Ui.OtpGroup[
Ui.OtpInput,
Ui.OtpInput,
Ui.OtpInput
],
Ui.OtpSeparator,
Ui.OtpGroup[
Ui.OtpInput,
Ui.OtpInput,
Ui.OtpInput
]
];
}—
Behaviour
The keys are Flux's
The cells are the browser's while they are typed into: the group carries data-rask-otp and the runtime moves between them inside the key's own event, so typing as fast as a keyboard allows loses nothing and no key waits for a round trip.
Key | What happens |
|---|---|
| A character | Fills the cell and moves on; over a filled cell it replaces the character. A character the Mode does not take is refused. |
| Backspace | Deletes and steps back; in an empty cell it only steps back. |
| Delete | Closes the code up from where it is. |
| ArrowLeft, ArrowRight | Walk the cells, and stop at the first empty one. |
| A paste, or the code the phone offers | Fills the cells from the first, keeping only what the Mode takes. |
One field for the code
The cells are rendered with no value and no handler, and one <input type="hidden"> inside the group carries the code. That field is what Bind, or Value with OnChange, sees, and Name is its name, so a plain form posts the code once. Set the bound string yourself (to clear a wrong code) and the cells follow. The code has no gaps and holds only what the Mode takes: digits, or .Alphanumeric and .Alpha with letters upper-cased in the value, as Flux's are.
OnComplete runs with the code each time its last cell is filled, bound or not. It stands where Flux's submit="auto" submits the form; the form itself is not submitted for you.
Its markup
Flux's: a role="group" named by the field's label, each cell named "Character 2 of 6", one tab stop (the first empty cell), autocomplete="one-time-code" on the first cell (Autocomplete("off") stops the browser offering a code), inputmode="numeric" for digits and type="password" with Private(). An invalid code is the bound member's message, which marks every cell, and Disabled() disables every cell. Flux's label:sr-only, error:icon and error:class are not steps yet.
Reference
Ui.Otp
A cell per character, and the code they spell as one string. Its children place the cells by hand.
Step | Takes | What it does |
|---|---|---|
.Bind(…) | () => a string | The model's code it reads and writes: () => m.Code. Or Value with OnChange. |
.Value(…) | string | The code, for a page that holds it itself. |
.OnChange(…) | a handler given the new value | Runs with the code as it is typed. |
.AfterBind(…) | a handler given the new value | Runs after Bind has written the model. |
.Validate(…) | a validator | A rule the code is checked by when it is written. |
.Live() | bool | Writes the model as it is typed, not with the next action. |
.OnComplete(…) | a handler given the code | Runs with the code each time its last cell is filled: where Flux submits. |
.Length(…) | int | How many cells it draws, when they are not placed by hand. |
.Mode(…) | Ui.OtpModeNumeric Alphanumeric Alpha Each is a step of its own too, unless an HTML tag has its name. | What a cell takes. Digits when unset. |
.Private() | bool | Masks each character, as a password does. |
.Autocomplete(…) | string | The first cell's autocomplete: "off" stops the browser offering a code. one-time-code when unset. |
.Label(…) | string | Draws a field around it, with this label and the error. |
.DescriptionTrailing(…) | string | Help text under the cells. |
.Disabled() | bool | Disables every cell. |
.Name(…) | string | What the code is posted under in a form. |
.ShowValidation() | bool | False leaves the member's message to a Ui.Error placed elsewhere. |
Ui.OtpInput
One cell, placed by hand.
No steps of its own: what goes inside it is its content.
Ui.OtpSeparator
A dash between cells or groups.
No steps of its own: what goes inside it is its content.
Ui.OtpGroup
Cells joined into one box.
No steps of its own: what goes inside it is its content.