@@ -0,0 +1,210 @@
|
||||
# Switch
|
||||
|
||||
A toggle switch (on/off). Renders as a hidden `<input type="checkbox">` with a styled track and thumb driven by JavaScript. Fires no custom events — read the underlying checkbox value in form submissions.
|
||||
|
||||
---
|
||||
|
||||
## HTML structure
|
||||
|
||||
```
|
||||
label[for={id}].flex.items-center.gap-3.cursor-pointer
|
||||
div.switch-root.relative.w-11.h-6.rounded-full ← outer track
|
||||
input[type=checkbox, id, name, class="sr-only", $$Checked$$] ← hidden; holds true state
|
||||
div.switch-thumb.absolute.top-0.5.left-0.5... ← animated thumb
|
||||
span.text-sm.select-none ← label text (omitted when empty)
|
||||
{label}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSS mechanics
|
||||
|
||||
| Class | Effect |
|
||||
|---|---|
|
||||
| `sr-only` | Hides the real checkbox visually but keeps it accessible |
|
||||
| `switch-root` | `bg-input` (off) / `bg-primary` (on) — toggled by JS adding `switch-on` class |
|
||||
| `switch-thumb` | `h-5 w-5 rounded-full bg-background shadow transition-transform` |
|
||||
| `translate-x-5` | Added to thumb by JS when switch is on (slides right) |
|
||||
|
||||
---
|
||||
|
||||
## JavaScript (`initSwitch` in `components.js`)
|
||||
|
||||
Runs on `DOMContentLoaded` and `htmx:afterSwap`.
|
||||
|
||||
**Per-switch initialization:**
|
||||
|
||||
1. Guard `_switchInit` prevents double-binding
|
||||
2. Sync visual state from the hidden checkbox `checked` property on load
|
||||
3. On `label` click: toggle `checked`, toggle `switch-on` on the track, toggle `translate-x-5` on the thumb
|
||||
|
||||
---
|
||||
|
||||
## Constructor signature
|
||||
|
||||
```csharp
|
||||
public Switch(
|
||||
string id,
|
||||
string label = "",
|
||||
string name = "",
|
||||
bool isChecked = false)
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
|---|---|
|
||||
| `id` | Element id for the hidden checkbox; label's `for` attribute |
|
||||
| `label` | Optional visible text to the right of the toggle |
|
||||
| `name` | Form field name for the hidden checkbox |
|
||||
| `isChecked` | Initial on/off state |
|
||||
|
||||
---
|
||||
|
||||
## Usage examples
|
||||
|
||||
### Basic on/off toggle
|
||||
|
||||
```csharp
|
||||
new Switch(
|
||||
id: "notifications",
|
||||
label: "Enable notifications",
|
||||
name: "enableNotifications",
|
||||
isChecked: true)
|
||||
```
|
||||
|
||||
### Toggle without label
|
||||
|
||||
```csharp
|
||||
new Switch(id: "darkMode", name: "darkMode")
|
||||
```
|
||||
|
||||
### Reading in a form handler
|
||||
|
||||
```csharp
|
||||
public record Command(
|
||||
[property: FromForm] string? EnableNotifications = null
|
||||
);
|
||||
|
||||
bool notificationsOn = command.EnableNotifications != null;
|
||||
```
|
||||
|
||||
> Like all checkboxes, an unchecked switch is not included in the form submission. Use `null` as the default in your command record.
|
||||
|
||||
### HTMX auto-save on change
|
||||
|
||||
```csharp
|
||||
// The hidden checkbox is named, so wrap in a form or use hx-include:
|
||||
new Switch(
|
||||
id: "maintenance",
|
||||
name: "maintenanceMode",
|
||||
label: "Maintenance mode",
|
||||
isChecked: currentState)
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- Parent form with HTMX:
|
||||
<form hx-post="/settings" hx-trigger="change from:#maintenance">
|
||||
$$MaintenanceSwitch$$
|
||||
</form>
|
||||
-->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips and tricks
|
||||
|
||||
- The hidden checkbox carries the value `"on"` when checked (standard checkbox default). If you need `"true"`, add `value="true"` by subclassing or via a wrapper form.
|
||||
- Because the click is handled on the `<label>` element, the switch works correctly even when the hidden input is not directly clicked.
|
||||
- For an HTMX auto-save switch, trigger on `change` from the hidden checkbox using `hx-trigger="change from:#myId"` on a parent element.
|
||||
- Pairing `isChecked` with a server-read preference ensures the switch reflects the saved state on every page load.
|
||||
- Pairing `isChecked` with a server-read preference ensures the switch reflects the saved state on every page load.
|
||||
|
||||
---
|
||||
|
||||
## Complete page example
|
||||
|
||||
**`Templates/NotificationsPage.htmx`**
|
||||
```html
|
||||
<div class="max-w-md mx-auto py-10">
|
||||
<h1 class="text-2xl font-bold mb-6">Notifications</h1>
|
||||
<form method="post" action="/notifications">
|
||||
$$AntiforgeryToken$$
|
||||
<div class="space-y-5 mb-8">
|
||||
$$EmailSwitch$$
|
||||
$$PushSwitch$$
|
||||
$$SmsSwitch$$
|
||||
</div>
|
||||
$$SaveBtn$$
|
||||
</form>
|
||||
$$SuccessAlert$$
|
||||
</div>
|
||||
```
|
||||
|
||||
**`Templates/NotificationsPage.htmx.cs`**
|
||||
```csharp
|
||||
namespace Htmx.ApiDemo.Templates;
|
||||
|
||||
public sealed class NotificationsPage : NotificationsPageBase
|
||||
{
|
||||
private readonly IHtmxComponent _email;
|
||||
private readonly IHtmxComponent _push;
|
||||
private readonly IHtmxComponent _sms;
|
||||
private readonly IHtmxComponent _save;
|
||||
private readonly IHtmxComponent _success;
|
||||
private readonly byte[] _afToken;
|
||||
|
||||
public NotificationsPage(
|
||||
IAntiforgery af,
|
||||
HttpContext ctx,
|
||||
NotificationPrefs? prefs = null,
|
||||
bool saved = false)
|
||||
{
|
||||
var tokens = af.GetAndStoreTokens(ctx);
|
||||
_afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();
|
||||
|
||||
_email = new Components.Switch(id: "email-notif", label: "Email notifications", name: "emailNotif", isChecked: prefs?.Email ?? true);
|
||||
_push = new Components.Switch(id: "push-notif", label: "Push notifications", name: "pushNotif", isChecked: prefs?.Push ?? false);
|
||||
_sms = new Components.Switch(id: "sms-notif", label: "SMS notifications", name: "smsNotif", isChecked: prefs?.Sms ?? false);
|
||||
_save = new Components.Button("Save", type: "submit");
|
||||
_success = saved ? new Components.Alert(title: "Notification preferences saved.") : HtmxEmpty.Instance;
|
||||
}
|
||||
|
||||
protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
|
||||
protected override void RenderEmailSwitch(HtmxRenderContext ctx) => _email.Render(ctx.Next());
|
||||
protected override void RenderPushSwitch(HtmxRenderContext ctx) => _push.Render(ctx.Next());
|
||||
protected override void RenderSmsSwitch(HtmxRenderContext ctx) => _sms.Render(ctx.Next());
|
||||
protected override void RenderSaveBtn(HtmxRenderContext ctx) => _save.Render(ctx.Next());
|
||||
protected override void RenderSuccessAlert(HtmxRenderContext ctx) => _success.Render(ctx.Next());
|
||||
}
|
||||
```
|
||||
|
||||
**POST handler**
|
||||
```csharp
|
||||
[Handler]
|
||||
[MapPost("/notifications")]
|
||||
public static partial class PostNotificationsHandler
|
||||
{
|
||||
public record Command(
|
||||
[property: FromForm] string? EmailNotif = null,
|
||||
[property: FromForm] string? PushNotif = null,
|
||||
[property: FromForm] string? SmsNotif = null);
|
||||
|
||||
private static Task<IResult> HandleAsync(
|
||||
[AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
|
||||
{
|
||||
var prefs = new NotificationPrefs(
|
||||
Email: cmd.EmailNotif != null,
|
||||
Push: cmd.PushNotif != null,
|
||||
Sms: cmd.SmsNotif != null);
|
||||
// Persist prefs…
|
||||
return ctx.WriteHtmxPage(
|
||||
new NotificationsPage(af, ctx, prefs, saved: true), title: "Notifications");
|
||||
}
|
||||
}
|
||||
|
||||
public record NotificationPrefs(bool Email, bool Push, bool Sms);
|
||||
```
|
||||
|
||||
**`AppJsonSerializerContext.cs`**
|
||||
```csharp
|
||||
[JsonSerializable(typeof(PostNotificationsHandler.Command), TypeInfoPropertyName = "NotificationsCommand")]
|
||||
```
|
||||
Reference in New Issue
Block a user