ウィジェット開発

出典:ArcGIS Maps SDK for JavaScript - Widget development

ウィジェットは再利用可能な UI コンポーネントで、リッチな UX を提供する鍵となります。ArcGIS Maps SDK for JavaScript では、完全にカスタム化されたウィジェットを HTML 要素として View に含めることができます。これには、完全に新しい機能を持つ独自のビューを開発する必要があり、ウィジェットの ViewModel を拡張することとは異なります。

カスタムウィジェットを作成する場合、@arcgis/core ES モジュールをお好みのフロントエンド JavaScript フレームワークと共に使用することをお勧めします。React、Angular、Vue.js などのフレームワークは必須ではありませんが、最大の拡張性を提供します。SDK と疎結合で、フレームワークのよく知られた UI/UX パターンとコンポーネントのライフサイクルに準拠した複数のウィジェットを作成できます。独自のカスタムウィジェットの作成方法については、jsapi-custom-widget のサンプルを参照してください。

非推奨のお知らせ
バージョン 4.27 以降、完全なカスタム ウィジェットを構築するための基礎として、/esri/widgets/Widget をサブクラス化または拡張することは推奨されなくなりました。このパターンでは、SDK 内部への依存関係が作成され、文書化されていない変更が頻繁に発生する可能性があります。以下のガイド内容は、レガシー パターンに従ったものであり、既存のウィジェットを保守する場合にのみ使用してください。

このフレームワークはすべての Dijit を直接差し替えることを意図していません。例えば、dgrid の利用には Dijit が必要です。

トピック

開発要件

独自のカスタムウィジェットを作成する前に、自分のマシンでローカルにアプリケーションをビルドできるなど、最低限の要件を満たしていることを確認してください。このプロセスは、Web サーバー上でホストされ、ブラウザー上で実行される標準的な JavaScript アプリケーションを作成する場合に比べ、いくつかのステップが増えます。

TypeScript

TypeScript を使うことをお勧めします。その利点のひとつは、デコレーターを使って機能を強化できることです。TypeScript のセットアップ ガイド ページでは、ArcGIS Maps SDK for JavaScript を使用して TypeScript 開発環境をセットアップするための基本的な手順を説明しています。また、TypeScriptとは何か、なぜ使われるのか、どのように使うのかについて詳しく説明したオンライン リソースもたくさんあります。これらの基本に慣れることで、ウィジェット開発プロセスがより簡単になります

JSX

JSX は、JavaScriptの拡張構文で、HTMLと同様にウィジェットのユーザーインターフェイスを記述することができます。一般的には React と関連していますが、他の実装においても使うことができます。JavaScript とインラインで使えるという点では、HTML に似ています。カスタム ウィジェットは、TypeScript と JSX を組み合わせた拡張子 .tsx を使ってビルドされ、JavaScript に直接コンパイルされます。

esri/core/Accessor の理解

Accessor は 4.x の主要な特徴のひとつであり、ウィジェットを含むすべてのクラスの基盤です。既存の API クラスを拡張してサブクラス、つまり子クラスを作成するため、カスタムウィジェットもこれに含まれます。カスタムウィジェットは、super() メソッドを呼び出すことで、親クラスからメソッドとプロパティを継承します。その他の詳細や使用パターンについては、Implementing Accessor のトピックを参照してください。

arcgis/core

@arcgis/core のESモジュールは、AMD モジュールと同じ API 機能を持ちます。 ES モジュールは専用の AMD ローダーを必要としないため、ほぼすべての主要な JavaScript フレームワークやビルドツールでシームレスに動作し、ブラウザや最新のビルドツールでネイティブに消費できるため、カスタムウィジェットの構築には @arcgis/core を使用することをお勧めします。モジュールは npm を使って開発マシンのローカルにインストールします。

ウィジェット ライフサイクル

開発をはじめる前に、ウィジェット ライフサイクルを理解している必要があります。ウィジェットの種類にかかわらず、ライフサイクル特有の一般的な概念は同じです。

  1. constructor (params) - プロパティを設定している間に、ウィジェットが最初に作成されるところです。ウィジェットは Accessor から生じるので、get、set、watch を使いプロパティへアクセスできます(get、set、watch の詳細は Working with properties をご参照ください)。
  2. postInitialize() - このメソッドは、ウィジェット作成後、UI の描画前に呼ばれます。
  3. render() - ウィジェット開発で唯一の必須となるメソッドです。UI を描画するために使用されます。
  4. destroy() - ウィジェットのインスタンスを解放するためのメソッドです。

TypeScript デコレーター

ウィジェット開発では TypeScript のデコレーターが活用され、API ではデコレーターがクラスの作成に使用される基礎的なグルーとして使用されます。これにより、既存のプロパティ、メソッド、コンストラクタに共通する動作を設計時に拡張することができます。ウィジェット デコレーターの最も一般的なタイプについて、以下で説明します。

API デコレーターを使用する場合は、後方互換性のために useDefineForClassFields フラグを false に設定する必要があります。詳細は TSConfig を参照してください。

@subclassdeclared と共に使われます)

@subclass() デコレーターは、API クラスを拡張するために使用します。また、コンストラクタで declaredClass プロパティを文字列で指定することもできます。これは、declaredClass が読み取り専用である既存のクラスと、作成しようとしているカスタム クラスを区別するのに役立ちます。

以下のスニペットは、ベースとなる Widget クラスをインポートして継承し、render メソッドで UI を定義しています。JSX は UI を定義するために使用されます。この単純なシナリオでは、div 要素が作成され、そのコンテンツとして John Smith という文字列が定義されます。

import Widget from "@arcgis/core/widgets/Widget.js";

@subclass("esri.widgets.HelloWorld")
class HelloWorld extends declared(Widget) {
  render() {
    return (
      <div>John Smith</div>
    );
  }
}

@property()

Accessor プロパティを定義するには @property() デコレーターを使用します。このデコレーターで定義されたプロパティはすべて、getset ができます。さらに、プロパティの変更を watch することもできます。

@property()
name: string;

@aliasOf()

@aliasOf() デコレーターは、デコレートするプロパティとそのメンバーの内部プロパティとの間に双方向バインディングを作成します。

@aliasOf("initialCenter.extent")
extent: Extent;

ウィジェットの実装

以下のステップは、独自のカスタム ウィジェットを実装する際に必要なステップの簡単な概要を提供します。

  • ウィジェットの拡張
  • プロパティとメソッドの実装
  • ウィジェットのレンダリング
  • エクスポート モジュール
  • 新しいウィジェット クラスの作成

    ベースとなる Widget クラスを拡張し、@subclass() コンストラクタで宣言されたクラスを指定することから始めます。

    // 拡張に使用する Widget クラスのインポート
    import Widget from "@arcgis/core/widgets/Widget.js";
    
    @subclass("esri.widgets.HelloWorld")
    class HelloWorld extends Widget {
    
    }
    

    プロパティとメソッドの実装

    次に、ウィジェット特有のプロパティとメソッドを実装します。このコードは、プロパティ作成において、デコレーターをどのように利用するのかを示しています。

    // 'name' プロパティの作成
    @property()
    name: string = "John Smith";
    
    // private _onNameUpdateメソッドの作成
    private _onNameUpdate(): string { return '${this.name}';}
    

    デフォルトでは、要素の中で参照される関数は、実際の要素を参照する this を持ちます。必要に応じて、this の参照先を変更するために bind 属性を使用します。以下では、name プロパティを更新するときに呼ばれる _onNameUpdate コールバック関数を結び付けています。これは次の postInitialize メソッドに表示されます。

    class HelloWorld extends Widget {
    
      constructor(params?: any) {
        super(params);
        this._onNameUpdate = this._onNameUpdate.bind(this);
      }
    
    }
    

    postInitialize メソッドは、ウィジェットのプロパティは用意されているが、描画される前に呼ばれます。以下のコードは name プロパティを監視しています。プロパティが更新されると、_onNameUpdate コールバック関数を呼びます。watchUtils.init()WatchHandle オブジェクトを返し、own() に渡されます。これは、ウィジェットを削除した時点で、リソースを解放するメソッドです。

      import * as watchUtils from "@arcgis/core/core/watchUtils.js";
    
      postInitialize() {
        const handle = watchUtils.init(this, "name", this._onNameUpdate);
    
        // Helper used for cleaning up resources once the widget is destroyed
        this.own(handle);
      }
    
    

    ウィジェットのレンダリング

    プロパティが実装された後、ウィジェットの UI は JSX を使用して描画されます。これはウィジェットの renderer メソッドに処理されます。これは、ウィジェットの実装に必要な唯一のメソッドです。

    JSX 要素として作成されたウィジェットは、まだサポートされていないことに注意してください。例えば、以下のスニペットは動作しません。
    
    const search = <Search view={view} />;
    
    // UI の描画
    render() {
      return (
        <div>
          {this._onNameUpdate()}
        </div>
      );
    }
    

    最後に、destroy を呼びます。このメソッドはウィジェットを削除し、postInitialize() 内で参照されている own() メソッドに登録された、すべてのリソースを解放します。

    postInitialize() {
      const handle = watchUtils.init(this, "name", this._onNameUpdate);
    
      // ウィジェットが削除された時点でリソースを解放するメソッド
      this.own(handle);
    }
    

    エクスポート モジュール

    コードページの一番最後に、モジュールをエクスポートする行を追加します。エクスポートは、ウィジェットの機能を import 文によって利用可能にします。

    export default HelloWorld;
    

    完成したコード

    jsapi-custom-widget サンプルは、widget.tsx ファイル全体を示します。TypeScript ファイルは、クラスが JSX を使用していることを示すために次のような拡張子を使用しています。例:.ts + .jsx = .tsx

    ウィジェット レンダリング

    以下のプロパティは、ウィジェットのレンダリングに使用できます。

  • classes: ウィジェットの class プロパティの値を構築するために使用されるユーティリティ メソッドです。これは、CSS クラスのセットアップを簡略化するのに役立ちます。
  • // Widget のクラス ヘルパー メソッドで動作する新しいメソッド
    render() {
      const dynamicClass = {
        [CSS.bold]: this.isBold,
        [CSS.italic]: this.isItalic
      }
    
      return {
        <div class={this.classes(CSS.base, dynamicClass)}>Hello World!</div>
      };
    }
    
  • styles: スタイルを動的に変更できるようにします。
  • render() {
      const dynamicStyles = {
        background-color: this.__hasBackgroundColor ? "chartreuse" : ""
      };
    
      return (
        <div styles={dynamicStyles}>Hello World!</div>
      );
    }
    
  • afterCreate: このコールバック メソッドは、ノードが DOM に追加された後に実行されます。子ノードやプロパティはすでに適用されています。実際の DOM ノードにアクセスするには、render 内でこのメソッドを使用します。要素ごとに使用することも可能です。
  • private _doSomethingWithRootNode(element: Element){
      console.log(element);
    }
    
    private _doSomethingWithChildNode(element: Element){
      console.log(element);
    }
    
    // Access real DOM node within render()
    render() {
      return (
        <div afterCreate={this._doSomethingWithRootNode}>Hello World!</div>
      );
    }
    
    // Can also be used per element
    
    render() {
      return (
        <div afterCreate={this._doSomethingWithRootNode}>
          <span afterCreate={this._doSomethingWithChildNode}>Hello World!<span>
        </div>
      );
    }
    
  • afterUpdate: このコールバック メソッドは、ノードが更新されるたびに実行されます。
  • private _afterUpdate(element: Element){
      console.log(element);
    }
    
    render() {
      return (
        <div afterUpdate={this._afterUpdate}>Hello world!</div>
      );
    }
    
  • bind: このプロパティは、イベント ハンドラーの `this` の値を設定するために使用される。
  • private _whatIsThis(): void {
      console.log('this === widget: ${this}');
    }
    
    render() {
      return (
        <div bind={this} onclick={this._whatIsThis}>'this' is the widget instance</div>
      );
    }
    
  • key: これは、兄弟要素の中で DOM ノードを一意に識別するために使用されます。これは、同じセレクターを持つ兄弟要素があり、要素が動的に追加/削除される場合に重要です。
  • // キーは、以下のサンプルでは文字列で指定されていますが、数値やオブジェクトを指定することもできます。
    
    render() {
      const top = this.hasTop ? <li class={CSS.item} key="top">Top</header> : null;
      const middle = this.hasMiddle ? <li class={CSS.item} key="middle">Middle</section> : null;
      const bottom = this.hasBottom ? <li class={CSS.item} key="bottom">Bottom</footer> : null;
    
      return (
        <ol>
          {top}
          {middle}
          {bottom}
        </ol>
      );
    }
    

    上記のメソッドに加えて、storeNode という便利なメソッドもあります。HTMLElement の DOM ノード リファレンスを変数に代入するときに使います。これは、カスタム データ属性 data-node-ref を使用して、要素の DOM ノードへの参照を格納します。これを正しく動作させるには、以下のスニペットに示すように、bind={this} のように、ウィジェット インスタンスにもバインドする必要があります。

    // data-node-ref 属性を DOM ノード値に割り当てます。
    //これは `bind` プロパティと組み合わせて使用する必要があり、
    // storeNode コンビニエンス メソッドを使用するときに使用します。
    
    rootNode: HTMLElement = null;
    
    render() {
      return (
        <div afterCreate={storeNode} bind={this} data-node-ref="rootNode" />
      );
    }
    

    関連リンク