Many websites include a form for uploading files. For some services, such as OneDrive or Google Drive, file uploading is a core feature. As web pages have become increasingly interactive, users now expect richer interactions, such as dragging and dropping files or directories, or pasting screenshots.
Let's explore what browsers provide for uploading files!
The basic way is by using a form and an input with the file type:
HTML
<form method="post" enctype="multipart/form-data">
<input type="file" name="photo">
<input type="submit" value="Submit">
</form>
You can improve this with two optional attributes:
multiple: allows multiple files to be selectedaccept: choose expected mime types, for instance: image/*, application/pdf
#Drag and drop files
Most users expect to be able to drag and drop files onto the page. This feature is well-supported across browsers and straightforward to implement. Once you have the files, you can upload them to an API or read their content using the FileReader api.
TypeScript
const dropZone = document.body;
if (dropZone) {
const hoverClassName = "hover";
// Handle drag* events to handle style
// Add the css you want when the class "hover" is present
dropZone.addEventListener("dragenter", function (e) {
e.preventDefault();
dropZone.classList.add(hoverClassName);
});
dropZone.addEventListener("dragover", function (e) {
e.preventDefault();
dropZone.classList.add(hoverClassName);
});
dropZone.addEventListener("dragleave", function (e) {
e.preventDefault();
dropZone.classList.remove(hoverClassName);
});
// This is the most important event, the event that gives access to files
dropZone.addEventListener("drop", function (e) {
e.preventDefault();
dropZone.classList.remove(hoverClassName);
const files = Array.from(e.dataTransfer.files);
console.log(files);
// TODO do somethings with files...
});
}
You can easily upload files to an API using the fetch API:
TypeScript
if (files.length > 0) {
const data = new FormData();
for (const file of files) {
data.append('file', file);
}
fetch('/upload', {
method: 'POST',
body: data
})
.then(() => console.log("file uploaded"))
.catch(reason => console.error(reason));
}
#Drag and drop directories
Google Chrome and Microsoft Edge support dropping directories. This is especially useful when uploading files to OneDrive or Google Drive. The API is a bit cumbersome since it uses callbacks with a recursive tree structure, but it is manageable. Let's see some code:
TypeScript
// drag* events are omitted for brevity (get them from the previous section).
dropZone.addEventListener("drop", async function (e) {
e.preventDefault();
dropZone.classList.remove(hoverClassName);
console.log(await getFilesAsync(e));
});
async function getFilesAsync(dataTransfer: DataTransfer) {
const files: File[] = [];
for (let i = 0; i < dataTransfer.items.length; i++) {
const item = dataTransfer.items[i];
if (item.kind === "file") {
if (typeof item.webkitGetAsEntry === "function") {
const entry = item.webkitGetAsEntry();
const entryContent = await readEntryContentAsync(entry);
files.push(...entryContent);
continue;
}
const file = item.getAsFile();
if (file) {
files.push(file);
}
}
}
return files;
}
// Returns a promise with all the files of the directory hierarchy
function readEntryContentAsync(entry: FileSystemEntry) {
return new Promise<File[]>((resolve, reject) => {
let reading = 0;
const contents: File[] = [];
readEntry(entry);
function readEntry(entry: FileSystemEntry) {
if (isFile(entry)) {
reading++;
entry.file(file => {
reading--;
contents.push(file);
if (reading === 0) {
resolve(contents);
}
});
} else if (isDirectory(entry)) {
readReaderContent(entry.createReader());
}
}
function readReaderContent(reader: FileSystemDirectoryReader) {
reading++;
reader.readEntries(function (entries) {
reading--;
for (const entry of entries) {
readEntry(entry);
}
if (reading === 0) {
resolve(contents);
}
});
}
});
}
// for TypeScript typing (type guard function)
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
function isDirectory(entry: FileSystemEntry): entry is FileSystemDirectoryEntry {
return entry.isDirectory;
}
function isFile(entry: FileSystemEntry): entry is FileSystemFileEntry {
return entry.isFile;
}
#Paste file
Currently, you can only paste image data, not image files or other file types. This may change in the future, but for now it is enough to handle pasted screenshots. I often paste images when creating an issue on JIRA or VSTS 😃
Supporting image pasting is a quick win because you can reuse the previous code. Simply handle the paste event and call the getFilesAsync function from above:
TypeScript
dropZone.addEventListener("paste", async function (e) {
e.preventDefault();
const files = await getFilesAsync(e.clipboardData);
console.log(files);
});
You now know how to upload files and directories using every method your browser supports (at the time of writing)!
Do you have a question or a suggestion about this post? Contact me!