Fetch data from a REST API in an Oracle JET virtual DOM app

Introduction

This tutorial shows you how to access a REST service, integrate it into your Oracle JavaScript Extension Toolkit (Oracle JET) virtual DOM 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 Virtual DOM App

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

  1. Rename jet-virtual-dom-app-temp.zip as JET-Virtual-DOM-app.zip. Extract the contents to the JET-Virtual-DOM-app directory.

  2. Navigate to the JET-Virtual-DOM-app directory, and restore the Oracle JET virtual DOM app.

    npm install
    

    The virtual DOM 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

  1. Navigate to the JET-Virtual-DOM-app/src/components/ directory and open the ParentContainer1.tsx file in an editor.

  2. At the start of the ParentContainer1.tsx file, import the RESTDataProvider module and delete or comment out the import statements for the MutableArrayDataProvider module and the store_data.json file.

    We also import the useMemo hook that we’ll use when creating the RESTDataProvider later.

    import { h } from "preact";
    . . .
    // import MutableArrayDataProvider = require("ojs/ojmutablearraydataprovider");
    // import * as storeData from "text!./store_data.json";
    import { RESTDataProvider } from "ojs/ojrestdataprovider";
    import { useState, useMemo } from "preact/hooks";
    . . .
    
  3. Create a keyattributes variable and a restServerURLActivities variable that references the activity key attribute and the REST endpoint that you’ll pass to the RESTDataProvider instance that you’ll create in the next step.

    let keyAttributes: string = 'id';
    // REST endpoint that returns Activity data
    const restServerURLActivities: string =
      'https://apex.oracle.com/pls/apex/oraclejet/lp/activities/';
    
  4. Create a new activityDataProvider variable that references the RESTDataProvider module and delete or comment out the preexisting activityDataProvider variable that referenced the MutableArrayDataProvider module.

    We create the new activityDataProvider variable within the ParentContainer1 function and we wrap it within a useMemo hook to ensure that the data provider instance is re-created only if the data in the data provider actually changes.

    const ParentContainer1 = () => {
    
    const activityDataProvider = useMemo(() => new RESTDataProvider<Activity["id"], Activity>({
       keyAttributes: keyAttributes,
       url: 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 that extracts data and other properties from the endpoint response body 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.

  5. Save the ParentContainer1.tsx file.

    Your ParentContainer1.tsx file should look similar to ParentContainer1-a.tsx.txt.

  6. Navigate to the JET-Virtual-DOM-app/src/components/Activity directory and open the ActivityContainer.tsx file in an editor.

  7. At the start of the ActivityContainer.tsx file, import the RESTDataProvider module and comment out or delete the import statement for the MutableArrayDataProvider module.

    import { h, ComponentProps } from "preact";
    . . .
    // import MutableArrayDataProvider = require("ojs/ojmutablearraydataprovider");
    import { RESTDataProvider } from "ojs/ojrestdataprovider";
    . . .
    
    
  8. In the Props type alias, modify the optional data property to reference the RESTDataProvider type instead of the preexisting type MutableArrayDataProvider<Activity["id"], Activity>.

    type Props = {
       data?: RESTDataProvider<Activity["id"], Activity>;
       // data?: MutableArrayDataProvider<Activity["id"], Activity>;
    . . .
    };
    
  9. Save the ActivityContainer.tsx file.

    Your ActivityContainer.tsx file should look similar to ActivityContainer.tsx.txt.

Task 4: 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. In the ParentContainer1.tsx file from the JET-Virtual-DOM-app/src/components/ directory, import the useRef hook from Preact.

    . . .
    import { RESTDataProvider } from "ojs/ojrestdataprovider";
    import { useState, useMemo, useRef } from "preact/hooks";
    . . .
    
  2. In the activityDataProvider variable that references RESTDataProvider add the error option and a reference to the callback function that it invokes (fetchErrorHandler).

    const ParentContainer1 = () => {
    
       const activityDataProvider = useMemo(() => new RESTDataProvider<Activity["id"], Activity>({
          keyAttributes: keyAttributes,
          url: restServerURLActivities,
          error: fetchErrorHandler,
          transforms: {
          . . .
    
  3. Before the activityDataProvider variable, add the code for fetchErrorHandler and the hooks (useState and useRef) that we use to determine if the attempt to fetch data succeeded.

    . . .
    const ParentContainer1 = () => {
    
    const [fetchStatus, setFetchStatus] = useState(true);
    const fetchError = useRef<string>();
    
    const fetchErrorHandler = (errorDetail: RESTDataProvider.FetchErrorDetail<number, Activity> |
                                            RESTDataProvider.FetchResponseErrorDetail<number, Activity>) => {
       setFetchStatus(false);
       if (errorDetail.hasOwnProperty('response')) {
          fetchError.current = `${(errorDetail as RESTDataProvider.FetchResponseErrorDetail<number, Activity>).response.status}`;
       }
       else {
          fetchError.current = (errorDetail as RESTDataProvider.FetchErrorDetail<number, Activity>).error.message;
       }
    }
    
    const activityDataProvider = new RESTDataProvider<Activity["id"], Activity>({
    . . .
    
  4. In the return statement at the end of the the ParentContainer1.tsx file, add a check that determines whether you display the list of activities or a message in the case where the attempt to fetch data failed.

    . . .
    return (
       <div>
          {fetchStatus ? (
          <div id="parentContainer1" class="oj-flex oj-flex-init">
             <ActivityContainer data={activityDataProvider} onActivityChanged={activityChangedHandler} />
             {showActivityItems() && (<ParentContainer2 activity={selectedActivity} />)}
             {!showActivityItems() && (<h4 class="oj-typography-subheading-sm">Select activity to view items</h4>)}
          </div>) :
          (<p>Sorry that we couldn't get your product information right now. Please contact your system administrator.</p>
          )}
       </div>
    );
    };
    
    export default ParentContainer1;
    
  5. Save the ParentContainer1.tsx file.

    Your ParentContainer1.tsx file should look similar to ParentContainer1-b.tsx.txt.

Task 5: Create a Data Provider to Fetch Item Data

Use another RESTDataProvider instance to fetch a subset of the data, the list of items for a particular activity. You do this by supplying a new URL that contains the selected activity ID.

  1. Navigate to the JET-Virtual-DOM-app/src/components/ directory and open the ParentContainer2.tsx file in an editor.

  2. At the start of the ParentContainer2.tsx file, import the RESTDataProvider module and delete or comment out the import statements for the MutableArrayDataProvider module and the store_data.json file. Also import the TextFilter interface that we’ll use when we enable the filtering capability in the RESTDataProvider instance that we create.

    import { h } from "preact";
    . . .
    import { RESTDataProvider } from "ojs/ojrestdataprovider";
    import { TextFilter } from "ojs/ojdataprovider";
    
    // import MutableArrayDataProvider = require("ojs/ojmutablearraydataprovider");
    // import * as storeData from "text!./store_data.json";
    . . .
    
    
  3. After the Item type alias, create a baseServiceUrl variable to reference the REST endpoint that you’ll pass to the instance of the RESTDataProvider that you’ll create in the next step.

    type Item = {
       . . .
     };
    
    const baseServiceUrl =
      "https://apex.oracle.com/pls/apex/oraclejet/lp/activities/";
    
  4. Create an initial instance of the RESTDataProvider that you’ll pass in subsequent steps to the useState and useEffect Preact hooks.

    const baseServiceUrl = 'https://apex.oracle.com/pls/apex/oraclejet/lp/activities/';
    
    let INIT_DATAPROVIDER = new RESTDataProvider<ActivityItem['id'], ActivityItem>({
    keyAttributes: 'id',
    url: baseServiceUrl,
    transforms: {
       fetchFirst: {
          request: null!,
          response: (): any => {
          return { data: [] };
          },
       },
    },
    });
    
  5. Comment out or delete the pre-existing code that created a variable to read data from the store_data.json file and that created an initial instance of the MutableArrayDataProvider.

    // const activityData = JSON.parse(storeData);
    // let activityItemsArray = activityData[0].items;
    
    // // Create data provider instance for the array of activity items for the selected activity
    // const INIT_DATAPROVIDER = new MutableArrayDataProvider<ActivityItem["id"], ActivityItem>(activityItemsArray, {
    //   keyAttributes: "id",
    // })
    
  6. In the ParentContainer2 function, replace the existing useEffect hook that manages the instance of MutableArrayDataProvider with a new definition that creates a RESTDataProvider for the activity items that corresponds to the selected activity ID. This new definition also includes a text filter to filter on the activity item name field.

    const ParentContainer2 = (props: Props) => {
    . . .
    useEffect(() => {
       setactivityItemDP(
          new RESTDataProvider<ActivityItem["id"], ActivityItem>({
          keyAttributes: "id",
          capabilities: {
             filter: {
                textFilter: true,
             },
          },
          url: baseServiceUrl + "/" + props.activity?.id + "/items/",
          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 };
                },
             },
          },
          })
       );
    }, [props.activity]);
    
    return (
    . . .
    
  7. Save the ParentContainer2.tsx file.

    Your ParentContainer2.tsx file should look similar to ParentContainer2.tsx.txt.

  8. Navigate to the JET-Virtual-DOM-app/src/components/ActivityItem directory and open the ActivityItemContainer.tsx file in an editor.

  9. At the start of the ActivityItemContainer.tsx file, import the RESTDataProvider module and comment or delete the import statement for the MutableArrayDataProvider module.

    import { h, ComponentProps } from "preact";
    . . .
    // import MutableArrayDataProvider = require("ojs/ojmutablearraydataprovider");
    import { RESTDataProvider } from "ojs/ojrestdataprovider";
    . . .
    
  10. In the Props type alias, modify the data property to reference the RESTDataProvider type instead of the preexisting type MutableArrayDataProvider<Activity["id"], Activity>.

    type Props = {
      // data?: MutableArrayDataProvider<ActivityItem["id"], ActivityItem>;
      data?: RESTDataProvider<ActivityItem['id'], ActivityItem>;
      selectedActivity: Item | null;
      onItemChanged: (item: Item) => void;
    };
    
  11. Save the ActivityItemContainer.tsx file.

    Your ActivityItemContainer.tsx file should look similar to ActivityItemContainer.tsx.txt.

Task 6: Test the Virtual DOM App

  1. In the terminal window, change to the JET-Virtual-DOM-app directory and run the virtual DOM app.

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

    Fetched Records screen

    Description of the illustration fetch_records.png

  3. Close the browser window or tab that displays your running virtual DOM 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 virtual DOM app using the following additional command-line arguments.

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

    On this occasion, the virtual DOM app displays the following message because the REST service that it trys 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.