Files
Htmx/docs/Components/RadioGroup.md
T
2026-05-04 19:57:48 +05:00

212 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RadioGroup
A group of radio buttons sharing the same `name` attribute. Supports horizontal or vertical layout. One option can be pre-selected.
---
## HTML structure
```
div.flex.flex-col.gap-1.5
label.text-sm.font-medium ← group label (omitted when empty)
{label}
div.flex.{direction}.gap-3 ← flex-col or flex-row
label.flex.items-center.gap-2.cursor-pointer ← one per option
input[type=radio, name, value, class, $$Checked$$]
span.text-sm
{option label}
```
---
## CSS mechanics
| Class | Effect |
|---|---|
| `accent-primary` | Radio circle color follows `--color-primary` CSS variable |
| `h-4 w-4` | 16×16 radio circle |
| `cursor-pointer` | Pointer cursor on the label |
| `flex-col` (default) | Stacks options vertically |
| `flex-row` | Places options side by side |
---
## Constructor signature
```csharp
public RadioGroup(
string name,
IEnumerable<(string Value, string Label, bool Selected)> options,
string label = "",
string direction = "flex-col")
```
| Parameter | Description |
|---|---|
| `name` | Shared `name` attribute for all radio inputs in the group |
| `options` | List of `(Value, Label, Selected)` tuples |
| `label` | Optional visible group heading above the options |
| `direction` | `"flex-col"` (vertical, default) or `"flex-row"` (horizontal) |
---
## Usage examples
### Vertical list
```csharp
new RadioGroup(
name: "plan",
label: "Select a plan",
options: new[]
{
("free", "Free", true),
("pro", "Pro", false),
("teams", "Teams", false),
})
```
### Horizontal inline options
```csharp
new RadioGroup(
name: "size",
label: "Size",
direction: "flex-row",
options: new[]
{
("sm", "S", false),
("md", "M", true),
("lg", "L", false),
("xl", "XL", false),
})
```
### Reading in a form handler
```csharp
public record Command([property: FromForm] string Plan);
// command.Plan == "free" | "pro" | "teams"
```
### Dynamic options from database
```csharp
var options = categories
.Select((cat, i) => (cat.Slug, cat.Name, i == 0))
.ToArray();
new RadioGroup(name: "category", label: "Category", options: options)
```
---
## 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")]
```