LoginSignup
3
0

More than 1 year has passed since last update.

UI5 Web Components for React で参戦ライブを振り返る(TypeScript 編)

Last updated at Posted at 2021-12-20

はじめに

この記事は SAP Advent Calendar 2021 の12月21日分の記事として執筆しています。

コロナ禍で多くのイベントがオンライン開催となっていますが、SAP のテクニカルカンファレンスである「SAP TechEd」もそのひとつです。参加無料で多くの最新情報に触れる絶好の機会ですので、ご興味ある方は、以下にアクセスしてみてください。

さて、タイトルに TypeScript 編とありますが、この記事は「UI5 Web Components for React で参戦ライブを振り返る」の続編となります。そもそも 「Web Components とは?」など基本情報をまとめていますので、Web Coponents そのものについて知りたい方は、こちらも合わせてご覧ください。

普段、SAP と関わりのない方々は、「SAP って ERP の会社でしょ?」、「SAP なのに Web Components? React?」と思うかも知れません。しかし、SAP 単なる ERP 企業ではなく、自身がクラウドシフト(ERP自体のクラウド版へのシフトや、拡張開発のためのクラウドラットフォームの提供など)を進める上で、オープンソースコミュニティをうまく巻き込み、コミュニティへの貢献を強力にコミットしているクラウド企業の側面も持ちます。

この記事でご紹介する UI5 Web Components for React も Apache License 2.0 ライセンスによる SAP 謹製オープンソースのひとつです(SAP の公式 GitHub では、200 を超えるリポジトリ上で、様々なプログラミング言語による開発が進められています)。この記事が皆さんにとって SAP オープンソースの入り口となれば幸いです。

React & TypeScript プロジェクトを作成する

まず、はじめに React & TypeScript プロジェクトを作成し、起動確認までしてみましょう。

テンプレートを活用した初期プロジェクトの作成

古い人間なので、型に厳しい言語の方が好ましいこともあり、TypeScript で React プロジェクト(プロジェクト名:lives-in-my-life-ts)を作成します。以前の投稿 では、npx を利用していましたが、今回は yarn にしてみました。

yarn create react-app lives-in-my-life-ts --template typescript

早速、起動確認してみましょう。

cd lives-in-my-life-ts
yarn start

極めてシンプルなテンプレートアプリが起動されました。
screenshot-localhost_3001-2021.12.13-17_07_54.png

UI5 Web Components for React モジュールの追加

公式サイトに記載されている 3 つのモジュールを追加してみましょう。

  • @ui5/webcomponents
  • @ui5/webcomponents-react
  • @ui5/webcomponents-fiori
yarn add @ui5/webcomponents @ui5/webcomponents-react @ui5/webcomponents-fiori

package.json に指定した 3 モジュールが追加されています。@ui5/webcomponents-react は、まだ 0.20.3 ですね、、、。一方で、@ui5/webcomponents@ui5/webcomponents-fiori は、1.0.2 として正式リリース済みでした。以前の投稿 の時点では、まだ 1.0.0-rc リリース候補(Release Candidate)でしたが、2021年11月10日に正式リリースされたようです!

package.json
{
  "name": "lives-in-my-life-ts",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "@types/jest": "^26.0.15",
    "@types/node": "^12.0.0",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "@ui5/webcomponents": "^1.0.2",
    "@ui5/webcomponents-fiori": "^1.0.2",
    "@ui5/webcomponents-react": "^0.20.3",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3",
    "typescript": "^4.1.2",
    "web-vitals": "^1.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

エラー修正前に関数コンポーネント化しておく

以前の投稿 の JavaScript ソース App.js の内容を App.tsx に丸ごと貼り付けただけでは、大量の TypeScript エラーが発生します。このエラーをひとつひとつ潰していきたいのですが、以前の投稿 以降、React 自体が進化し、よりモダンかつスマートな実装が可能となっています。エラー解消よりも先に、コードをモダナイズしておきます。

App クラスコンポーネントの関数コンポーネント化

ここでは手始めに、メインの App クラスコンポーネントを App 関数コンポーネントに書き換えておきましょう。以下の変更を加えていきます。コンポーネントについて詳しく知りたい方は、React 公式リファレンス をご参照ください。

  • class をシンプルなアロー関数へ変更
  • stateuseState フックでスッキリと
  • filterTextstring 型であることを明示
  • handleFilterTextChange 内は filterText のセッターでスッキリと
  • 自身が関数となったため、render() メソッドを削除してスッキリと
  • 自身が関数となったため、this 参照や this.state 参照を削除してスッキリと
App クラスコンポーネント(変更前)
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      filterText: "",
    };
  }

  handleFilterTextChange = (filterText) => {
    this.setState({
      filterText: filterText,
    });
  };

  render() {
    return (
      <ThemeProvider>
        <FlexBox
          style={{ width: "100%", height: "100%" }}
          direction={FlexBoxDirection.Column}
          justifyContent={FlexBoxJustifyContent.Center}
          alignItems={FlexBoxAlignItems.Center}
        >
          <LiveToolbar
            filterText={this.state.filterText}
            onFilterTextChange={this.handleFilterTextChange}
          />
          <LiveTimeline filterText={this.state.filterText} />
        </FlexBox>
      </ThemeProvider>
    );
  }
}
App 関数コンポーネント(変更後)
const App = () => {
  const [filterText, setFilterText] = useState<string>("");
  const handleFilterTextChange = (filterText: string) => {
    setFilterText(filterText);
  };

  return (
    <ThemeProvider>
      <FlexBox
        style={{ width: "100%", height: "100%" }}
        direction={FlexBoxDirection.Column}
        justifyContent={FlexBoxJustifyContent.Center}
        alignItems={FlexBoxAlignItems.Center}
      >
        <LiveToolbar
          filterText={filterText}
          onFilterTextChange={handleFilterTextChange}
        />
        <LiveTimeline filterText={filterText} />
      </FlexBox>
    </ThemeProvider>
  );
};

だいぶスッキリとしましたね!

LiveTimeline クラスコンポーネントの関数コンポーネント化

続けて、LiveTimeline コンポーネントです。以下の変更を加えていきます。

  • classReact.FC 型へ変更(FC は Function Component の略)
  • パラメータ propsLiveTimelineProps 型であることを明示
  • LiveTimelineProps 型を宣言しておく
  • 自身が関数となったため、this 参照を削除してスッキリと
LiveTimeline コンポーネント(変更前)
const LiveTimeline extends React.Component {
  render() {
    const filteredLives = LIVES.filter((element) => {
      return (
        (element.date + element.title + element.venue)
          .toLocaleLowerCase()
          .indexOf(this.props.filterText.toLowerCase()) > -1
      );
    });

    if (filteredLives.length > 0) {
      return (
        <Timeline className="Lives-timeline">
          {filteredLives.map((element, index) => {
            return (
              <TimelineItem
                key={index}
                icon="calendar"
                itemName={element.title}
                subtitleText={element.date}
              >
                <div>{element.venue}</div>
              </TimelineItem>
            );
          })}
        </Timeline>
      );
    } else {
      return (
        <MessageStrip className="Lives-timeline">No data found.</MessageStrip>
      );
    }
  }
}
LiveTimeline コンポーネント(変更後)
type LiveTimelineProps = {
  filterText: string;
};
// 途中省略
const LiveTimeline: React.FC<LiveTimelineProps> = (props) => {
  const filteredLives = LIVES.filter((element) => {
    return (
      (element.date + element.title + element.venue)
        .toLocaleLowerCase()
        .indexOf(props.filterText.toLowerCase()) > -1
    );
  });

  if (filteredLives.length > 0) {
    return (
      <Timeline className="Lives-timeline">
        {filteredLives.map((element, index) => {
          return (
            <TimelineItem
              key={index}
              icon="calendar"
              itemName={element.title}
              subtitleText={element.date}
            >
              <div>{element.venue}</div>
            </TimelineItem>
          );
        })}
      </Timeline>
    );
  } else {
    return (
      <MessageStrip className="Lives-timeline">No data found.</MessageStrip>
    );
  }
};

LiveToolbar クラスコンポーネントの関数コンポーネント化

最後は、LiveToolbar コンポーネントです。以下の変更を加えていきます。

  • classReact.FC 型へ変更(FC は Function Component の略)
  • パラメータ propsLiveToolbarProps 型であることを明示
  • LiveToolbarProps 型を宣言しておく
  • handleFilterTextChange 関数のパラメータが Ui5CustomEvent<HTMLInputElement> 型であることを明示
  • Ui5CustomEvent をインポートしておく
  • 自身が関数となったため、this 参照を削除してスッキリと
LiveToolbar コンポーネント(変更前)
class LiveToolbar extends React.Component {
  handleFilterTextChange = (e) => {
    this.props.onFilterTextChange(e.target.value);
  };

  render() {
    return (
      <ShellBar
        primaryTitle="Lives in My Life"
        logo={
          <img
            alt="SAPUI5 Logo"
            src="https://sap.github.io/ui5-webcomponents/assets/images/ui5.png"
          />
        }
        profile={
          <Avatar image="https://avatars0.githubusercontent.com/u/25473342?s=400&u=b399ebf80c62121616c0435bed3f3c39b4fc9c9b&v=4" />
        }
        searchField={
          <Input
            value={this.props.filterText}
            placeholder="Please input ..."
            onInput={this.handleFilterTextChange}
          />
        }
      />
    );
  }
}
LiveToolbar コンポーネント(変更後)
import { Ui5CustomEvent } from '@ui5/webcomponents-react/interfaces/Ui5CustomEvent';
// 途中省略
type LiveToolbarProps = {
  filterText: string;
  onFilterTextChange: (filterText: string) => void;
};
// 途中省略
const LiveToolbar: React.FC<LiveToolbarProps> = (props) => {
  const handleFilterTextChange = (e: Ui5CustomEvent<HTMLInputElement>) => {
    props.onFilterTextChange(e.target.value);
  };

  return (
    <ShellBar
      primaryTitle="Lives in My Life"
      logo={
        <img
          alt="SAPUI5 Logo"
          src="https://sap.github.io/ui5-webcomponents/assets/images/ui5.png"
        />
      }
      profile={
        <Avatar image="https://avatars0.githubusercontent.com/u/25473342?s=400&u=b399ebf80c62121616c0435bed3f3c39b4fc9c9b&v=4" />
      }
      searchField={
        <Input
          value={props.filterText}
          placeholder="Please input ..."
          onInput={handleFilterTextChange}
        />
      }
    />
  );
};

以上で、AppLiveToolbarLiveTimeline、合わせて 3 つのクラスコンポーネントの関数コンポーネント化が完了しました。関数コンポーネント化しただけの状態では、幾つかのエラーが残っていますので、それらを潰していきましょう。

残っているエラーをコツコツと修正する

以下のエラーを順番に解消していきます。

  • Theme の型定義ファイルが見つからない
  • TimelineItem に itemName プロパティを指定することはできない
  • Avatar に image プロパティを指定することはできない

エラーその1:Theme の型定義ファイルが見つからない

まずは、型に厳密な TypeScript らしいエラーから対応していきます。TypeScript 対応あるあるですが、https://github.com/SAP/ui5-webcomponents-react/discussions/1136 で回答されているように、一部のコンポーネントについては、TypeScript 型定義ファイルがまだ用意されていないようです。ここで推奨されているように、自分で型定義ファイルを用意しましょう。

エラー発生箇所
import { setTheme } from "@ui5/webcomponents-base/dist/config/Theme.js";
エラーメッセージ
Could not find a declaration file for module '@ui5/webcomponents-base/dist/config/Theme.js'. '/Users/shunichirock/Repositories/lives-in-my-life-ts/node_modules/@ui5/webcomponents-base/dist/config/Theme.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/ui5__webcomponents-base` if it exists or add a new declaration (.d.ts) file containing `declare module '@ui5/webcomponents-base/dist/config/Theme.js';`ts(7016)

src/@types フォルダに Theme.d.ts ファイルを追加し、モジュールを明示的に宣言することで、エラーが解消されます。tsconfig.json"strict": false を指定することでもエラーは解消されますが、せっかく TypeScript を利用するのですから、厳密にまいりましょう。

@src/types/Theme.d.ts
declare module "@ui5/webcomponents-base/dist/config/Theme";

ついでに import 時の拡張子(.js)も削除しておきます。

ソース修正箇所
- import "@ui5/webcomponents/dist/Assets.js";
- import "@ui5/webcomponents-react/dist/Assets.js";
- import "@ui5/webcomponents-fiori/dist/Assets.js"; // Only if using the @ui5/webcomponents-fiori package
- import "@ui5/webcomponents-icons/dist/Assets.js"; // Only if using the @ui5/webcomponents-icons package
- import { setTheme } from "@ui5/webcomponents-base/dist/config/Theme.js";
+ import "@ui5/webcomponents/dist/Assets";
+ import "@ui5/webcomponents-react/dist/Assets";
+ import "@ui5/webcomponents-fiori/dist/Assets"; // Only if using the @ui5/webcomponents-fiori package
+ import "@ui5/webcomponents-icons/dist/Assets"; // Only if using the @ui5/webcomponents-icons package
+ import { setTheme } from "@ui5/webcomponents-base/dist/config/Theme";

エラーその2:TimelineItem に itemName プロパティを指定することはできない

こちらは、UI5 Web Components for React に限らず、ライブラリ全般あるあるですが、バージョンアップにより仕様が変わったようです。最新のリファレンス を確認してみましょう。

エラーメッセージ
Type '{ children: Element; key: number; icon: string; itemName: string; subtitleText: string; }' is not assignable to type 'IntrinsicAttributes & Pick<TimelineItemPropTypes, "children" | "id" | "color" | "translate" | "hidden" | "dir" | "slot" | "style" | ... 252 more ... | "nameClickable"> & RefAttributes<...>'.
  Property 'itemName' does not exist on type 'IntrinsicAttributes & Pick<TimelineItemPropTypes, "children" | "id" | "color" | "translate" | "hidden" | "dir" | "slot" | "style" | ... 252 more ... | "nameClickable"> & RefAttributes<...>'.ts(2322)

最新のリファレンス(TimelineItem) によると、itemName プロパティが廃止され、代わりに titleText プロパティが追加されたようですので、プロパティ名を変更します。これでエラーが解消されました。

エラー修正箇所
-               itemName={element.title}
+               titleText={element.title}

エラーその3:Avatar に image プロパティを指定することはできない

こちらも、エラーその 2 と同様のエラーですね。最新のリファレンス を確認してみましょう。

エラーメッセージ
Type '{ image: string; }' is not assignable to type 'IntrinsicAttributes & Pick<AvatarPropTypes, "children" | "id" | "color" | "translate" | "hidden" | "dir" | "slot" | "style" | "title" | ... 252 more ... | "initials"> & RefAttributes<...>'.
  Property 'image' does not exist on type 'IntrinsicAttributes & Pick<AvatarPropTypes, "children" | "id" | "color" | "translate" | "hidden" | "dir" | "slot" | "style" | "title" | ... 252 more ... | "initials"> & RefAttributes<...>'.ts(2322)

最新のリファレンス(Avatar) によると、image プロパティは廃止され、子要素として指定するように変更されたようですので、imgタグに変更します。これでエラーが解消されました。

エラー修正箇所
-         <Avatar image="https://avatars0.githubusercontent.com/u/25473342?s=400&u=b399ebf80c62121616c0435bed3f3c39b4fc9c9b&v=4" />
+         <Avatar>
+           <img
+             alt="Profile"
+             src="https://avatars0.githubusercontent.com/u/25473342?s=400&u=b399ebf80c62121616c0435bed3f3c39b4fc9c9b&v=4"
+           />
+         </Avatar>

動作確認してみる

これで全てのエラーが解消されましたので、早速、実行してみましょう。

yarn start

React アプリは正常に起動はしましたが、以下 2 点の最終調整が必要なようです。

  • SAPUI5 ロゴのリンクが切れている
  • カレンダーアイコンが表示されない

screenshot-localhost_3000-2021.12.18-13_49_54.png

最終調整その1:SAPUI5 ロゴのリンクが切れている

こちらは、SAPUI5 公式のロゴへの URL リンクだったのですが、ファイル名が変更されたようです。最新パスを確認して修正します。細かいポイントですが、リソース名へのこだわりが垣間見えて好ましいですね。

エラー修正箇所
-         src="https://sap.github.io/ui5-webcomponents/assets/images/ui5.png"
+         src="https://sap.github.io/ui5-webcomponents/assets/images/ui5-logo.png"

最終調整その2:カレンダーアイコンが表示されない

公式のリファレンス によると、Assets ではなく AllIcons を指定するか、利用するアイコンを個別指定(今回は calendar)するようです。バージョンアップの過程で、より分かりやすい名称に変更されたようです。こちらも好ましいですね。

エラー修正箇所
- import "@ui5/webcomponents-icons/dist/Assets"; // Only if using the @ui5/webcomponents-icons package
+ import "@ui5/webcomponents-icons/dist/AllIcons"; // Only if using the @ui5/webcomponents-icons package

最終調整結果はこちら

これにて SAPUI5 ロゴもカレンダーアイコンも正常に表示されました。これにて、参戦ライブ記録アプリ lives-in-my-life-ts の TypeScript 化は完了となります。

screenshot-localhost_3000-2021.12.18-14_30_26.png

ちなみに、変更後のソースコードは以下の通りです。(以前の投稿 と同様ですが)JSON 直書きだったり、日付データが yyyy/MM/dd 形式だったり、i18n 対応していなかったり、コード分割を検討していなかったり、色々と考えるべきことはありますが、お手軽な初期バージョンとして晒します。

App.tsx
import {
  Avatar,
  FlexBox,
  FlexBoxAlignItems,
  FlexBoxDirection,
  FlexBoxJustifyContent,
  Input,
  MessageStrip,
  ShellBar,
  ThemeProvider,
  Timeline,
  TimelineItem,
} from "@ui5/webcomponents-react";
import React, { useState } from "react";
import { Ui5CustomEvent } from "@ui5/webcomponents-react/interfaces/Ui5CustomEvent";
import "./App.css";
import "@ui5/webcomponents/dist/Assets";
import "@ui5/webcomponents-react/dist/Assets";
import "@ui5/webcomponents-fiori/dist/Assets"; // Only if using the @ui5/webcomponents-fiori package
import "@ui5/webcomponents-icons/dist/calendar"; // Only if using the @ui5/webcomponents-icons package
import { setTheme } from "@ui5/webcomponents-base/dist/config/Theme";
setTheme("sap_fiori_3_dark");

type LiveTimelineProps = {
  filterText: string;
};

type LiveToolbarProps = {
  filterText: string;
  onFilterTextChange: (filterText: string) => void;
};

const LIVES = [
  {
    date: "1991/12/31",
    title: "METALLICA / EUROPE / TESLA / THUNDER - FINAL COUNTDOWN 1991",
    venue: "東京ドーム",
  },
  /* (以下、省略) */
];

const LiveTimeline: React.FC<LiveTimelineProps> = (props) => {
  const filteredLives = LIVES.filter((element) => {
    return (
      (element.date + element.title + element.venue)
        .toLocaleLowerCase()
        .indexOf(props.filterText.toLowerCase()) > -1
    );
  });

  if (filteredLives.length > 0) {
    return (
      <Timeline className="Lives-timeline">
        {filteredLives.map((element, index) => {
          return (
            <TimelineItem
              key={index}
              icon="calendar"
              titleText={element.title}
              subtitleText={element.date}
            >
              <div>{element.venue}</div>
            </TimelineItem>
          );
        })}
      </Timeline>
    );
  } else {
    return (
      <MessageStrip className="Lives-timeline">No data found.</MessageStrip>
    );
  }
};

const LiveToolbar: React.FC<LiveToolbarProps> = (props) => {
  const handleFilterTextChange = (e: Ui5CustomEvent<HTMLInputElement>) => {
    props.onFilterTextChange(e.target.value);
  };

  return (
    <ShellBar
      primaryTitle="Lives in My Life"
      logo={
        <img
          alt="SAPUI5 Logo"
          src="https://sap.github.io/ui5-webcomponents/assets/images/ui5-logo.png"
        />
      }
      profile={
        <Avatar>
          <img
            alt="Profile"
            src="https://avatars0.githubusercontent.com/u/25473342?s=400&u=b399ebf80c62121616c0435bed3f3c39b4fc9c9b&v=4"
          />
        </Avatar>
      }
      searchField={
        <Input
          value={props.filterText}
          placeholder="Please input ..."
          onInput={handleFilterTextChange}
        />
      }
    />
  );
};

const App = () => {
  const [filterText, setFilterText] = useState<string>("");
  const handleFilterTextChange = (filterText: string) => {
    setFilterText(filterText);
  };

  return (
    <ThemeProvider>
      <FlexBox
        style={{ width: "100%", height: "100%" }}
        direction={FlexBoxDirection.Column}
        justifyContent={FlexBoxJustifyContent.Center}
        alignItems={FlexBoxAlignItems.Center}
      >
        <LiveToolbar
          filterText={filterText}
          onFilterTextChange={handleFilterTextChange}
        />
        <LiveTimeline filterText={filterText} />
      </FlexBox>
    </ThemeProvider>
  );
};

export default App;

さいごに

以前の投稿「UI5 Web Components for React で参戦ライブを振り返る」から、早いもので約一年が経ちました。残念ながら一度もライブに参戦できておらず、App.js の JSON 配列へのエントリ追加は実現しませんでした。その間に UI5 Web Components for React のバージョンは0.12.3 から 0.20.3 へ、for React のベースである UI5 Web Components のバージョンは、1.0.0-rc.10 から、1.0.2 に粛々とバージョンアップしました。歩みを止めないソフトウェア開発/コミュニティ貢献に、頭が下がる思いです。

以下は、以前の投稿 からの引用ですが、UI5 Web Components が正式リリースされたことで、エンタープライズアプリケーションの世界でも採用しやすくなったのではないでしょうか? 個人的には「for React」の方も一日も早く正式リリースされることを心待ちにしています。なにかと MUI(Material UI)を採用しがちですが、エンタープライズ向けには UI5 Web Components の優位性が際立ちます。自社のアセット開発だけでなく、クライアント向けにも積極採用していきたいと思います。

エンタープライズアプリケーションの世界では、どのような UI テーマ(デザイン)を採用すべきか悩みどころですが、少なくとも SAP は Fiori という答えを用意してくれています。2013 年にリリースされた Fiori は、単なるモダン Web フレームワークではなく、SAP の統一的・横断的な UX として進化を遂げてきました。「Fiori にしておけば間違いない」というのは、(エンタープライズアプリケーションの世界では)大きな魅力です。機会があれば、積極的に採用していきたいと思います。

参考リンク

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0