@@ -0,0 +1,212 @@
|
||||
# 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
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```csharp
|
||||
new FileInput(
|
||||
id: "avatar",
|
||||
name: "avatar",
|
||||
accept: "image/*",
|
||||
label: "Profile picture",
|
||||
description: "PNG, JPG or GIF up to 2 MB")
|
||||
```
|
||||
|
||||
### Multiple files
|
||||
|
||||
```csharp
|
||||
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
|
||||
|
||||
```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"""")
|
||||
```
|
||||
|
||||
### No label
|
||||
|
||||
```csharp
|
||||
new FileInput(id: "doc", name: "document", accept: ".pdf")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips and tricks
|
||||
|
||||
- `accept` filters 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"` in `hxAttrs`; HTMX does not infer encoding from the input type.
|
||||
- Multiple files are bound as a list: `IFormFileCollection` or `List<IFormFile>` in the handler. `FromForm` attribute 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 `change` event and sets `src` on an `<img>` element via `URL.createObjectURL(e.target.files[0])`.
|
||||
- `extraClasses` is added to the `<input>` element, not the wrapper `<div>` — use it for overriding width, borders, or custom ring colors.
|
||||
- `extraClasses` is 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`**
|
||||
```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");
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user