Custom Recenter Widget
This tutorial goes over creating a custom widget that displays the MapView.center's X/Y coordinates in addition to its MapView.scale. In addition to displaying coordinates and scale, the view can also be recentered by clicking on the widget.
Before beginning this tutorial, make certain you have already completed Create custom widget where we go over the basics of creating a HelloWorld
widget. This tutorial will assume that all the necessary requirements are installed.
For a detailed discussion on setting up TypeScript within your environment, please refer to the TypeScript Setup guide topic.
The proceeding steps will begin with implementing the widget in the .tsx file.
Tutorial steps
- Implement Recenter widget
- Compiling the TSX file
- Add the widget to the application
- Source code
- Additional information
1. Implement Recenter TSX file
Add dependency paths and import statements
Create a .tsx file and name it Recenter
. Add the following lines of code.
/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />
import {subclass, declared, property} from "esri/core/accessorSupport/decorators";
import Widget = require("esri/widgets/Widget");
import watchUtils = require("esri/core/watchUtils");
import { renderable, tsx } from "esri/widgets/support/widget";
import Point = require("esri/geometry/Point");
import MapView = require("esri/views/MapView");
Create a type alias and interfaces
First we're going to create a Coordinates
type alias. Coordinates
is an esri/geometry/Point type that takes an array of numbers or any
. The latter is specified because the Point constructor also takes in other types besides just numbers.
type Coordinates = Point | number[] | any;
Next, we will create a few Typescript interfaces to aid in reusing object types.
interface Center {
x: number;
y: number;
}
interface State extends Center {
interacting: boolean;
scale: number;
}
interface Style {
textShadow: string;
}
- The first interface,
Center
, takes two number properties,x
andy
. - Next,
State
extends off ofCenter
. This means that it will have the samex
andy
properties thatCenter
has, in addition to a boolean property calledinteracting
and a number property calledscale
. - Last, an interface called
Style
takes one string property calledtextShadow
.
Add CSS variable
After adding the interfaces and type alias, set a CSS
variable with a base
property. This is used within the widget's render() method.
const CSS = {
base: "recenter-tool"
};
Extend Widget base class with constructor logic
Now in the Recenter.tsx file, add the following lines of code.
@subclass("esri.widgets.Recenter")
class Recenter extends declared(Widget) {
constructor() {
super();
this._onViewChange = this._onViewChange.bind(this);
}
}
Here, we are extending the Widget base class. The @subclass decorator is used in conjunction with declared and is necessary as they are both key components needed for constructing subclasses off of a given base class.
The constructor logic is binding the _onViewChange()
method to this
widget instance.
By default, functions referenced in your elements will have this
set to the actual element.
Add postInitialize logic
The postInitialize method is called after the widget is created but before the UI is rendered. In this particular case, we will initialize watchUtils to watch for changes to the View's
center, interacting, and scale properties in which it then calls the method, _onViewChange
.
Add the following code to handle this,
postInitialize() {
watchUtils.init(this, "view.center, view.interacting, view.scale", () => this._onViewChange());
}
Add widget properties
Within this class implementation, add these properties:
//----------------------------------
// view
//----------------------------------
@property()
@renderable()
view: MapView;
//----------------------------------
// initialCenter
//----------------------------------
@property()
@renderable()
initialCenter: Coordinates;
//----------------------------------
// state
//----------------------------------
@property()
@renderable()
state: State;
All of these properties have @property and @renderable decorators. The first one is used to define an Accessor property. By specifying this decorator within the property, you give this property the same behavior as other properties within the API. By specifying @renderable you are telling the widget to schedule a renderer for any modifications made to it.
In addition, you may notice that these properties are set to return an interface type or type alias specified in the beginning of this tutorial. For example, the state
property is of type State
which took two x
and y
number properties in addition to boolean interacting
and number scale
properties.
Add widget methods
Now, you will add both public and private methods to the widget.
// Public method
render() {
const {x, y, scale} = this.state;
const styles: Style = {
textShadow: this.state.interacting ? '-1px 0 red, 0 1px red, 1px 0 red, 0 -1px red' : ''
};
return (
<div
bind={this}
class={CSS.base}
styles={styles}
onclick={this._defaultCenter}>
<p>x: {Number(x).toFixed(3)}</p>
<p>y: {Number(y).toFixed(3)}</p>
<p>scale: {Number(scale).toFixed(5)}</p>
</div>
);
}
// Private methods
private _onViewChange() {
let { interacting, center, scale } = this.view;
this.state = {
x: center.x,
y: center.y,
interacting,
scale
};
}
private _defaultCenter() {
this.view.center = this.initialCenter;
}
The render() method is the only required member of the API that must be implemented. This method must return a valid UI representation. JSX is used to define the UI. With this said, it is important to note that we are not using React. The transpiled JSX is processed using a custom JSX factory therefore there is no direct equivalency between implementing a custom Widget and a React component.
The snippet above creates a style
variable of type Style
. The textShadow
property updates to the specified string value upon interaction. In addition, three variables: x
, y
, and scale
are set to the values of this.state
.
The UI is rendered based on the specified div
element attributes:
- First, the
bind
attribute is set tothis
, e.g.bind={this}
. - Next,
class
is set to theCSS.base
value, i.e. "recenter-tool". - The
styles
will reflect thetextShadow
property set a few lines prior. - The
onclick
event (note the lower case 'c'), is set to call the private_defaultCenter
method once the widget is clicked. - Lastly, the widget UI itself will display the
x: <value>
,y: <value>
, andscale: <value>
of the current view.
Export widget
At the very end of the code page, add a line to export the object into an easily- consumable external module.
export = Recenter;
For the full code sample, please refer to the Source code.
2. Compiling the TSX file
Now that the widget's code is implemented, compile the .tsx file to its underlying JavaScript implementation.
In the command prompt, browse to the location of this sample directory and type tsc
.
This compiles any specified .tsx files within the tsconfig.json's files to their equivalent .js files. You should now have a new Recenter.js file generated in the same directory as its .tsx file, in addition to a Recenter.js.map sourcemap file.
3. Add the widget to the application
Now that you generated the underlying .js file for the widget, it can be added into your JavaScript application. In the same Recenter directory, create an index.html file.
For the complete index.html file, please refer to the Source code.
Add CSS
The widget references the `.recenter-tool' class. Add a style element that references this class as seen below.
.recenter-tool {
padding: 2em;
position: absolute;
top: 1em;
right: 1em;
width: 150px;
height: 50px;
color: #fff;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.recenter-tool>p {
margin: 0;
}
Add the custom widget reference
Once you've created the custom widget, you need to load it. This comes down to telling Dojo's module loader how to resolve the path for your widget which means mapping a module identifier to a file on your web server. On the SitePen blog, there's a post discussing the differences between aliases, paths and packages which may help alleviate any questions specific to this.
Add a script element that handles loading this custom widget as seen below.
<script>
var locationPath = location.pathname.replace(/\/[^\/]+$/, "");
window.dojoConfig = {
packages: [
{
name: "app",
location: locationPath + "/app"
}
]
};
</script>
Reference and use the custom widget
Now that Dojo knows where to find modules in the app folder, require
can be used to load it along with other modules used by the application.
Here's a require block that loads the app/Recenter
module in addition to some others. There is also a reference to a few global variables for map
, recenter
, and view
.
var map, recenter, view;
require([
"esri/Map",
"esri/views/MapView",
"app/Recenter",
"esri/layers/VectorTileLayer"
function(Map, MapView, Recenter, VectorTileLayer) { }
The pertinent code snippet within this file is when the Recenter
widget is instantiated as seen below.
recenter = new Recenter({
view: view,
initialCenter: [-100.33, 43.69]
});
view.ui.add(recenter, "top-right");
Source code
Put it all together
The index.html code should look similar to the following.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no" />
<title>Custom Recenter Widget - 4.9</title>
<link rel="stylesheet" href="https://js.arcgis.com/4.9/esri/css/main.css">
<style>
html,
body,
#viewDiv {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}
html,
body {
font-family: sans-serif, 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif
}
.recenter-tool {
padding: 2em;
width: 150px;
height: 50px;
color: #fff;
background: rgba(0, 0, 0, 0.5);
}
.recenter-tool>p {
margin: 0;
}
</style>
<script>
var locationPath = location.pathname.replace(/\/[^\/]+$/, "");
window.dojoConfig = {
packages: [
{
name: "app",
location: locationPath + "/app"
}
]
};
</script>
<script src="https://js.arcgis.com/4.9/"></script>
<script>
var map, recenter, view;
require([
"esri/Map",
"esri/views/MapView",
"app/Recenter",
"esri/layers/VectorTileLayer"
], function(
Map, MapView, Recenter, VectorTileLayer
) {
map = new Map({
basemap: "gray-vector"
});
var tileLayer = new VectorTileLayer({
url: "https://www.arcgis.com/sharing/rest/content/items/92c551c9f07b4147846aae273e822714/resources/styles/root.json"
});
map.add(tileLayer);
view = new MapView({
container: 'viewDiv',
map: map,
center: [-100.33, 43.69],
zoom: 4
});
view.when(function() {
recenter = new Recenter({
view: view,
initialCenter: [-100.33, 43.69]
});
view.ui.add(recenter, "top-right");
});
});
</script>
</head>
<body>
<div id="viewDiv"></div>
</body>
</html>
/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />
import { subclass, declared, property } from "esri/core/accessorSupport/decorators";
import Widget = require("esri/widgets/Widget");
import watchUtils = require("esri/core/watchUtils");
import { renderable, tsx } from "esri/widgets/support/widget";
import Point = require("esri/geometry/Point");
import MapView = require("esri/views/MapView");
type Coordinates = Point | number[] | any;
interface Center {
x: number;
y: number;
}
interface State extends Center {
interacting: boolean;
scale: number;
}
interface Style {
textShadow: string;
}
const CSS = {
base: "recenter-tool"
};
@subclass("esri.widgets.Recenter")
class Recenter extends declared(Widget) {
constructor() {
super();
this._onViewChange = this._onViewChange.bind(this)
}
postInitialize() {
watchUtils.init(this, "view.center, view.interacting, view.scale", () => this._onViewChange());
}
//--------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------
//----------------------------------
// view
//----------------------------------
@property()
@renderable()
view: MapView;
//----------------------------------
// initialCenter
//----------------------------------
@property()
@renderable()
initialCenter: Coordinates;
//----------------------------------
// state
//----------------------------------
@property()
@renderable()
state: State;
//-------------------------------------------------------------------
//
// Public methods
//
//-------------------------------------------------------------------
render() {
const { x, y, scale } = this.state;
const styles: Style = {
textShadow: this.state.interacting ? '-1px 0 red, 0 1px red, 1px 0 red, 0 -1px red' : ''
};
return (
<div
bind={this}
class={CSS.base}
styles={styles}
onclick={this._defaultCenter}>
<p>x: {Number(x).toFixed(3)}</p>
<p>y: {Number(y).toFixed(3)}</p>
<p>scale: {Number(scale).toFixed(5)}</p>
</div>
);
}
//-------------------------------------------------------------------
//
// Private methods
//
//-------------------------------------------------------------------
private _onViewChange() {
let { interacting, center, scale } = this.view;
this.state = {
x: center.x,
y: center.y,
interacting,
scale
};
}
private _defaultCenter() {
this.view.goTo(this.initialCenter);
}
}
export = Recenter;
/// <amd-dependency path="esri/core/tsSupport/declareExtendsHelper" name="__extends" />
/// <amd-dependency path="esri/core/tsSupport/decorateHelper" name="__decorate" />
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
define(["require", "exports", "esri/core/tsSupport/declareExtendsHelper", "esri/core/tsSupport/decorateHelper", "esri/core/accessorSupport/decorators", "esri/widgets/Widget", "esri/core/watchUtils", "esri/widgets/support/widget"], function (require, exports, __extends, __decorate, decorators_1, Widget, watchUtils, widget_1) {
var CSS = {
base: "recenter-tool"
};
var Recenter = (function (_super) {
__extends(Recenter, _super);
function Recenter() {
var _this = _super.call(this) || this;
_this._onViewChange = _this._onViewChange.bind(_this);
return _this;
}
Recenter.prototype.postInitialize = function () {
var _this = this;
watchUtils.init(this, "view.center, view.interacting, view.scale", function () { return _this._onViewChange(); });
};
//-------------------------------------------------------------------
//
// Public methods
//
//-------------------------------------------------------------------
Recenter.prototype.render = function () {
var _a = this.state, x = _a.x, y = _a.y, scale = _a.scale;
var styles = {
textShadow: this.state.interacting ? '-1px 0 red, 0 1px red, 1px 0 red, 0 -1px red' : ''
};
return (widget_1.tsx("div", { bind: this, class: CSS.base, styles: styles, onclick: this._defaultCenter },
widget_1.tsx("p", null,
"x: ",
Number(x).toFixed(3)),
widget_1.tsx("p", null,
"y: ",
Number(y).toFixed(3)),
widget_1.tsx("p", null,
"scale: ",
Number(scale).toFixed(5))));
};
//-------------------------------------------------------------------
//
// Private methods
//
//-------------------------------------------------------------------
Recenter.prototype._onViewChange = function () {
var _a = this.view, interacting = _a.interacting, center = _a.center, scale = _a.scale;
this.state = {
x: center.x,
y: center.y,
interacting: interacting,
scale: scale
};
};
Recenter.prototype._defaultCenter = function () {
this.view.goTo(this.initialCenter);
};
return Recenter;
}(decorators_1.declared(Widget)));
__decorate([
decorators_1.property(),
widget_1.renderable()
], Recenter.prototype, "view", void 0);
__decorate([
decorators_1.property(),
widget_1.renderable()
], Recenter.prototype, "initialCenter", void 0);
__decorate([
decorators_1.property(),
widget_1.renderable()
], Recenter.prototype, "state", void 0);
Recenter = __decorate([
decorators_1.subclass("esri.widgets.Recenter")
], Recenter);
return Recenter;
});
//# sourceMappingURL=Recenter.js.map
Additional information
The files used in this example can be accessed from the source code above. Please use these files as a starting point to begin creating your own custom classes and widgets.
- TypeScript Setup
- Accessor class
- Implementing Accessor
- Create custom widget sample
- Styling
- TypeScript reference
- JSX reference
- Migrating from 3.x to 4.9