b530bb8c97
Co-authored-by: Copilot <copilot@github.com>
200 lines
6.3 KiB
Markdown
200 lines
6.3 KiB
Markdown
# FileInput
|
|
|
|
A styled file upload field. Use it when you need users to attach files to a form — profile pictures, documents, CSV imports, and so on.
|
|
|
|
---
|
|
|
|
## Quick example
|
|
|
|
```csharp
|
|
new FileInput(
|
|
id: "avatar",
|
|
name: "avatar",
|
|
accept: "image/*",
|
|
label: "Profile picture",
|
|
description: "PNG, JPG or GIF up to 2 MB")
|
|
```
|
|
|
|
---
|
|
|
|
## All the options
|
|
|
|
```csharp
|
|
public FileInput(
|
|
string id,
|
|
string name = "",
|
|
string accept = "",
|
|
bool multiple = false,
|
|
string label = "",
|
|
string description = "",
|
|
string extraClasses = "",
|
|
string hxAttrs = "")
|
|
```
|
|
|
|
| Parameter | What it does |
|
|
|---|---|
|
|
| `id` | The element id. Also used by the `<label for="...">`. |
|
|
| `name` | Form field name — required if you want the file submitted with the form. |
|
|
| `accept` | MIME types or extensions to filter the picker, e.g. `"image/*"` or `".pdf,.docx"`. Does not validate server-side. |
|
|
| `multiple` | Allow selecting more than one file at a time. |
|
|
| `label` | Visible text label above the field. |
|
|
| `description` | Hint text below the field (e.g. "Max 5 MB"). |
|
|
| `extraClasses` | Additional Tailwind classes on the `<input>` element. |
|
|
| `hxAttrs` | Extra HTML attributes appended verbatim (HTMX, `data-*`, etc.). |
|
|
|
|
---
|
|
|
|
## Real-world examples
|
|
|
|
### Multiple document attachments
|
|
|
|
```csharp
|
|
new FileInput(
|
|
id: "attachments",
|
|
name: "attachments",
|
|
accept: ".pdf,.docx,.xlsx",
|
|
multiple: true,
|
|
label: "Attachments",
|
|
description: "Select one or more documents")
|
|
```
|
|
|
|
### Auto-upload on file selection (HTMX)
|
|
|
|
```csharp
|
|
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"""")
|
|
```
|
|
|
|
When using HTMX for file uploads, always include `hx-encoding="multipart/form-data"` — HTMX does not infer it from the input type.
|
|
|
|
### Reading uploaded files in a handler
|
|
|
|
```csharp
|
|
public static IResult Handle(HttpContext ctx, IFormFile? avatar)
|
|
{
|
|
if (avatar is null || avatar.Length == 0)
|
|
return Results.BadRequest("No file uploaded");
|
|
|
|
// validate file type server-side (accept= only filters in the browser)
|
|
var allowed = new[] { "image/jpeg", "image/png", "image/gif" };
|
|
if (!allowed.Contains(avatar.ContentType))
|
|
return Results.BadRequest("Invalid file type");
|
|
|
|
using var stream = avatar.OpenReadStream();
|
|
// save the file...
|
|
return Results.Ok();
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## How it works
|
|
|
|
FileInput renders a standard `<input type="file">`. The browser's built-in "Choose file" button is styled using `::file-selector-button` CSS pseudo-element (via Tailwind's `file:` prefix) so it matches the rest of the UI.
|
|
|
|
---
|
|
|
|
## Complete page example
|
|
|
|
**`Templates/UploadPage.htmx`**
|
|
```html
|
|
<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`**
|
|
```csharp
|
|
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**
|
|
```csharp
|
|
[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");
|
|
}
|
|
}
|
|
```
|