出典:ArcGIS Experience Builder - Guide - Create UI for widget
Experience Builder のベースとなる Widget クラスは、React のコンポーネントサブクラスから拡張されています。PureComponent から拡張されており、render() と呼ばれる関数を提供しています。UI の作業のほとんどは、この関数の内部で行われることが予想されます。
UI テンプレートの作成に使用される React の構文は JSX と呼ばれています。これは HTML を書くのと非常に似ていますが、JavaScript の機能を完全に組み込んでいます。
以下は、クラス コンポーネントを使ってウィジェットの UI に基本的な HTML 要素を追加する簡単な例です。
// widget.tsx 内:
import { React, AllWidgetProps } from 'jimu-core';
export default class Widget extends React.PureComponent<AllWidgetProps<{}>, any>{
render() {
return <div className="myWidget">
<p>This is a sample widget</p>
<button type="button" style={{background: 'orange'}}>I'm a button</button>
</div>;
}
}
PureComponent クラスは、renderer() という関数を提供しており、UI のほとんどの処理はこの関数内で実装されます。
クラス コンポーネントのほかに、関数コンポーネントを使ってウィジェットを作成することもできます。
以下は、関数コンポーネントとして書かれた同等の例です。
// 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>
);
}
関数コンポーネントでは、useState や useEffect などの React フックを使って状態の管理や副作用の処理ができるため、非常に強力で柔軟性があります。
Output 例:

Jimu フレームワークは、開発者がウィジェット開発で使用するコンポーネントの UI ライブラリを提供しています。
Storybook サイト(https://developers.arcgis.com/experience-builder/storybook)では、よく使われるコンポーネントやアイコンの多くをプレビューできます。
詳細は Experience Builder の Storybook を参照してください。
Jimu UI は Experience Builder の公式 UI ライブラリであり、このライブラリのコンポーネントを考慮して UI 開発を利用することを強くお勧めします。その理由は以下の通りです。
基本的な 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 コンポーネントの使用が推奨されます。
const Widget = () => {
return <Paper variant="flat" shape="none" className="jimu-widget widget-xxx">...</Paper>
}
Paper コンポーネントはデフォルトで角が丸くなっています。shape="none" を設定するとその角が取り除かれ、ウィジェットの角のスタイルがレイアウトのデザインに追従するようになります。
Paper 上に直接表示されるテキストについては、Paper から継承されるためテキスト カラーを指定する必要はありません。
デフォルトのスタイル:
theme.sys.color.surface.papertheme.sys.color.surface.paperTextより明るいテキストを表示したい場合は、surface.paperHint を使用します。推奨される方法は <Typography color="paperHint" /> を使うことです。
<Typography color="paperHint">Secondary text</Typography>
その他のオプション:
<Paper variant="outlined" /><Paper shape="shape1" />(デフォルトは shape2)<Paper variant="flat" transparent />詳細はこちらのドキュメントをご参照ください。
Jimu UI は Experience Builder の主要なコンポーネント ライブラリーであり、カスタム ウィジェットやテーマを Experience Builder 全体のテーマと一貫性を持たせるために、まずこちらを使用することが望ましいです。Calcite Design System を使用する必要がある場合は、それも可能です。Calcite のサンプル ウィジェットを参照してください。Calcite コンポーネントを使用する際は、@esri/calcite-components ではなく、calcite-components からインポートするようにしてください。
Experience Builder にはウィジェットをスタイリングするいくつかの方法があります。
CSS-in-JS とは、ベンダープレフィックス、スコープ付き CSS、JS ロジック、テーマ機能など、CSS では解決できない問題に対処するために JavaScript で CSS を書く方法のことを指します。
Styled Components や Emotion など、よく知られている CSS-in-JS のライブラリがたくさんあります。Experience Builder では、スタイリングとテーマ設定を目的としたフレームワークとして Emotion を使用しています。
Emotion には 2 つのスタイリング パターンがあります。
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 例:

このパターンは 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 例:

Jimu UI は、UI 要素に素早くスタイルを適用するための同様の CSS ユーティリティー クラスを提供しています。
ここでは、w-100、p-3、bg-paper、text-paper、border を追加して、要素に以下のスタイルを適用します。
// render() 関数内:
return <div className="w-100 p-3 bg-paper text-paper border">
<p>This is a sample widget</p>
</div>;
Output 例:

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、.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 例:

この設定は、ウィジェットをアプリケーション全体のデザインと一貫性のあるものにしたい場合や、テーマ変更時に自動的にデザインを更新したい場合に必要です。
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 を構築する場合は、テーマを使用するコンポーネントにさらに分割することができます。
// 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>;
}

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

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