b530bb8c97
Co-authored-by: Copilot <copilot@github.com>
217 lines
6.5 KiB
Markdown
217 lines
6.5 KiB
Markdown
# RadioGroup
|
|
|
|
A set of radio buttons where only one option can be selected at a time. Use it when you want the user to pick exactly one value from a short list — pricing plans, delivery options, account types.
|
|
|
|
---
|
|
|
|
## Quick example
|
|
|
|
```csharp
|
|
new RadioGroup(
|
|
name: "plan",
|
|
label: "Select a plan",
|
|
options: new[]
|
|
{
|
|
("free", "Free", true), // pre-selected
|
|
("pro", "Pro", false),
|
|
("teams", "Teams", false),
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## All the options
|
|
|
|
```csharp
|
|
public RadioGroup(
|
|
string name,
|
|
IEnumerable<(string Value, string Label, bool Selected)> options,
|
|
string label = "",
|
|
string direction = "flex-col")
|
|
```
|
|
|
|
| Parameter | What it does |
|
|
|---|---|
|
|
| `name` | The shared form field name for all radio buttons in the group. |
|
|
| `options` | The list of choices. Each is a `(Value, Label, Selected)` tuple. |
|
|
| `label` | Optional heading displayed above the options. |
|
|
| `direction` | `"flex-col"` stacks options vertically (default). `"flex-row"` places them side by side. |
|
|
|
|
**Option tuple fields:**
|
|
|
|
| Field | What it does |
|
|
|---|---|
|
|
| `Value` | The string submitted when this option is selected. |
|
|
| `Label` | The text shown next to the radio button. |
|
|
| `Selected` | Pre-select this option on render. Only one should be `true`. |
|
|
|
|
---
|
|
|
|
## Real-world examples
|
|
|
|
### Pricing plan selector
|
|
|
|
```csharp
|
|
new RadioGroup(
|
|
name: "plan",
|
|
label: "Choose your plan",
|
|
options: new[]
|
|
{
|
|
("free", "Free — up to 3 projects", true),
|
|
("pro", "Pro — unlimited projects", false),
|
|
("enterprise", "Enterprise — custom pricing", false),
|
|
})
|
|
```
|
|
|
|
Reading on the server:
|
|
|
|
```csharp
|
|
public record Command([property: FromForm] string Plan);
|
|
// command.Plan == "free" | "pro" | "enterprise"
|
|
```
|
|
|
|
### Horizontal size selector
|
|
|
|
```csharp
|
|
new RadioGroup(
|
|
name: "size",
|
|
label: "Size",
|
|
direction: "flex-row",
|
|
options: new[]
|
|
{
|
|
("sm", "S", false),
|
|
("md", "M", true),
|
|
("lg", "L", false),
|
|
("xl", "XL", false),
|
|
})
|
|
```
|
|
|
|
### Options built dynamically from the database
|
|
|
|
```csharp
|
|
var options = categories
|
|
.Select((cat, i) => (cat.Slug, cat.Name, i == 0)) // first option pre-selected
|
|
.ToArray();
|
|
|
|
new RadioGroup(name: "category", label: "Category", options: options)
|
|
```
|
|
|
|
---
|
|
|
|
## How it works
|
|
|
|
Each option is a `<label>` element containing a native `<input type="radio">` and a `<span>` with the label text. Because the `<input>` is inside the `<label>`, clicking anywhere on the label text selects the option. All radio buttons in the group share the same `name` attribute — the browser ensures only one can be selected at a time.
|
|
|
|
The radio dot colour follows your primary theme colour via `accent-primary`.
|
|
```
|
|
|
|
---
|
|
|
|
## Tips and tricks
|
|
|
|
- Only one option in the group can have `Selected = true`; if multiple are marked selected the last one wins (standard HTML behavior).
|
|
- An unselected `RadioGroup` submits nothing — validate server-side that the field is present.
|
|
- For a "none of the above" option, add a tuple with the intended empty value: `("", "None", false)`.
|
|
- To conditionally show additional fields when a radio is selected, add an `htmx` attribute via inline HTML after the component — or use a custom slot that includes both the radio and a reveal div.
|
|
- To conditionally show additional fields when a radio is selected, add an `htmx` attribute via inline HTML after the component — or use a custom slot that includes both the radio and a reveal div.
|
|
|
|
---
|
|
|
|
## Complete page example
|
|
|
|
**`Templates/SurveyPage.htmx`**
|
|
```html
|
|
<div class="max-w-md mx-auto py-10">
|
|
<h1 class="text-2xl font-bold mb-2">Quick survey</h1>
|
|
<p class="text-sm text-muted-foreground mb-8">Help us improve BeepBoop.</p>
|
|
<form method="post" action="/survey">
|
|
$$AntiforgeryToken$$
|
|
<div class="space-y-8 mb-8">
|
|
$$ExperienceGroup$$
|
|
$$FeatureGroup$$
|
|
</div>
|
|
$$SubmitBtn$$
|
|
</form>
|
|
$$SuccessAlert$$
|
|
</div>
|
|
```
|
|
|
|
**`Templates/SurveyPage.htmx.cs`**
|
|
```csharp
|
|
namespace Htmx.ApiDemo.Templates;
|
|
|
|
public sealed class SurveyPage : SurveyPageBase
|
|
{
|
|
private readonly IHtmxComponent _experience;
|
|
private readonly IHtmxComponent _feature;
|
|
private readonly IHtmxComponent _submit;
|
|
private readonly IHtmxComponent _success;
|
|
private readonly byte[] _afToken;
|
|
|
|
public SurveyPage(IAntiforgery af, HttpContext ctx, bool submitted = false)
|
|
{
|
|
var tokens = af.GetAndStoreTokens(ctx);
|
|
_afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();
|
|
|
|
_experience = new Components.RadioGroup(
|
|
name: "experience",
|
|
label: "How would you rate your experience?",
|
|
options: new[]
|
|
{
|
|
("1", "Poor", false),
|
|
("2", "Fair", false),
|
|
("3", "Good", true),
|
|
("4", "Very good", false),
|
|
("5", "Excellent", false),
|
|
},
|
|
direction: "flex-row");
|
|
|
|
_feature = new Components.RadioGroup(
|
|
name: "favourite",
|
|
label: "Which feature do you use most?",
|
|
options: new[]
|
|
{
|
|
("htmx", "HTMX integration", false),
|
|
("aot", "AOT publishing", false),
|
|
("generator", "Source generator", false),
|
|
("tailwind", "Tailwind CSS", false),
|
|
});
|
|
|
|
_submit = new Components.Button("Submit", type: "submit");
|
|
_success = submitted
|
|
? new Components.Alert(title: "Thank you!", description: "Your responses have been recorded.")
|
|
: HtmxEmpty.Instance;
|
|
}
|
|
|
|
protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
|
|
protected override void RenderExperienceGroup(HtmxRenderContext ctx) => _experience.Render(ctx.Next());
|
|
protected override void RenderFeatureGroup(HtmxRenderContext ctx) => _feature.Render(ctx.Next());
|
|
protected override void RenderSubmitBtn(HtmxRenderContext ctx) => _submit.Render(ctx.Next());
|
|
protected override void RenderSuccessAlert(HtmxRenderContext ctx) => _success.Render(ctx.Next());
|
|
}
|
|
```
|
|
|
|
**POST handler**
|
|
```csharp
|
|
[Handler]
|
|
[MapPost("/survey")]
|
|
public static partial class PostSurveyHandler
|
|
{
|
|
public record Command(
|
|
[property: FromForm] string Experience,
|
|
[property: FromForm] string Favourite);
|
|
|
|
private static Task<IResult> HandleAsync(
|
|
[AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
|
|
{
|
|
// Persist responses…
|
|
return ctx.WriteHtmxPage(new SurveyPage(af, ctx, submitted: true), title: "Survey");
|
|
}
|
|
}
|
|
```
|
|
|
|
**`AppJsonSerializerContext.cs`**
|
|
```csharp
|
|
[JsonSerializable(typeof(PostSurveyHandler.Command), TypeInfoPropertyName = "SurveyCommand")]
|
|
```
|