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:

  1. Implement Recenter widget
  2. Compiling the TSX file
  3. Add the widget to the application
  4. Source code
  5. 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, jsxFactory } 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;
}
  1. The first interface, Center, takes two number properties, x and y.
  2. Next, State extends off of Center. This means that it will have the same x and y properties that Center has, in addition to a boolean property called interacting and a number property called scale.
  3. Last, an interface called Style takes one string property called textShadow.

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

  1. First, the bind attribute is set to this, e.g. bind={this}.
  2. Next, class is set to the CSS.base value, i.e. "recenter-tool".
  3. The styles will reflect the textShadow property set a few lines prior.
  4. The onclick event (note the lower case 'c'), is set to call the private _defaultCenter method once the widget is clicked.
  5. Lastly, the widget UI itself will display the x: <value>, y: <value>, and scale: <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",
  "dojo/domReady!",

  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.3</title>
  <link rel="stylesheet" href="https://js.arcgis.com/4.3/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.3/"></script>
  <script>
    var map, recenter, view;
    require([
      "esri/Map",
      "esri/views/MapView",
      "app/Recenter",
      "esri/layers/VectorTileLayer",
      "dojo/domReady!"
    ], function(
      Map, MapView, Recenter, VectorTileLayer
    ) {
      map = new Map({
        basemap: "gray-vector"
      });
      var tileLyr = new VectorTileLayer({
        url: "https://www.arcgis.com/sharing/rest/content/items/bf79e422e9454565ae0cbe9553cf6471/resources/styles/root.json"
      });
      map.add(tileLyr);
      view = new MapView({
        container: 'viewDiv',
        map: map,
        center: [-100.33, 43.69],
        zoom: 4
      });
      view.then(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, jsxFactory } 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) {

  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 (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    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) {

    "use strict";
    var CSS = {
        base: "recenter-tool"
    };
    var Recenter = (function (_super) {
        __extends(Recenter, _super);
        function Recenter() {
            _super.apply(this, arguments);
        }
        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.jsxFactory.createElement("div", {bind: this, class: CSS.base, styles: styles, onclick: this._defaultCenter}, 
                widget_1.jsxFactory.createElement("p", null, 
                    "x: ", 
                    Number(x).toFixed(3)), 
                widget_1.jsxFactory.createElement("p", null, 
                    "y: ", 
                    Number(y).toFixed(3)), 
                widget_1.jsxFactory.createElement("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);
        };
        __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;
    }(decorators_1.declared(Widget)));
    return Recenter;
});

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.

Sample search results

TitleSample

There were no match results from your search criteria.