Widget development

Widgets are reusable user-interface components and are key to providing a rich user experience. The ArcGIS Maps SDK for JavaScript provides the ability to build custom widgets that fit your unique requirements.

This guide topic discusses the basic fundamentals of custom widget development and specific areas that you should focus on when transitioning to this framework. The foundation for creating custom widgets remains consistent, regardless of the widget's intended functionality. The Additional information section has extra resources to help get you started.

Development requirements

Prior to creating your own custom widgets, make certain you meet the minimum requirements including being able to build the application locally on your machine. This process has several more steps when compared to creating a vanilla JavaScript application hosted on a web server and run in a browser.

TypeScript

The suggested approach to widget development is through TypeScript. One of the many advantages of this approach is the use of decorators to enhance functionality. There is an excellent TypeScript Setup guide page that provides basic steps to set up your TypeScript development environment with the ArcGIS Maps SDK for JavaScript. There are also many great online resources that go into detail on what TypeScript is, why it is used, and how you use it. Becoming familiarized with these basics will make the widget development process much easier.

JSX

JSX is a JavaScript extension syntax that allows us to describe the widget user interface similarly to HTML. It is commonly associated with React, but it can also be used in other implementations. It looks similar to HTML in that it can be used inline with JavaScript. Custom widgets are built using the .tsx extension which combines TypeScript with JSX, and it is compiled directly to JavaScript.

Familiarity with esri/core/Accessor

Accessor is one of the core features of 4.x and is the base for all classes. This includes custom widgets because they extend an existing API class to create a subclass, or child class. The custom widget inherits the methods and properties from its parent class by calling the super() method. Please see the Implementing Accessor topic for additional details and usage patterns.

@arcgis/core

The @arcgis/core ES modules have the same underlying API functionality as the AMD modules. It is recommended to use @arcgis/core for building custom widgets because ES modules work seamless with almost all major JavaScript frameworks and build tools because they do not need a specialized AMD loader, and they can be consumed natively in the browser and in modern build tools. The modules are installed locally on your development machine using npm.

Widget life cycle

Before you begin developing, it's important to have a general understanding of a widget's life cycle. Regardless of the type of widget, the general concepts specific to its life cycle remain the same. These are:

  1. constructor (params) - This is where the widget is initially created while setting any needed properties. Since the widget is derived from Accessor, you get access to getting, setting, and watching properties as discussed in the Working with properties topic.
  2. postInitialize() - This method is called after the widget is created but before the UI is rendered.
  3. render() - This is the only required method and is used to render the UI.
  4. destroy() - Method to release the widget instance.

TypeScript decorators

Widget development takes advantage of TypeScript decorators, and the API uses decorators as the underlying glue that is used to create classes. This allows for enhancing common behaviors in existing properties, methods, and constructors at design time. We discuss the most common types of widget decorators below.

@subclass()

The @subclass() decorator is used to extend an API class. It also allows you to specify a declaredClass property as a string in the constructor. This helps the API differentiate between the existing class that you are extending where the declaredClass is read-only, and the custom one you are building.

The snippet below imports and extends the base Widget class and defines the UI in the render method. JSX is used to define the UI. In this simple scenario, a div element is created and defines a string called John Smith as its content.

Use dark colors for code blocksCopy
          
1
2
3
4
5
6
7
8
9
10
import Widget from "@arcgis/core/widgets/Widget.js";

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

@property()

The @property() decorator is used to define an Accessor property. Any property defined with this decorator can now be get and set. In addition, you can watch for any property changes.

Use dark colors for code blocksCopy
  
1
2
@property()
name: string;

@aliasOf()

The @aliasOf() decorator creates a two-way binding between the property it decorates and an inner property of one of its members.

Use dark colors for code blocksCopy
  
1
2
@aliasOf("initialCenter.extent")
extent: Extent;

Widget implementation

The following steps provide a simple overview of the steps needed when implementing your own custom widget:

Create a new widget class

Start by extending the base Widget class and specifying the declaredClass in the @subclass() constructor.

Use dark colors for code blocksCopy
      
1
2
3
4
5
6
import Widget from "@arcgis/core/widgets/Widget.js";

@subclass("esri.widgets.HelloWorld")
class HelloWorld extends Widget {

}

Implement properties and methods

Next, you can implement any properties and/or methods specific for that widget. This snippet shows how to take advantage of decorators for these properties.

Use dark colors for code blocksCopy
      
1
2
3
4
5
6
// Create 'name' property
@property()
name: string = "John Smith";

// Create private _onNameUpdate method
private _onNameUpdate(): string { return '${this.name}';}

By default, functions referenced in your elements will have this set to the actual element. Optionally, you can use the bind attribute to update this. The following binds the _onNameUpdate callback method which is used when listening for name property updates. This is displayed in the postInitialize method below.

Use dark colors for code blocksCopy
        
1
2
3
4
5
6
7
8
class HelloWorld extends Widget {

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

}

The postInitialize method is called when the widget's properties are ready, but before it is rendered. In the snippet below we are watching for the name property. Once updated, it calls the _onNameUpdate callback method. The watchUtils.init() call returns a WatchHandle object which is then passed into own(). This helps tidy up any resources once the widget is destroyed.

Use dark colors for code blocksCopy
        
1
2
3
4
5
6
7
8
  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);
  }

Render the widget

After the properties are implemented, the widget's UI is rendered using JSX. This is handled in the widget's render method, which is the only required method needed for widget implementation.

Please note that widgets created as JSX elements are not yet supported. For example, the following snippet will not work.

const search = <Search view={view} />;

Use dark colors for code blocksCopy
       
1
2
3
4
5
6
7
render() {
  return (
    <div>
      {this._onNameUpdate()}
    </div>
  );
}

Lastly, call destroy on the widget to dispose of the widget and free up all resources registered with the own() method referenced in postInitialize below.

Use dark colors for code blocksCopy
      
1
2
3
4
5
6
postInitialize() {
  const handle = watchUtils.init(this, "name", this._onNameUpdate);

  // Helper used for cleaning up resources once the widget is destroyed
  this.own(handle);
}

Export module

At the very end of the code page, add a line to export the module. Exporting makes the capabilities of the widget available via an import statement.

Use dark colors for code blocksCopy
 
1
export default HelloWorld;

Completed code

The jsapi-custom-widget sample shows the entire widget.tsx file. This TypeScript file uses this extension to indicate that the class uses JSX, e.g. .ts + .jsx = .tsx.

Widget rendering

The properties listed below can be used for rendering the widget:

  • classes: A utility method used for building the value for a widget's class property. This aids in simplifying CSS class setup.
  • styles: Allows styles to be changed dynamically.
  • afterCreate: This callback method executes after the node is added to the DOM. Any child nodes and properties have already been applied. Use this method within render to access the real DOM node. It is also possible to use per element.
  • afterUpdate: This callback method executes every time the node is updated.
  • bind: This property is used to set the value of this for event handlers.
  • key: This is used to uniquely identify a DOM node among its siblings. This is important if you have sibling elements with the same selector and the elements are added/removed dynamically.
classesclassesstylesafterCreateafterUpdatebindkey
Use dark colors for code blocksCopy
           
1
2
3
4
5
6
7
8
9
10
11
// Newer method which works with the Widget's classes helper method.
render() {
  const dynamicClass = {
    [CSS.bold]: this.isBold,
    [CSS.italic]: this.isItalic
  }

  return {
    <div class={this.classes(CSS.base, dynamicClass)}>Hello World!</div>
  };
}

In addition to the methods mentioned above, there is also a storeNode convenience method. You would use this to assign an HTMLElement DOM node reference to a variable. This uses a custom data attribute, data-node-ref, to store a reference to an element's DOM node. In order for this to work correctly, it must also be bound to the widget instance, e.g. bind={this}, as shown in the snippet below.

Use dark colors for code blocksCopy
           
1
2
3
4
5
6
7
8
9
10
11
// Assign the data-node-ref attribute to a DOM node value.
// It should be used in conjunction with the `bind` property
// and is used when working with the storeNode convenience method.

rootNode: HTMLElement = null;

render() {
  return (
    <div afterCreate={storeNode} bind={this} data-node-ref="rootNode" />
  );
}

Additional information

Please refer to these additional links for further information:

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.