5 Use Oracle JET Components and Data Providers

Review the following recommendations to make effective use of Oracle JET components and associated APIs, such as data providers, so that the app that you develop is performant and provides an optimal user experience.

Access Subproperties of Oracle JET Component Properties

JSX does not support the dot notation that allows you to access a subproperty of a component property. You cannot, for example, use the following syntax to access the max or count-by subproperties of the Input Text element’s length property.

<oj-input-text
  length.max={3}
  length.count-by="codeUnit"
</oj-input-text>

To access these subproperties using JSX, first access the element’s top-level property and set values for the subproperties you want to specify. For example, for the countBy and max subproperties of the Oracle JET oj-input-text element’s length property, import the component props that the Input Text element uses. Then define a length object, based on the type InputTextProps["length"], and assign it values for the countBy and max subproperties. Finally, pass the length object to the oj-input-text element as the value of its length property.

import {ComponentProps } from "preact";

type InputTextProps = ComponentProps<"oj-input-text">;

const length: InputTextProps["length"] = {
  countBy: "codeUnit",
  max: 3
};

function Parent() {
  return (   
    <oj-input-text length={ length } />
  )
}

Mutate Properties on Oracle JET Custom Element Events

Unlike typical Preact components, mutating properties on JET custom elements invokes property-changed callbacks. As a result, you can end up with unexpected behavior, such as infinite loops, if you:

  1. Have a property-changed callback
  2. The property-changed callback triggers a state update
  3. The state update creates a new property value (for example, copies values into a new array)
  4. The new property value is routed back into the same property

Typically, Preact (and React components) invoke callbacks in response to user interaction, and not in response to a new property value being passed in by the parent component. You can simulate the Preact componnent-type behavior when you use JET custom elements if you check the value of the event.detail.updatedFrom field to determine if the property change is due to user interaction (internal) instead of your app programmatically mutating the property value (external). For example, the following event callback is only invoked in response to user interaction:

. . .
const onSelection = (event) => {
      if (event.detail.updatedFrom === 'internal') {
        const selectedValues = event.detail.value;
        setSelectedOptions([selectedValues]);
      }
    };
. . .

Avoid Repeated Data Provider Creation

Do not re-create a data provider each time that a VComponent renders.

For example, do not do the following in a VComponent using an oj-list-view component, as you re-create the MutableArrayDataProvider instance each time the VComponent renders:

<oj-list-view
  data={new MutableArrayDataProvider....}
  . . . >
</oj-list-view>

Instead, consider using Preact's useMemo hook to ensure that a data provider instance is re-created only if the data in the data provider actually changes. The following example demonstrates how you include the useMemo hook to ensure that the data provider providing data to an oj-list-view component is not re-created, even if the VComponent that includes this code re-renders.

import MutableArrayDataProvider = require("ojs/ojmutablearraydataprovider");
import { Task, renderTask } from "./data/task-data";
import { useMemo } from "preact/hooks";
import "ojs/ojlistview";
import { ojListView } from "ojs/ojlistview";

type Props = {
  tasks: Array<Task>;
}

export function ListViewMemo({ tasks }: Props) {
  const dataProvider = useMemo(() => {
      return new MutableArrayDataProvider(
        tasks, {
          keyAttributes: "taskId"
        }
      );
    },
    [ tasks ]
  );

  return (
    <oj-list-view data={dataProvider} class="demo-list-view">
      <template slot="itemTemplate" render={renderTask} />
    </oj-list-view>
  )
}

The reason for this recommendation is that a VComponent can re-render for any number of reasons, but as long as the data provided by the data provider does not change, there is no need to incur the cost of re-creating the data provider instance. Re-creating a data provider can affect your app's performance, due to unnecessary rendering, and usability as collection components may flash and lose scroll position as they re-render.

Avoid Data Provider Re-creation When Data Changes

Previously, we described how the useMemo hook avoids re-creating a data provider unless data changes. When data does change, we still end up re-creating the data provider instance, and this can result in collection components, such as list view, re-rendering all data displayed by the component and losing scroll position.

Here, we'll try to illustrate how you can opitimize the user experience by implementing fine-grained updates and maintaining scroll position for collection components, even if the data referenced by the data provider changes. To accomplish this, the data provider uses a MutableArrayDataProvider instance. With a MutableArrayDataProvider instance, you can mutate an existing instance by setting a new array value into the MutableArrayDataProvider's data field. The collection component is notified of the specific change (create, update, or delete) that occurs, which allows it to make a fine-grained update and maintain scroll position.

In the following example, an app displays a list of tasks in a list view component and a Done button that allows a user to remove a completed task. These components render in the Content component of a virtual DOM app created with the basic template (appRootDir/src/components/content/index.tsx).

The surrounding text describes the image

To implement fine-grained updates and maintain scroll position for the oj-list-view component, we store the list view's MutableArrayDataProvider in local state using Preact’s useState hook and never re-create it, and we also use Preact’s useEffect hook to update the data field of the MutableArrayDataProvider when a change to the list of tasks is detected. The user experience is that a click on Done for an item in the tasks list removes the item (with removal animation). No refresh of the oj-list-view component or scroll position loss occurs.

import "ojs/ojlistview";
import { ojListView } from "ojs/ojlistview";
import MutableArrayDataProvider = require("ojs/ojmutablearraydataprovider");
import { Task, renderTask } from "./data/task-data";
import { useState, useEffect } from "preact/hooks";

type Props = {
  tasks: Array<Task>;
}

export function ListViewState({ tasks, onTaskCompleted }: Props) {
  const [ dataProvider ] = useState(() => {
    return new MutableArrayDataProvider(
      tasks, {
        keyAttributes: "taskId"
      }
    );
  });

  useEffect(() => {
    dataProvider.data = tasks;
  }, [ tasks ]);
  
  return (
      <oj-list-view data={dataProvider} class="demo-list-view">
          <template slot="itemTemplate" render={renderTaskWithCompletedCallback} />
        </oj-list-view>
  
  )
}

Use Oracle JET Popup and Dialog Components

To use Oracle JET's popup or dialog components (popup content) in a VComponent or a virtual DOM app, you need to create a reference to the popup content. We recommend too that you place the popup content on its own within a div element so that it continues to work when used with Preact's reconciliation logic.

Currently, to launch popup content from within JSX, you must create a reference to the custom element and manually call open(), as in the following example for a VComponent class component that uses Preact's createRef function:

import { customElement, ExtendGlobalProps } from "ojs/ojvcomponent";
import { h, Component, . . . createRef } from "preact";
. . .
import "ojs/ojdialog";
import { DialogElement } from "ojs/ojdialog";
. . . 

@customElement('popup-launching-component')
export class PopupLaunchingComp extends Component<GlobalProps> {
 
  private dialogRef = createRef();
 
  render(props) {
    return (
      <div>
        <oj-button onojAction={ this.showDialog }>Show Dialog</oj-button>
          <div>
            <oj-dialog ref={this.dialogRef} cancelBehavior="icon" modality="modeless">
             . . . </oj-dialog>
          </div>  
      </div>
    );
  }
 
  showDialog = () => {
      this.dialogRef.current?.open();
  }
}

As a side effect of the open() call, Oracle JET relocates the popup content DOM to a JET-managed popup container, outside of the popup-launching component. This works and the user can see and interact with the popup.

If, while the popup is open, the popup-launching component is re-rendered by, for example, a state change, Preact's reconciliation logic detects that the popup content element is no longer in its original location and will reparent the still-open popup content back to its original parent. This interferes with JET's popup service, and unfortunately leads to non-functional popup content. To avoid this issue, we recommend that you ensure that the popup content is the only child of its parent element. In the following functional component example, we illustrate one way to accomplish this by placing the oj-dialog component within its own div element.

Note:

In the following example we use Preact's useRef hook to get the reference to the DOM node inside the functional component. Use Preact's createRef function to get the reference to the popup content DOM node in class-based components.
import { h } from "preact";
import { useRef } from "preact/hooks";
import "ojs/ojdialog";
import "ojs/ojbutton";
import { ojButton } from "ojs/ojbutton";
import { DialogElement  } from "ojs/ojdialog";

export function Content() {

  const dialogRef = useRef<DialogElement>(null);

  const onSubmit = (event: ojButton.ojAction) => {
    event.preventDefault();
    dialogRef.current?.open();
    console.log("open dialog");
  };

  const close = () => {
    dialogRef.current?.close();
    console.log("close dialog");
  };

  return (
    <div class="oj-web-applayout-max-width oj-web-applayout-content">
      <oj-button onojAction={onSubmit} disabled={false}>
        Open dialog
      </oj-button>
      <div>
        <oj-dialog
          ref={dialogRef}
          dialogTitle="Dialog Title"
          cancelBehavior="icon">
          <div>Hello, World!</div>
          <div slot="footer">
            <oj-button id="okButton" onojAction={close}>
              Close dialog
            </oj-button>
          </div>
        </oj-dialog>
      </div>
    </div>
  );
}