Rask
Rask
v0.23.1-alpha.0.478

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

OtpExample.cs
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."
            ]
        ];
}
Live result

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.

OtpFormExample.cs
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}…"]
        ];
}
Live result
Verify your account

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.

OtpAutoSubmitExample.cs
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}…"
            ]
        ];
}
Live result
Verify your account

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.

OtpAlphanumericExample.cs
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}"]
        ];
}
Live result
Enter the license key printed on the installation disc

Key: L49R4

Private

Masks each character, as a password field does.

OtpPrivateExample.cs
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");
}
Live result

Separator

Place the cells yourself to put a separator between them; they are counted, and Length is not needed.

OtpSeparatorExample.cs
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
        ];
}
Live result

—

Group

A group joins its cells into one box.

OtpGroupExample.cs
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
            ]
        ];
}
Live result

Group separator

OtpGroupSeparatorExample.cs
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
            ]
        ];
}
Live result

—

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 characterFills the cell and moves on; over a filled cell it replaces the character. A character the Mode does not take is refused.
BackspaceDeletes and steps back; in an empty cell it only steps back.
DeleteCloses the code up from where it is.
ArrowLeft, ArrowRightWalk the cells, and stop at the first empty one.
A paste, or the code the phone offersFills 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

flux: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 stringThe model's code it reads and writes: () => m.Code. Or Value with OnChange.
.Value(…)stringThe code, for a page that holds it itself.
.OnChange(…)a handler given the new valueRuns with the code as it is typed.
.AfterBind(…)a handler given the new valueRuns after Bind has written the model.
.Validate(…)a validatorA rule the code is checked by when it is written.
.Live()boolWrites the model as it is typed, not with the next action.
.OnComplete(…)a handler given the codeRuns with the code each time its last cell is filled: where Flux submits.
.Length(…)intHow many cells it draws, when they are not placed by hand.
.Mode(…)
Ui.OtpMode
Numeric
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()boolMasks each character, as a password does.
.Autocomplete(…)stringThe first cell's autocomplete: "off" stops the browser offering a code. one-time-code when unset.
.Label(…)stringDraws a field around it, with this label and the error.
.DescriptionTrailing(…)stringHelp text under the cells.
.Disabled()boolDisables every cell.
.Name(…)stringWhat the code is posted under in a form.
.ShowValidation()boolFalse leaves the member's message to a Ui.Error placed elsewhere.

Ui.OtpInput

flux:otp.input

One cell, placed by hand.

No steps of its own: what goes inside it is its content.

Ui.OtpSeparator

flux:otp.separator

A dash between cells or groups.

No steps of its own: what goes inside it is its content.

Ui.OtpGroup

flux:otp.group

Cells joined into one box.

No steps of its own: what goes inside it is its content.

Confirm