@@ -0,0 +1,211 @@
|
||||
# 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")]
|
||||
```
|
||||
Reference in New Issue
Block a user