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:
- Development requirements
- Widget life cycle
- TypeScript decorators
- Widget implementation
- Completed code
- Widget rendering
- Additional information
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:
- 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.
- postInitialize() - This method is called after the widget is created but before the UI is rendered.
- render() - This is the only required method and is used to render the UI.
- 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.
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: