Hide Table of Contents
Writing a Class

This tutorial covers the following:

  1. Using modules and classes to manage your code
  2. Create a module
  3. Create a class
  4. Load a custom module
  5. Use a custom class
  6. Inherit from Esri classes
  7. Additional examples
  1. Using modules and classes to manage your code

    Most developers who have worked on the web for a significant amount of time have likely evolved in how they manage their JavaScript code. When first starting out, it is common to put everything (HTML, CSS and JavaScript) in a single file. Eventually, this becomes cumbersome and JavaScript (as well as CSS) is moved to a separate file or files. As the amount of JS code continues to grow, so do the number of JS files. Over time, managing several different files that reference variables and objects defined in other places becomes hard to maintain.

    One solution to this problem is to use patterns from object-oriented (OO) programming to handle large JavaScript code bases. By using an object-oriented style, you can avoid spaghetti-code in your apps as well as increase the likelihood for code to be re-used. Application maintenance is also simplified allowing for bugs to be fixed faster and new features to be implemented in less time.

    The primary goals of this tutorial are to save developers some of the headache of figuring this out on their own, provide some examples of how to write classes with the tools provided by Dojo, how to use classes provided by both Dojo and the ArcGIS API for JavaScript in your own classes and how to package classes as modules that conform to the Asynchronous Module Definition (AMD) specification.

    This tutorial will walk through writing a class to search Seat Geek via their API which accepts a latitude, longitude coordinate pair and a search radius. The Seat Geek API returns information for events (concerts, baseball games, etc.) that fall within the specified area of interest.

    The class to search Seat Geek will be called "SeatGeekSearch" and will live in a folder named "extras". The module where this class will live is "extras/SeatGeekSearch" and, rather than hard-coding that name, the module name is derived from the path to SeatGeekSearch.js.

    Before working with the various Dojo components that help make OO JS possible, spend a little time reading through a couple of Dojo tutorials:

    Those tutorials provide the basic concepts that will be used throughout this tutorial to illustrate best practices in organizing JavaScript code.

  2. Create a module

    Before writing a class, we need to create a module where a class will live. define() is how we do this and also load any dependencies required by a class. The define function is part of Dojo's module loader and is similar to define provided by other AMD loaders, such as require.js.

    Note that a module and a class are not the same thing. In the example shown here, the module created contains a single class. This is the recommended pattern to follow. It is possible for a module to contain multiple classes, but that is not encouraged.

    The code below goes at the top of SeatGeekSearch.js and calls define():

    define(
      ["dojo/_base/declare", "dojo/_base/lang", "esri/request"],
      function(declare, lang, esriRequest) { ... }
    );
    

    The first argument to define is an array of module identifiers. Modules listed in the array of module identifiers will be loaded and passed to the callback function (the second argument to define). Preferred argument aliases are used to name the arguments to callback function. These argument names can be anything you like, but the recommendation is to use Dojo's preferred argument aliases and Esri's preferred argument aliases for Dojo and Esri modules, respectively.

    SeatGeekSearch is a simple module and has three dependencies: dojo/_base/declare, dojo/_base/lang and esri/request. Declare is used to create a class that can be instantiated, the lang module is brought in so that its hitch function is available to provide the proper context for the response from the SeatGeek API and esri/request is used to talk to the SeatGeek API.

  3. Create a class

    Once inside the callback function passed to define we know that all dependencies are loaded so we can create a class:

    declare(null, { ... });
    

    As used here, declare takes two arguments: superclass(es) and an object with properties and methods. See Dojo's "Classy JavaScript" tutorial for more information on each parameter. Two arguments are used here so that an anonymous class is created. The advantage of creating an anonymous class is that the class is not tied to a specific module path and no globals are created.

    All code for a custom class lives inside the properties and methods object that is passed to declare as the last argument. Below is the object that is passed to declare to implement the SeatGeekSearch class's functionality:

    {
      distance: null,
      lastSearchResult: null,
      perPage: null,
      queryParams: null,
      seatGeekUrl: null,
    
      constructor: function(options){
        // specify class defaults
        this.distance = options.distance || "20mi"; // default seat geek range is 20mi
        this.perPage = options.perPage || 50; // default to 50 results per page
        this.seatGeekUrl = "http://api.seatgeek.com/2/events";
    
        // returnEvents is called by an external function, esriRequest
        // hitch() is used to provide the proper context so that returnEvents
        // will have access to the instance of this class
        this.returnEvents = lang.hitch(this, this.returnEvents);
      },
    
      searchByLoc: function(geopoint) {
        var eventsResponse;
    
        this.queryParams = {
          "lat": geopoint.y,
          "lon": geopoint.x,
          "page": 1,
          "per_page": this.perPage,
          "range": this.distance
        }
    
        // seat geek endpoints:
        // petco park search using lat, lon:
        // http://api.seatgeek.com/2/events?lat=32.7078&lon=-117.157&range=20mi&callback=c
        // lat, lon for petco park:  32.7078, -117.157
        eventsResponse = esriRequest({
          "url": this.seatGeekUrl,
          "callbackParamName": "callback",
          "content": this.queryParams
        });
        return eventsResponse.then(this.returnEvents, this.err);
      },
    
      getMore: function() {
        var eventsResponse;
    
        // increment the page number
        this.queryParams.page++;
    
        eventsResponse = esriRequest({
          "url": this.seatGeekUrl,
          "callbackParamName": "callback",
          "content": this.queryParams
        });
        return eventsResponse.then(this.returnEvents, this.err);
      },
    
      returnEvents: function(response) {
        // check number of results
        if ( response.meta.total == 0 ) {
          // console.log("Seat Geek returned zero events: ", response);
          return null;
        }
    
        // save search result
        this.lastSearchResult = response;
        // console.log("set last search result: ", response, this);
    
        return response;
      },
    
      err: function(err) {
        console.log("Failed to get results from Seat Geek due to an error: ", err);
      }
    }
    

    Starting at the top, various class properties are initialized to null. When the class's constructor runs (this happens whenever new SeatGeekSearch() is called), class properties are populated with values supplied via the object passed to the class constructor or with default values. The code in the constructor is also using lang.hitch to provide the proper context for the returnEvents method so that it will be able to access the correct instance properties when called from another function. If the lang.hitch call were omitted, returnEvents would not be able to access properties of the class when called as a callback from esriRequest.

    That's it for creating a custom module that defines a class.

  4. Load a custom module

    Once you've created a custom module, you need to load it. This comes down to telling Dojo's module loader how to resolve the path for your module which means mapping a module identifier to a file on your web server.

    Dojo provides multiple options for doing this, the simplest of which is dojoConfig.paths. On the SitePen blog, there's a post discussing the differences between aliases, paths and packages which is a good read. You can use dojoConfig.paths or dojoConfig.packages to tell Dojo where to find your custom module(s), but paths has the simplest syntax.

    Here's how we can load the SeatGeekSearch module when using the JSAPI from the esri cdn:

    var dojoConfig = {
      paths: { extras: location.pathname.replace(/\/[^/]+$/, "") + "/extras" }
    };
    

    The value for extras looks like gibberish but is necessary to derive an absolute path for the extras folder from the location of the current .html page. There's more info about this in the Using Custom Modules with a CDN Dojo tutorial as well as in a forum post in the JSAPI forum that goes into detail about the regular expression used.

  5. Use a custom class

    Now that Dojo knows where to find modules in the extras folder, require can be used to load it along with other modules used by an application.

    Here's a require block that loads the extras/SeatGeekSearch module as well as a couple of other Dojo and Esri modules:

    require([
      "dojo/_base/array",
      "extras/SeatGeekSearch",
      "esri/map",
      "esri/geometry/webMercatorUtils",
      "dojo/domReady!"
    ], function(
      arrayUtils, SeatGeekSearch, Map, webMercatorUtils
    ) {
      // SeatGeekSearch, as well other modules, are available here
    });
    

    The next step is to create an instance of the SeatGeekSearch class using JavaScript's new keyword:

    // instantiate the Seat Geek Search class
    var sg = new SeatGeekSearch({
      distance: "20mi",
      perPage: 10
    });
    

    Finally, hook up an event listener to call SeatGeekSearch.searchByLoc when the map is clicked:

    // search Seat Geek for events when the map is clicked
    map.on("click", function(e) {
      // Seat Geek expects latitude, longitude
      var geographic = webMercatorUtils.webMercatorToGeographic(e.mapPoint);
      // searchByLoc returns a deferred
      // once the deferred is resolved,
      // pass the results to a callback function
      var sgResults = sg.searchByLoc(geographic);
      sgResults.then(searchSucceeded, searchFailed);
    });
    

    If the request to the Seat Geek API completes successfully, searchSucceeded will run. Otherwise, searchFailed will run and log the error that occurred.

    A full example is also available: SeatGeekSearch example. Results from Seat Geek are logged to the browser's console after clicking the map.

  6. Inherit from Esri classes

    Occasionally, it is necessary to extend an Esri provided class to create a new class that behaves differently than the Esri version. To do this, the same patterns as creating a custom module and class from scratch apply. But there are a couple of differences. Specifically:

    • Load the class to inherit from as a dependency.
    • Specify the class or classes to inherit from as a superclass in declare
    • Override methods or properties to implement custom functionality

    For instance, if a developer wanted to create a custom renderer, the workflow would be to load the appropriate renderer module, specify the renderer class as a super class via declare and override getSymbol to create and return an appropriate symbol.

  7. Additional examples

    There are several samples in the SDK that create custom classes: