odoe

Thinking in modules

Blog Post created by odoe on Aug 12, 2015

esrijs-modules.jpg

A while ago I wrote an article on how to Embrace your AMD modules. A couple of questions popped up on seeing examples of how to do so, using best practices and recommended ways of working with modules. So I thought I would try to help out with that today.

 

What I did was take a sample from the docs that I had updated to add some features a while ago.

 

I wanted to think about how I could break this up into a more modular app using AMD.

 

So first things, first, let's look at the code, ignoring the HTML for now.

require([
  "esri/Color", "esri/dijit/PopupTemplate", "esri/layers/FeatureLayer", "esri/map", "esri/renderers/BlendRenderer",
  "esri/symbols/SimpleFillSymbol", "esri/symbols/SimpleLineSymbol", "dojo/on", "dojo/domReady!"
], function (Color, PopupTemplate, FeatureLayer, Map, BlendRenderer, SimpleFillSymbol, SimpleLineSymbol, on){
  map = new Map("map", {
    basemap: "topo",
    center: [-118.40, 34.06],
    zoom: 15
  });
  //Set the blendRenderer's parameters
  var blendRendererParams = {
    //blendMode:"overlay" //By default, it uses "source-over", uncomment to display different mode
    //See: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
    symbol: new SimpleFillSymbol().setOutline(new SimpleLineSymbol().setWidth(0)),
    fields: [
      {
        field: "OWNER_CY",
        label: "Owner Occupied",
        color: new Color([0, 0, 255])
      }, {
        field: "RENTER_CY",
        label: "Renter Occupied",
        color: new Color([255, 0, 0])
      }, {
        field: "VACANT_CY",
        label: "Vacant",
        color: new Color([0, 255, 0])
      }
    ],
    opacityStops: [
      {
        value: 0.1,
        opacity: 0
      },
      {
        value: 1,
        opacity: 0.7
      }
    ],
    normalizationField: "TOTHU_CY"
  };
  //Create the PopupTemplate to be used to display demographic info
  var template = new PopupTemplate({
    "title": "Housing Status by Census Block",
    "fieldInfos": [
      {
        "fieldName": "OWNER_CY",
        "label": "Number of Owner Occupied Houses",
        "visible": true,
        "format": {
          "places": 0,
          "digitSeparator": true
        }
      }, {
        "fieldName": "RENTER_CY",
        "label": "Number of Renter Occupied Houses",
        "visible": true,
        "format": {
          "places": 0,
          "digitSeparator": true
        }
      }, {
        "fieldName": "VACANT_CY",
        "label": "Number of Vacant Houses",
        "visible": true,
        "format": {
          "places": 0,
          "digitSeparator": true
        }
      }, {
        "fieldName": "TOTHU_CY",
        "label": "Total Housing Units",
        "visible": true,
        "format": {
          "places": 0,
          "digitSeparator": true
        }
      }
    ]
  });
  var layerUrl = "http://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/Blocks%20near%20Wilshire%20enriched%20with%20Key%20Facts/FeatureServer/0";
  var renderer = new BlendRenderer(blendRendererParams);
  layer = new FeatureLayer(layerUrl, {
    id: "blendedLayer",
    outFields: ["TOTHU_CY", "RENTER_CY", "OWNER_CY", "VACANT_CY"],
    opacity: 1,
    definitionExpression: "TOTHU_CY > 0",
    infoTemplate: template
  });
  layer.setRenderer(renderer);
  map.addLayer(layer);
  on(document.getElementById("blendSelect"), "change", function(e) {
    renderer.setBlendMode(e.target.value);
    layer.redraw();
  });
});

This isn't so bad for a small app, but I like to think about how can I scale my app? Where can I break it up a bit and what exactly is happening?

 

Warning - This is just my opinion on how you could modularize this app, others may have differing opinions.

 

Step by step

First off, this app makes use of the BlendRenderer that I discussed here. That has me thinking I could probably break all that functionality out. I'm also creating a PopupTemplate and the BlendRenderer right in this single file. When I see stuff like this, that is kind of simple parameters type stuff, I have tendency to drop them into utility modules, meaning they can be reused in multiple modules pretty easily and are part of a common core to my application.

 

So I can break them out into their own utility modules which you can see in this sample repo.

 

Ok, that was pretty simple, just a copy/paste into a couple of modules. Now let's think about what my app does. For simplicity sake, let's say I am focused on visualizing some population information. Let's focus this into a widget. This widget could look something like this:

 

define([
  'dojo/_base/declare',
  'dojo/_base/lang',
  'dojo/topic',
  'dijit/_WidgetBase',
  'dijit/_TemplatedMixin',
  'esri/layers/FeatureLayer',
  'dojo/text!./widget.html'
], function(
  declare, lang, topic,
  _WidgetBase, _TemplatedMixin,
  FeatureLayer,
  templateString
) {
  var hitch = lang.hitch;
  return declare([_WidgetBase, _TemplatedMixin], {
    templateString: templateString,
    baseClass: 'population-info',
    postCreate: function() {
      var layerOptions = this.get('layerOptions');
      var renderer = this.get('renderer');
      var url = this.get('url');
      var map = this.get('map');
      var layer = new FeatureLayer(url, layerOptions);
      layer.setRenderer(renderer);
      map.addLayer(layer);
      this.set('layer', layer);
      this.own( // do this so the widget can clean up memory if it's destroyed
        topic.subscribe('blend-select-update', hitch(this, 'updateBlendMode'))
      );
    },
    updateBlendMode: function(mode) {
      console.log('update blend mode with dojo/topic');
      var layer = this.get('layer');
      var renderer = this.get('renderer');
      renderer.blendMode = mode;
      layer.setRenderer(renderer);
      layer.refresh();
    }
  });
});

 

This is a pretty simple widget. We use a postCreate method to set stuff up. I talk about the dijit lifecycle in this video. This method is where I set up my layer and assign the renderer that was passed in the options. There's a simple HTML template which is just a copy of the HTML from the sample the has a description of the data. I'm also using dojo/topic, which I talked about here. I'm going to demonstrate a couple of different methods of widget communication, one using dojo/topic and one using dojo/Evented.

 

This module also has an updateBlendMode method that simply handle the dojo/topic subscribe and updates the blendMode. This means any module in your application publish an update to the blendMode or whatever reason. In my opinion, this is an ideal method of widget communication, because the individual widgets do not need to be aware of each other.

 

The other thing the sample had was a select menu that allowed you to update the blendMode. Again, I think this is the perfect candidate for another widget. The code for this widget could look something like this:

define([
  'dojo/_base/declare',
  'dojo/Evented',
  'dojo/topic',
  'dijit/_WidgetBase',
  'dijit/_TemplatedMixin',
  'dojo/text!./widget.html'
], function(
  declare, Evented, topic,
  _WidgetBase, _TemplatedMixin,
  templateString
) {
  return declare([_WidgetBase, _TemplatedMixin, Evented], {
    templateString: templateString,
    baseClass: 'blend-select',
    onChange: function(e) {
      var val = e.target.value;
      if (this.cboxNode.checked) {
      topic.publish('blend-select-update', val);
      } else {
        this.emit('blend-select-update', val);
      }
    }
  });
})

This widget uses the dojo/Evented and dojo/topic. You'll notice that the widget itself extends dojo/Evented using dojo/_base/declare. This allows you to use this.emit() to emit events from your widget. This widget has the select-menu for blendModes and also a checkbox to set whether or not you want to emit an event with the new blendMode or publish the blendMode via dojo/topic.

 

The template for this looks like this:

<div>
  <div>
  <input type="checkbox" data-dojo-attach-point="cboxNode"> Use dojo/topic
  </div>
  <select data-dojo-attach-event="change:onChange">
  <option value="source-over">source-over</option>
  <option value="source-in">source-in</option>
  <option value="source-out">source-out</option>
  <option value="source-atop">source-atop</option>
  <option value="destination-over">destination-over</option>
  <option value="destination-in">destination-in</option>
  <option value="destination-out">destination-out</option>
  <option value="destination-atop">destination-atop</option>
  <option value="lighter">lighter</option>
  <option value="copy">copy</option>
  <option value="xor">xor</option>
  <option value="overlay">overlay</option>
  <option value="normal">normal</option>
  <option value="multiply">multiply</option>
  <option value="screen">screen</option>
  <option value="darken">darken</option>
  <option value="lighten">lighten</option>
  <option value="color-dodge">color-dodge</option>
  <option value="color-burn">color-burn</option>
  <option value="hard-light">hard-light</option>
  <option value="soft-light">soft-light</option>
  <option value="difference">difference</option>
  <option value="exclusion">exclusion</option>
  <option value="hue">hue</option>
  <option value="saturation">saturation</option>
  <option value="color">color</option>
  <option value="luminosity">luminosity</option>
  </select>
</div>

Ok, so we have two widgets, one that creates the FeatureLayer with the blendRenderer and another widget that updates the blendMode of the renderer.

 

Let's wire this up in a main.js module to get things started.

define([
  'esri/map',
  'esri/layers/ArcGISTiledMapServiceLayer',
  'app/widgets/population/widget',
  'app/widgets/blendselection/widget',
  'app/utils/popupUtil',
  'app/utils/rendererUtil'
], function(
  Map, ArcGISTiledMapServiceLayer,
  PopulationWidget, BlendSelectionWidget,
  popup,
  renderer
) {
  var map = new Map('map', {
    center: [-100, 38],
    zoom: 5
  });
  var tileLayer = new ArcGISTiledMapServiceLayer('http://tiles.arcgis.com/tiles/nzS0F0zdNLvs7nc8/arcgis/rest/services/US_Counties_basemap/MapServer');
  map.addLayer(tileLayer);
  map.on('load', function() {
    var populationWidget = new PopulationWidget({
      layerOptions: {
        outFields: ['WHITE', 'POP2012', 'AMERI_ES', 'HISPANIC', 'BLACK', 'ASIAN', 'POP12_SQMI', 'NAME', 'STATE_NAME'],
        opacity: 1,
        infoTemplate: popup
      },
      renderer: renderer,
      map: map,
      url: 'http://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/USA_Counties_Generalized/FeatureServer/0'
    }, 'population-container');
    var blendSelectionWidget = new BlendSelectionWidget({}, 'blend-selection-container');
    // one way to do update the blendMode
    blendSelectionWidget.on('blend-select-update', function(mode) {
      console.log('update blend mode with events');
      renderer.blendMode = mode;
      populationWidget.get('layer').setRenderer(renderer);
      populationWidget.get('layer').refresh();
    });
  });
});

Ok, so the purpose of the main file is to create the map and load the widgets, passing the required options while also using the utils we created earlier. You will also notice that I am using blendSelectionWidget.on('blend-select-update', function(mode){}) to listen for when an event is emitted and manually update the renderer for the layer in the population widget. This is another way you can do communication between widgets, where you can do it in a main file or maybe you'll have a WidgetController that handles all your applications widget communication, it really depends on what tickles your fancy. Normally, I would even break out the map creation as it's own widget as well.

 

I'd also like to point out the dojoConfig for this application.

var dojoConfig = {
  isDebug: true,
  deps: ['app/main'],
  packages: [{
  name: 'app',
  location: location.pathname.replace(new RegExp(/\/[^\/]+$/), '') + 'app'
  }]
};

That is dead simple. Doing it this way, I have defined an app package and all my modules live in this package. This means I don't have to create a widgets package or a utils package, I can just reference them as app/widgets and app/utils. Notice the deps property too. This will tell the Dojo loader to load this module when Dojo is loaded. You can check the docs here. This makes my actual index.html file really simple.

<!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>Modular App Demo</title>
  <link rel="stylesheet" href="http://js.arcgis.com/3.14/esri/css/esri.css">
  <link rel="stylesheet" href="css/main.css">
  <script src="dojoConfig.js"></script>
  </head>
  <body>
  <div id="blend-selection-container"></div>
  <div id="population-container"></div>
  <div id="map"></div>
  <script src="http://js.arcgis.com/3.14/"></script>
  </body>
</html>

You can read about other methods to initialize your application here.

 

Just take it piece by piece

You can find the entire modularized application in this github repo. I hope this provides a little more insight into how you might break up an application into modules and how you can start thinking about modularity in your own development. I tend to like breaking out features of an application into their own widgets, whether it be editing, searching, geocoding even just adding some behavior.

 

You can see some other samples of modularity in stuff I worked on in the past in this starter-kit, which is fully configurable or my latest experiments in this yeoman generator, and sample app. Another app that is very modular is something like the cmv app.

 

Again, this is simply my opinion on how I think you could break up an app into smaller modules that make it not only easier to maintain over time but to easily add new functionality as well.

 

For more geodev tips and tricks, check out my blog.

Outcomes