Extending Superfile

April 22, 2026 · View on GitHub

This guide shows how to create custom Superfile addons by extending the base class.

Basic Pattern

import Superfile from "./superfile.js";

class MyAddon extends Superfile {
  constructor(inputElement) {
    super(inputElement);
    // Your initialization
  }
  
  static init(selector = "input[type=file]") {
    const list = document.querySelectorAll(selector);
    for (const el of list) {
      new MyAddon(el);
    }
  }
}

export default MyAddon;

Key Principles

1. Always Call Super Constructor

constructor(inputElement) {
  super(inputElement); // Sets up all base functionality
  
  // Your custom init
  this.customData = null;
}

2. Override Methods Strategically

MethodWhen to OverrideCall Super?
handleResizeImage()Pre/post processing hooksYes, unless fully replacing
resizeImage()Custom resize algorithmOptional
createProcessedFile()Modify File creationUsually yes
clearPreview()Cleanup custom dataYes
dispose()Cleanup custom listenersYes

3. Use Method Naming Convention

Event handlers use $ prefix:

  • $change() - File selected
  • $click() - Button clicked
  • $drop() - File dropped
  • $dragover() - Drag over
  • $dragleave() - Drag leave

Common Patterns

Pre-Processing Hook

Run code before resize:

handleResizeImage(file, callback) {
  // Extract data before processing
  this.extractData(file);
  
  // Call parent for actual resize
  super.handleResizeImage(file, callback);
}

Post-Processing Hook

Run code after resize:

handleResizeImage(file, callback) {
  const originalCallback = callback;
  
  super.handleResizeImage(file, () => {
    // Post-processing
    this.onProcessed();
    
    // Call original callback
    originalCallback();
  });
}

Replace Processing Entirely

Skip parent and do it yourself:

handleResizeImage(file, callback) {
  if (shouldSkipCustom) {
    super.handleResizeImage(file, callback);
    return;
  }
  
  // Your custom resize
  this.customResize(file, (result) => {
    this.createProcessedFile(file, result, callback);
  });
}

Add Custom Event Listeners

constructor(inputElement) {
  super(inputElement);
  
  // Add your listeners
  this.customButton = this.holderElement.querySelector(".my-button");
  if (this.customButton) {
    this.customButton.addEventListener("click", this);
  }
}

// Handle custom events
$click(e) {
  // Check if it's your button first
  if (e.target.closest(".my-button")) {
    this.doCustomAction();
    return;
  }
  
  // Call parent for standard buttons
  super.$click(e);
}

dispose() {
  // Remove your listeners
  if (this.customButton) {
    this.customButton.removeEventListener("click", this);
  }
  
  // Always call super.dispose()
  super.dispose();
}

Real Examples

Watermark Addon

Add a watermark to all uploaded images:

class WatermarkSuperfile extends Superfile {
  constructor(inputElement) {
    super(inputElement);
    this.watermarkText = inputElement.dataset.watermark || "© My Site";
  }
  
  resizeImage(file, img, callback) {
    // Do standard resize
    const canvas = document.createElement("canvas");
    // ... resize logic ...
    
    // Add watermark
    const ctx = canvas.getContext("2d");
    ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
    ctx.font = "20px Arial";
    ctx.fillText(this.watermarkText, 10, canvas.height - 20);
    
    // Continue
    ctx.canvas.toBlob((blob) => {
      this.createProcessedFile(file, blob, callback);
    }, file.type, this.quality);
  }
}

File Validation Addon

Add file validation with custom error display:

class ValidatingSuperfile extends Superfile {
  constructor(inputElement) {
    super(inputElement);
    this.errorElement = this.holderElement.querySelector(".error-message");
    this.maxFileSize = parseInt(inputElement.dataset.maxSize, 10) || 5 * 1024 * 1024; // 5MB
  }
  
  handleResizeImage(file, callback) {
    // Validate before processing
    if (file.size > this.maxFileSize) {
      this.showError(`File too large: ${(file.size / 1024 / 1024).toFixed(1)}MB (max ${(this.maxFileSize / 1024 / 1024).toFixed(1)}MB)`);
      callback(); // Still call callback to complete flow
      return;
    }
    
    if (!file.type.match(/image.*/)) {
      this.showError("Only images are allowed");
      callback();
      return;
    }
    
    this.clearError();
    super.handleResizeImage(file, callback);
  }
  
  showError(message) {
    if (this.errorElement) {
      this.errorElement.textContent = message;
      this.errorElement.style.display = "block";
    }
  }
  
  clearError() {
    if (this.errorElement) {
      this.errorElement.style.display = "none";
    }
  }
  
  clearPreview() {
    super.clearPreview();
    this.clearError();
  }
}

Analytics Addon

Track upload metrics:

class AnalyticsSuperfile extends Superfile {
  constructor(inputElement) {
    super(inputElement);
    this.startTime = null;
    this.originalSize = 0;
  }
  
  handleResizeImage(file, callback) {
    this.startTime = performance.now();
    this.originalSize = file.size;
    
    const originalCallback = callback;
    
    super.handleResizeImage(file, () => {
      // Track metrics
      const processedFile = Array.from(this.inputElement.files).find(
        f => f.name === file.name
      );
      
      if (processedFile) {
        this.trackMetrics({
          originalSize: this.originalSize,
          processedSize: processedFile.size,
          compressionRatio: processedFile.size / this.originalSize,
          processingTime: performance.now() - this.startTime,
          fileType: file.type,
          dimensions: { width: this.maxWidth, height: this.maxHeight }
        });
      }
      
      originalCallback();
    });
  }
  
  trackMetrics(data) {
    // Send to analytics service
    console.log("[Analytics]", data);
    
    // Example: Send to your backend
    // fetch("/api/analytics", { method: "POST", body: JSON.stringify(data) });
  }
}

Best Practices

1. Graceful Degradation

Always handle missing optional dependencies:

handleResizeImage(file, callback) {
  if (typeof window.myOptionalLibrary === "undefined") {
    console.warn("[MyAddon] Optional library not loaded, using standard processing");
    super.handleResizeImage(file, callback);
    return;
  }
  
  // Custom processing
}

2. Cleanup in dispose()

Always clean up custom resources:

dispose() {
  // Custom cleanup
  this.customData = null;
  this.observer?.disconnect();
  
  // Important: Call super last
  super.dispose();
}

3. Preserve Callback Chain

Always call the callback, even on errors:

handleResizeImage(file, callback) {
  try {
    // Processing
    super.handleResizeImage(file, callback);
  } catch (e) {
    console.error(e);
    callback(); // Still call to complete the flow
  }
}

4. Access Parent Element

The holder element is the .superfile container:

constructor(inputElement) {
  super(inputElement);
  
  // Find custom elements within the component
  this.customElement = this.holderElement.querySelector(".my-custom-element");
}

5. State Management

Store custom state on the instance:

constructor(inputElement) {
  super(inputElement);
  this.state = {
    isProcessing: false,
    retryCount: 0,
    customData: null
  };
}

Testing Your Addon

Use the same test setup as Superfile:

import { describe, it, expect, beforeEach } from "bun:test";
import "./setup.js";
import MyAddon from "../my-addon.js";

describe("MyAddon", () => {
  let container;
  
  beforeEach(() => {
    document.body.innerHTML = "";
    container = document.createElement("div");
    container.className = "superfile";
    document.body.appendChild(container);
  });
  
  it("should initialize with custom data", () => {
    container.innerHTML = `<input type="file" data-custom="value" />`;
    const input = container.querySelector("input");
    const addon = new MyAddon(input);
    
    expect(addon.customData).toBe("value");
  });
});

See Also