ArcGIS for Developers

Widget implementation

An Experience Builder widget consist of the following files:

  • src: the widget source code folder

    • runtime: folder

      • widget.tsx: main entry file
      • assets: folder for assets used by widget.tsx
      • translations: folder for strings used by widget
    • setting: folder

      • setting.tsx: file for settings used by widget
      • assets: folder for assets used by settings
      • translations: folder for strings used by settings
  • dist: the compiled code folder of the widget. It uses the same structure as the source code folder

  • icon.svg: the icon of the widget in the widget panel

  • config.json: the widget's defaul config

  • manifest.json: view jim-core/lib/types/manifest for the list of properties

Client server

After you install the necessary modules for the developer edition of Experience Builder, you will run npm start in the client directory to launch the webpack server. This service will watch for any changes in the existing files and it will compile automatically. In most cases, you will not need to restart the webpack server when you edit your source code. However, in the following scenarios you will need to restart the server:

  • Installing a new module.
  • Adding, removing or renaming of a widget.
  • Editing the widget's manifest.json.
  • Adding, removing or renaming a file or folder.
Tip

You can use ctrl + c to stop the server.

Creating a widget

The easiest way to create the required files for a widget is to copy the demo widget located in the samples repo and paste it in the client\your-extensions\widget directory. Rename the demo widget folder, change the name and label in the manifest.json and the _widgetLabel property in the default.ts file located in the translations folder of the widget. There are two ways to create widget/react components; class and function. Below are some of the differences between the two.

Class components

  • Make use of ES6 class and extend the component class in React.
  • Can maintain its own data with state.
  • Passes props (properties) down to class components and access them with this.props.
  • Uses the render() method.

Function components

  • Basic JavaScript functions using arrows functions, but you can also use regular function keyword.
  • Can accept and use props.
  • Uses React Hooks to use state and other features.
  • There is no render() method.

All the samples used in the developer documentation with the exception of one are based on class components. We will add additional samples for function components in incremental releases.

Creating a widget using a class component

The example below demonstrates how to create a simple hello world class widget/component by extending the React.PureComponent class. The widget is declared with the type AllWidgetProps as export default class Widget extends React.Component<AllWidgetProps>, any> {, which uses props of the widget. The render() method is called to return the text hello world and the widget name coming from the propery _widgetLabel in the translations file.

               
//a custom pragma to transform your jsx into plain JavaScript
/** @jsx jsx */
import { React, AllWidgetProps, jsx } from "jimu-core";
export default class Widget extends React.Component<AllWidgetProps, any> {

    render() {
      return (
        <div className="widget-starter jimu-widget" style={{ overflow: "auto" }}>
          <p>Hello world!</p>
          <p>Widget Name: {this.props.label}</p>
        </div>
      );
    }

}

Creating a widget using a function component

The example below loads the modules needed to create a simple hello world function widget/component. The widget is declared as a function with the type AllWidgetProps as export default function Widget (props: AllWidgetProps) {, which uses props of the widget. The widget returns the text hello world and the widget name coming from the propery _widgetLabel in the translations file.

         
/** @jsx jsx */
import { AllWidgetProps, jsx} from "jimu-core";

export default function Widget (props:AllWidgetProps) {
    return <div className="widget-starter jimu-widget" style={{ overflow: "auto" }}>
        <p>Hello world!</p>
        <p>Widget Name: {props.label}</p>
      </div>
  }

Creating a setting UI for the widget

Creating the setting UI for a widget is similiar to creating a widget, with one exception, there is a setting.tsx in the setting folder. There are two ways to create the widget setting; class and function. Using a class component you can extend the BaseWidgetSetting class, which is part of the jimu-for-builder package. In this example, it demonstrates how to add a data source and interact with a config.json file in the settings panel. There are couple of imports to pay close attention to in this example including the following:

  • import React.Component is used to extend the class.
  • import DataSourceTypes is used for the data source type.
  • import SettingSection and SettingRow are some useful UI components for the settings.
  • import DataSourceSelector is another component used to select the data source.
  • import IMConfig is used for the config.json file.
       
import {React, Immutable, FormattedMessage} from 'jimu-core';
import {AllWidgetSettingProps} from 'jimu-for-builder';
import {DataSourceTypes} from 'jimu-arcgis';
import {SettingSection, SettingRow} from 'jimu-ui/advanced/setting-components';
import {DataSourceSelector} from 'jimu-ui/advanced/data-source-selector';
import {IMConfig} from '../config';
import defaultI18nMessages from './translations/default'

The BaseWidgetSetting class is declared with the types AllWidgetSettingProps and IMConfig. The supportedTypes property is used for the data source type webmap throughout the class. onDataSourceSelected is a class property with a function to handle the selection of the data source. The function this.props.OnSettingChange() is used to notify changes in the setting UI. The onP1Change and onP2Change class properties use React's event handling to help with setting the value for the config.json file.

         
export default class Setting extends React.Component{
  supportedTypes = Immutable([DataSourceTypes.WebMap]);

  onDataSourceSelected = (useDataSources: UseDataSource[]) => {
    this.props.onSettingChange({
      id: this.props.id,
      useDataSources: useDataSources
    });
  }
             
    onP1Change = (evt: React.FormEvent<HTMLInputElement>) => {
      this.props.onSettingChange({
        id: this.props.id,
        config: this.props.config.set('p1', evt.currentTarget.value)
      });
    }

    onP2Change = (evt: React.FormEvent<HTMLInputElement>) => {
      this.props.onSettingChange({
        id: this.props.id,
        config: this.props.config.set('p2', evt.currentTarget.value)
      });
    }

There are components in the jimu library that you can use in your widgets. For example, to help with the UI for the selection of the web map, the DataSourceSelector component is used to handle the type, the id, and the callback for the selected data source. In addition, SettingSection and SettingRow components are used to handle the formating of the container for the strings coming from the translations and the config file.

                 

render(){
    return <div className="sample-map-view-setting p-2">
      <DataSourceSelector
        types={this.supportedTypes}
        mustUseDataSource
        useDataSources={this.props.useDataSources}
        onChange={this.onDataSourceSelected}
        widgetId={this.props.id}
      />
      <SettingSection>
        <SettingRow label={<FormattedMessage id="p1" defaultMessage={defaultI18nMessages.p1}/>}> <input defaultValue={this.props.config.p1} onChange={this.onP1Change}/></SettingRow>
        <SettingRow label={<FormattedMessage id="p2" defaultMessage={defaultI18nMessages.p2}/>}> <input defaultValue={this.props.config.p2} onChange={this.onP2Change}/></SettingRow>
      </SettingSection>
    </div>
  }
}

Props

There are various props injected into a widget. You can access them through this.props for a class component or the props parameter {props}for a function component. For example, to access the props in the config.json for a widget using a class component, this.props.config can be used. To access it in a function component, use props.config. To learn more about the available properties, please review client/jimu-core/lib/types/props.ts in Experience Builder.

In some cases, you might need to access properties that are not in this.props, to accomplish this define a static function like the code snippet below in the widget class:

      

static mapExtraStateProps = (state: IMState) => {
    return {
      appMode: state && state.appRuntimeInfo && state.appRuntimeInfo.appMode
    };
  };

i18n support

Experience Builder uses the react-intl library to support i18n. To enable support for languages in your widget, declare the locales for the translatedLocales property in the widget's manifest.json. By convention the default locale must be first. For example, in the snippet below the default locale for the translatedLocales property is English (United States), followed by Spanish, and Mandarin Chinese.

     
 "translatedLocales": [
    "en",
    "es",
    "zh-cn"
  ]

The translations strings need to be a file called default.ts, which is located in the runtime/translations and settings/translations folders. The default.ts defines the default strings, which you can import into your widget and use for the default message. For instance, in the widget.tsx, there are a couple ways to access the translated strings such as:

        
Class component
this.props.intl.formatMessage({id: '_widgetLabel', defaultMessage: defaultMessages._widgetLabel})

Function component
props.intl.formatMessage({id: '_widgetLabel', defaultMessage: defaultMessage._widgetLabel})

JSX
<FormattedMessage id="widgetProperties" defaultMessage={defaultMessages.widgetProperties}/>

MapView/SceneView

In most experiences, a widget will need to work with a map view/scene view and also access layers in the views as well. To ensure we have a consistent extensibility model a MapViewManager class is available in jimu-arcgis in addition to other classes and methods to make it easier to use within the Experience Builder framework. Basically every widget can call the createJimuMapView method to add a map/scene view to the MapViewManager and make it available for use by other widgets.

      
      MapViewManager.getInstance().createJimuMapView({
        mapWidgetId: this.props.id,
        view: new MapView(options),
        datasourceId: webmapDs.id,
        isActive: true
      })

Other widgets that need to use the map view/scene view can use the JimuMapViewSelector to select it in the setting UI. The selected map/scene is saved in WidgetJson.useMapWidgetsIds.

 
<JimuMapViewSelector onSelect={this.onMapWidgetSelected} useMapWidgetIds={this.props.useMapWidgetIds} />

Modules in the ArcGIS API for JavaScript

By default, Experience Builder does not load the ArcGIS API for JavaScript (JSAPI) when the app loads. There are two options to utilize JSAPI modules:

  • Widgets dependent on the JSAPI (e.g., almost nothing can be done without JSAPI)
    • Declare jimu-arcgis dependency in the widget's manifest.json.
    • Import the module you need import Query = require('esri/tasks/support/Query') in the widget.tsx.
     
  const query = new Query({
    where: `${typeIdField} = ${graphic.attributes[objectIdField]}`,
    outFields: ['FirstName'],
    returnGeometry: true
  })
  • Widgets dependent on JSAPI conditionally (e.g., can do something without JSAPI)
    • use import {loadArcGISJSAPIModules} and loadArcGISJSAPIModules([]) to dynamically load modules.
      
  loadArcGISJSAPIModules(['esri/widgets/Directions']).then(modules => {
          [this.Directions] = modules;
          this.setState({
            apiLoaded: true
         });
        })

Support inline editing

Every configurable widget should provide a setting page to allow the configuration of a widget. However, in some configurations it will be easier to modify settings on the widget rather than the setting panel. In this workflow, a widget can support this feature by providing inline editing.

There are a couple of way to implement inline editing:

  • Declare supportInlineEditing in the widget's manifest.json under the properties object. In this instance, the widget will have an edit toolbar when the widget is launched in builder. Text widget is implemented this way.
  • Declare hasEmbeddedLayout in widget's manifest.json under the properties object. In this case, we recommend using a layout component to ensure users can drag & drop other widgets in/out of the widget. There are two layout components exported from jimu-layouts/layout-builder and jimu-layouts/layout-runtime. In your widget, you will need to use the layout component exported from jimu-layouts/layout-runtime. To access the component exported from jimu-layouts/layout-builder, use this.props.builderSupportModules.LayoutClass. List widget uses this technique.
  • Declare CONTEXT_TOOL extensions in the widget's manifest.json. The declared extensions will be available in the selection toolbar. Image widget is used in this way by adding shape and crop tools to the selection toolbar.

To support inline editing, the widget might have some modules that are only required when the widget is launched in the builder. In this scenario, you would place these modules in the builder-support.tsx. This file should be in the same folder with widget.tsx. The modules in this file will be available in this.props.builderSupportModules.widgetModules once the widget is launched in builder.

Best practices

  • Provide a root CSS class name for your widget, using widget-<widget name> as the widget's class name and widget-setting-<widget name> as the widget setting's class name.
  • Use import {} from 'jimu-core' to load the built in 3rd party libraries. For instance, import {React} from 'jimu-core; if using import {} from '3rd_lib', the size of your widget will be larger because the lib will be built into your widget.
  • Create a Typesafe config file in the widget src folder and use it in both widget.tsx and setting.tsx.
  • Utilize the out-of-box UI components whenever you can, learn more about creating UI for your widgets.