Image Upload

An image picker that uploads the picked file straight away, with progress, and posts only the value the server returns

<sa-image-upload name="CoverPhoto" value="kyoto.jpg" src="/cities/kyoto.jpg" upload-url="/ImageUpload?handler=Upload"
                 label="Cover photo" description="Shown at the top of the tour page." />

The demos on this page answer uploads in the browser instead of sending them to a server, so nothing you pick leaves your machine.

Usage

<sa-image-upload> is a self-closing tag. Bind it with asp-for, or give it a name and value, and point upload-url at the endpoint that stores images. Set src to the current image's URL when the stored value is a file name or key rather than a URL.

<sa-image-upload asp-for="..." src="..." upload-url="..." />

A picked or dropped file uploads straight away, with its progress over the image, and the value the endpoint returns goes in a hidden input, which is all the form posts. Replacing and removing the image are staged until the form is saved, and the note under the control offers Undo until then. While an upload is running, the control holds the form's submit and asks the user to wait.

The upload endpoint

The control posts the file as the multipart field file to upload-url, with the form's antiforgery token both as the __RequestVerificationToken field and in the RequestVerificationToken header. Outside a form, such as in an editing table row, it uses the nearest token around the control. The endpoint stores the file and answers with JSON:

{ "value": "lisbon-4f2a.jpg", "url": "https://cdn.voyager.example/photos/lisbon-4f2a.jpg" }

value is what the form saves. For a failed upload, answer with an error status and { "error": "..." }, which the control shows under the image.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UploadPhoto(IFormFile file, CancellationToken cancellationToken)
{
    if (file.Length > 5 * 1024 * 1024)
    {
        return StatusCode(StatusCodes.Status413PayloadTooLarge, new { error = $"{file.FileName} is larger than 5 MB." });
    }

    var value = await photoStore.SaveAsync(file, cancellationToken);
    return Json(new { value, url = photoStore.GetUrl(value) });
}

The control checks the file's type and size in the browser before uploading it, but a request can come from anywhere, so check both again on the server.

Examples

Compact

Set layout to ImageUploadLayout.Compact for a small thumbnail beside Replace and Remove buttons, where the image is a detail of the record rather than its subject.

<sa-image-upload name="Thumbnail" value="lisbon.jpg" src="/cities/lisbon.jpg" upload-url="/ImageUpload?handler=Upload"
                 layout="ImageUploadLayout.Compact" label="Destination thumbnail" />

Attachment

ImageUploadLayout.Attachment shows the image as an attachment row with the file's name, for forms that list files.

<sa-image-upload name="HotelPhoto" value="chiang-mai.jpg" src="/cities/chiang-mai.jpg" upload-url="/ImageUpload?handler=Upload"
                 layout="ImageUploadLayout.Attachment" label="Hotel photo" />

File size and types

accept takes a comma-separated list of media types, and max-file-size a size in bytes. They default to JPEG, PNG, WebP, GIF and AVIF, up to 5 MB, and the hint under the control describes them. A file that doesn't match is rejected with a message before it is uploaded. SVG isn't accepted by default, since an SVG file can carry scripts.

<sa-image-upload name="MapScan" upload-url="/ImageUpload?handler=Upload" label="Route map"
                 accept="image/png,image/webp" max-file-size="1048576" />

Table cell

ImageUploadLayout.Cell fits an editing table row: the thumbnail is the button that picks a file, a small badge on its corner removes the image, and a ring marks a change until the row is saved. It renders no label, so name it with aria-label. Errors appear under the thumbnail.

<sa-table>
    <sa-table-header>
        <sa-table-row>
            <sa-table-head>Photo</sa-table-head>
            <sa-table-head>Destination</sa-table-head>
        </sa-table-row>
    </sa-table-header>
    <sa-table-body>
        <sa-table-row>
            <sa-table-cell>
                <sa-image-upload name="rows[1].Photo" value="cape-town.jpg" src="/cities/cape-town.jpg" upload-url="/ImageUpload?handler=Upload"
                                 layout="ImageUploadLayout.Cell" aria-label="Photo of Cape Town" />
            </sa-table-cell>
            <sa-table-cell>Cape Town</sa-table-cell>
        </sa-table-row>
        <sa-table-row>
            <sa-table-cell>
                <sa-image-upload name="rows[2].Photo" upload-url="/ImageUpload?handler=Upload"
                                 layout="ImageUploadLayout.Cell" aria-label="Photo of Reykjavík" />
            </sa-table-cell>
            <sa-table-cell>Reykjavík</sa-table-cell>
        </sa-table-row>
    </sa-table-body>
</sa-table>

The control holds a form's submit while it uploads, but a row saved with htmx or fetch doesn't submit a form. Check the element's uploading property before sending such a request.

Disabled

Set disabled to show the image without letting the user change it. A disabled control needs no upload-url.

<sa-image-upload name="ArchivedPhoto" value="cape-town.jpg" src="/cities/cape-town.jpg" layout="ImageUploadLayout.Compact"
                 label="Archived tour photo" disabled="true" />

Accessibility

The control renders with role="group", named by its label. Its buttons are real buttons reached with Tab, and the drop zone is a button too, so a file can be picked without dragging. The note under the control is a polite live region that announces the upload's progress, errors and staged changes. The current image opens larger in a dialog, as an image does.

Part classes

Pass an ImageUploadClassNames instance from the StellarAdmin.UI namespace using class-names. Each property accepts an optional string of space-separated CSS classes, which are added to the part's existing classes.

PropertyTarget
RootThe generated field wrapper, when the control renders an implicit field.
LabelThe generated field label, when rendered.
DescriptionThe generated help text, when rendered.
ErrorThe generated validation message element, when rendered.
ContentThe generated container around the label and supporting text in a horizontal implicit field.
ControlThe element that holds the parts of the layout.
ImageThe image.
NoteThe note under the control, with the hint, progress, errors and staged changes.

Root, Label, Description, Error, and Content are inherited from FieldClassNames; see Field part classes.

API Reference

<sa-image-upload>

Renders a web component containing a hidden file input, a hidden <input> that posts the value, and the parts of the chosen layout.

Prop

Type

On this page