5 Oracle JETコンポーネントおよびデータ・プロバイダの使用

次の推奨事項を確認して、Oracle JETコンポーネントおよび関連するAPI(データ・プロバイダなど)を効果的に使用し、開発したアプリケーションがパフォーマンスに優れ、最適なユーザー・エクスペリエンスを提供できるようにします。

Oracle JETコンポーネント・プロパティのサブプロパティへのアクセス

JSXでは、コンポーネント・プロパティのサブプロパティにアクセスできるドット表記はサポートされていません。たとえば、次の構文を使用して、入力テキスト要素のlengthプロパティのmaxまたはcount-byサブプロパティにアクセスすることはできません。

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

JSXを使用してこれらのサブプロパティにアクセスするには、まず要素の最上位プロパティにアクセスし、指定するサブプロパティの値を設定します。たとえば、Oracle JET oj-input-text要素のlengthプロパティのcountByおよびmaxサブプロパティの場合、入力テキスト要素が使用するコンポーネント・プロパティをインポートします。次に、InputTextProps["length"]型に基づいてlengthオブジェクトを定義し、countByおよびmaxサブプロパティの値を割り当てます。最後に、lengthオブジェクトをそのlengthプロパティの値としてoj-input-text要素に渡します。

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 } />
  )
}

Oracle JETカスタム要素イベントでのプロパティの変更

一般的なPreactコンポーネントとは異なり、Oracle JETカスタム要素のプロパティを変更すると、プロパティ変更コールバックが呼び出されます。その結果、次の場合に、無限ループなどの予期しない動作が発生する可能性があります:

  1. プロパティ変更コールバックがあります
  2. プロパティ変更コールバックは状態更新をトリガーします
  3. 状態の更新により、新しいプロパティ値が作成されます(たとえば、値を新しい配列にコピーします)
  4. 新しいプロパティ値は同じプロパティに戻されます

通常、Preact(およびReactコンポーネント)は、親コンポーネントによって渡される新しいプロパティ値への応答ではなく、ユーザーとの対話に応じてコールバックを呼び出します。Oracle JETカスタム要素を使用する場合、event.detail.updatedFromフィールドの値をチェックして、プロパティの変更が、アプリケーションによってプログラムで変更された(external)のではなく、ユーザーとの対話によるもの(internal)かどうかを判断すると、Preactコンポーネント・タイプの動作をシミュレートできます。たとえば、次のイベント・コールバックは、ユーザーとの対話に応じてのみ呼び出されます:

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

データ・プロバイダ作成の繰返しの回避

VComponentがレンダリングされるたびにデータ・プロバイダを再作成しないでください。

たとえば、VComponentがレンダリングされるたびにMutableArrayDataProviderインスタンスが再作成されるため、oj-list-viewコンポーネントを使用してVComponentで次のようにしないでください:

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

かわりに、PreactのuseMemoフックを使用して、データ・プロバイダのデータが実際に変更された場合にのみデータ・プロバイダ・インスタンスを再作成することを検討してください。次の例は、このコードを含むVComponentが再レンダリングされた場合でも、oj-list-viewコンポーネントにデータを提供するデータ・プロバイダが再作成されないようにするために、useMemoフックを含める方法を示しています。

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>
  )
}

このように推奨するのは、VComponentは様々な理由で再レンダリングされる可能性がありますが、データ・プロバイダによって提供されるデータが変更されないかぎり、データ・プロバイダ・インスタンスの再作成というコストを発生させる必要はないからです。データ・プロバイダを再作成すると、不要なレンダリングが発生するため、アプリケーションのパフォーマンスに影響を与える可能性があります。また、再レンダリング時にコレクション・コンポーネントがフラッシュしてスクロール位置が失われる可能性があるため、ユーザビリティに影響を与える可能性があります。

データ変更時のデータ・プロバイダの再作成の回避

これより前に、データが変更されないかぎり、useMemoフックでデータ・プロバイダの再作成を回避する方法について説明しました。データが変更されると、データ・プロバイダ・インスタンスを再作成することになり、リスト・ビューなどのコレクション・コンポーネントで、コンポーネントによって表示されるすべてのデータが再レンダリングされ、スクロール位置が失われる可能性があります。

ここでは、データ・プロバイダによって参照されるデータが変更された場合でも、ファイングレイン更新を実装し、コレクション・コンポーネントのスクロール位置を維持することで、ユーザー・エクスペリエンスを最適化する方法を説明します。これを実現するために、データ・プロバイダはMutableArrayDataProviderインスタンスを使用します。MutableArrayDataProviderインスタンスでは、MutableArrayDataProviderのデータ・フィールドに新しい配列値を設定することで、既存のインスタンスを変更できます。コレクション・コンポーネントには、発生した特定の変更(作成、更新または削除)が通知され、これによってきめ細かな更新およびスクロール位置の維持が可能になります。

次の例では、アプリケーションはリスト・ビュー・コンポーネントにタスクのリストを表示し、完了したタスクをユーザーが削除できる「Done」ボタンを表示します。これらのコンポーネントは、基本テンプレート(appRootDir/src/components/content/index.tsx)で作成された仮想DOMアプリケーションのコンテンツ・コンポーネントでレンダリングされます。

このイメージについては周囲のテキストで説明しています

oj-list-viewコンポーネントのファイングレイン更新を実装し、スクロール位置を維持するために、PreactのuseStateフックを使用してリスト・ビューのMutableArrayDataProviderをローカル状態に格納して再作成しないようにし、PreactのuseEffectフックを使用して、タスクのリストに対する変更が検出されると、MutableArrayDataProviderのデータ・フィールドを更新するようにします。ユーザー・エクスペリエンスでは、タスク・リスト内の項目の「Done」をクリックすると、(削除アニメーション付きで)その項目が削除されます。oj-list-viewコンポーネントがリフレッシュされたり、スクロール位置が失われることはありません。

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>
  
  )
}

Oracle JETポップアップおよびダイアログのコンポーネントの使用

VComponentまたは仮想DOMアプリケーションでOracle JETのポップアップまたはダイアログのコンポーネント(ポップアップ・コンテンツ)を使用するには、ポップアップ・コンテンツへの参照を作成します。また、ポップアップ・コンテンツは、Preactのリコンシリエーション・ロジックと組み合せて使用したときに機能するようにdiv要素に単独で配置することをお薦めします。

現在、JSX内からポップアップ・コンテンツを起動するには、PreactのcreateRef関数を使用したVComponentクラス・コンポーネントの次の例に示すように、カスタム要素への参照を作成し、open()を手動でコールする必要があります。

import { customElement, ExtendGlobalProps } from "ojs/ojvcomponent";
import { h, Component, ComponentChild, createRef } from "preact";
import "ojs/ojdialog";
import "oj-c/button";
. . .
@customElement("popup-launching-component")
export class PopupLaunchingComponent extends Component<ExtendGlobalProps<Props>> {

  private dialogRef = createRef();

  showDialog = () => {
    this.dialogRef.current?.open();
    console.log("open dialog");
  }

  render(): ComponentChild {
    return <div>
      <oj-c-button onojAction={this.showDialog} label="Show Dialog"></oj-c-button>
      <div>
        <oj-dialog ref={this.dialogRef} cancelBehavior="icon" 
                   modality="modeless" dialogTitle="Dialog Title">
          <div>Hello, World!</div>
        </oj-dialog>
      </div>
    </div>
  }
}

open()コールの副次的な影響として、Oracle JETによって、ポップアップ・コンテンツDOMがポップアップ起動コンポーネントの外部にあるOracle JET管理のポップアップ・コンテナに再配置されます。これがうまくいき、ユーザーにポップアップが表示されて、ポップアップと対話できます。

ポップアップが開いている間に、状態の変化などによりポップアップ起動コンポーネントが再レンダリングされると、Preactのリコンシリエーション・ロジックによってポップアップ・コンテンツ要素が元の位置になくなったことが検出され、まだ開いているポップアップ・コンテンツが元の親に戻されます。これがOracle JETのポップアップ・サービスの妨げとなり、ポップアップ・コンテンツが正常に機能しなくなります。この問題を回避するために、ポップアップ・コンテンツを必ずその親要素の唯一の子にすることをお薦めします。次の機能コンポーネントの例で、oj-dialogコンポーネントを単独のdiv要素内に配置することでこれを実現する方法の1つを示します。

ノート:

次の例では、PreactのuseRefフックを使用して、機能コンポーネント内のDOMノードへの参照を取得します。PreactのcreateRef関数を使用して、クラスベース・コンポーネント内のポップアップ・コンテンツDOMノードへの参照を取得します。
import { h } from "preact";
import { useRef } from "preact/hooks";
import "ojs/ojdialog";
import "oj-c/button";
import { CButtonElement } from "oj-c/button";
import { DialogElement } from "ojs/ojdialog";

export function Content() {

  const dialogRef = useRef<DialogElement>(null);

  const onSubmit = (event: CButtonElement.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-c-button onojAction={onSubmit} disabled={false} label="Open dialog">
       </oj-c-button>

    <div>
      <oj-dialog ref={dialogRef} dialogTitle="Dialog Title" cancelBehavior="icon">
        <div>Hello, World!</div>
        <div slot="footer">
          <oj-c-button id="okButton" onojAction={close} label="Close dialog">
              </oj-c-button>
        </div>
      </oj-dialog>
    </div>
  </div>;
}