ee8797c142
Co-authored-by: Copilot <copilot@github.com>
7.0 KiB
7.0 KiB
FileInput
A styled file upload field with an optional visible label and description. Supports accept MIME types, multiple file selection, and HTMX attributes for server-driven interactions.
HTML structure
div.flex.flex-col.gap-1.5
label[for={id}].text-sm.font-medium ← omitted when label is empty
{label text}
input[type=file, id, name, accept, class, $$Multiple$$, $$HxAttrs$$]
p.text-sm.text-muted-foreground ← omitted when description is empty
{description text}
CSS mechanics
| Class | Effect |
|---|---|
file:mr-4 file:py-2 file:px-4 file:rounded-md |
Styles the browser's "Choose file" button via ::file-selector-button |
file:border-0 file:bg-primary file:text-primary-foreground |
Gives the file button the primary color |
file:text-sm file:font-semibold file:cursor-pointer |
Consistent text treatment |
hover:file:bg-primary/90 |
Slight darkening on hover |
w-full rounded-md border border-input bg-background text-sm |
Full-width field with border |
Constructor signature
public FileInput(
string id,
string name = "",
string accept = "",
bool multiple = false,
string label = "",
string description = "",
string extraClasses = "",
string hxAttrs = "")
| Parameter | Description |
|---|---|
id |
Element id and label for target |
name |
Form field name |
accept |
MIME types or file extensions, e.g. "image/*" or ".pdf,.docx" |
multiple |
Allows selecting more than one file |
label |
Visible label above the field |
description |
Helper text below the field |
extraClasses |
Additional Tailwind classes on the input |
hxAttrs |
Verbatim HTMX / data attributes appended to the input |
Usage examples
Basic single file
new FileInput(
id: "avatar",
name: "avatar",
accept: "image/*",
label: "Profile picture",
description: "PNG, JPG or GIF up to 2 MB")
Multiple files
new FileInput(
id: "attachments",
name: "attachments",
accept: ".pdf,.docx,.xlsx",
multiple: true,
label: "Attachments",
description: "Select one or more documents")
HTMX auto-upload on change
new FileInput(
id: "import-csv",
name: "csv",
accept: ".csv",
label: "Import CSV",
hxAttrs: """hx-post="/import" hx-encoding="multipart/form-data" hx-target="#result" hx-trigger="change"""")
No label
new FileInput(id: "doc", name: "document", accept: ".pdf")
Tips and tricks
acceptfilters in the browser's file picker dialog but does not validate server-side — always validate the uploaded file type in your handler.- For HTMX file uploads set
hx-encoding="multipart/form-data"inhxAttrs; HTMX does not infer encoding from the input type. - Multiple files are bound as a list:
IFormFileCollectionorList<IFormFile>in the handler.FromFormattribute on the command record field is required. - To show a preview of the selected image before upload, add a small JS snippet that listens to the
changeevent and setssrcon an<img>element viaURL.createObjectURL(e.target.files[0]). extraClassesis added to the<input>element, not the wrapper<div>— use it for overriding width, borders, or custom ring colors.extraClassesis added to the<input>element, not the wrapper<div>— use it for overriding width, borders, or custom ring colors.
Complete page example
Templates/UploadPage.htmx
<div class="max-w-md mx-auto py-10">
<h1 class="text-2xl font-bold mb-6">Upload document</h1>
<form method="post" action="/upload" enctype="multipart/form-data">
$$AntiforgeryToken$$
<div class="space-y-4 mb-6">
$$FileField$$
</div>
$$SubmitBtn$$
</form>
$$Result$$
</div>
Templates/UploadPage.htmx.cs
namespace Htmx.ApiDemo.Templates;
public sealed class UploadPage : UploadPageBase
{
private readonly IHtmxComponent _fileField;
private readonly IHtmxComponent _submitBtn;
private readonly IHtmxComponent _result;
private readonly byte[] _afToken;
public UploadPage(IAntiforgery af, HttpContext ctx, string? uploadedFileName = null, string? error = null)
{
var tokens = af.GetAndStoreTokens(ctx);
_afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();
_fileField = new Components.FileInput(
id: "document",
name: "document",
accept: ".pdf,.docx",
label: "Select a file",
description: "PDF or Word document, max 10 MB");
_submitBtn = new Components.Button("Upload", type: "submit");
_result = error is not null
? new Components.Alert(title: "Upload failed", description: error, variant: "destructive")
: uploadedFileName is not null
? new Components.Alert(title: "Uploaded!", description: $"Saved as: {uploadedFileName}")
: HtmxEmpty.Instance;
}
protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
protected override void RenderFileField(HtmxRenderContext ctx) => _fileField.Render(ctx.Next());
protected override void RenderSubmitBtn(HtmxRenderContext ctx) => _submitBtn.Render(ctx.Next());
protected override void RenderResult(HtmxRenderContext ctx) => _result.Render(ctx.Next());
}
GET + POST handlers
[Handler]
[MapGet("/upload")]
public static partial class GetUploadHandler
{
public record Query();
private static Task<IResult> HandleAsync(Query _, HttpContext ctx, IAntiforgery af, CancellationToken ct)
=> ctx.WriteHtmxPage(new UploadPage(af, ctx), title: "Upload");
}
[Handler]
[MapPost("/upload")]
[DisableRequestSizeLimit]
public static partial class PostUploadHandler
{
public record Command([property: FromForm] IFormFile? Document = null);
private static async Task<IResult> HandleAsync(
[AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
{
if (cmd.Document is null)
{
return await ctx.WriteHtmxPage(
new UploadPage(af, ctx, error: "No file was selected."), title: "Upload");
}
var allowedTypes = new[] { ".pdf", ".docx" };
var ext = Path.GetExtension(cmd.Document.FileName).ToLowerInvariant();
if (!allowedTypes.Contains(ext))
{
return await ctx.WriteHtmxPage(
new UploadPage(af, ctx, error: "Only PDF and Word files are allowed."), title: "Upload");
}
var safeName = Path.GetRandomFileName() + ext;
var savePath = Path.Combine("uploads", safeName);
await using var stream = File.Create(savePath);
await cmd.Document.CopyToAsync(stream, ct);
return await ctx.WriteHtmxPage(
new UploadPage(af, ctx, uploadedFileName: safeName), title: "Upload");
}
}