Rask
Rask
v0.23.1-alpha.0.494

Components

File upload

An area that takes files, clicked, reached by keyboard or dropped on, and a row for each file. Where Flux binds a Livewire property, the files come to the page through OnFiles, and the page draws the list from its own state.

Drawn and behaving as Flux UI's file upload does, measured against its live docs.

File upload

A picture is read in OnFiles and shown as its own preview; the cross takes a file off the page's list.

FileUploadExample.cs
using Rask.Core.Forms;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class FileUploadExample : Component
{
    // What a picture may weigh to be shown as its own preview.
    private const long PreviewLimit = 2 * 1024 * 1024;

    private static readonly HashSet<string> Pictures = new(StringComparer.OrdinalIgnoreCase) { "image/png", "image/jpeg", "image/gif", "image/webp" };

    // One file is there from the start, as on Flux's page.
    private readonly List<Photo> _photos = [new("Profile_pic.jpg", 162400, "/img/avatars/sky.svg")];

    protected override Component? Render() =>
        Div[
            Ui.FileUpload.Label("Upload files").Multiple().Accept("image/*").OnFiles(Keep).Id("ui-upload-photos")[
                Ui.FileUploadDropzone.Heading("Drop files here or click to browse").Text("JPG, PNG, GIF up to 10MB")
            ],
            Div.Class("mt-4 flex flex-col gap-2")[
                _photos.Select((photo, index) =>
                    Ui.FileItem.Key(photo.Name + index).Heading(photo.Name).Image(photo.Preview).Size(photo.Size)
                        .Actions(Ui.FileItemRemove.AriaLabel("Remove file: " + photo.Name).OnClick(() => _photos.RemoveAt(index))))
            ]
        ];

    // A file can be read only while its handler runs, so what the list shows is taken here.
    private async Task Keep(IReadOnlyList<IRaskFile> files)
    {
        foreach (var file in files)
        {
            _photos.Add(new Photo(file.Name, file.Size, await ReadPreview(file)));
        }
    }

    // The picture as a data: address. Its type is the browser's word, so only the four an <img> draws are taken.
    private static async Task<string?> ReadPreview(IRaskFile file)
    {
        if (file.Size > PreviewLimit || !Pictures.Contains(file.ContentType))
        {
            return null;
        }

        await using var stream = file.OpenReadStream(PreviewLimit);
        using var bytes = new MemoryStream((int)file.Size);
        await stream.CopyToAsync(bytes);

        return $"data:{file.ContentType};base64,{Convert.ToBase64String(bytes.GetBuffer(), 0, (int)bytes.Length)}";
    }

    private sealed record Photo(string Name, long Size, string? Preview);
}
Live result
Profile_pic.jpg
159 KB

Inline layout

The icon beside the words, for a more compact form.

FileUploadInlineExample.cs
using Rask.Core.Forms;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class FileUploadInlineExample : Component
{
    private readonly List<string> _files = ["Profile_pic.jpg", "Brand_banner.jpg"];

    protected override Component? Render() =>
        Div[
            Ui.FileUpload.Label("Upload files").Multiple().OnFiles(Keep).Id("ui-upload-inline")[
                Ui.FileUploadDropzone.Heading("Drop files or click to browse").Text("JPG, PNG, GIF up to 10MB").Inline()
            ],
            Div.Class("mt-3 flex flex-col gap-2")[
                _files.Select((name, index) =>
                    Ui.FileItem.Key(name + index).Heading(name)
                        .Actions(Ui.FileItemRemove.AriaLabel("Remove file: " + name).OnClick(() => _files.RemoveAt(index))))
            ]
        ];

    private void Keep(IReadOnlyList<IRaskFile> files) => _files.AddRange(files.Select(file => file.Name));
}
Live result
Profile_pic.jpg
Brand_banner.jpg

Progress indicator

A bar in place of the text while files upload. In a WebAssembly app nothing is sent, so the bar is how much of the files the handler has read.

FileUploadProgressExample.cs
using Rask.Core.Forms;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class FileUploadProgressExample : Component
{
    private const long ReadLimit = 10 * 1024 * 1024;

    private readonly List<(string Name, long Size)> _read = [];

    protected override Component? Render() =>
        Div[
            Ui.FileUpload.Label("Upload files").Multiple().OnFiles(ReadToTheEnd).Id("ui-upload-progress")[
                Ui.FileUploadDropzone.Heading("Drop files or click to browse").Text("JPG, PNG, GIF up to 10MB").WithProgress().Inline()
            ],
            Div.Class("mt-3 flex flex-col gap-2")[
                _read.Select((file, index) => Ui.FileItem.Key(file.Name + index).Heading(file.Name).Size(file.Size))
            ]
        ];

    // The bar is how much of the files this handler has read: here, every byte, to the end.
    private async Task ReadToTheEnd(IReadOnlyList<IRaskFile> files)
    {
        var buffer = new byte[64 * 1024];
        foreach (var file in files.Where(file => file.Size <= ReadLimit))
        {
            long arrived = 0;
            await using var stream = file.OpenReadStream(ReadLimit);
            for (var read = await stream.ReadAsync(buffer); read > 0; read = await stream.ReadAsync(buffer))
            {
                arrived += read;
            }

            _read.Add((file.Name, arrived));
        }
    }
}
Live result

Disabled state

No click, no key and no drop, and the dropzone drawn muted.

FileUploadDisabledExample.cs
namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class FileUploadDisabledExample : Component
{
    protected override Component? Render() =>
        Ui.FileUpload.Label("Upload files").Multiple().Disabled().Id("ui-upload-disabled")[
            Ui.FileUploadDropzone.Heading("Drop files or click to browse").Text("JPG, PNG, GIF up to 10MB").Inline()
        ];
}
Live result

Custom uploader

Markup of your own in place of the dropzone, with the same input behind it. in-data-dragging: and in-data-loading: style the two states.

FileUploadCustomExample.cs
using Rask.Core.Forms;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class FileUploadCustomExample : Component
{
    private const string Avatar =
        "relative flex size-20 cursor-pointer items-center justify-center rounded-full border border-zinc-200 "
        + "bg-zinc-100 transition-colors hover:border-zinc-300 hover:bg-zinc-200 dark:border-white/10 "
        + "dark:bg-white/10 dark:hover:bg-white/15 dark:in-data-dragging:bg-white/15";

    private string? _photo;

    protected override Component? Render() =>
        Div[
            Ui.FileUpload.Accept("image/*").OnFiles(Pick).Id("ui-upload-avatar")[
                Div.Class(Avatar)[
                    Span.Class("sr-only")["Upload a profile photo"],
                    Ui.Icon.Name(Ui.IconName.User).Solid.Class("text-zinc-500 dark:text-zinc-400"),
                    Div.Class("absolute right-0 bottom-0 rounded-full bg-white dark:bg-zinc-800")[
                        Ui.Icon.Name(Ui.IconName.ArrowUpCircle).Solid.Class("text-zinc-500 dark:text-zinc-400")
                    ]
                ]
            ],
            Ui.Text.Class("mt-2")[_photo is null ? "No photo chosen." : "Chosen: " + _photo]
        ];

    private void Pick(IReadOnlyList<IRaskFile> files) => _photo = files.Count > 0 ? files[0].Name : null;
}
Live result

No photo chosen.

Single file upload

One file, kept by the page until it is saved or removed.

FileUploadSingleExample.cs
using Rask.Core.Forms;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class FileUploadSingleExample : Component
{
    private (string Name, long Size)? _photo;
    private string _saved = "Nothing saved yet.";

    protected override Component? Render() =>
        Div.Class("space-y-3")[
            Ui.FileUpload.Label("Upload file").Accept("image/*").OnFiles(Pick).Id("ui-upload-single")[
                Ui.FileUploadDropzone.Heading("Drop file here or click to browse").Text("JPG, PNG, GIF up to 10MB")
            ],
            Div.Class("flex flex-col gap-2")[
                _photo is { } photo
                    ? Ui.FileItem.Heading(photo.Name).Size(photo.Size)
                        .Actions(Ui.FileItemRemove.AriaLabel("Remove file: " + photo.Name).OnClick(() => { _photo = null; }))
                    : null
            ],
            Ui.Button.OnClick(Save)["Save"],
            Ui.Text[_saved]
        ];

    // Where Livewire stores the file on save, a Rask page reads it in OnFiles: this one keeps its name and size.
    private void Pick(IReadOnlyList<IRaskFile> files) => _photo = files.Count > 0 ? (files[0].Name, files[0].Size) : null;

    private void Save() => _saved = _photo is { } photo ? "Saved " + photo.Name + "." : "No file to save.";
}
Live result

Nothing saved yet.

Multiple files upload

Several files, each with its own remove button named after it.

FileUploadMultipleExample.cs
using Rask.Core.Forms;

namespace Rask.Site.Features.UiDocs.Examples;

public sealed partial class FileUploadMultipleExample : Component
{
    private readonly List<(string Name, long Size)> _photos = [];
    private string _saved = "Nothing saved yet.";

    protected override Component? Render() =>
        Div.Class("space-y-3")[
            Ui.FileUpload.Label("Upload files").Multiple().Accept("image/*").OnFiles(Keep).Id("ui-upload-multiple")[
                Ui.FileUploadDropzone.Heading("Drop files here or click to browse").Text("JPG, PNG, GIF up to 10MB")
            ],
            Div.Class("flex flex-col gap-2")[
                _photos.Select((photo, index) =>
                    Ui.FileItem.Key(photo.Name + index).Heading(photo.Name).Size(photo.Size)
                        .Actions(Ui.FileItemRemove.AriaLabel("Remove file: " + photo.Name).OnClick(() => _photos.RemoveAt(index))))
            ],
            Ui.Button.OnClick(Save)["Save"],
            Ui.Text[_saved]
        ];

    private void Keep(IReadOnlyList<IRaskFile> files) => _photos.AddRange(files.Select(file => (file.Name, file.Size)));

    private void Save() => _saved = _photos.Count == 1 ? "Saved 1 file." : $"Saved {_photos.Count} files.";
}
Live result

Nothing saved yet.

Behaviour

A real file input

Ui.FileUpload is a <label> around a real <input type="file">, so a click anywhere opens the picker and the input keeps the keyboard (Space and Enter open it) with no script; the focus ring is drawn on the dropzone. The field's label is a <label for>, and names the input through aria-labelledby, as Flux's does, so the dropzone's words are not read out as part of its name.

Dragging and progress

While files are dragged over it the runtime writes data-dragging on the upload (its data-rask-dropzone hook) and lays the input over the whole area, so the drop is the browser's own. A drag that carries no files, such as a row of a sortable list, is not an offer to upload.

From the moment files are chosen until OnFiles has rendered, the runtime writes data-loading and the dropzone shows a spinner, or with WithProgress() a bar as wide as --ui-file-upload-progress, with --ui-file-upload-progress-as-string beside it: Flux's two variables under the kit's prefix, 0% at rest. On the Server host that is the upload request's own progress; in a WebAssembly app it is how much of the files the handler has read through OpenReadStream.

The files are the page's

OnFiles hands the page an IRaskFile for each file, readable only while the handler runs; its name, size and type are the browser's word, so check them where the file lands. Ui.FileItem draws a row from what the page kept: Size in bytes is written as B, KB, MB or GB, and Ui.FileItemRemove in its Actions removes nothing itself.

Write the remove handler where the list is: a handler re-renders the component it closes over, and one built in a static helper that closed over the list alone would remove the file and redraw nothing.

Previews

A preview of a file that was only just chosen is the page's to make: read the picture in OnFiles and hand Ui.FileItem.Image a data: address, or the address it was stored under. Take the type from the browser only for the few an <img> draws, and cap the size, as the first example does.

Reference

Ui.FileUpload

flux:file-upload

The area that takes files. Its children are a dropzone, or markup of your own.

Step
Takes
What it does
.OnFiles(…)a handler given the filesRuns with the files chosen or dropped, readable while it runs: Rask's stand-in for wire:model.
.Multiple()boolLets the reader choose, or drop, more than one file.
.Accept(…)stringThe input's own accept list: "image/*". A filter on the picker and nothing more.
.Label(…)stringDraws a field around it, with this label.
.Description(…)stringHelp text between the label and the upload.
.Error(…)stringA message under the upload, which also marks it invalid.
.Name(…)stringThe input's name, for a form that is posted.
.Disabled()boolTakes no click, no key and no drop, and draws the dropzone muted.

Ui.FileUploadDropzone

flux:file-upload.dropzone

The dashed area that says what to drop, and shows that an upload runs.

Step
Takes
What it does
.Heading(…)stringThe line it leads with.
.Text(…)stringThe smaller line under it: what is accepted, and how much.
.Icon(…)Ui.IconName — one of 317 namesThe icon. A cloud with an arrow when unset.
.Inline()boolThe compact layout: the icon beside the words.
.WithProgress()boolA bar that fills in place of Text while files upload, instead of a spinner.

It is one HTML element, so Id, Class, Style, Data, Aria and the events are its steps too.

Ui.FileItem

flux:file-item

One file as a row: a preview or an icon, its name, its size, and its actions.

Step
Takes
What it does
.Heading(…)stringThe file's name, or another title for it.
.Text(…)stringThe line under the heading. Written from Size when unset.
.Image(…)stringThe address of a preview, shown in place of the icon.
.Size(…)Int64The file's size in bytes, shown as B, KB, MB or GB.
.Icon(…)Ui.IconName — one of 317 namesThe icon shown when there is no image. A document when unset.
.Invalid()boolDraws it as a file that failed.
.Actions(…)ComponentWhat sits at the end of the row: a Ui.FileItemRemove.

Ui.FileItemRemove

flux:file-item.remove

The small cross for a file item's actions. It removes nothing itself.

Step
Takes
What it does
.OnClick(…)a handler, with no argumentRuns when it is pressed: Rask's stand-in for wire:click.
.AriaLabel(…)stringIts accessible name. "Remove file" when unset; name the file in a list of several.
Confirm