ee8797c142
Co-authored-by: Copilot <copilot@github.com>
230 lines
6.6 KiB
Markdown
230 lines
6.6 KiB
Markdown
# 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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
new Textarea(
|
|
id: "comment",
|
|
name: "comment",
|
|
placeholder: "Write a comment…",
|
|
label: "Comment",
|
|
rows: 5)
|
|
```
|
|
|
|
### Bio field with default value
|
|
|
|
```csharp
|
|
new Textarea(
|
|
id: "bio",
|
|
name: "bio",
|
|
label: "Bio",
|
|
description: "Tell us about yourself (max 280 characters)",
|
|
defaultValue: user.Bio ?? "")
|
|
```
|
|
|
|
### Auto-expand with HTMX
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
public record Command([property: FromForm] string Comment);
|
|
|
|
// command.Comment contains the textarea value
|
|
```
|
|
|
|
---
|
|
|
|
## Tips and tricks
|
|
|
|
- HTML-encode the `defaultValue` if it contains user-supplied content — it is placed directly inside the `<textarea>` element.
|
|
- `rows` controls the initial visible height but the user can resize vertically. For a fixed-height textarea, add `resize-none` in `extraClasses`.
|
|
- For a character counter, add `maxlength` via `hxAttrs` and pair with a small JS snippet or a sibling `<span>` updated on `input`.
|
|
- `placeholder` text is not submitted — always use `defaultValue` for edit forms where existing content should be pre-filled.
|
|
- `placeholder` text is not submitted — always use `defaultValue` for edit forms where existing content should be pre-filled.
|
|
|
|
---
|
|
|
|
## Complete page example
|
|
|
|
**`Templates/FeedbackPage.htmx`**
|
|
```html
|
|
<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`**
|
|
```csharp
|
|
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**
|
|
```csharp
|
|
[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`**
|
|
```csharp
|
|
[JsonSerializable(typeof(PostFeedbackHandler.Command), TypeInfoPropertyName = "FeedbackCommand")]
|
|
```
|