The ListView component is an effective tool if you’re looking to manipulate relational data using specific components for each row. While options are really endless for utilizing the ListView, in this tutorial we’ll be looking at how to use one to create a dynamic document uploader, giving you the option to preview, rename files and save them to an Amazon s3 bucket. We’ll also be explaining some of the basics around the concept of the magical ‘i’, and how to use it when developing ListView elements in your apps.
Pros and cons of tables vs. ListViews
There are many elements of ListViews that are shared with table components, and there are a couple of reasons why you might choose either. The most important is that ListViews give you the chance to use all kinds of different components within them, from images to upload buttons to pdf viewers and more - while the table component relegates you to supported column types.
In general, ListViews are useful when the workflow includes engaging with a row of data multiple times, i.e. a table is great for single-click actions, but if you need to enact several processes on a single row of data, a ListView is probably better. ListViews are also better if you want to customize the layout and UI of each row.
A con of ListViews, however, can sometimes be the presentation of your data, as with several components per row, things can quickly become clunky. So, if you are looking for a way to neatly present and evaluate structured data, a ListView might not be it. You could opt for a table with a side panel instead.
How the ListView works: the magic "i"
ListViews are basically repeating containers. You drag the components you want to exist in each row, and then use JavaScript to populate those components with the content you want. The ListView exposes an index variable – i – that refers to the current row, and that's the magic.
Although the implementation of ‘i’ can often be a little confusing, the fundamentals are relatively simple: [i] is the index of the row, or ‘instance’ in which you are performing an action: the position in which the action has been taken. So essentially, when coding inside of the ListView component, ‘i’ is a dynamic value which gives a kind of unique ID to each instance of your ListView, a little like a unique identifier in a dataset, or the ‘currentRow’/’selectedRow’ functions of a table component.
To put this into context, let’s say you have a couple of textInput components in your ListView, followed by a button in the same row which triggers your ‘save changes’ query. The query that would perform this ‘save changes’ would update the data according to the values entered into the textInput components in ListView row ‘i’, and this ‘i’ is determined by the row in which the button was clicked.
Here’s an example of how part of a query like this might look:
This short reference is simply telling the query to upload the base64PDF binary code for the pdfViewer in the triggered row of the ListView. As there are multiple “instances” of the pdfViewer component in our ListView, using “i” lets the query know which one’s data to use.
Where do I put the ‘i’ when inside of the ListView & Out?
The potentially confusing part of the “i” variable is where it is and isn’t defined.
Inside a ListView Component
Inside of a ListView, we have access to the index of each row.
To illustrate this with an example: if we want each row of our list view to display a pdf from an array of PDFs uploaded to a filepicker, we can use the index (i) of the list view to index into the array of the PDFUploader component, where multiple PDFs are stored. Using this method, the uploaded file in the position of index “i” of the ListView is displayed in the ListView in this position (after .value). So, in theory, if we upload three documents, document [0] will appear in the first row [0] of the list view, document [1] in row [1] and so on. The below example shows the reference we would use inside a PDF viewer component that is placed inside of a ListView:
Outside the ListView Component
When outside of the ListView, [i] represents the index of the ListView row in which an action was triggered. Since the ListView repeats components, each internal ListView component becomes an array outside of the ListView. In the example below in which data is uploaded from pdfViewer1, the PDFViewer exists for each row in the ListView (which means it's an array). We then need to reference [i], or the index of the triggered row, to index into the data specific to the triggered query.
File Input + ListView components
So to make our file uploader app, we are going to be using a File Input to upload one or more pdfs which auto-populate a ListView. This ListView then allows users to categorize the file type, name the file, and add a date to help organize the filing system, which we have set up through an AWS S3 bucket. When you’re done, it should look like this:
Using the ListView component as opposed to a table here is particularly helpful, as it allows for a pdf viewer component to show a preview of the document (useful for checking you’ve uploaded the right thing) and have several inputs with a submit button that allows for a dynamic, somewhat compartmentalized process for uploading and categorizing files. It’s also a great way to include data validation to ensure everything is completed correctly before upload (something that is currently unavailable inside the table component).
First things first, you need to pull in a File Input component, as well as a ListView and set it up with a couple of inputs, a pdf viewer and a button, according to your needs. For ours, we included the pdf viewer on the left, a select/dropdown component to select a document type, a ‘received from’ text input to allow users to enter a name and a ‘date received’ input (Date Time component) to help with file naming.
Once you’ve inserted these components, it is worth adjusting the height of each ListView row in the ListView component settings to reflect the space they need. In this case, we entered ‘5’ as our height, but you can also set dynamic row heights.
The next useful step is to set the ListView length to be dynamic, according to the number of files uploaded. This is a simple line of code that looks like this:
While we’re using the state of a Retool component (the filepicker) to populate our ListView,, we can also populate a ListView from a query’s data. If our query had a nested JSON documents attribute or the like, we could set the length dynamically by referencing the query and drilling down to the attribute followed by ‘.length’.
Setting up your FileInput
The next step is setting up your File Input to pull PDFs into your ListView. Here, we set the file type to be [‘pdf’] and thus accept pdfs only - you can set other documents to be accepted, or leave it blank to accept all types, but in this tutorial we are looking for the document type to be validated before upload. Next set the selection type to ‘Multiple Files’, and then set the label and prefix text as below.
Setting up your ListView Components (PDF viewer, select, text input)
Next up are the components in your ListView. The PDF viewer simply requires a short reference in the Base64-encoded PDF value box, which pulls out the relevant PDF base64 according to the row (‘i’):
As we explained above, in this case the [i] goes at the end to refer to the current row of the ListView. This is because it is selecting an object from an array outside of the ListView, according to the ListView row index.
For the select component, we simply hard-coded some options, which come into play in the file name when saving:
For the Last Name text input we simply added labels and a placeholder:
And for the date dropdown we only changed the label and formatted the date string as such:
Setting up your Submit Button and Query
First things first, you’ll need to set up a connection to an S3 bucket. You can follow the detailed Retool documentation to do this.
Once you’re done setting up your S3 resource, set up a query in your app with this resource connected and set it to ‘only run when manually triggered’. Enter the bucket name, and in the case of PDFs ‘binary’ into the S3 Content-Type box.
These first steps should look something like this:
Next, you can start building your dynamic file name using the input components in your ListView. If you are looking to generate a UUID for the file you could use the nifty UUID generator that Retool has pre-installed: {{uuid.v4}}. Here’s how ours looked:
{{select_documentType[i].value}}/{{dateTime_dateReceived[i].formattedString}}_
{{textInput_receivedFrom[i].value}}.pdf
The first reference in the query is to the document type in the select component. As these values are all found within the ListView and being referenced from outside, you will place the [i] index directly after the component name. Anything you put before a ‘/’ will sort the document into this folder, or create a new one if it doesn’t exist, so the first part of this value above sorts the documents into the folder according to their document type.
After the slash, the second reference enters the date received and formats it as a string so it can be saved as a file name. After the underscore (which you can use to separate elements in a file name), we have the ‘received from’ name, and ‘.pdf’ on the end so that the document type is recognized.
Now, your file name should look something like: 09.09.21_Smith.pdf, and should be saved into the relevant folder.
Next, you need to select the data you will upload. In this case, it will be the base64 binary code for your pdf. It’s easy here to link it to the pdf viewer, which has already parsed the base64, like below:
Or you can do it in a slightly different way and link it to your file uploader instead:
Note the slight difference in [i] positioning. In the first case, we are referencing the pdf viewer inside the ListView, to return the value of the pdfViewer in row [i]. In the second example, we are referring to the fileuploader outside of the ListView, which will return an array of values, so the [i] comes at the end to indicate the file in position [i] of the array.
All that is left is to add to this query is your ‘Submit’ button event handlers. This can look as simple as this:
With that, you have a fully functioning app - where you can upload PDFs into your Retool app, preview them, set some naming conventions, and upload them to your S3 bucket!
While this app will work, we do recommend following the next steps to create some simple data validation to make sure uploads are always completed properly, as well as setting some notifications to signal to your user why something may not be working quite right. Keep reading for more!
Data Validation and Responsive Notifications
One simple way to validate the submission to the S3 bucket is via the ‘only run when’ value of the click event handler in the upload button we’ve created. The below code will help prevent submissions if any fields are empty:
This code is using the _.isEmpty lodash function to check if the component in parentheses is (you guessed it) empty or not - since we want to only run it when it is not empty, we need to add the exclamation point to invert the meaning. Essentially, the code means: Only run when: [component] is[not]Empty. The [i] follows the component name since the button is in the same row as the component. The double ampersands (&&) will also join conditions.
Now, while this is enough to stop people from being able to submit files when the fields are empty, it doesn’t tell them why it won’t submit, nor does it indicate that the query hasn’t actually run.
So, to tell our users why their button isn’t working, we are going to add some notification event handlers, with specific information.
For a simple option, you could just add a notification to indicate success and failure, but we actually want to be a bit more technical and explain the problem. For this, we need three event handlers, one per input component.
The first looks like this:
Set the handler to run on click and ‘show notification’, then add your dynamic title: we added the code {{i+1}} to indicate more clearly which row has the issue, since indexes start at 0.
Finally, in the ‘only run when’ box, enter code similar to the above data validation, but this time we want to run the handler when the component ‘select_documentType’ in row [i] is empty.
Do the same for your other components, simply changing out the component. Don’t forget that for the date you need to add ‘.formattedString’.
Finally, go ahead and add a ‘success’ handler to your submitFile S3 query, so that when the query has been successfully fired, the user knows. For this, we also added the file name for clarity’s sake:
This is pulling the final file name in from the query itself, and displaying the message for 15 seconds to allow the user to verify their action.
For our final trick, we are going to disable the submit button once the query has been successful. This can get a little complicated in a ListView, given the nature of the repeated components To get around this, we used a sneaky text component to store the state of our submit button (but you could probably do something similar and perhaps more effective inside of a temporary state).
First pull a text component into your ListView and set the value input to be ‘false’. Also, this component should be hidden:
Then, in your ‘submit’ button, set the button to disable when the text value in the same row is actually ‘true’:
Finally, in your submit file query, you will add another success handler, that sets the text value to ‘true’:
Now, when the query fires successfully, it will change the value of the text component to ‘true’, and therefore disable the submit button, and avoid the risk of a second submit. While this is not the most technical approach to the problem - it’s a simple way to create a tempstate-like element within your ListView (and can be useful for other tricky scenarios too).
With that final step, you have used a ListView to create a dynamic PDF uploader, with data validation and specific UI-friendly notifications to guide your user to the end goal.