Create a custom widget

View live

Working with widgets is an essential part of the ArcGIS API for JavaScript. Typically, a widget is thought of as a piece of the API that encapsulates a specific set of functionality. The API provides ready-to-use widgets with predefined functionality. There may be circumstances where you may need to customize a widget to your own specifications. In these cases, a custom widget may be what is needed. The following steps go through creating a very basic "Hello World" widget. In addition, this sample also shows how you can work with your own localized message bundles within a custom widget. These steps are provided as a basic foundation needed when creating your own custom widget.

Description

This sample describes how to implement and create a custom widget. In addition to this basic "HelloWorld" sample, the application also shows how to work with message bundles to display localized strings within the widget. The steps to implement this are discussed below.

For the full code, please refer to Step 6 and click on the drop down for various files.

Before you get started

Widget development is written in TypeScript and compiled to JavaScript. In order to follow the steps below, you will need to make certain you have TypeScript installed. Any text editor should suffice as long as you are able to compile the resulting TypeScript code into a resulting JavaScript file. In addition to this, you should also be familiar with JSX. This allows you to define the widget's UI similarly to HTML. Lastly, widget development is largely reliant upon familiarity of Implementing Accessor. More information on these requirements can be found at:

Tutorial steps

1. Create project directory and file structure

There is no one-specific IDE required for writing TypeScript. As long as you have access to the compiler needed to generate the underlying JavaScript files, any IDE should work.

  • Create a new directory to contain all the widget files. In the screen capture below, Visual Studio Code is used. This is a free download and works well with TypeScript. A new folder called HelloWorld is created. This folder should also be accessible by a web server.

  • Inside the HelloWorld directory, create another directory called app, this will only contain the widget's files. Inside the app directory, create a new file called HelloWorld.tsx.

Each widget belongs in a .tsx file, which allows you to use JSX to define the UI.

  • In the HelloWorld directory, create a tsconfig.json file. This file is necessary to tell the compiler how the project should be compiled.

In the tsconfig.json file, add the following snippet:

                   
{
  "compilerOptions": {
    "module": "amd",
    "lib": ["ES2019", "DOM"],
    "noImplicitAny": true,
    "esModuleInterop": true,
    "sourceMap": true,
    "jsx": "react",
    "jsxFactory": "tsx",
    "target": "ES5",
    "experimentalDecorators": true,
    "preserveConstEnums": true,
    "suppressImplicitAnyIndexErrors": true,
    "importHelpers": true,
    "moduleResolution": "node"
  },
  "include": ["./app/*"],
  "exclude": ["node_modules"]
}

The tsconfig.json file specifies the root files and the compiler options required to compile the project.

Please refer to the tsconfig.json documentation for additional information specific to the include and exclude options.

tsconfig.png

2. Install the ArcGIS API for JavaScript 4.x Type Definitions

For a detailed discussion on setting up TypeScript within your environment, please refer to the TypeScript Setup guide topic.

In order to work with the ArcGIS API for JavaScript typings, you will need to install them via a simple command line command.

  • Open up a command prompt and browse to the HelloWorld directory.
  • Type the following into the command line
  
npm init --yes
npm install --save @types/arcgis-js-api

The syntax above can also be referenced with explanation at TypesScript Setup - Install the ArcGIS API for JavaScript Typings. These typings can be directly accessed at the jsapi-resources GitHub repo.

What you should now see is a package.json file in the root of the project directory in addition to a new node_modules directory that contains all of these ArcGIS API for JavaScript typings.

hello-world-types.png

3. Implement HelloWorld widget

Now you're ready to actually implement the custom widget.

Add dependency paths and import statements

Open HelloWorld.tsx and add the following lines of code.

          
import { subclass, property } from "esri/core/accessorSupport/decorators";

import Widget from "esri/widgets/Widget";

import { tsx, messageBundle } from "esri/widgets/support/widget";

const CSS = {
  base: "esri-hello-world",
  emphasis: "esri-hello-world--emphasis"
};
  • These lines import specific modules used for the widget implementation.

Take note that even though tsx is not explicitly being used in the code sample.

This is not required, but if using the tsconfig.json option "noUnusedLocals": true, you will need to reference tsx within the code, similar to

  
import { tsx } from "esri/widgets/support/widget";
tsx; // Reference tsx here, this will be used after compilation to JavaScript
  • Lastly, we set a CSS object with base and emphasis properties. These are used within the widget's render() method and is used as a lookup for classes, thereby centralizing all the CSS within the widget.

Extend Widget base class

Now in the HelloWorld.tsx file, add the following lines of code.

      
@subclass("esri.widgets.HelloWorld")
class HelloWorld extends Widget {
   constructor(params?: any) {
    super(params);
  }
}

Here, we are extending the Widget base class.The @subclass decorator is necessary for constructing subclasses off of a given base class.

Add widget properties

Within this class implementation, add these properties:

                                    
@subclass("esri.widgets.HelloWorld")
class HelloWorld extends Widget {

  constructor(params?: any) {
    super(params);
  }

  //----------------------------------
  //  firstName
  //----------------------------------

  @property()
  firstName: string = "John";

  //----------------------------------
  //  lastName
  //----------------------------------

  @property()
  lastName: string = "Smith";

  //----------------------------------
  //  emphasized
  //----------------------------------

  @property()
  emphasized: boolean = false;

  //----------------------------------
  //  messages
  //----------------------------------

  @property()
  @messageBundle("HelloWorld/assets/t9n/widget")
  messages: { greeting: any; } = null;
}

The first three properties have a @property decorator. This decorator is used to define an Accessor property. By specifying this decorator, you give this property the same behavior as other properties within the API.

The last property @messageBundle sets the bundleId used to localize a widget. This is useful as it will automatically populate the widget with the localized message bundle specified by the passed in bundleId. This bundleId must first be registered. More information on how to do this is shown in steps further below.

Add widget methods

Now, you will add both a public and private method to the widget.

                  
// Public method
render() {
  const greeting = this._getGreeting();
  const classes = {
    [CSS.emphasis]: this.emphasized
  };

  return (
    <div class={this.classes(CSS.base, classes)}>
      {greeting}
    </div>
  );
}

// Private method
private _getGreeting(): string {
  return `Hello, my name is ${this.firstName} ${this.lastName}!`;
}

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 sets two variables called greeting and classes. By default, functions referenced in your elements will have this set to the actual element. You can use the special bind attribute to change this, e.g. bind={this}.

The class property cannot be changed within the render() method. If there are dynamic classes, use the classes helper method instead.

For example,

         
render() {
  const baseClass = this.isBold && this.isItalic ? `${CSS.base} ${CSS.bold} ${CSS.italic}` :
    this.isBold ? `${CSS.base} ${CSS.bold}` :
    this.isItalic ? `${CSS.base} ${CSS.italic}` :
    CSS.base;
  return (
    <div class={baseClass}>Hello World!</div>
  );
}

will throw a runtime error because class cannot be changed. Instead use,

         
render() {
  const dynamicClasses = {
    [CSS.bold]: this.isBold,
    [CSS.italic]: this.isItalic
  };
  return (
    <div class={this.classes({CSS.base, dynamicClasses})>Hello World!</div>
  );
}

Lastly, the greeting messages returns Hello, my name is ${this.firstName} ${this.lastName}!

Export widget

At the very end of the code page, add a line to export the object into an easily-consumable external module.

 
export = HelloWorld;

For the full code sample, please refer to the Source code.

4. 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 the HelloWorld directory and typetsc. This command will look at your tsconfig.json file and compile the TypeScript based on its configuration.

You should now have a new HelloWorld.js file generated in the same directory as its .tsx file, in addition to a HelloWorld.js.map sourcemap file.

The sourcemap is provided if needed. For this particular example, it really isn't necessary but we show how you can create one as it may be useful in certain circumstances.

hello-world-compiled.png

5. 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 HelloWorld directory, create an index.html file.

For the complete index.html file, please refer to the Source code.

Add CSS

The widget references .esri-hello-world and .esri-hello-world-emphasis classes. Add a style element that references these classes similar as seen below.

            
<style>
  #btnSpace {
    padding: 10px;
  }

  .esri-hello-world {
    display: inline-block;
  }
  .esri-hello-world--emphasis {
    font-weight: bold;
  }
</style>

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.

Although Dojo has been removed from a majority of the API, it is still needed in this case to load the AMD modules.

Add a script element that handles loading this custom widget as seen below.

           
<script>
  let 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/HelloWorld and esri/intl modules.

   
require(["app/HelloWorld", "esri/intl"], (HelloWorld, intl) => {

});

First, in order to update the locale to use a set message bundle's strings, you must first register it. There are a couple ways of doing this. Both are provided in the sample code. The first option is provided here, whereas the second one is commented out in the source and does the same thing.

Register the message bundle

A message bundle is an object containing translations and can be stored as a file on disk, or as an object within code. Internally, the ArcGIS API for JavaScript uses JSON files containing localized translations. These bundles are identified by a unique string, ie. bundleId. For this specific example, two separate JSON files were created with strings provided for both English and French. For more informaton on working with locales and registering message bundles please refer to the esri/intl API documentation.

The following assumes a folder structure similar what is shown below.

         
root-folder/
  app/
    .tsx
    .js
  assets/
    t9n/
      widget_en.json
      widget_fr.json
  index.html
             
intl.registerMessageBundleLoader({
  pattern: "widgets-custom-widget/assets/",
  async fetchMessageBundle(bundleId, locale) {
    const [, filename] = bundleId.split("/t9n/");
    const knownLocale = intl.normalizeMessageBundleLocale(locale);
    const bundlePath = `./assets/t9n/${filename}_${knownLocale}.json`;

    // bundlePath is "https://domain-URL/widgets-custom-widget/assets/t9n/widget_<locale>.json"

    const response = await fetch(bundlePath);
    return response.json();
  }
});

Next, we'll create an array of names which will be used to cycle through and display in the widget's greeting.

      
let names = [
    { firstName: "John", lastName: "Smith" },
    { firstName: "Jackie", lastName: "Miller" },
    { firstName: "Anna", lastName: "Price" }
  ],
  nameIndex = 0;

We'll now instantiate the widget using the following syntax.

     
const widget = new HelloWorld({
  firstName: names[nameIndex].firstName,
  lastName: names[nameIndex].lastName,
  container: "widgetDiv"
});

Next, we'll create a function to cycle through the names.

     
function changeName() {
  widget.set(names[nameIndex++ % names.length]);
}

setInterval(changeName, 1000);

Lastly, create a simple button and wire up its click event handler to change the locale to either English or French.

         
const btnLocale = document.createElement("button");
btnLocale.style.padding = "10px";
btnLocale.classList.add("app-locale-button")
btnLocale.innerHTML = "Toggle Locale";
document.getElementById("btnSpace").appendChild(btnLocale);

btnLocale.addEventListener("click", () => {
  intl.getLocale() === "fr" ? intl.setLocale("en") : intl.setLocale("fr");
});

Source code

Put it all together

Your finished tutorial should look similar to the files below.

index.htmlHelloWorld.tsxHelloWorld.jswidget_en.jsonwidget_fr.json
                                                                                                                           
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />

    <title>Create a custom widget | Sample | ArcGIS API for JavaScript 4.19</title>

    <style>
      #btnSpace {
        padding: 10px;
      }

      .esri-hello-world {
        display: inline-block;
      }

      .esri-hello-world--emphasis {
        font-weight: bold;
      }
    </style>
    <script>
      let locationPath = location.pathname.replace(/\/[^\/]+$/, "");
      window.dojoConfig = {
        packages: [
          {
            name: "app",
            location: locationPath + "/app"
          }
        ]
      };
    </script>
    <script src="https://js.arcgis.com/4.19/"></script>
    <script>
      let widget;

    require(["app/HelloWorld", "esri/intl"], (HelloWorld, intl) => {
      // register message bundle loader for HelloWorld messages
      intl.registerMessageBundleLoader({
        pattern: "widgets-custom-widget/assets/",
        async fetchMessageBundle(bundleId, locale) {
          const [, filename] = bundleId.split("/t9n/");

          const knownLocale = intl.normalizeMessageBundleLocale(locale);
          const bundlePath = `./assets/t9n/${filename}_${knownLocale}.json`;

          const response = await fetch(bundlePath);
          return response.json();
        }
      });

      // It is also possible to register the message bundle
      // loader using syntax similar to what is provided below.
      // The 'createJSONLoader' is a helper function that provides similar
      // functionality to what the intl. registerMessageBundleLoader
      // function does above.

      // const bundleName = "widgets-custom-widget/assets/t9n/widget";
      // (async () => {

      //   intl.registerMessageBundleLoader(
      //     intl.createJSONLoader({
      //       pattern: "widgets-custom-widget/",
      //       base: "widgets-custom-widget",
      //       location: new URL("./", window.location.href)
      //     })
      //   );

      //   let bundle = await intl.fetchMessageBundle(bundleName);
      // })();

      let names = [{
            firstName: "John",
            lastName: "Smith"
          },
          {
            firstName: "Jackie",
            lastName: "Miller"
          },
          {
            firstName: "Anna",
            lastName: "Price"
          }
        ],
        nameIndex = 0;

      const widget = new HelloWorld({
        firstName: names[nameIndex].firstName,
        lastName: names[nameIndex].lastName,
        container: "widgetDiv"
      });

      function changeName() {
        widget.set(names[nameIndex++ % names.length]);
      }

      setInterval(changeName, 1000);

      // Add a very basic button that allows toggling the locale of the
      // displayed string.
      const btnLocale = document.createElement("button");
      btnLocale.style.padding = "10px";
      btnLocale.classList.add("app-locale-button")
      btnLocale.innerHTML = "Toggle Locale";
      document.getElementById("btnSpace").appendChild(btnLocale);

      // Add an event listener for when the button is clicked
      // If the locale is French, change to English
      // If the locale is English, change to French
      btnLocale.addEventListener("click", () => {
        intl.getLocale() === "fr" ? intl.setLocale("en") : intl.setLocale("fr");
      });
    });
  </script>
</head>

<body>
  <div id="widgetDiv" class="esri-widget"></div>
  <div id="container" class="esri-widget">
    <div id="btnSpace"></div>
  </div>
</body>
</html>

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.