How to Handle File Uploads with Progress Tracking and Custom Upload Behavior in Element UI

The Element UI Upload component provides a complete file upload solution with built-in progress tracking through XHR-based requests, lifecycle hooks, and customizable HTTP implementations via the httpRequest prop.

Handling file uploads with progress tracking and custom upload behavior in the ElemeFE/element repository relies on a modular architecture that separates UI concerns from network logic. The component found in packages/upload/src/upload.vue orchestrates file selection, validation, and request management, while delegating actual HTTP transmission to a pluggable request handler. Understanding this separation allows developers to intercept uploads for preprocessing, replace the default XHR implementation, or create completely custom user interfaces while retaining progress reporting capabilities.

Understanding the Upload Component Architecture

The Upload component consists of three coordinated modules that handle distinct responsibilities:

This architecture allows you to replace any individual piece—such as substituting the default AJAX transport with a fetch-based implementation—without modifying the component's core logic.

The File Upload Lifecycle

File Selection and Queueing

When users select files through the native <input type="file"> element, the handleChange method in upload.vue captures the FileList and initiates processing:

handleChange(ev) {
  const files = ev.target.files;
  if (!files) return;
  this.uploadFiles(files);
}

(see lines 62-66 of upload.vue)

The uploadFiles method enforces the limit and multiple constraints, then invokes onStart for each file. If autoUpload is enabled (the default), it immediately triggers the upload process; otherwise, files queue until manually started.

Before-Upload Validation

Before any network activity begins, the upload method checks the beforeUpload prop (lines 87-118). This hook supports three return types:

  • false – Cancels the upload immediately
  • File or Blob – Proceeds with the returned file (synchronous transformation)
  • Promise<File|Blob> – Waits for async operations like image compression or server-side validation before proceeding

If the promise resolves, the component preserves raw-file metadata and proceeds to post; if rejected or returned as false, the upload aborts.

HTTP Request Execution

The post method constructs an options object containing callbacks for progress, success, and error, then delegates to the httpRequest prop (lines 135-161). By default, this references the ajax function, but you can inject any transport mechanism that conforms to the expected interface:

post(rawFile) {
  const { uid } = rawFile;
  const options = {
    headers: this.headers,
    withCredentials: this.withCredentials,
    file: rawFile,
    data: this.data,
    filename: this.name,
    action: this.action,
    onProgress: e => {
      this.onProgress(e, rawFile);
    },
    onSuccess: res => {
      this.onSuccess(res, rawFile);
      delete this.reqs[uid];
    },
    onError: err => {
      this.onError(err, rawFile);
      delete this.reqs[uid];
    }
  };
  const req = this.httpRequest(options);
  this.reqs[uid] = req;
}

The request object is stored in this.reqs keyed by the file's uid, enabling individual or bulk abortion via the component's abort(file?) method.

Progress Tracking and UI Updates

In packages/upload/src/ajax.js, the default transport attaches an onprogress listener to the XHR upload object (lines 39-46):

xhr.upload.onprogress = function progress(e) {
  if (e.total > 0) {
    e.percent = e.loaded / e.total * 100;
  }
  option.onProgress(e);
};

This calculates a percentage and invokes the onProgress callback supplied by upload.vue, which updates the file's percentage property. Meanwhile, upload-list.vue monitors the file status and renders an <el-progress> element whenever status === 'uploading' (lines 40-44).

Implementing Custom Upload Behavior

Custom HTTP Requests with httpRequest

Override the default XHR behavior by providing a function to the httpRequest prop. Your function receives an options object containing headers, withCredentials, file, data, filename, action, onProgress, onSuccess, and onError. This enables integration with alternative transports like fetch, Axios, or cloud storage SDKs.

Async Validation and Transformation

Use the beforeUpload hook to perform client-side validation or file manipulation. Return a Promise to handle async operations such as:

  • Image compression or format conversion
  • Virus scanning via client-side wasm
  • Duplicate checking against existing uploads

The resolved value replaces the original file in the upload pipeline.

Manual Upload Control

Set :auto-upload="false" to queue files without immediate transmission. Trigger uploads manually by calling uploadFiles on the component reference, allowing batch operations or user-initiated submission flows.

Practical Implementation Example

The following implementation demonstrates disabled auto-upload, custom request handling with fetch, file size validation, and progress logging:

<template>
  <el-upload
    ref="upload"
    class="custom-uploader"
    action="https://api.example.com/upload"
    :auto-upload="false"
    :multiple="true"
    :limit="5"
    :on-start="handleStart"
    :on-progress="handleProgress"
    :on-success="handleSuccess"
    :on-error="handleError"
    :before-upload="validateFile"
    :http-request="customRequest"
  >
    <el-button slot="trigger" size="small" type="primary">Select Files</el-button>
    <el-button 
      style="margin-left: 10px;" 
      size="small" 
      type="success"
      @click="submitUpload"
    >
      Upload to Server
    </el-button>
  </el-upload>
</template>

<script>
export default {
  methods: {
    handleStart(file) {
      console.log('Upload starting:', file.name);
    },
    
    handleProgress(event, file) {
      console.log(`Upload progress for ${file.name}: ${event.percent.toFixed(1)}%`);
    },
    
    handleSuccess(response, file) {
      console.log('Upload completed:', file.name, response);
    },
    
    handleError(error, file) {
      console.error('Upload failed:', file.name, error);
    },
    
    validateFile(file) {
      const maxSize = 2 * 1024 * 1024; // 2MB
      if (file.size > maxSize) {
        this.$message.error('File size exceeds 2MB limit');
        return false;
      }
      
      // Async transformation example
      return new Promise((resolve) => {
        this.compressImage(file, (compressedBlob) => {
          const transformedFile = new File(
            [compressedBlob], 
            file.name, 
            { type: file.type }
          );
          resolve(transformedFile);
        });
      });
    },
    
    customRequest(options) {
      const formData = new FormData();
      
      if (options.data) {
        Object.entries(options.data).forEach(([key, value]) => {
          formData.append(key, value);
        });
      }
      
      formData.append(options.filename, options.file);
      
      return fetch(options.action, {
        method: 'POST',
        body: formData,
        credentials: options.withCredentials ? 'include' : 'same-origin',
        headers: options.headers
      })
      .then(response => {
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
      })
      .then(options.onSuccess)
      .catch(options.onError);
    },
    
    submitUpload() {
      this.$refs.upload.submit();
    },
    
    compressImage(file, callback) {
      // Image compression logic here
      callback(file);
    }
  }
};
</script>

This example integrates all major customization points: synchronous validation, asynchronous file transformation, a fetch-based transport replacing the default XHR, and manual upload triggering.

Summary

  • Modular Architecture – The Upload component separates concerns between upload.vue (orchestration), ajax.js (transport), and upload-list.vue (UI), enabling targeted customizations.
  • Progress Tracking – Built-in progress reporting relies on xhr.upload.onprogress in the default implementation, passing percentage data through the onProgress callback to update UI elements.
  • Custom Transport – Replace the default XHR behavior by providing a custom http-request function that conforms to the options interface, supporting fetch, Axios, or SDK-based uploads.
  • Lifecycle Hooks – Use before-upload for validation and transformation (supports async/Promise), on-start for initiation side effects, and on-success/on-error for result handling.
  • Manual Control – Disable auto-upload to queue files indefinitely, then trigger uploads individually or in batches via component methods.

Frequently Asked Questions

How does Element UI calculate and display upload progress?

Element UI calculates upload progress in packages/upload/src/ajax.js by attaching an onprogress event listener to the XMLHttpRequest's upload property. When the browser fires progress events, the code computes e.percent = e.loaded / e.total * 100 and invokes the onProgress callback. This updates the file object's percentage property, which upload-list.vue binds to an <el-progress> component for visual feedback.

Can I use the Fetch API instead of XMLHttpRequest for file uploads?

Yes. Pass a custom function to the http-request prop that accepts the standard options object and returns a Promise or request handle. Your implementation should call options.onProgress with percentage data if needed, then resolve with options.onSuccess or reject with options.onError. This completely replaces the default XHR-based ajax.js implementation.

How do I abort an ongoing file upload?

Call the abort(file) method on the Upload component instance. If you pass a specific file object (or its uid), it aborts that individual request by looking up the stored request handle in this.reqs[uid]. Calling abort() without arguments iterates through all pending requests in this.reqs and aborts each one, effectively canceling all active uploads.

What is the difference between the before-upload and on-start hooks?

The before-upload hook executes before any network request begins and can block or transform the upload—return false to cancel, a File/Blob to replace the file, or a Promise for async validation. The on-start hook fires immediately after before-upload resolves successfully and purely signals that the upload is beginning; it cannot prevent the upload and is used for side effects like logging or UI state changes.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →