ee8797c142
Co-authored-by: Copilot <copilot@github.com>
6.6 KiB
6.6 KiB
Textarea
A styled multi-line text input with optional label, description, default value, and HTMX attributes.
HTML structure
div.flex.flex-col.gap-1.5
label[for={id}].text-sm.font-medium ← omitted when label is empty
{label}
textarea[id, name, placeholder, rows, class, $$HxAttrs$$]
{defaultValue}
p.text-sm.text-muted-foreground ← omitted when description is empty
{description}
CSS mechanics
| Class | Effect |
|---|---|
flex min-h-[80px] w-full rounded-md border border-input bg-background |
Full-width field with minimum height |
px-3 py-2 text-sm |
Inner padding and text size |
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring |
Keyboard focus ring |
disabled:cursor-not-allowed disabled:opacity-50 |
Disabled state |
placeholder:text-muted-foreground |
Muted placeholder text |
resize-y |
Allows vertical resize only |
Constructor signature
public Textarea(
string id,
string name = "",
string placeholder = "",
string label = "",
string description = "",
string defaultValue = "",
string extraClasses = "",
string hxAttrs = "",
int rows = 3)
| Parameter | Description |
|---|---|
id |
Element id and label for target |
name |
Form field name |
placeholder |
Placeholder text |
label |
Optional visible label |
description |
Optional helper text below the field |
defaultValue |
Pre-filled content of the textarea |
extraClasses |
Additional Tailwind classes on the textarea |
hxAttrs |
Verbatim HTMX / data attributes |
rows |
Number of visible rows (default: 3) |
Usage examples
Comment field
new Textarea(
id: "comment",
name: "comment",
placeholder: "Write a comment…",
label: "Comment",
rows: 5)
Bio field with default value
new Textarea(
id: "bio",
name: "bio",
label: "Bio",
description: "Tell us about yourself (max 280 characters)",
defaultValue: user.Bio ?? "")
Auto-expand with HTMX
new Textarea(
id: "notes",
name: "notes",
label: "Notes",
rows: 3,
hxAttrs: """oninput="this.style.height=''; this.style.height=this.scrollHeight+'px'"""")
Auto-save on input
new Textarea(
id: "draft",
name: "content",
label: "Draft",
hxAttrs: """hx-post="/drafts/save" hx-trigger="keyup changed delay:500ms" hx-include="[name='content']"""")
Reading in a form handler
public record Command([property: FromForm] string Comment);
// command.Comment contains the textarea value
Tips and tricks
- HTML-encode the
defaultValueif it contains user-supplied content — it is placed directly inside the<textarea>element. rowscontrols the initial visible height but the user can resize vertically. For a fixed-height textarea, addresize-noneinextraClasses.- For a character counter, add
maxlengthviahxAttrsand pair with a small JS snippet or a sibling<span>updated oninput. placeholdertext is not submitted — always usedefaultValuefor edit forms where existing content should be pre-filled.placeholdertext is not submitted — always usedefaultValuefor edit forms where existing content should be pre-filled.
Complete page example
Templates/FeedbackPage.htmx
<div class="max-w-lg mx-auto py-10">
<h1 class="text-2xl font-bold mb-2">Send feedback</h1>
<p class="text-sm text-muted-foreground mb-6">We read every message.</p>
<form method="post" action="/feedback">
$$AntiforgeryToken$$
<div class="space-y-5 mb-6">
$$SubjectInput$$
$$MessageArea$$
</div>
$$SubmitBtn$$
</form>
$$SuccessAlert$$
</div>
Templates/FeedbackPage.htmx.cs
namespace Htmx.ApiDemo.Templates;
public sealed class FeedbackPage : FeedbackPageBase
{
private readonly IHtmxComponent _subject;
private readonly IHtmxComponent _message;
private readonly IHtmxComponent _submit;
private readonly IHtmxComponent _success;
private readonly byte[] _afToken;
public FeedbackPage(
IAntiforgery af,
HttpContext ctx,
string subjectError = "",
string messageError = "",
bool submitted = false)
{
var tokens = af.GetAndStoreTokens(ctx);
_afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();
_subject = new Components.Input(
id: "subject",
name: "subject",
label: "Subject",
placeholder: "What's on your mind?",
errorMessage: subjectError);
_message = new Components.Textarea(
id: "message",
name: "message",
label: "Message",
rows: 6,
placeholder: "Describe your feedback in detail…",
errorMessage: messageError);
_submit = new Components.Button("Send feedback", type: "submit");
_success = submitted
? new Components.Alert(title: "Thank you!", description: "Your feedback has been received.")
: HtmxEmpty.Instance;
}
protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
protected override void RenderSubjectInput(HtmxRenderContext ctx) => _subject.Render(ctx.Next());
protected override void RenderMessageArea(HtmxRenderContext ctx) => _message.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
[Handler]
[MapPost("/feedback")]
public static partial class PostFeedbackHandler
{
public record Command(
[property: FromForm] string Subject,
[property: FromForm] string Message);
private static Task<IResult> HandleAsync(
[AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(cmd.Subject))
return ctx.WriteHtmxPage(
new FeedbackPage(af, ctx, subjectError: "Subject is required."), title: "Feedback");
if (string.IsNullOrWhiteSpace(cmd.Message))
return ctx.WriteHtmxPage(
new FeedbackPage(af, ctx, messageError: "Message is required."), title: "Feedback");
// Persist feedback…
return ctx.WriteHtmxPage(
new FeedbackPage(af, ctx, submitted: true), title: "Feedback");
}
}
AppJsonSerializerContext.cs
[JsonSerializable(typeof(PostFeedbackHandler.Command), TypeInfoPropertyName = "FeedbackCommand")]