Full Stack File Management Part II: The Frontend

2021-07-24

javascript
react.js
next.js

Every full stack developer would need to implement file uploads, downloads and access control at some point in their projects. This handy guide explores implementation of all aspects of file management, with part II focusing on UI and client side things.

Every full stack developer will have to implement file uploads, downloads and access control at some point. On the client side you want to fetch, upload and delete access to files, based on access, in a system dealing with files.

In this article, we shall cover how to handle the client side stuff for file uploads and downloads. In Part I of this guide, we covered how to model the data and implement the backend for file management. This is targeted at an intermediate level front-end developer audience, A prior knowledge of Javascript, HTML/CSS and React is expected.

Part II: The Frontend

On the front-end side of things, we'd want a control that displays files, loads them whenever accessed and allows file deletion. We would also need a control to let the users upload the said files.

Using react, we're going to create reusable components for both of the above, starting with a file display container. We will be following the file meta structure covered in Part I of this tutorial.

Setting up File Display

If we're displaying file links inline, we could just display the file name and link it to the file. If we take a more tabular or card approach, we should display a little more information about the file itself. Based on our example file data model, they could be:

  • file name
  • mime type (best displayed with an icon)
  • file size
  • creation date
  • uploader
  • type of access

Let's create a component that takes in these properties. We'll feed it stuff

Where file is the file object following the schema, containing { name, path, size, mime, date, desc, uploader, access, etc. }. Ideally it'd be type-checked with typescript, but we're keeping this example simple.

Properties can also be passed directly to the component, this is especially useful when using the component with the FileUpload control, as we don't have a formed file object.

Properties small and inline change how the control is rendered.

File display component variants
File display component variants

Accessing and Removing Files

Now we want to add a utility function that opens up the file in a new tab. For buttons and links we can just refer it to the href tag, but for customized controls we'd have to create a function to map to the onClick event.

Since we're generating the access links to the bucket on request, we're actually calling our API instead of the bucket URL.

We also want to optionally support deletion within this component. This will be done by using the onRemove prop, which, if present will allow the component to support file deletion. Let's add a helper function to delete the file.

First we'll need a state variable for UI updates while deleting

Now for the file delete helper function, we'll call our delete endpoint which removes the file from the bucket and triggers the callback to be handled by the parent component.

Next, we need some icons to display based on the file and access type. We can create a utility function to return us the icon from a list of preset icons (e.g. FontAwesome) and map it to the mime value. E.g. an application/pdf mime should have a pdf file icon, images should have an image icon, etc. Similarly, icons for the access note can be based off private, group, organization and public → user, users, building and globe icons.

Display Component Variants

Let's setup our renderer for the variants of the component.

File Variants Loki
File Variants Loki

If inline, just return a standard link.

Else we'll render the component in either full or small variants.

Where getFileSizeTextFromSize is a utility to get readable sizes from bytes.

And formatDateTime returns formatted locale time from an ISO date.

Modal launches a confirmation dialog, which performs an action on button click.

That should be enough to setup a component capable of displaying and removing files. Now we have to allow the users a way to upload files. Let's make a new component for that (yay! /s)

User dropping files on the client
User dropping files on the client

Accepting Uploads

Again, there's various ways of achieving this. But in essence, a reusable FileUpload component should have the following features:

  • Accept user files using native file picker modals
  • Accept user dropping files into a container
  • Accepting selective files (Not all files, not all sizes, depending on the application requirements)
  • Upload files to the server
  • Display uploaded files and uploading states
  • Support multiple files
  • Support file deletion

With these in mind, we will be building a simple select/drop solution that can upload files to the bucket (by getting an access URL from the server, as described in Part I).

We'll build a component that accepts a single file, display uploading, error, and invalid states.

Let's start by defining states in an "enum".

Setting up FileUpload

Let's define our FileUpload component and the properties it can take up. Our component is going to look and function like:

Where title, icon and subtitle define what's displayed on the control.

disabled disables the control whenever required by the parent (e.g. loading data, etc.).

uploadPath = ['path', 'to', 'location'] determines where the file must go into the bucket. We're following the scheme discussed in the backend for this, where, for example, ['account', 'profile'] means the file will be uploaded to /organization_id/account/profile.

onChange and onRemove are callbacks called when a file upload or delete is successful. Error and intermediate states are handled by the component.

allowedMimeTypes = [] contains a list of MIME (document) types supported by this control. DEFAULT_MIME_TYPES is filled with document types such as application/pdf, application/msword, text/plain, etc.

maxSize (bytes) determines the maximum size of files accepted by this control.

dropProps, containerProps and fileContainerProps contain additional props passed down to the containers or drops.

Upload States and Functions

Great, now that definitions are ready, let's setup state variables. We'll use one status variable to determine the status of file in the control. A separate dropStatus to control the status of the drop box (the part where the files are dropped into), A file variable to store the information of the selected file, and one error variable to store and display error information.

Let's define the function uploading the file cloud. Note that this component only accepts a single file at a time. The javascript callback for file selection emits a list of files.

Once we've established a single file is dropped on to the control, let's validate the file for mime types and size. This can also be done during the dragging callbacks, but we also need to support native modal inputs.

Next, we update the file object and the status variable.

Now we just need to request an bucket upload URL from our server.

And another helper to handle file deletions (when the current uploaded file is removed).

Great, now that files are ready to be tackled, let's code in the methods to support dragging and dropping.

Handling Drops Inside a Container

To support dropping into the container, we need to implement four methods for the event types dragEnter, dragOver, dragLeave and drop catering to when the file is dragged into, over, outside, and dropped onto the container.

onDragOver is overridden to prevent browser to overriding drag over events.

onDragEnter is called when a mouse with a draggable item enters the drop context.

And we need to reset the UI states when a draggable item exits the container. onDragLeave is the event called in this process.

Now for handling drops within the container, we implement the onDrop method.

Now, we just need to setup the UI for this control.

FileUpload Interface

Since we're only allowing one file per instance of this component, we only need to display the drop container when the status is NOFILES or ERROR. Our drop box should look like this:

'File upload component states'
'File upload component states'

Without delving deep into the CSS and all, what we want to do is change border colors depending on the drop status, handle component wide disabling, implement drag and drop methods, and it's onClick counterpart. So, something like

Where fileInputRef is a ref to a hidden input to handle native file modals onClick.

If the file is in the uploading state, we can use the file display component in disabled form with a spinner. Remember, our display component also handled deletion.

Finally, we can display upload errors either in a modal or as a text.

And that should be it for a reactive interface of file uploa...

Wait, how do I do multiple files?

Ah, yes, right. To implement file uploads for multiple files, we can either handle drop and selection for multiple files at once, or re-use this component in a wrapper capable of handling multiple files - Something that displays the upload control again once a file has been successfully uploaded and emits an Array to the parent component. You could also update the backend to support link generation for multiple files at once, or use a different technique all-together, but that's beyond the scope of this tutorial.

Conclusion

Welp, this should cover some aspects of how to display files, design and implement file uploads, and

Wew, this was a long tutorial, way exhausting than I'd thought it'd be. Thank you for reaching the end. Hope this has helped somehow.

© avikantz, 2024