Newer Version Available

This content describes an older version of this product. View Latest

Image Upload Example

Here’s a quick example of uploading an image to Salesforce using the createContentDocumentAndVersion adapter. This technique works on mobile devices, whether they’re online or offline. This technique works only in Salesforce mobile apps.

This component is intended to be used as a quick action in a record context, for example, added to a record detail page. Uploaded images are attached to the associated record, and they can be viewed in the Notes & Attachments related items panel for that record.

1<!-- fileUpload.html -->
2<template>
3    <h1>File Upload</h1>
4
5    <!-- File selection controls. Always displayed.
6         Set `accept="*/*"` to allow uploads of any type of file. -->
7    <div>
8        <lightning-input
9                type="file"
10                name="fileUploader"
11                label="Select file to upload"
12                multiple="false"
13                accept="image/*"
14                onchange={handleFilesInputChange}
15            >
16            </lightning-input>
17        </div>
18
19    <!-- If a file is selected, display additional input controls. -->
20    <div if:true={fileName}>
21
22        <!-- Show the filename (read-only) -->
23        <p>Selected file:</p>
24        <p>{fileName}</p>
25        
26        <!-- Form fields for upload details -->
27        <div class="inputs">
28            <lightning-input
29                type="text"
30                label="Title"
31                value={titleValue}
32                onchange={handleTitleInputChange}
33            ></lightning-input>
34            <lightning-input
35                type="text"
36                label="Description"
37                value={descriptionValue}
38                onchange={handleDescriptionInputChange}
39            ></lightning-input>
40        </div>
41
42        <!-- Button to actually do the upload (enqueued as a draft) -->
43        <button
44            class="slds-button slds-button_brand slds-var-m-top_medium"
45            disabled={uploadingFile}
46            onclick={handleUploadClick}
47        >
48            <label>Upload</label>
49        </button>
50    </div>
51
52    <!-- If there are errors, show them here -->
53    <div if:true={errorMessage}>
54        <lightning-card title="Error">
55            <div class="card-body">{errorMessage}</div>
56        </lightning-card>
57    </div>
58</template>

The user interface has three main sections.

  • A local file selection widget, which is always displayed.
  • A selected file info panel, which is displayed only when there’s a file selected. This panel also contains an Upload button that triggers the file upload to Salesforce.
  • An error messages panel, which is displayed only when there’s an error with an upload.

This template is standard markup for a simple widget. All the magic happens on the other side of those four onchange attributes, and the handler functions that perform actions when the controls are used.

Here’s the component’s JavaScript implementation.

1// fileUpload.js
2import { LightningElement, api, track, wire } from "lwc";
3import { ShowToastEvent } from "lightning/platformShowToastEvent";
4import {
5  createContentDocumentAndVersion,
6  createRecord,
7} from "lightning/uiRecordApi";
8// Imports for forced-prime ObjectInfo metadata work-around
9import { getObjectInfos } from "lightning/uiObjectInfoApi";
10import CONTENT_DOCUMENT from "@salesforce/schema/ContentDocument";
11import CONTENT_VERSION from "@salesforce/schema/ContentVersion";
12import CONTENT_DOCUMENT_LINK from "@salesforce/schema/ContentDocumentLink";
13
14export default class FileUpload extends LightningElement {
15  @api
16  recordId;
17
18  @track
19  files = undefined;
20
21  @track
22  uploadingFile = false;
23
24  @track
25  titleValue = "";
26
27  @track
28  descriptionValue = "";
29
30  @track
31  errorMessage = "";
32
33  // Object metadata, or "ObjectInfo", is required for creating records
34  // while offline. Use the getObjectInfos adapter to "force-prime" the
35  // necessary object metadata. This is a work-around for the static analyzer
36  // not knowing enough about the file object schema.
37  @wire(getObjectInfos, {
38    objectApiNames: [ CONTENT_DOCUMENT, CONTENT_VERSION, CONTENT_DOCUMENT_LINK ],
39  })
40  objectMetadata;
41
42  // Getter used for local-only processing. Not needed for offline caching.
43  // eslint-disable-next-line @salesforce/lwc-graph-analyzer/no-getter-contains-more-than-return-statement
44  get fileName() {
45    // eslint-disable-next-line @salesforce/lwc-graph-analyzer/no-unsupported-member-variable-in-member-expression
46    const file = this.files && this.files[0];
47    if (file) {
48      return file.name;
49    }
50    return undefined;
51  }
52
53  // Input handlers
54  handleFilesInputChange(event) {
55    this.files = event.detail.files;
56    this.titleValue = this.fileName;
57  }
58
59  handleTitleInputChange(event) {
60    this.titleValue = event.detail.value;
61  }
62
63  handleDescriptionInputChange(event) {
64    this.descriptionValue = event.detail.value;
65  }
66
67  // Restore UI to default state
68  resetInputs() {
69    this.files = [];
70    this.titleValue = "";
71    this.descriptionValue = "";
72    this.errorMessage = "";
73  }
74
75  // Handle uploading a file, initiated by user clicking Upload button
76  async handleUploadClick() {
77    // Make sure we're not already uploading something
78    if (this.uploadingFile) {
79      return;
80    }
81
82    // Make sure we have something to upload
83    const file = this.files && this.files[0];
84    if (!file) {
85      return;
86    }
87
88    try {
89      this.uploadingFile = true;
90
91      // Create a ContentDocument and related ContentDocumentVersion for
92      // the file, effectively uploading it
93      const contentDocumentAndVersion =
94        await createContentDocumentAndVersion({
95          title: this.titleValue,
96          description: this.descriptionValue,
97          fileData: file,
98        });
99        console.log("ContentDocument and ContentDocumentVersion records created.");
100
101      // If component is run in a record context (recordId is set), relate
102      // the uploaded file to that record
103      if (this.recordId) {
104        const contentDocumentId = contentDocumentAndVersion.contentDocument.id;
105
106        // Create a ContentDocumentLink (CDL) to associate the uploaded file
107        // to the Files related list of the target recordId
108        await this.createContentDocumentLink(this.recordId, contentDocumentId);
109      }
110
111      // Status and state updates
112      console.log("File upload created and enqueued.");
113      this.notifySuccess();
114      this.resetInputs();
115    } catch (error) {
116      console.error(error);
117      this.errorMessage = error;
118    } finally {
119      this.uploadingFile = false;
120    }
121  }
122
123  // Create link between new file upload and target record
124  async createContentDocumentLink(recordId, contentDocumentId) {
125    await createRecord({
126      apiName: "ContentDocumentLink",
127      fields: {
128        LinkedEntityId: recordId,
129        ContentDocumentId: contentDocumentId,
130        ShareType: "V",
131      },
132    });
133    console.log("ContentDocumentLink record created.");
134  }
135
136  notifySuccess() {
137    this.dispatchEvent(
138      new ShowToastEvent({
139        title: "Upload Successful",
140        message: "File enqueued for upload.",
141        variant: "success",
142      })
143    );
144  }
145}

For the purpose of explanation, we can divide the implementation into these four sections.

  • Import statements
  • State tracking
  • Simple convenience functions and change handlers
  • The file upload handler

Import Statements

The only thing interesting about the import statements is the API function, createContentDocumentAndVersion. The file upload discussion describes how to use this function.

State Tracking

This component’s state tracking consists of one @api public attribute and five @track internal state attributes.

  • recordId is public and allows the component to receive a record context. This context is used to associate (attach) files that are uploaded to the record from which the component is launched. For example, to attach photos of equipment installed to the Service Appointment record of a technician’s visit.
  • files holds the currently selected local file prior to being uploaded. This variable is used to hold a file locally while the Title and Description are edited. It’s an array so that, with some minor code changes, you can upload multiple files at a time.
  • titleValue and descriptionValue hold the form field values for editing via the form fields.
  • uploadingFile indicates active processing and is used to manage the Upload button’s enabled or disabled state.
  • errorMessage holds messages about any errors that occur when the actual upload is attempted.

Convenience and Handler Functions

  • The fileName getter is used locally only. It’s not relevant to or used for analysis of what to prime, so it’s exempt from the “simple getters only” rule.
  • resetInputs resets the form fields and state after a successful upload.
  • handleFilesInputChange, handleTitleInputChange, and handleDescriptionInputChange each update internal state values, in response to user changes on the form.

The code for each of these handlers is short, simple, and reasonably self-explanatory. They’re common for any LWC that handles user input via a form. We’ll talk about how they’re used in the next section, but we’ll leave these few lines of implementation code for you to read through yourself.

File Upload Handler Functions

The handleUploadClick and createContentDocumentLink functions together perform all of the work to upload a file to Salesforce, and link that file to an associated record. Both functions are defined as asynchronous using the async keyword.

handleUploadClick handles the user interface event (clicking the Upload button), and also creates the file upload. createContentDocumentLink is a utility function that creates the relationship between the file upload and the “owning” record. These functions are fairly different in how they work, so they’re described separately.

handleUploadClick is called when the user clicks the Upload button, which can only happen after they select a local file to be uploaded. It nevertheless begins by checking for a couple of situations where the upload can’t succeed:

  • If an upload is already in progress, don’t start a new one.
  • If there’s no actual file to upload, don’t try to upload a nonexistent file.

The user interface state should prevent these situations by disabling the Upload button when either of those conditions are true. However, a well-written function verifies its inputs. These checks ensure that you don’t cause an error if these important assumptions aren’t correct.

The actual upload processing takes place within a try block because all data mutations have the opportunity to fail, especially when you’re allowing for them to occur while offline. The first part of uploading a file is creating a file upload with a call to the new API function.

1const contentDocumentAndVersion =
2  await createContentDocumentAndVersion({
3    title: this.titleValue,
4    description: this.descriptionValue,
5    fileData: file,
6  });

This one call creates two related records. One record is a ContentDocument representing the file, including the name and description. The second is a related ContentVersion record that holds the file data and represents the current version of the uploaded file.

This one call does a lot of work, including creating the relationship between the two records. While you can achieve the same end result using the createRecord adapter, you can do that only while online. Creating and preserving the relationship between the two isn’t possible using createRecord while offline, mostly due to the complexity of the representation of files in Salesforce. createContentDocumentAndVersion abstracts that complexity, making file uploads as simple as the preceding snippet, which is just one line of code, wrapped for readability.

createContentDocumentAndVersion creates a file upload, but it does not associate that uploaded file with the “owning” record for the record context (if any). After it completes the upload (notice the await keyword before the call), we verify that we have a record context (recordId). If so, call the createContentDocumentLink helper function to create that association, in the form of a ContentDocumentLink record.

If handleUploadClick is exotic for using a mobile only API function, createContentDocumentLink is boring, using createRecord, a staple of LWC code since the framework’s release.

1async createContentDocumentLink(recordId, contentDocumentId) {
2  await createRecord({
3    apiName: "ContentDocumentLink",
4    fields: {
5      LinkedEntityId: recordId,
6      ContentDocumentId: contentDocumentId,
7      ShareType: "V",
8    },
9  });

This code is another one-liner when you unwrap it. The trick is knowing enough about the representation of files and their relationship to other object types in Salesforce. In this case, it’s knowing that ContentDocumentLink represents a relationship between an uploaded file and another record, and knowing which fields to stick the relevant record IDs into.

ShareType: "V" might seem a bit mysterious, but it’s simple and not particularly relevant. It sets the sharing level to view-only. See ContentDocumentLink in the Object Reference for the Salesforce Platform for details.

Tip

Where’s the “Offline” Part?

You just finished looking at it. And it looks a lot like regular LWC code for an online-only mobile feature. The only thing new is the createContentDocumentAndVersion adapter. There’s nothing offline-specific about the code here—it works fine while you’re online, too. The offline details are behind the scenes. Follow the LWC Offline guidelines for optimizing priming, and you’re good to go.