CxJS

Multi File Upload

This example demonstrates how to use the UploadButton component to handle multiple file uploads with validation and progress tracking.

<div controller={PageController}>
  <ValidationGroup invalid={m.form.invalid} errors={m.form.errors}>
    <Validator
      value={m.form.files}
      onValidate={(files: FileEntry[] = []) =>
        files.length < 1 && "Please select at least one file."
      }
    />
    <Validator
      value={m.form.files}
      onValidate={(files: FileEntry[]) =>
        files?.some((f) => f.invalid) &&
        "Only images with size up to 1 MB are allowed."
      }
    />

    <UploadButton
      text="Choose files"
      url="#"
      multiple
      onUploadStarting="onUploadStarting"
      icon="search"
    />

    <LabelsTopLayout vertical mod="stretch">
      <LabeledContainer label="Files to upload:">
        <Grid
          records={m.form.files}
          recordAlias={m.$record}
          emptyText="Select image files (max 1 MB each)."
          columns={[
            {
              header: "File name",
              field: "file.name",
              class: expr(m.$record.invalid.type, (invalid) =>
                invalid ? "text-red-600 italic" : "",
              ),
            },
            {
              header: "Size",
              value: computable(m.$record.file.size, (s) =>
                formatFileSize(s ?? 0),
              ),
              align: "right",
              defaultWidth: 80,
              class: expr(m.$record.invalid.size, (invalid) =>
                invalid ? "text-red-600 italic" : "",
              ),
            },
            {
              align: "center",
              defaultWidth: 50,
              items: (
                <Button
                  icon="x"
                  mod="hollow"
                  onClick="onRemoveFile"
                  disabled={m.form.uploadInProgress}
                />
              ),
            },
          ]}
        />
      </LabeledContainer>
      <LabeledContainer label="Progress:" visible={m.form.uploadInProgress}>
        <ProgressBar value={m.form.progress} />
      </LabeledContainer>
    </LabelsTopLayout>

    <div visible={isNonEmpty(m.form.errors)} class="mt-4">
      <Repeater
        records={m.form.errors}
        recordAlias={m.$error}
        visible={expr(
          m.form.invalid,
          m.form.visited,
          (invalid, visited) => invalid && visited,
        )}
      >
        <div class="text-red-600 italic" text={m.$error.message} />
      </Repeater>
    </div>

    <div class="flex justify-between mt-4">
      <Button
        text="Clear"
        onClick="onClearForm"
        icon="trash"
        disabled={m.form.uploadInProgress}
      />
      <Button
        text="Upload"
        onClick="upload"
        icon="upload"
        disabled={m.form.uploadInProgress}
      />
    </div>
  </ValidationGroup>
</div>
Choose files
File nameSize
Select image files (max 1 MB each).

How It Works

By default, UploadButton automatically uploads each file as soon as it is selected using XMLHttpRequest. When multiple files are selected, each file is sent in a separate request.

You can customize this behavior by defining the onUploadStarting callback. By preserving the selected files and returning false from the callback, the upload is deferred, giving you full control over the upload logic.

onUploadStarting(xhr, instance, file) {
  // Validate the file
  const invalidSize = file.size > 1e6;
  const invalidType = !file.type.startsWith("image/");

  // Store the file for later upload
  instance.store.update(m.form.files, append, {
    file,
    invalid: invalidSize || invalidType ? { size: invalidSize, type: invalidType } : undefined,
  });

  // Return false to prevent automatic upload
  return false;
}

Progress Tracking

XMLHttpRequest provides better support for upload progress tracking than fetch:

function uploadFilesWithProgress(url, files, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    const formData = new FormData();

    files.forEach((file) => formData.append("file", file));

    xhr.upload.onprogress = (e) =>
      e.lengthComputable && onProgress(e.loaded / e.total);

    xhr.onload = () =>
      xhr.status >= 200 && xhr.status < 300
        ? resolve(xhr)
        : reject(new Error(`Failed: ${xhr.status}`));

    xhr.open("POST", url);
    xhr.send(formData);
  });
}

Validation

Use ValidationGroup with Validator components to validate file selection:

<ValidationGroup invalid={m.form.invalid} errors={m.form.errors}>
  <Validator
    value={m.form.files}
    onValidate={(files = []) =>
      files.length < 1 && "Please select at least one file."
    }
  />
  <Validator
    value={m.form.files}
    onValidate={(files) =>
      files?.some((f) => f.invalid) && "Only valid images allowed."
    }
  />
  {/* ... form content ... */}
</ValidationGroup>