X

Technical Articles relating to Oracle Development Tools and Frameworks

Uploading and Reading a Text File in Visual Builder

Duncan Mills
Architect

Introduction

A question came up the the Oracle Visual Builder Forum yesterday about how to upload and read a CSV file into an Visual Builder application.  Rather than just answering the question there I though that it would be useful to create a more permanent blog entry as it raises an important use case - that of dealing with module functions that are asynchronous - I'll explain in more detail when we get to the appropriate part what I mean by that. 

The end result that we want to get to is a simple screen like this where the user can upload a file and then just view the contents:

In reality of course you'll probably want to do something more interesting with the data you have uploaded than just echoing it into a text-area, but that's all string parsing and not too interesting, it's the getting of the file and the reading of the data we care about here. 

So let's step through it, starting with a totally empty application. 

Step 1 - Define the basic UI 

The UI is not complex in this case, we just need a file picker and a text area that we'll use to display the CSV content. The text area is simple, you can just drag that onto the design surface, however, in the version of Visual Builder I'm using here (19.1.3) the file picker component is not exposed as an entry in the component pallet, so we have to add it manually.  For more background on adding "hidden" JET components you can refer to the blog Adding New Oracle JET UI Component to Visual Builder Cloud Service.

In this case we just click on the metadata tab of the main-start page in the editor and add the import we need:


{
  "pageModelVersion": "19.1.3",
  "title": "Simple CSV Import Demo",
  "description": "Application showing how to upload and access text data from a file on the users machine",
  "variables": {},
  "types": {},
  "chains": {},
  "eventListeners": {},
  "imports": {
    "components": {
      "oj-file-picker": {
        "path": "ojs/ojfilepicker"
      }
    }
  }
}

Once this is done we can go ahead an define the page, using the picker (by adding it into the HTML code view), and of course just dragging and dropping the other components as needed to create the basic UI. For reference here's the source view of the page pictured above (before any of the event handling or data is wired in):


<div class="oj-flex">
    <div class="oj-flex-item oj-sm-12 oj-md-2">
        <oj-bind-text value="Drag or Upload file here"></oj-bind-text>
    </div>
</div>
<div class="oj-flex">
    <oj-file-picker id="oj-file-picker-1084591017-1"></oj-file-picker>
</div>
<div class="oj-flex">
    <hr id="hr-1084591017-1" class="oj-flex-item oj-sm-12 oj-md-12">
</div>
<div class="oj-flex">
    <oj-label id="oj-label-1084591017-1" for="oj-text-area-1084591017-1" class="oj-flex-item oj-sm-12 oj-md-2">File Content</oj-label>
</div>
<div class="oj-flex">
    <oj-text-area id="oj-text-area-1084591017-1" class="oj-flex-item oj-sm-12 oj-md-12" placeholder="Waiting for data" rows="20"></oj-text-area>
</div>

Step 2 - Configure the File Picker

In this case we want to file picker to accept just a single file and constrain the type that the file picker shows to CSV only. This can all be done through the property inspector when the file picker is selected:

Note how the accept property is configured with a JSON array with just one member in it. You can of course list multiple mime-types here as part of that array if you want more that one file type to be selectable (or you can omit the accept property all together if you want to be able to upload any file.

Step 3 - Create the Data Variable

Next just create a basic string variable in the page to hold the content that is read from the CSV file. As I mentioned, normally you'll want to do more that just dump the contents into a single variable - you might want to parse it into an array of objects to display in a table for example.

This is just a standard VB variable definition, and I'll bind it to the data property of the text area to display the final result.

Step 4 - Create a Page Function

Before we create the action chain that will handle the upload, create a stub page function that will be responsible for the basic parsing of the uploaded file. I'll fill out the implementation of this in a following step, so for now this will be essentially empty but enough to test the basics. From the main-start page, choose the JS (functions) side tab to open the page module JS file and create the stub as follows:


define([], function() {
  'use strict';

  var PageModule = function PageModule() {};
  
  PageModule.prototype.parseUploadedFile = function(fileSet){
    return "Not the real data";
  }

  return PageModule;
});

So this creates a function called parseUploadedFile that takes a single parameter (will be for an array of file objects) and returns a (dummy) string value

Step 5 - Create a Action Chain to Handle the File Upload

When using the JET File picker, you get an ojSelect event raised when the user has selected a file. To wire this up, we switch back to the Designer view on the main-start page and select the file picker component on the page. Click the Events tab in the property inspector and press the New Event button, selecting New Custom Event. On the event picker screen, expand File Picker Events and select the ojSelect event as shown here:

 On the following Select Action Chain screen, press the New Action Chain button. This will create a new chain for you with a name something like FilePickerSelectChain and open it in the editor. This action chain will be automatically set up with a chain variable called detail which will contain the information about the selected file that we need.

Next drag a Call Module Function action onto the chain and select the parseUploadedFile function that was created in Step 4. Using the property inspector, set the return type for the function to String and map the fileSet input parameter to the expression: $chain.variables.detail.files

Next drag on an Assign Variables action to follow on from the function call action and map the results of the function to the csvData page variable created in Step 3 - the expression will be something like: $chain.results.callModuleFunction1

Now run the page and select a CSV file in the picker, what should happen is that the Text Area that you bound to the csvData variable should update to read "Not the real data". With this we've established the basic wiring, now we need to move onto actually reading the real data.

Step 6 - File Parsing Function

The file picker has done the hard work and will pass us a handle to one or more files as part of its event payload which we're wired into the parseUploadedFile function via the fileSet parameter. To actually read the files(s) you use the HTML File API which provides various routines for reading both text and binary file types in various ways. The basics of reading a text file using the readAsText() function are not complicated, here for example, is a very basic implementation of the parseUploadedFile function that:

  1. Ensures that there is at least one file to read
  2. Ensures that the file type uploaded is a CSV
  3. Reads the contents of the file into a variable called readCSVData

  PageModule.prototype.parseUploadedFile = function(fileSet){
    var readCSVData;
    if (fileSet.length > 0){
      //Grab the first (and only) file
      var csvFile = fileSet[0];
      //Check it's the correct type
      if (csvFile.type === 'text/csv') {
        //Create a File reader and its onload callback
        var fileReader = new FileReader();
        fileReader.onload = function(fileReadEvent){
          readCSVData = fileReadEvent.target.result;
      };
      fileReader.readAsText(csvFile);        
      }
    }
    return readCSVData;
  }

If you where to run this, however, it would not work, the value copied into the $page.variables.csvData would be undefined. however, if you debugged the function when running  then you would see the readCSVData variable in the function being populated. So what's going on? Well the stumbling block is the fact that the onload() function defined for the fileReader is asynchronous and will actually not be called until after the parseUploadedFile function has ended, so the return value that is copied by the Assign Variable action in the chain will not have been populated yet. 

To implement this correctly, we can take advantage of a feature of page module functions that is explicitly designed to handle this kind of situation. Rather than just returning the result as a string, the function can return a Promise to a result. In such a situation, the action chain will actually wait for the promise to resolve (or error out) before continuing. This allows us to carry out the asynchronous data read and still process the result in a natural way in the action chain. 

Here's the amended version of the code that uses a promise to deliver the result:

  PageModule.prototype.parseUploadedFile = function(fileSet){
    var readDataPromise = new Promise(function(resolve) {
      if (fileSet.length > 0) {
        //Grab the first (and only) file
        var csvFile = fileSet[0];
        //Check it's the correct type
        if (csvFile.type === 'text/csv') {
          //Create a File reader and its onload callback
          var fileReader = new FileReader();
          fileReader.onload = function(fileReadEvent) {
            var readCSVData = fileReadEvent.target.result;
            resolve(readCSVData);
          };
          fileReader.readAsText(csvFile);
        }
      }
    });
    return readDataPromise;
  }

So in this version you can see that the return statement for the function returns the readDataPromise as a whole, then inside the onload() callback the resolve() call is made, passsing back the data read from the file.  Its this data passed through resolve() that will then be used as the "result" of the method call action and the action chain will wait for it to be called. 

Wrap Up 

This has been a simple illustration of how to use the file picker in Visual builder and of course your implementations may be more complex and have to handle things such as reading the input file in chunks, or parsing the contents into a structured array.  However, the core skill you will have now acquired is an understanding of how  a promise can be used to work in these scenarios that involve asynchronous callbacks.

Join the discussion

Comments ( 1 )
  • Gunther Thielemann Thursday, May 2, 2019
    Many thanks for the interesting article. The advice to use Promise at this point was very helpful.
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha