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 = require("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 = require("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.

The base CSS is set via the class attribute. Note that the base CSS should not change inside the render method.

render() {
  const classes = { "hello-world-emphasized": this.emphasized };
  return (
    <div class = "hello-world" join={classes}>
      {this._onNameUpdate()}
    </div>
  );
}

If needing a more dynamic approach, use classes. This expects an object where the key represents the CSS class to toggle. The class is added if its value is true and removed if false.

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: This property allows CSS classes to be added and removed dynamically.
  • 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 method invokes event handlers to this and its respective value.
  • 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.


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

Additional information

Please refer to these additional links for further information: