Components
Field
A label, a control, help text and the message the form holds for it, stacked and wired together: the label is the control's, and the help text and the message are what a screen reader reads with it.
Drawn and behaving as Flux UI's field does, measured against its live docs.
Field
The page decides the address is wrong and says why: the message shows under the input, and the input is marked invalid.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class FieldExample : Component
{
private string _email = "";
protected override Component? Render()
{
var bad = _email.Length > 0 && !_email.Contains('@');
return Ui.Field.Class("max-w-sm")[
Ui.Label["Email"],
Ui.Input.Value(_email).Type(InputType.Email).Invalid(bad).OnChange(email => { _email = email; }),
Ui.Error.Message(bad ? "That does not look like an email address." : null)
];
}
}Shorthand props
Every form control takes Label and Description itself, and draws the field and its error around it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class FieldShorthandExample : Component
{
private string Email { get; set; } = "";
protected override Component? Render() =>
Ui.Input.Bind(() => Email).Label("Email").Type(InputType.Email).Class("max-w-sm");
}With trailing description
A description after the control sits under it. DescriptionTrailing says the same on the control.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class FieldTrailingDescriptionExample : Component
{
private const string Rules =
"Must be at least 8 characters long, include an uppercase letter, a number, and a special character.";
protected override Component? Render() =>
Div.Class("grid max-w-sm gap-8")[
Ui.Field[
Ui.Label["Password"],
Ui.Input.Of<string>().Type(InputType.Password),
Ui.Error,
Ui.Description[Rules]
],
// The same field, from the input's own steps.
Ui.Input.Of<string>().Type(InputType.Password).Label("Password").DescriptionTrailing(Rules)
];
}With badge
A badge beside the label says what a reader might not expect: "Required", "Optional".
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class FieldBadgeExample : Component
{
protected override Component? Render() =>
Div.Class("grid max-w-sm gap-8")[
Ui.Field[
Ui.Label.Badge("Required")["Email"],
Ui.Input.Of<string>().Type(InputType.Email).Attributes(("required", "")),
Ui.Error
],
Ui.Field[
Ui.Label.Badge("Optional")["Phone number"],
Ui.Input.Of<string>().Type(InputType.Tel).Placeholder("(555) 555-5555").Mask("(999) 999-9999"),
Ui.Error
]
];
}Split layout
Two fields in one row are a grid of yours.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class FieldSplitExample : Component
{
protected override Component? Render() =>
Div.Class("grid grid-cols-2 gap-4")[
Ui.Input.Of<string>().Label("First name").Placeholder("Mara"),
Ui.Input.Of<string>().Label("Last name").Placeholder("Lind")
];
}Fieldset
Related fields under one legend, in a real fieldset.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class FieldFieldsetExample : Component
{
private string? _country = "United States";
protected override Component? Render() =>
Ui.Fieldset[
Ui.Legend["Shipping address"],
Div.Class("space-y-6")[
Ui.Input.Of<string>().Label("Street address line 1").Placeholder("123 Main St").Class("max-w-sm"),
Ui.Input.Of<string>().Label("Street address line 2").Placeholder("Apartment, studio, or floor").Class("max-w-sm"),
Div.Class("grid grid-cols-2 gap-x-4 gap-y-6")[
Ui.Input.Of<string>().Label("City").Placeholder("San Francisco"),
Ui.Input.Of<string>().Label("State / Province").Placeholder("CA"),
Ui.Input.Of<string>().Label("Postal / Zip code").Placeholder("12345"),
Ui.Select.Value(_country).OnChange(country => { _country = country; }).Label("Country")[
Ui.SelectOption["United States"],
Ui.SelectOption["Canada"],
Ui.SelectOption["Mexico"]
]
]
]
];
}Bound to a model
Rask's own: the same controls with no handler between them and the model. Bind writes back, and a bound int is a number field with nothing said at the call site.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class FieldBoundExample : Component
{
private readonly Signup _signup = new();
protected override Component? Render() =>
Div.Class("space-y-4")[
Form.Model(_signup)[
Div.Class("grid gap-4 sm:grid-cols-2")[
Ui.Input.Bind(() => _signup.Email).Live().Label("Email").Type(InputType.Email),
// T is the model's, so this is a number field with nothing said here.
Ui.Input.Bind(() => _signup.Seats).Live().Label("Seats")
],
Div.Class("mt-4")[Ui.Checkbox.Bind(() => _signup.Agreed).Live().Label("I agree to the terms")]
],
Ui.Text.Data("testid", "ui-bound-state")[
$"{_signup.Email} · {_signup.Seats.ToString(System.Globalization.CultureInfo.InvariantCulture)} seats"
+ $" · agreed {(_signup.Agreed ? "yes" : "no")}"
]
];
// An ordinary model: nothing on it knows it is being edited by a UI kit.
public sealed class Signup
{
public string Email { get; set; } = "";
public int Seats { get; set; } = 1;
public bool Agreed { get; set; }
}
}· 1 seats · agreed no
Behaviour
The label is a real label
Ui.Label is a <label for>. Inside a field it points at the field's control (a kit control by the id it derives, a plain element by its Id), so a click on the label focuses the control with no script. Outside a field, For(id) names the control. Flux draws its parts as custom elements its script wires together; the kit ships none, so each part is the native element that already behaves that way: <label>, <fieldset>, <legend>.
Ui.Field.Inline puts the label beside the control, for a checkbox or a switch; the default stacks them.
The error reads the form
Where Flux's name looks a key up in Laravel's error bag, a Rask field is a member of a model, so Ui.Error shows the first message the form holds for that member: bare inside a field (the control's own Bind), For(() => model.Email) anywhere, or Name("Email") for a member of the form's model. Message("…") shows your own words whatever the form says, and Icon(false) leaves the triangle out.
It is always in the page and hidden while there is nothing to say, because it is a role="alert" live region: a screen reader announces a message only when it arrives in a region it already knows. A bound control is invalid on its own while its member holds a message: aria-invalid, data-invalid and the red border, with aria-describedby naming the error first and the description after it.
Flux's bag and deep have no counterpart: a form has one edit context, and a nested member is named by its expression rather than by a dotted path.
Where the description goes
A Ui.Description before the control sits under the label; after the control it sits under the control. Label, Description and DescriptionTrailing on a control draw the same field around it, with its Ui.Error, and ShowValidation(false) leaves the message to a Ui.Error placed somewhere else. Markers follow Flux's: data-ui-field, data-ui-label, data-ui-description, data-ui-error, data-ui-fieldset, data-ui-legend.
Reference
Ui.Field
A label, a control, help text and the control's message, stacked and wired together.
Step | Takes | What it does |
|---|---|---|
.Variant(…) | Ui.FieldVariantBlock Inline Each is a step of its own too, unless an HTML tag has its name. | Inline puts the label beside the control, for a checkbox or a switch. Stacked when unset. |
Ui.Label
A <label> for the field's control. Its children are its words.
Step | Takes | What it does |
|---|---|---|
.Badge(…) | string | A word beside the label: "Required", "Optional". |
.Trailing(…) | Component | Content at the far end of the label's row: the slider's value. |
.For(…) | string | The id of the control it names, for a label outside a field. |
Ui.Description
Help text: under the label before the control, under the control after it.
No steps of its own: what goes inside it is its content.
Ui.Error
The first message the form holds for the field's control, in a live region.
Step | Takes | What it does |
|---|---|---|
.For(…) | () => a member | The member whose message it shows, anywhere in a form: () => m.Email. |
.Name(…) | string | The name of a member of the form's model, in place of For. |
.Message(…) | string | Words of your own, whatever the form says. Nothing is shown while it is null. |
.Icon(…) | a Ui.IconName, or false | The icon before the message, or false for none. A triangle when unset. |
Ui.Fieldset
A <fieldset> around related fields.
Step | Takes | What it does |
|---|---|---|
.Legend(…) | string | The fieldset's heading, or place a Ui.Legend yourself. |
.Description(…) | string | Help text under the legend. |
Ui.Legend
A fieldset's heading. Its children are its words.
No steps of its own: what goes inside it is its content.