Widget development

Widgets are reusable user-interface components and are key to providing a rich user experience. The ArcGIS for JavaScript API provides a set of ready-to-use widgets. Beginning with version 4.2, it also provides a foundation for you to create custom widgets.

This guide topic discusses the basic fundamentals of widget development. It does so by discussing specific areas that you should focus on when transitioning to this new 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.

Please note that this framework is not intended to be a direct replacement for all Dijits. One such example would be when working with dgrid. Here, you would still need to use Dijit.

This topic discusses:

This guide is intended to provide a high-level overview of the widget framework. Please refer to the Create custom widget and Recenter widget samples for examples of how to create your own custom widget.

Development requirements

Prior to creating your own custom widgets, you will need to make certain you have the needed requirements. These will vary based on your widget requirements. The ones listed below are a bare minimum for widget development.

TypeScript

TypeScript is a superset of JavaScript. Once written, it can be compiled to plain JavaScript. The suggested approach to widget development is through TypeScript. There is a multitude of great online resources that go into detail on what TypeScript is, why it is used, and how you use it. Getting yourself familiarized with these basics will make the widget development process much easier to navigate.

JSX

JSX is a JavaScript extension syntax that allows us to describe our widget UI's similarly to HTML. It looks similar to HTML in that it can be used inline with JavaScript.

Familiarity with esri/core/Accessor

Accessor is one of the core features of 4.x and is the base for all classes, including widgets. Please see the Implementing Accessor topic for additional details on how this works and its usage patterns.

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. This allows us to define and modify common behavior in existing properties, methods, and constructors at design time. We discuss the most common types of widget decorators below.

@subclass (used in conjunction with declared)

These decorators can be thought of as the underlying glue that is used to create 4.x classes.

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

import Widget from "esri/widgets/Widget";

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

@property()

This 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.

@property()
name: string;

@renderable()

This decorator is used to schedule the render whenever a property is modified, the rendering is automatically scheduled and updated.

@renderable()
name: string;

Usually, when implementing class properties, you will use both @property() and @renderable(). For example,

@property()
@renderable()
name: string;

@aliasOf()

This decorator allows us to define a property alias. This can help keep code clean so as to not duplicate existing properties, (e.g. already implemented within a ViewModel). The full sample provided above does not use this decorator. If there was an associated HelloWorldViewModel associated with this file, its properties could be accessed directly via this approach therefore avoiding code duplication.

@aliasOf("viewModel.name")
name: string;

Widget implementation

The following steps provide a very high-level overview of the steps needed when implementing your own custom widget:

Extend the widget

At the very basic level, you will start by creating a widget by extending from the base Widget class.

// Import used to extend off of base Widget class
import Widget from "esri/widgets/Widget";

@subclass("esri.widgets.HelloWorld")
class HelloWorld extends declared(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.

// Create 'name' property
@property()
@renderable()
name: string = "John Smith";

// Create 'emphasized' property
@property()
@renderable()
emphasized: boolean = false;

// 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.

class HelloWorld extends declared(Widget) {

  constructor() {
    super();
    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.

  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.

See Widget rendering below for additional information specific to this.

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

The widget CSS is set via the class attribute.

render() {
  const classes = { "hello-world-emphasized": this.emphasized };

  return (
    <div class = {this.classes("hello-world",classes)}>
      {this._onNameUpdate()}
    </div>
  );
}

Prior to version 4.7, if you needed a more dynamic approach you would use classes. This expected an object where the key represented the CSS class to toggle. The class was added if its value was true and removed if false.

Beginning with 4.7, do not use this property. Rather use the classes helper method. This method simplifies setting the CSS class attribute.

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

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 object.

export = HelloWorld;

Completed code

The Create custom widget sample shows the .tsx file in its entirety. 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: (Deprecated at 4.7) This property allows CSS classes to be added and removed dynamically. Beginning with version 4.7, please use the classes helper method instead.
  • 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.
// 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>
  };
}

// Older method which works with the classes property.
// Use this if working in versions prior to 4.7.
render() {
  const dynamicClass = {
    [CSS.bold]: this.isBold,
    [CSS.italic]: this.isItalic
  };

  return (
    <div class={CSS.base} classes={dynamicClass}>Hello World!</div>
  );
}
render() {
  const dynamicStyles = {
    background-color: this.__hasBackgroundColor ? "chartreuse" : ""
  };

  return (
    <div styles={dynamicStyles}>Hello World!</div>
  );
}
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>
  );
}

private _afterUpdate(element: Element){
  console.log(element);
}

render() {
  return (
    <div afterUpdate={this._afterUpdate}>Hello world!</div>
  );
}
private _whatIsThis(): void {
  console.log('this === widget: ${this}');
}

render() {
  return (
    <div bind={this} onclick={this._whatIsThis}>'this' is the widget instance</div>
  );
}
// Key is specified as 'string's in sample below but can also be a number or object.

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

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.

// 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:

Content