ウィジェット UI の作成

出典:ArcGIS Experience Builder - Guide - Create UI for widget

ArcGIS Experience Builder 用のカスタム ウィジェットを開発する際には、魅力的で機能的なユーザー インターフェース(UI)を作成することが不可欠です。このトピックでは、JSX、Web コンポーネント、およびさまざまなスタイリング手法を使用して、ウィジェットの UI を構築する手順を説明します。

JSX の記述

ArcGIS Experience Builder のウィジェットは React コンポーネントです。UI は JSX を使って宣言します。JSX は、JavaScript に埋め込まれた HTML に似たマークアップを用いてコンポーネント ツリーを表現できる構文拡張です。JSX は React 要素の生成にコンパイルされ、式、props、コンポジション、条件付きレンダリングをサポートします。

Experience Builder は クラスコンポーネントと関数コンポーネントの両方をサポートしています。ウィジェットの開発では、次のいずれかを使用します。

  • ライフサイクル メソッドや PureComponent のセマンティクスを使用したい場合はクラス コンポーネントを使用します。
  • 状態管理や副作用処理に React フックを使用する場合は関数コンポーネントを使用します。

クラス コンポーネント

クラス コンポーネントは React.PureComponent(または React.Component) を継承し、render() メソッドを実装して JSX を返します。

// widget.tsx 内:
// in widget.tsx:
import { React, AllWidgetProps } from 'jimu-core';

export default class Widget extends React.PureComponent<AllWidgetProps<{}>, unknown> {
  render() {
    return (
      <div className="myWidget">
        <p>This is a sample widget</p>
        <button type="button" style={{ background: 'orange' }}>I'm a button</button>
      </div>
    );
  }
}
React.PureComponent は、props と state の浅い比較を行い、不要な再レンダリングをスキップします。そのため、イミュータブル (不変) な更新を優先し、インプレース (破壊的) な変更は避けるようにしてください。

関数コンポーネント

関数コンポーネントとは JSXを返す単純な関数です。これはほとんどのウィジェットにおいて推奨されるスタイルで、React フックとシームレスに連携します。

// widget.tsx 内:
import { React, AllWidgetProps } from 'jimu-core';

export default function Widget(props: AllWidgetProps<{}>) {
  return (
    <div className="myWidget">
      <p>This is a sample widget</p>
      <button type="button" style={{ background: 'orange' }}>I'm a button</button>
    </div>
  );
}

関数コンポーネントでは、ローカルな状態管理や副作用の処理を扱うためのフック (useStateuseEffect など) を使用できます。

Output 例:

Jimu UI ライブラリーの使用

Jimu フレームワークは、開発者がウィジェット開発で使用するコンポーネントの UI ライブラリーを提供しています。

  • 基本的な UI コンポーネント:button、dropdown、inputs、icon、nav、modal、paper など
  • 高度な UI コンポーネント:date picker、resource selector など

Storybook サイト(https://developers.arcgis.com/experience-builder/storybook)では、よく使われるコンポーネントやアイコンの多くをプレビューできます。

詳細は Experience Builder の Storybook を参照してください。

Jimu UI は Experience Builder の公式 UI ライブラリーであり、このライブラリーのコンポーネントを考慮して UI 開発を利用することを強くお勧めします。その理由は以下の通りです。

  • UI/UX の一貫性:ウィジェットの全体的な外観と操作感、それを使って作成されたアプリは、一貫したパターンに従います。
  • テーマ性:コンポーネントのスタイルは設定可能でテーマ性があるため、さまざまなテーマに対応したウィジェットを簡単に作ることができます。
  • Experience Builder および ArcGIS とよりスムーズに連携できます。

コンポーネントのインポート

基本的な UI コンポーネントは「jimu-ui」から直接インポートでき、高度な UI コンポーネントはパスを使用して個別にインポートする必要があります。

import { Button, Icon, Paper, TextInput } from 'jimu-ui'; // 基本
import { DatePicker } from 'jimu-ui/date-picker'; // 高度

コード例

ここでは、“primary” スタイルの Button コンポーネントとスター アイコンをウィジェットに追加しています。

// widget.tsx 内:
import { React, AllWidgetProps } from 'jimu-core';
import { Button, Icon } from 'jimu-ui'; // import components
import { StarFilled } from 'jimu-icons/filled/application/star'

// Icon コンポーネントを使用して SVG アイコンを作成:
const iconNode = <StarFilled />;

export default class Widget extends React.PureComponent<AllWidgetProps, any>{
  render(){
    // ウィジェットにアイコンを含む Button コンポーネントを追加:
    return <Button type="primary">{iconNode} primary button</Button>;
  }
}

Output 例:

Paper コンポーネントをウィジェット コンテナーとして使用

ウィジェットのスタイルを一貫性のあるものに保つためには、ウィジェットのコンテナーに Paper コンポーネントの使用が推奨されます。

const Widget = () => {
  return <Paper variant="flat" shape="none" className="jimu-widget widget-xxx">...</Paper>
}
  • Paper コンポーネントはデフォルトで角が丸くなっています。shape="none" を設定するとその角が取り除かれ、ウィジェットの角のスタイルがレイアウトのデザインに追従するようになります。
  • Paper 上に直接表示されるテキストについては、Paper から継承されるためテキスト カラーを指定する必要はありません。

デフォルトのスタイル:

  • Background:theme.sys.color.surface.paper
  • Text:theme.sys.color.surface.paperText

より明るいテキストを表示したい場合は、surface.paperHint を使用します。推奨される方法は <Typography color="paperHint" /> を使うことです。

<Typography color="paperHint">Secondary text</Typography>

その他のオプション:

  • Border:<Paper variant="outlined" />
  • BorderRadius:<Paper shape="shape1" />(デフォルトは shape2
  • Transparent background:<Paper variant="flat" transparent />

詳細はこちらのドキュメントをご参照ください。

Calcite コンポーネントの使用

Jimu UI は Experience Builder の主要なコンポーネント ライブラリーであり、カスタム ウィジェットやテーマを Experience Builder 全体のテーマと一貫性を持たせるために、まずこちらを使用することが望ましいです。Calcite Design System を使用する必要がある場合は、それも可能です。Calcite のサンプル ウィジェットを参照してください。Calcite コンポーネントを使用する際は、@esri/calcite-components ではなく、calcite-components からインポートするようにしてください。

ArcGIS Maps SDK for JavaScript コンポーネントの使用

ArcGIS Maps SDK for JavaScript のコンポーネントをウィジェットに追加できます。<arcgis-map> など、これらの Web コンポーネントは、再利用可能なカスタム要素としてマッピング機能を提供します。これらはフレームワークに依存せず、React、プレーンな JavaScript、またはほとんどのモジュール バンドラーと一緒に使用できるため、最小限のセットアップでマッピング UI を簡単に組み込むことができます。 始め方については、ArcGIS Maps SDK のサンプル ウィジェットを参照してください。

ウィジェット スタイル設定

Experience Builder にはウィジェットをスタイリングするいくつかの方法があります。

CSS-in-JS (推奨)

CSS-in-JS とは、ベンダー プレフィックス、スコープ付き CSS、JS ロジック、テーマ機能など、CSS では解決できない問題に対処するために JavaScript で CSS を書く方法のことを指します。

Styled Components や Emotion など、よく知られている CSS-in-JS のライブラリがたくさんあります。Experience Builder では、スタイリングとテーマ設定を目的としたフレームワークとして Emotion を使用しています。

Emotion には 2 つのスタイリング パターンがあります。

1. css prop

Emotion の css prop を使うと、React の style prop に比べて、より自然で親しみやすい方法で CSS スタイルを書くことができます。CSS スタイルは template literals で書くことができるので、CSS の中に JS ロジックを書くことができます。

例えば、以下のサンプルの Counter ウィジェットは、カウント値が 2 以上になるとテキストの色が赤から緑に変わります。

// widget.tsx:
import { React, css, type AllWidgetProps } from 'jimu-core';
import { Button, ButtonGroup } from 'jimu-ui';

interface State {
  count: number;
}

export default class Widget extends React.PureComponent<AllWidgetProps<{}>, State>{
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }
  render() {
    const numberStyle = css`
      font-size: 2.5rem;
      color: ${this.state.count > 2 ? 'green' : 'red'};
    `;
    return <div className="text-center">
      <p css={numberStyle}>{this.state.count}</p>
      <ButtonGroup variant="outlined" color="primary">
        <Button onClick={e => {setCount(count - 1)}}> - </Button>
        <Button onClick={e => {setCount(count + 1)}}> + </Button>
      </ButtonGroup>
    </div>;
  }
}

または

// widget.tsx:
import { React, css, type AllWidgetProps } from 'jimu-core';
import { Button, ButtonGroup } from 'jimu-ui';

const Widget = (props: AllWidgetProps<{}>): React.ReactElement => {
  const [count, setCount] = React.useState(0);
  const numberStyle = css`
    font-size: 2.5rem;
    color: ${count > 2 ? 'green' : 'red'};
  `;
  return <div className="text-center">
    <p css={numberStyle}>{count}</p>
    <ButtonGroup variant="outlined" color="primary">
      <Button onClick={e => {setCount(count - 1)}}> - </Button>
      <Button onClick={e => {setCount(count + 1)}}> + </Button>
    </ButtonGroup>
  </div>;
}

Output 例:

2. Styled Components

このパターンは Styled-Components ライブラリーにインスピレーションされたもので、使い方は非常に似ています。“styled” アプローチは、ウィジェット内で再利用可能なコンポーネントを作成するのに最適です。

import { React, AllWidgetProps } from 'jimu-core';
import { styled } from 'jimu-theme';

// スタイルが設定された Button コンポーネント:
const StyledButton = styled.button`
  color: white;
  background-color: blue;
  transition: 0.15s ease-in all;
  &:hover {
    background-color: darkblue;
  }
`;

export default class Widget extends React.PureComponent<AllWidgetProps<{}>>{
  render() {
    return <StyledButton>
      A styled HTML Button
    </StyledButton>;
  }
}

Output 例:

CSS ユーティリティー クラス

Jimu UI は、UI 要素に素早くスタイルを適用するための同様の CSS ユーティリティー クラスを提供しています。

クイック サンプル

ここでは、w-100p-3bg-papertext-paperborder を追加して、要素に以下のスタイルを適用します。

  • 親要素の幅を 100% を取得
  • 12px のパディングを設定
  • テーマの paper カラーを背景色として使用
  • テーマの paper テキスト カラーを文字色として使用
  • テーマの divider の primary カラーを境界線の色として使用
// render() 関数内:

return <div className="w-100 p-3 bg-paper text-paper border">
  <p>This is a sample widget</p>
</div>;

Output 例:

インライン CSS

React のコンテキストでは、インラインの CSS スタイルは JavaScript オブジェクトとして記述され、DOM 要素の style 属性に適用されます。

// render() 関数内:

const containerStyle = {
  background: 'darkblue',
  color: 'white',
  width: 200,
  height: 150,
  padding: '1rem',
  borderRadius: 5
};

return <div
  style={containerStyle} // CSS スタイルの適用
> content </div>;

Output 例:

外部 CSS スタイルシート

別の方法としては、外部スタイルシートのファイルで CSS スタイルを定義し、ウィジェット内で個別にインポートする方法があります。使用できるスタイルシートのファイルの拡張子は .css.sass、および .scss です。

先ほどのコードサンプルを例に、CSS スタイルを別のスタイルシート (例: style.css) に移動します。

/* style.css */
.my-widget {
  background: 'darkblue';
  color: 'white';
  width: 200px;
  height: 150px;
  padding: '1rem';
  border-radius: 5px;
}

ウィジェットにファイルをインポートします。

// widget.tsx:
import 'path/to/style.css';

style.css で定義されている DOM 要素にクラス名を追加することを忘れないでください。

// widget.tsx:
// render() 関数内:

return <div className="my-widget"> content </div>;

Output 例:

テーマを使った作業

この設定は、ウィジェットをアプリケーション全体のデザインと一貫性のあるものにしたい場合や、テーマ変更時に自動的にデザインを更新したい場合に必要です。

ウィジェットの props からテーマ変数にアクセス

Experience Builder フレームワークでは、テーマ変数が JSON オブジェクトとして提供され、ウィジェットにプロパティとして挿入します。これにより、色、タイポグラフィー、シャドウなど、すべてのテーマ変数にアクセスできます。

ウィジェット内で props.theme を使用することでテーマ変数にアクセスでき、CSS 宣言内でそれらを参照できます。例えば、以下のように使用します。

import { React, AllWidgetProps } from 'jimu-core';
import { css } from 'jimu-core';

export default class Widget extends React.PureComponent<AllWidgetProps<{}>>{
  render() {
    const theme = this.props.theme;
    const style = css({
      color: theme.sys.color.surface.paperText,
      backgroundColor: theme.sys.color.surface.paper,
      padding: theme.sys.spacing(3),
      borderRadius: theme.sys.shape.shape2
    });
    return <div css={style}>
      <p>This is a sample widget</p>
    </div>;
  }
}

または

import { React, css, AllWidgetProps } from 'jimu-core';

export default function Widget(props: AllWidgetProps<{}>) {
  const { theme } = props;
  const style = css({
    color: theme.sys.color.surface.paperText,
    backgroundColor: theme.sys.color.surface.paper,
    padding: theme.sys.spacing(3),
    borderRadius: theme.sys.shape.shape2
  });
  return <div css={style}>
    <p>This is a sample widget</p>
  </div>;
}

Output 例:

コンポーネント内でのテーマ変数へのアクセス

ウィジェット内でより複雑な UI を構築する場合は、テーマを使用するコンポーネントにさらに分割することができます。

スタイル付きコンポーネント(styled component)を使ってテーマ変数にアクセスする方法(推奨)

// my-component.tsx
import { React } from 'jimu-core';
import { styled } from 'jimu-theme'

const MyComponent = styled('div')(({ theme }) => ({
    color: theme.sys.color.surface.paperText,
    backgroundColor: theme.sys.color.surface.paper,
    padding: theme.sys.spacing(3),
    borderRadius: theme.sys.shape.shape2
}));

export default MyComponent;

//widget.tsx
import { React, AllWidgetProps } from 'jimu-core';
import MyComponent from './my-component';

export default function Widget(props: AllWidgetProps<{}>) {
  return (
    <MyComponent>
      <p>This is a sample widget</p>
    </MyComponent>
  );
}

フックを使用したテーマ変数へのアクセス

// my-component.tsx
import { React, css } from 'jimu-core';
import { useTheme } from 'jimu-theme'

const MyComponent = ({ children }) => {
  const theme = useTheme();
  const style = css({
    color: theme.sys.color.surface.paperText,
    backgroundColor: theme.sys.color.surface.paper,
    padding: theme.sys.spacing(3),
    borderRadius: theme.sys.shape.shape2
  });
  return <div css={style}>
    {children}
  </div>;
};
export default MyComponent;

// widget.tsx
import { React, AllWidgetProps } from 'jimu-core';
import MyComponent from './my-component';

export default function Widget(props: AllWidgetProps<{}>) {
  return <MyComponent>
    <p>This is a sample widget</p>
  </MyComponent>;
}

HOC withTheme を使用したテーマ変数へのアクセス

// my-component.tsx
import { React, css } from 'jimu-core';
import { withTheme } from 'jimu-theme'

const MyComponent = ({ theme, children }) => {

  const style = css({
    color: theme.sys.color.surface.paperText,
    backgroundColor: theme.sys.color.surface.paper,
    padding: theme.sys.spacing(3),
    borderRadius: theme.sys.shape.shape2
  });

  return <div css={style}>
    {children}
  </div>
};

export default withTheme(MyComponent)

// widget.tsx
import { React, AllWidgetProps } from 'jimu-core';
import MyComponent from './my-component';

export default function Widget(props: AllWidgetProps<{}>) {
  return <MyComponent>
    <p>This is a sample widget</p>
  </MyComponent>;
}

ArcGIS Maps SDK コンポーネント

Jimu テーマと Calcite テーマの間のテーマ トークンのマッピングはフレームワークによって処理されているため、Calcite コンポーネントのスタイルを気にする必要はありません。ただし、Calcite コンポーネントや Jimu コンポーネントを使用しなかった際(例えば、ArcGIS Maps SDK の Web コンポーネントを使用する場合)、そのコンポーネントのスタイルがテーマに合わない場合は、ウィジェット内でトークンを上書きする必要があるかもしれません。

Calcite-JimuTheme Token mapping ドキュメントを参照してください。

リファレンス