Fetch data from a REST API in an Oracle JET web app

Introduction

This tutorial shows you how to access a REST service, integrate it into your Oracle JavaScript Extension Toolkit (Oracle JET) web app, and bind data to a list view in your user interface.

Objectives

In this tutorial, you will learn how to create instances of the RESTDataProvider class. This class represents data available from JSON-based REST services.

Prerequisites

Task 1: Download the Starter App

Skip this task if you’re continuing to work in an app that you created in the previous learning path.

  1. Rename jet_web_application_temp.zip as JET_Web_Application.zip. Extract the contents to the JET_Web_Application folder.

  2. Navigate to the JET_Web_Application folder, and restore the Oracle JET web app.

    npm install
    

    The app is ready to use.

Task 2: Access the REST Service

Click the Apex link to view the REST data for the Activities resource endpoint.

The data contains a list of activities with various attributes.

{
  "items": [
      {
      "id": 1,
      "name": "Baseball",
      "short_desc": "Equipment we carry for baseball players.",
      "image": "css/images/product_images/baseball.jpg"
      },
   . . .
   ],
   "hasMore": false,
   "limit": 25,
   "offset": 0,
   "count": 4,
   "links": [
      {
      "rel": "self",
      "href": "https://apex.oracle.com/pls/apex/oraclejet/lp/activities/"
      },
      . . .
   ]
}

Familiarize yourself with the data and the properties that the endpoint returns. You’ll need to understand these details when you create an instance of RESTDataProvider later in this tutorial. Note, for example, how the endpoint returns an items property that references a series of individual activities.

Task 3: Create a Data Provider to Fetch Activity Data in the ViewModel

  1. Navigate to the JET_Web_Application/src/ts/viewModels directory and open the dashboard.ts file in an editor.

  2. At the start of the dashboard.ts file, import the RESTDataProvider class.

    import * as AccUtils from "../accUtils";
    . . .
    import { RESTDataProvider } from "ojs/ojrestdataprovider";
    . . .
    type Activity = {
       id: number;
    };
    . . .
    type ActivityItems = {
       id: number;
       name: string;
       items: Array<Item>;
       short_desc: string;
       image: string;
    };
    
    class DashboardViewModel {
    . . .
    

    Note: If you did not complete the previous learning path in this series, you also need to declare a type alias for Activity, which is the type that the activityDataProvider instance in the next step requires. You specify this type alias before the DashboardViewModel class declaration.

  3. In the DashboardViewModel class, declare the following variables and comment out or delete the preexisting activityDataProvider of type MutableArrayDataProvider<Activity["id"], Activity>.

    . . .
    class DashboardViewModel {
    
     // Activity key attribute that you'll pass as a parameter when creating
     // RESTDataProvider instance
     keyAttributes = "id";
     // REST endpoint that returns Activity data
     restServerURLActivities = "https://apex.oracle.com/pls/apex/oraclejet/lp/activities/";
     // RESTDataProvider instance
     activityDataProvider: RESTDataProvider<Activity["id"], Activity>;
     // MutableArraryDataProvider that we no longer use, and
     // so we comment it out.
     // activityDataProvider: MutableArrayDataProvider<Activity["id"], Activity>;
    
  4. In the constructor() method of the DashboardViewModel class, comment out or delete the preexisting activityDataProvider variable from the MutableArrayDataProvider class and create the RESTDataProvider instance.

    . . .
    class DashboardViewModel {
    . . .
    restServerURLActivities = "https://apex.oracle.com/pls/apex/oraclejet/lp/activities/";
    activityDataProvider: RESTDataProvider<Activity["id"], Activity>;
       . . .
    
       constructor() {
       . . .
       // this.activityDataProvider = new MutableArrayDataProvider<Activity["id"],
       //                      Activity>(JSON.parse(storeData), {
       //   keyAttributes: "id",
       // });
    
       this.activityDataProvider = new RESTDataProvider({
       keyAttributes: this.keyAttributes,
       url: this.restServerURLActivities,
       transforms: {
             fetchFirst: {
                   request: async (options) => {
                      const url = new URL(options.url);
                      const { size, offset } = options.fetchParameters;
                      url.searchParams.set("limit", String(size));
                      url.searchParams.set("offset", String(offset));
                      return new Request(url.href);
                   },
                   response: async ({ body }) => {
                      const { items, totalSize, hasMore } = body;
                      return { data: items, totalSize, hasMore };
                   },
                },
             },
       });
    . . .
    

    Note: The response function above extracts data and other properties from the endpoint response body and must return an object with a data property. Given that the endpoint we work with returns an items property, we assign this latter property to data in the response function.

Task 4: Create a Data Provider to Fetch Item Data in the ViewModel

You use another RESTDataProvider instance to fetch a subset of the data that is the list of items for a particular activity. You do this by supplying a new URL that contains the selected activity ID. You’ll create an initial placeholder RESTDataProvider instance to use until such time as a user selects an activity, at which point you’ll create the actual RESTDataProvider instance in the selectedActivityChanged event listener method.

  1. In the DashboardViewModel class of the open dashboard.ts file, initialize a placeholder instance of the RESTDataProvider for activity items, declare types for the additional variables needed to create the REST endpoint to return activity item data, and comment out or delete both the itemsDataProvider declaration of type MutableArrayDataProvider<Item["id"], Item> and the itemsDataProvider variable itself in the constructor() method.

    class DashboardViewModel {
    
       // Activity key attribute that you'll pass as a parameter when creating
       // RESTDataProvider instance
       keyAttributes = "id";
       // REST endpoint that returns Activity data
       restServerURLActivities = "https://apex.oracle.com/pls/apex/oraclejet/lp/activities/";
       // RESTDataProvider instance
       activityDataProvider: RESTDataProvider<Activity["id"], Activity>;
    
       // Initialize activityKey to 3 to construct an initial REST call
       activityKey: number = 3;
    
       // Initialize placeholder RESTDataProvider
       itemsDataProvider: RESTDataProvider<Item["id"], Item> = new RESTDataProvider<
          Item["id"],
          Item
       >({
          keyAttributes: "id",
          url: "",
          transforms: {
                fetchFirst: {
                   request: null!,
                   response: (): any => {
                      return { data: [] };
                   },
                },
          },
       });
       // REST endpoint that returns Item data
       restServerURLItems =
          "https://apex.oracle.com/pls/apex/oraclejet/lp/activities/" +
          this.activityKey +
          "/items/";
    
       constructor() {
          . . .
          // this.itemsDataProvider = new MutableArrayDataProvider<Item["id"], Item>(
          //    itemsArray,
          //    { keyAttributes: "id" }
          // );
    
  2. In the selectedActivityChanged method of the DashboardViewModel class, write code to create a new instance of the itemsDataProvider variable with the new activity items when a new activity is selected.

    This itemsDataProvider also includes a text filter to filter on the activity item name field.

    selectedActivityChanged = (
      event: ojListView.firstSelectedItemChanged<ActivityItems['id'], ActivityItems>
    ) => {
      /**
       *  If no items are selected then the firstSelectedItem property  returns an object
       *  with both key and data properties set to null.
       */
      let itemContext = event.detail.value.data;
    
      if (itemContext != null) {
        // If selection, populate and display list
        // Hide currently-selected activity item
        this.activitySelected(false);
    
        this.activityKey = event.detail.value.data.id;
        this.restServerURLItems =
          'https://apex.oracle.com/pls/apex/oraclejet/lp/activities/' + this.activityKey + '/items/';
    
        // Create the itemsDataProvider instance of RESTDataProvider
        this.itemsDataProvider = new RESTDataProvider({
          keyAttributes: this.keyAttributes,
          capabilities: {
            filter: {
              textFilter: true,
            },
          },
          url: this.restServerURLItems,
          textFilterAttributes: ['name'],
          transforms: {
            fetchFirst: {
              request: async (options) => {
                const url = new URL(options.url);
                const { size, offset } = options.fetchParameters;
                url.searchParams.set('limit', String(size));
                url.searchParams.set('offset', String(offset));
                const filterCriterion = options.fetchParameters.filterCriterion as TextFilter<Item>;
                const { textFilterAttributes } = options.fetchOptions;
                if (filterCriterion && filterCriterion.text && textFilterAttributes) {
                  const { text } = filterCriterion;
                  textFilterAttributes.forEach((attribute) => {
                    url.searchParams.set(attribute, text);
                  });
                }
                return new Request(url.href);
              },
              response: async ({ body }) => {
                const { items, totalSize, hasMore } = body;
                return { data: items, totalSize, hasMore };
              },
            },
          },
        });
    
        // Set List View properties
        this.activitySelected(true);
        this.itemSelected(false);
        this.selectedItem();
        this.itemData();
      } else {
        // If deselection, hide list
        this.activitySelected(false);
        this.itemSelected(false);
      }
    };
    
  3. At the start of the dashboard.ts file, import the TextFilter interface.

    import * as AccUtils from "../accUtils";
    . . .
    import { RESTDataProvider } from "ojs/ojrestdataprovider";
    import { TextFilter } from "ojs/ojdataprovider";
    . . .
    
    

    Your file should look similar to final-dashboard-rest-dp-ts.txt.

Task 5: Add an Error Handler to Manage a Failure to Fetch Data

The RESTDataProvider instance provides an error option that you can use to invoke a callback function when an attempt to fetch data fails. You’ll implement this capability for the scenario where an attempt to fetch the list of activities fails.

  1. Navigate to the JET_Web_Application/src/ts/viewModels directory and open the dashboard.ts file in an editor.

  2. In the DashboardViewModel class, before the constructor() method, add an observable for the status of the data fetch, plus a variable to reference an error message.

    class DashboardViewModel {
       . . .
       firstSelectedItem = ko.observable();
    
       dataFetched = ko.observable(true);
       fetchError: string = ('');
    
       constructor() {
       . . .
    
    
  3. In the activityDataProvider variable that references RESTDataProvider, add the error option and a reference to the callback function that it invokes (fetchErrorHandler).

    this.activityDataProvider = new RESTDataProvider({
      keyAttributes: this.keyAttributes,
      url: this.restServerURLActivities,
      error: this.fetchErrorHandler,
      transforms: {
        fetchFirst: {
    . . .
    
  4. After the constructor() method of the DashboardViewModel class, write code for the fetchErrorHandler callback function that the RESTDataProvider invokes if an attempt to fetch data fails.

    . . .
    } // end constructor
    
    fetchErrorHandler = (errorDetail: RESTDataProvider.FetchErrorDetail<number, Activity> |
                                                            RESTDataProvider.FetchResponseErrorDetail<number, Activity>) => {
     this.dataFetched(false);
    if (errorDetail.hasOwnProperty('response')) {
       this.fetchError = `${(errorDetail as RESTDataProvider.FetchResponseErrorDetail<number, Activity>).response.status}`;
    }
    else {
       this.fetchError = (errorDetail as RESTDataProvider.FetchErrorDetail<number, Activity>).error.message;
     }
    }
    . . .
    
  5. Save the dashboard.ts file.

    Your dashboard.ts file should look similar to final-dashboard-error-handler-ts.txt.

  6. Navigate to the JET_Web_Application/src/ts/views directory and open the dashboard.html file in an editor.

  7. Find the div element where id="parentContainer1" and wrap it with an oj-bind-if custom HTML element with the test attribute set to the state of the dataFetched() observable.

    . . .
    <h1>Product Information</h1>
    <oj-bind-if test="[[dataFetched()]]">
      <div id="parentContainer1" class="oj-flex oj-flex-init">
        . . .
      </div>
    </oj-bind-if>
    
  8. Before the last div element in the dashboard.html file add another oj-bind-if custom HTML element with the test attribute set to the state of the dataFetched() observable.

    This newer oj-bind-if custom HTML element wraps the HTML elements to display if the RESTDataProvider failed to fetch data. In our example, we render a <p> HTML element with a short message for the end user.

    . . .
    <oj-bind-if test="[[!dataFetched()]]">
      <p>Sorry that we couldn't get your product information right now. Please contact your system administrator.</p>
     </oj-bind-if>
    </div>
    
  9. Save the dashboard.html file.

    Your dashboard.html file should look similar to final-dashboard-error-handler-html.txt.

Task 6: Test the Web App

  1. In the terminal window, change to the JET_Web_Application directory and run the web app.

    npx ojet serve
    
  2. In the browser window, view the dynamic changes in your web app.

    Fetched Records screen

  3. Close the browser window or tab that displays your running web app.

  4. In the terminal window, press Ctrl+C, and if prompted, enter y to exit the Oracle JET tooling batch job.

  5. In the terminal window, run the web app using the following additional command-line arguments.

    npx ojet serve --server-port=8144 --livereload-port=8145
    

    On this occasion, the web app displays the following message because the REST service that it tries to access only accepts requests on the server port that the ojet serve command uses by default (8000), so the attempt by the RESTDataProvider to fetch from the REST service failed.

    Sorry that we couldn't get your product information right now. Please contact your system administrator.
    

Next Step

To proceed to the next tutorial in this learning path, click here.

More Learning Resources

Explore other labs on docs.oracle.com/learn or access more free learning content on the Oracle Learning YouTube channel. Additionally, visit education.oracle.com/learning-explorer to become an Oracle Learning Explorer.

For product documentation, visit Oracle Help Center.