subscribe our youtube channel popup

File Upload with Preview in LWC & Apex

Enabling users to upload files and instantly preview them is a powerful way to improve UX in Salesforce Lightning apps. In this blog, we will learn about the need we might want for this feature, our org and user stories before we start, steps to build it step by step (with full code), actual use cases of using this approach and the way to extend it beyond images. Let’s move forward and check on this blog to learn about File Upload with Preview in LWC & Apex.

Actual User Requirements

Before writing a single line of code, gather below core requirements as per any business:

  1. Record Context:
    Users must be on a specific record (e.g. Account, Case) so uploaded files attach to that record.
  2. Instant Preview:
    For images especially, users expect to see what they have chosen before moving forward to “Upload” in Salesforce.
  3. File Size & Type Limits:
    We need to decide the maximum size like 2 MB and acceptable file types like PNG, JPG.
  4. Security & Permissions:
    Running users need to create rights on ContentVersion and ContentDocumentLink.
  5. Graceful Feedback
    Disable the Upload button until a file is selected, show “Uploading…,” and handle errors cleanly or as per business requirement we can add any messages.

With these in hand, our dev plan becomes straightforward: a small Apex controller to save the file and a Lightning Web Component (LWC) for the UI.

Prerequisites

  1. API Version ≥ 50.0
    (The example uses 64.0 in metadata, but any modern org will do.)
    User Permissions
    Ensure profiles or permission sets grant:
    • Create on ContentVersion
    • Create on ContentDocumentLink
  2. Record Page
    We will deploy the LWC to a Record Page so that @api recordId is populated automatically.

Below are the steps for programs that we need to write as part of implementations:

Step 1: Apex Controller

Create a simple, @AuraEnabled Apex method that:

  1. Decodes the Base64 file data from the client
  2. Inserts a ContentVersion
  3. Finds the resulting ContentDocumentId
  4. Links it to the parent record via ContentDocumentLink
// File: FileUploadController.cls
public with sharing class FileUploadController {
    @AuraEnabled
    public static Id uploadFile(Id parentId, String base64Data, String fileName) {
        String base64Content = base64Data.substringAfter('base64,');
        ContentVersion cv = new ContentVersion(
            Title       = fileName,
            PathOnClient= fileName,
            VersionData = EncodingUtil.base64Decode(base64Content)
        );
        insert cv;
        Id docId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id = :cv.Id].ContentDocumentId;
        insert new ContentDocumentLink(
            ContentDocumentId = docId,
            LinkedEntityId    = parentId,
            ShareType         = 'V'
        );
        return cv.Id;
    }
}

Why ContentVersion + ContentDocumentLink?

  • ContentVersion holds the file itself.
  • ContentDocumentLink attaches it to any record.

Step 2: LWC Component

This LWC handles file selection, preview generation, and the upload call.

Markup (fileUpload.html)

<template>
  <lightning-card title="File Upload with Preview">
    <div class="slds-p-around_medium">
      <!-- 1) File input -->
      <lightning-input
        type="file"
        label="Choose Image"
        accept="image/*"
        onchange={handleFileChange}>
      </lightning-input>

      <!-- 2) Preview -->
      <template if:true={filePreviewUrl}>
        <div class="slds-m-top_medium">
          <b>Preview:</b><br/>
          <img src={filePreviewUrl} alt="preview" style="max-width:200px; max-height:200px;"/>
        </div>
      </template>

      <!-- 3) Upload button -->
      <lightning-button
        class="slds-m-top_medium"
        label="Upload to Salesforce"
        onclick={uploadFile}
        variant="brand"
        disabled={isUploadDisabled}>
      </lightning-button>
         <!-- 4) Status -->
      <template if:true={status}>
        <p class="slds-m-top_small">{status}</p>
      </template>
    </div>
  </lightning-card>
</template>

JavaScript (fileUpload.js)

import { LightningElement, api, track } from 'lwc';
import uploadFileApex from '@salesforce/apex/FileUploadController.uploadFile';

export default class FileUpload extends LightningElement {
  @api recordId;

  @track fileData;
  @track fileName;
  @track filePreviewUrl;
  @track isUploading = false;
  @track status;

  // Getter for disabling the button
  get isUploadDisabled() {
    return this.isUploading || !this.fileData;
  }

  handleFileChange(event) {
    const file = event.target.files[0];
    if (!file) {
      this.fileData = undefined;
      this.filePreviewUrl = undefined;
      return;
    }
    this.fileName = file.name;

    const reader = new FileReader();
    reader.onload = () => {
      // full data URI: "data:image/png;base64,iVBORw0KGgoAAAANS..."
      this.fileData = reader.result;
      this.filePreviewUrl = reader.result; // this drives the <img> src
    };
   reader.onerror = (err) => {
      this.status = 'Error reading file: ' + err;
    };
    reader.readAsDataURL(file);
  }

    uploadFile() {
  // 1) Ensure recordId is present
  if (!this.recordId) {
    this.status = 'Error: No record context. Please add this component to a record page.';
    return;
  }
  if (!this.fileData) {
    this.status = 'Please choose a file first.';
    return;
  }

  this.isUploading = true;
  this.status = 'Uploading...';

  uploadFileApex({
    parentId   : this.recordId,
    base64Data : this.fileData,
    fileName   : this.fileName
  })
  .then((newVersionId) => {
    this.status = 'Upload successful! Version Id: ' + newVersionId;
  })
  .catch((error) => {
    console.error('Upload failed:', error);
    // Extract human message...
    let msg = (error.body?.pageErrors?.[0]?.message)
               || error.body?.message
               || error.message
               || 'Unknown error';
    this.status = 'Upload failed: ' + msg;
  })
  .finally(() => {
    this.isUploading = false;
  });
}
}

Metadata (fileUpload.js‑meta.xml)

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
  <apiVersion>64.0</apiVersion>
  <isExposed>true</isExposed>
  <masterLabel>File Upload with Preview</masterLabel>
  <targets>
    <target>lightning__RecordPage</target>
  </targets>
</LightningComponentBundle>

Tip: Limiting to lightning__RecordPage prevents users from adding it to App/Home pages (where recordId is unavailable).

Below is the actual screenshot from UI for Reference:

Post selecting file to upload our implementation will looks like:

Once we hit on “Upload to Salesforce” button it will show as below and in above screenshot we can see once file added button got enabled:

Once id of file created we can see it in Attachment section as below:

Advantages of above Implementation

  1. Responsive UX
    Instant client‑side preview builds user confidence.
  2. Server‑Side Safety
    Apex handles file storage, respects sharing rules, and leverages standard ContentVersion APIs.
  3. Reusability
    We can drop this LWC on any record page Accounts, Cases or custom objects.
  4. Extensible
    Simply tweak acceptedFormats to support documents, spreadsheets, or PDFs.
  5. Minimal Footprint
    No third‑party libraries are pure LWC + Apex so it will be lean and easy to maintain.

Extending Beyond Images

Right now, acceptedFormats = ‘.png,.jpg,.jpeg’ and we preview only images. To support other file types:

Allow more extensions

acceptedFormats = ‘.png,.jpg,.jpeg,.pdf,.docx,.xlsx,.csv’;

  1. Remove or conditionally render the <img>
    For non‑image files we could show a generic icon instead of preview.
  2. Validate file sizes
    Add a check in handleFileChange for file.size before reading.

With these tweaks, the same pattern lets us upload PDFs, Word docs, Excel sheets or any file Salesforce supports as per user or business requirements.

Points to Remember

  • Always wrap JS expressions in {…} and keep attribute names lowercase in our HTML.
  • Deploy both Apex and LWC in the same API version (≥ 50.0).
  • Grant CRUD/FLS on ContentVersion and ContentDocumentLink to our users.
  • Use the Record Page target so @api recordId isn’t empty.
  • Test in Dev Console or with browser DevTools to inspect errors and object permissions.

Conclusion

Implementing file upload with preview in LWC is straightforward once we break it into two parts as :An Apex service to persist files via standard Content objects and A lightweight LWC for file selection, preview and status updates.With the help of this implementations we will be able to achieve any customized business requirements.

By following the steps above and remembering permissions, metadata exposure and user experience tips we will have a reusable component that improves productivity and delight. And when our business needs change, simply adjust the acceptedFormats or add file‑size checks to meet new requirements.

Happy coding:)

Satyam parasa
Satyam parasa

Satyam Parasa is a Salesforce and Mobile application developer. Passionate about learning new technologies, he is the founder of Flutterant.com, where he shares his knowledge and insights.

Articles: 46

Leave a Reply

Your email address will not be published. Required fields are marked *