Lesson 8: GUI Development

Lesson 8: GUI Development jed124

Overview

Overview jed124

Through this point in the course, we've worked with many of the most commonly used parts of Esri's Maps SDK for JavaScript. In Lesson 8, we're going to shift gears a bit to look at ways to enhance the user experience. We'll talk about some mapping widgets from Esri, HTML form elements for obtaining user input, and Calcite components for designing page layouts. My hope is that this lesson will be one of the more valuable ones, inspiring you to think about user interface designs that might work well for your data and setting the stage for you to draw from everything you've learned over the last several weeks in a final project of your choosing.

Objectives

At the successful completion of this lesson, you should be able to:

  • work with several of the widgets built into Esri's Maps SDK for JS;
  • build HTML form elements into an app to obtain user input;
  • handle important UI events, such as button clicks and selections from dropdown lists;
  • use Calcite components to aid in designing your app's layout.

Questions?

If you have any questions now or at any point during this week, please feel free to post them to the Lesson 8 Discussion Forum. (That forum can be accessed at any time by clicking on the Discussions tab.)

Checklist

Checklist jed124

Lesson 8 is one week in length. (See the Calendar in Canvas for specific due dates.) To finish this lesson, you must complete the activities listed below. You may find it useful to print out this page so that you can follow along with the directions.

Steps to Completing Lesson 8
Step Activity Access/Directions
1 Work through Lesson 8 content on UI development. Lesson 8

2

Complete the project described on the last page of the lesson.
 
  • Upload your zipped app files to the Assignment 8 page in Canvas.
  • Below the map or on your Porfolio page, include some reflection on what you learned from the lesson, any concepts that you found to be challenging, and what resources you used to complete the assignment (minimum 300 words).

Follow the directions throughout the lesson and on the last page.

3 Take Quiz 8 after you read the online content.

Click on "Lesson 8 Quiz" to begin the quiz.

8.1 Implementing SDK Widgets

8.1 Implementing SDK Widgets jed124

8.1 Implementing API Widgets

Esri provides several widgets that can be added to the GUI with little coding to improve the user experience. A common use for widgets is in enabling the user to change basemaps. The BasemapToggle widget is used when you have exactly two basemaps that you’d like the user to be able to switch between. The BasemapGallery widget allows the user to choose from any number of basemap options. Let’s have a look at how these and a few other widgets are implemented.

Note: You'll see that some of the widgets discussed here, such as the BasemapToggle, have been deprecated. This means that their use will still be supported for a short time, but you are encouraged to implement the functionality in a newer way. Esri is in the process of phasing out of all of its widgets in favor of web components. We'll talk more about web components, including how to implement a BasemapToggle using one, later in the lesson. We still cover the older widgets here because a) not all of them have a web component replacement yet, and b) you're likely to see widgets used in other people's apps. For all deprecated widgets, which we'll note, you should read over the discussion in this section as an FYI. For Assignment 8 and any other apps you develop going forward, you should be using the web components discussed in the next section whenever possible.

8.1.1 BasemapToggle

8.1.1 BasemapToggle ksc17

8.1.1 BasemapToggle (deprecated)

There are four key steps in implementing this widget:

  1. Set the basemap property of the Map object to the basemap you want the user to see when the app loads.
  2. Create the new BasemapToggle object, associating it with a View.
  3. Set the BasemapToggle’s nextBasemap property to the other basemap you’d like the user to be able to switch to.
  4. Add the widget to the desired screen location.

See the Pen L8_BasemapToggle_widget by Jim Detwiler (@jimdetwiler) on CodePen.

8.1.2 BasemapGallery

8.1.2 BasemapGallery ksc17

8.1.2 BasemapGallery (deprecated)

This widget provides the user a set of basemap options (with thumbnail previews) to choose from. It's often embedded within an Expand widget, as demonstrated below, allowing it to be expanded/collapsed by the user rather than open at all times. 

See the Pen BasemapGallery_widget by Jim Detwiler (@jimdetwiler) on CodePen.

8.1.3 Custom Basemaps

8.1.3 Custom Basemaps ksc17

Where the implementation of these widgets can become more complicated is if you want to offer non-Esri basemap options. First, have a look at this app, which provides a preview of many (mostly open-source) basemaps.

The app lets you preview a basemap by selecting it from the list of mini-maps on the right. Be aware that you can change the map extent to something other than Europe, if desired. After selecting a basemap, you’ll see the JS code that would be used to implement it in Leaflet (an open-source JS API). The Leaflet syntax is not quite the same as Esri’s, but we’ll be able to work out the differences.

Choose the Stadia.StamenTerrain option to follow along with the discussion below. (This is a tiled map developed by Stamen, a cartography and visualization company based in San Francisco, and delivered in partnership with Stadia, another geospatial company.)

Using this basemap in Leaflet involves creating a TileLayer object by specifying the URL of the server that hosts the map tiles, along with some optional parameters such as attribution info and subdomains. (Subdomains are often set up on the tile server to speed up the delivery of tiles to clients.) Note that the server URL contains the letters s, x, y and z in braces. These are placeholders for the subdomain, x coordinate, y coordinate and zoom level, respectively. The TileLayer class is programmed to insert the appropriate values for these placeholders to retrieve the necessary tiles.

In an Esri context, we instead create a WebTileLayer and assign the server URL to the urlTemplate property. Esri’s placeholders are a bit different: subDomain, level, col and row. The subdomains property that was set using a string of characters in Leaflet is the subDomains property set using an array of strings in Esri. Finally, the attribution property in Leaflet is instead the copyright property in Esri. If we wanted to create an Esri WebTileLayer based on the Stamen Terrain basemap, the code would look like this:

const terrainLayer = new WebTileLayer({ 
  urlTemplate: 'https://tiles.stadiamaps.com/tiles/stamen_terrain/{level}/{col}/{row}.png',
  subDomains: ["a","b","c","d"],
  copyright: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});

The next step is to create a new Basemap object from the WebTileLayer:

const terrain = new Basemap({
  baseLayers: [terrainLayer],
  title: "Stamen Terrain",
  id: "terrain",
  thumbnailUrl: 
"http://www.arcgis.com/sharing/rest/content/items/d9118dcf7f3c4789aa66834b6114ec70/info/thumbnail/terrain.png"
});

The thumbnailUrl property provides control over the preview image that appears for the basemap option when displayed by one of the basemap widgets. You’re welcome to create this thumbnail yourself based on some desired extent. (If on Windows, you can print the screen to the Windows clipboard and use an image editing app like Paint to crop and re-size. The image should be sized to 200x133 pixels.) If you don’t want to go to that trouble, you might have luck searching for the basemap in ArcGIS Online and copying the URL of the thumbnail that you find there. That is what I did in this case.

Have a look at the source code for the example below:

See the Pen Custom BasemapGallery by Jim Detwiler (@jimdetwiler) on CodePen.

In this app, I’ve implemented the BasemapGallery widget with two non-Esri basemaps: Stamen Terrain, discussed above, and the Positron basemap developed by Carto (a company based in Madrid). I want to draw your attention to a couple of important points:

  • after the creation of the two Basemap objects, a LocalBasemapsSource object is created and used to set the BasemapGallery’s source property,
  • after the source property is set, the activeBasemap property is set to one of the basemaps.

8.1.4 Home

8.1.4 Home ksc17

8.1.4 Home (deprecated)

The Home widget is used to provide users with the ability to return to the app’s initial viewpoint. To implement the widget, you simply need to create a new Home object and set its view property to the appropriate View. You then specify where to add the widget on the UI as seen with the earlier widgets. 

See the Pen HomeWidget by Jim Detwiler (@jimdetwiler) on CodePen.

8.1.5 LayerList

8.1.5 LayerList ksc17

The LayerList widget is used to provide users with a list of the app’s operational layers and the ability to toggle the layer visibility on/off.  As with the BasemapGallery, this is another widget that is often embedded within the Expand widget.

See the Pen LayerList_widget by Jim Detwiler (@jimdetwiler) on CodePen.

8.1.6 Legend

8.1.6 Legend ksc17

We saw the Legend widget used earlier in the course. Basic implementation of this widget is simple, only requiring you to specify the View containing the layers you’d like listed in the legend. By default, the widget will display an entry for each layer in the view, though this can be overridden. Here are a couple of examples, built on the UniqueValueRenderer example from Lesson 5:

Uncustomized legend

See the Pen Uncustomized Legend Demo by Jim Detwiler (@jimdetwiler) on CodePen.

Customized legend

See the Pen Legend Demo by Jim Detwiler (@jimdetwiler) on CodePen.

In the first example, the Legend has only its view property set. Note that each of the two layers displayed on the map are included in the legend and that the labels for each legend entry are taken from the layer source’s name.

In the second example, the layerInfos property is used to customize the legend a bit. Only one object is defined in the layerInfos array, for the cities layer, so the counties layer is not added to the legend. A more user-friendly title is also applied to the cities layer.

Esri’s Legend widget sample demonstrates a similar customization of a layer’s title. One thing I want to call your attention to is in how the reference to the layer is obtained. The layer is actually the first layer in a web map and is retrieved using the expression webmap.layers.getItemAt(0).

8.1.7 ScaleBar

8.1.7 ScaleBar ksc17

8.1.7 ScaleBar (deprecated)

I’m guessing you haven’t been living under a rock all your life and are familiar with the concept of a scale bar. The ScaleBar widget has two main properties that you may want to set: style and unit. The unit property can be set to "metric", "imperial" or "dual". The style property can be set to "ruler" or "line". Here's an example that shows a map with two ScaleBar widgets:

See the Pen ScaleBar demo by Jim Detwiler (@jimdetwiler) on CodePen.

The difference between the two styles is that the ruler style conveys multiple distances at once (e.g., 25, 50, 75, and 100 km), whereas the line style only conveys a single distance. Setting the unit to dual will produce a line with the metric label on top and the imperial label on the bottom.

8.2 HTML Form Elements

8.2 HTML Form Elements jed124

In the rest of this lesson, we’re going to look at a lot of examples that demonstrate how GUIs can be built. Studying these examples and practicing with the elements they demonstrate should really ramp up your ability to develop geospatial web apps.

Learn More

Before getting to the examples, please work through the HTML Forms tutorial at w3schools. There are many different form elements discussed in the tutorial that you might be able to incorporate into an app. Parts of the tutorial that you should disregard in this context are the parts dealing with form submission. The tutorial discusses submitting form values to a server, where they might be processed by a server-side language like PHP or Ruby. In our context, we’ll be processing the form values on the client device using JavaScript instead. So feel free to skip over the discussion of the Submit button, the action and method attributes, and GET vs. POST.

8.2.1 Checkbox

8.2.1 Checkbox ksc17

This Esri sample allows the user to switch easily back and forth between 3D terrain layers depicting an area before and after a landslide. A checkbox is used to toggle between the two layers. Let’s look at how this checkbox is coded.

First, a semi-transparent div (with id of "paneDiv") is positioned in the bottom right of the map. Embedded within that div are three child elements -- another div (id of "infoDiv") that provides brief instructions, an input element (type of "checkbox" and id of "elevAfter"), and a label that's been associated with that checkbox (done using the attribute setting for="elevAfter").  

There's a lot going on in this sample, most of it outside the scope of what I want to get across here.  Focusing on how to implement a checkbox, note the inclusion here of the checked attribute.  checked is an example of a Boolean attribute.  You don't need to set it equal to any value.  If the checked attribute is present, the box will be checked when the page loads; if that attribute is omitted, the box will be unchecked.  

The assignment of an id to the checkbox is an important step in the implementation as that's what enables working with the element in the JS code.  The JS code associated with the checkbox appears on lines 185-187. The DOM's getElementById() method is used to get a reference to the checkbox, plugging in the id that was assigned to the element in the HTML code. The DOM's addEventListener() method is then used to set up a "listener" for a certain kind of event to occur in relation to the checkbox – in this case, the "change" event. As outlined on the w3schools site, addEventListener() has two required arguments: the event to listen for and a function to execute when that event is triggered. And similar to how a promise returns an object to its callback function, the addEventListener() method passes an event object to its associated function. While this event object has a few different properties, the most applicable in most cases is the one used here – target. The target property returns a reference to the element that triggered the event, which in this case is an input element of type checkbox. So what’s happening on line 186 is the layer that represents the terrain after the landslide has its visible property set to event.target.checked. In other words, if the box is checked (event.target.checked returns true), then the "after" layer’s visible property is set to true. If the box is unchecked (event.target.checked returns false), then the layer’s visible property is set to false.  (The "after" layer is what we see when its visible property is true because the two layers were added to the Map up on line 56 with "before" coming first in the array and "after" second.  The "before" layer gets drawn first, then the "after" layer is drawn on top of it.  So the "before" layer will only be seen if the "after" layer is toggled off.)

If you look over the rest of the code, don't fret if you have trouble following.  It's fairly advanced.  Focus on the checkbox pieces, which have been discussed here.

8.2.2 Dropdown List

8.2.2 Dropdown List ksc17

As you saw in the w3schools tutorial, a dropdown list is created in HTML using a select element. This Esri sample shows a simple usage of a select element to provide the user a list of floors in a building. The user selecting one of the floor options causes only the features from that floor to be displayed.

First, have a look at the HTML. As with the earlier samples, a div is created to hold the UI element (here given an id of "optionsDiv"). Within the div is the select element and its child option elements. Each option has a value attribute (which can be accessed using JS) and the text that the user sees (the text between the start and end tags). In many cases, those two strings are the same. Here, the value attribute is assigned an expression in which the floor number is just part of a larger string. We’ll come back to that expression in a moment.

As we saw in the previous sample, the addEventListener() method is used here to set up a handler for an event associated with a form element.  In the previous sample, an anonymous callback function was embedded directly within the addEventListener() statement.  In this case, the name of a function defined elsewhere (showFloors) is specified instead.  This function will be executed whenever the floorSelect element has its change event triggered. 

The showFloors() function is defined on lines 135-149. The same expression we saw earlier (event.target) is used to get a reference to the element the listener is attached to. Unlike the checkbox sample, where the checked property was read, here the value property is read to obtain the select element’s value (e.g., "FLOOR = '1'", "FLOOR = '2'", etc.).

The logic behind the display of the selected floor’s features is pretty clever. A forEach() loop is used to iterate through each of the layers in the scene. The entire "Building Wireframe" layer is meant to always be visible, so line 141 basically says to ignore that layer. For all other layers, the definitionExpression property (discussed in Lesson 6) is modified to show just the features from the selected floor.

One wrinkle in setting the definitionExpression is that the building identifying field is not the same in all the layers. Part of the solution to this problem is the buildingQuery variable defined on lines 64-69. This object variable is defined having the layer names as the keys and the corresponding expressions needed to select building Q as the values. The definitionExpression has two parts: the first, built by retrieving the appropriate building Q selection expression from the buildingQuery variable (using layer.title as the key); the second, built using the value of the selected option in the dropdown list.

An interesting point to note is the way that the "All" option is handled. It’s assigned a value of "1=1", which may seem strange at first glance. However, it makes sense when you stop to think about it. Let’s say that the loop is processing the Walls layer. That layer will have its definitionExpression set to "BUILDINGKEY = 'Q' AND 1=1". In deciding whether a feature should be included in the layer, each side of the expression will be evaluated as either true or false. The AND operator indicates that both sides of the expression must be true. The expression 1=1 is always true, which gives the desired result of all features being displayed, regardless of their FLOOR value.

8.2.3 Button

8.2.3 Button ksc17

The next sample also makes use of the select element (three of them, actually), but unlike the previous sample, the script’s main logic isn’t carried out until a button is clicked. The button is created on line 282 as an HTML button element, and as seen in prior samples, is assigned an id. As in the previous sample, an event listener is defined (line 165). In this case, the listener is set up on the button’s click event. References to the three select elements are established on lines 168-170; they are then used to retrieve the selected options on line 191.

8.2.4 Text Box

8.2.4 Text Box ksc17

The pen below shows a simple app that provides the user a text box to enter the name of a hurricane to display on the map.

See the Pen Text box demo by Jim Detwiler (@jimdetwiler) on CodePen.

Some noteworthy aspects of this app:

  • A div (id of "paneDiv") is the container for all of the custom UI elements.
  • Within that div are an input element of type="text" and a button element.  Both are assigned ids so that they can be accessed in the JS code.
  • The input element has its placeholder attribute set to provide a prompt to the user on the sort of entry expected in the box.  This and other input attributes are described at w3schools.
  • Line 36 of the code establishes a listener for the button's click event, the getTrack() function.
  • That function obtains the name entered by the user into the text box by reading its value property on line 43.  
  • The storm name is then incorporated into a definitionExpression applied to the FeatureLayer that was created earlier in the code.

8.2.5 Range Slider

8.2.5 Range Slider jed124

A slider control can be an effective means of enabling the user to specify a numeric parameter within a range of possible values. The example below -- based on an Esri sample that appears to no longer be in their SDK -- demonstrates how a range slider can be implemented in an Esri JS app.  HTML5 makes it possible to insert a slider onto a page using an input element of type="range", and in fact, an earlier version of the Esri sample displayed the sliders using that element type.  However, the example below uses the Slider widget, which was introduced at v4.12 of Esri's Maps SDK for JS.  

See the Pen Slider widget demo by Jim Detwiler (@jimdetwiler) on CodePen.

Initial setup

Two divs are defined at the bottom of the HTML (lines 242 and 244) to serve as placeholders for the two Slider widgets.  The widget objects themselves are created early in the JS code, on lines 27-54.  The min and max property settings should be self-explanatory. The steps attribute specifies how much the slider value can be incremented when it is dragged, relative to its min value. Here, a step of 100 and a min of 0 means that the slider can take on values of 0, 100, 200, etc. If the min were changed to 50, possible values would be 50, 150, 250, etc. The values property specifies the positions on the slider where "thumbs" should be placed.  Each of the sample's sliders has a single thumb, but the widget allows for defining multiple thumbs (say, to enable the user to specify a range of quake magnitudes instead of just a minimum magnitude as the sample is written). 

Getting the slider value

Also near the top of the JS code, references are obtained to the UI's dropdown list and button using the getElementById() method we've seen in the previous samples. A listener is set up for the button's click event on lines 203-205, which specifies that a click on the button should trigger execution of a function called queryEarthquakes(). That function creates a Query object that looks for features in an earthquake layer that have a magnitude value greater than or equal to what the user specified through the magnitude slider. We talked about queries in the last lesson, so that’s not my focus here. What I want you to focus on is that the slider's value is obtained simply by reading its values property (the same property that was used to define the initial thumb position).  An index of [0] is specified here to get the only thumb value, but keep in mind that you would also need to specify an index of [1] if as suggested above you allowed the user to define a range of desired values.  The single user-selected magnitude value is then used to set the Query's where property,  That constraint combined with the well buffer distance ultimately determines which earthquakes will be added to the map as Graphic objects by the displayResults() function.  

8.2.6 Date

8.2.6 Date jed124

Many apps require the user to input one or more dates. It is possible to acquire dates through a plain text box. However, using a "date picker" widget can make the entry task a bit less tedious for the user, and just as importantly, help ensure that the date is supplied in the correct format. HTML5 introduced a new input type of Date that provides a date picker, and its use is demonstrated here:

See the Pen Date Picker v4.13+ by Jim Detwiler (@jimdetwiler) on CodePen.

Looking at the HTML at the bottom of the example, you should note the following:

  • The two input elements are assigned a type="date" attribute. 
  • An id is assigned to each element so that it can be manipulated with JS code. 
  • The min and max attributes can be used to limit the dates available to the user. These attributes are set dynamically via the setDates() JS function so that the app allows for selecting dates between today and 30 days ago. However, if such dynamic behavior isn't needed, the attribute values could be hard-coded in the HTML using a yyyy-mm-dd format.
  • The required attribute specifies whether or not the user should be allowed to skip over that UI element.

Now have a look at how the selected dates are retrieved in the getFires() function. First, just above the function, references to the dateFrom and dateTo elements are obtained using getElementById(). The selected dates are then retrieved by reading the value property and inserted into a definition expression that causes the layer to display only the features for which the FireDiscoveryDateTime field has a value that falls between the two selected dates. 

Of course, it's not always the case that a date picker needs to have its attributes set dynamically. Below is an example that shows data from another wildfire layer, this one containing historical data for the year 2019. As the date range is predetermined, the date pickers can have their attributes hard-coded in the HTML rather than computed on the fly in the JS. (Note that this is a polygon layer and depending on the range entered, you may need to zoom in a bit to see any of the returned polygons.)

See the Pen Date Picker v4.13+ (hard-coded dates) by Jim Detwiler (@jimdetwiler) on CodePen.

8.3 Clickable Sidebar List

8.3 Clickable Sidebar List jed124

One UI design seen frequently in geospatial apps is a sidebar containing a list of map features. The items in the list can be clicked to see where the feature is located on the map.

Below is an example that shows counties in Jen & Barry’s world that meet the 500-farm criterion. FYI, this example builds on an earlier one and is modeled after this Esri sample.

See the Pen Clickable sidebar demo by Jim Detwiler (@jimdetwiler) on CodePen.

Initial setup

In the HTML, the div that holds the map (id="viewDiv") is embedded within a parent div (class="panel-container"). Another div (class="panel-side") is also defined within the panel-container div. The panel-side div contains a header element along with an unordered list element (id="list_counties").

In the stylesheet, the important settings are:

  • The panel-side div is given a width of 300px, a height of 100%, absolute positioning, and a top and right position of 0. (This causes it to be 300px-wide box in the upper right of the window.)
  • Its overflow property is set to auto, which means that if its content can’t be fit into the div, a scrollbar will appear, allowing the user to scroll to see the rest of the content.
  • ul elements within the panel-side div are given a list-style of none. This means you won’t see the default bullet symbol (or any other symbol).
  • The list items are padded 5px on top and bottom, and 20px on left and right.
  • Elements with a class of panel-result (which we’ll see momentarily when we look at the JS code) have a cursor property setting that causes the cursor to change to a pointer if the user hovers over the element.
  • The panel-result elements also have their text turn orange if the user hovers over the element (or the element gets focus from the keyboard).

Changes made from the earlier example

You may recall this same map was displayed in an example from Lesson 6, which was focused on querying.  In that example, the features meeting the Query criterion were added to a GraphicsLayer.  In this example, the features are instead used to create a new FeatureLayer.  The farmQuery variable is defined on line 43.  Some important differences in this version of the farmQuery are:

  • It has its outFields property set to a subset of the layer's available fields (just those needed to populate the sidebar list and the popups).
  • Its orderByFields property is set so that the features in the sidebar are listed in an intuitive way.
  • The outSpatialReference property is set to match that of the basemap.  It turns out this isn't necessary for this particular app since the county data and the basemap are in the same spatial reference.  However, if that were not the case, clicks on the sidebar items would result in the pop-up not being anchored in the correct location.  

Note: Want to see the spatial references of the counties and basemap?  Add the following code to the view.when() callback function and be sure to open the browser's console:

console.log('Basemap SR: ' +view.map.basemap.baseLayers.items[0].spatialReference.wkid);
console.log('Counties SR: ' + counties.spatialReference.wkid);

Populating the list

The bulk of the list population logic is found in the displayResults() function (lines 54-82). The basic idea is that a new li element will be created for each item in the FeatureSet returned by the Query, then all of the li elements will be inserted into the ul element embedded within the panel-side div.

To accomplish this, the DOM’s createDocumentFragment() method is used to create a new empty object to store the li elements. A forEach loop is used to iterate through the Query’s FeatureSet. Within that loop, an li element is created using the createElement() method. After the li element is created, DOM methods are used to set some of its attributes. First, it is assigned to the CSS class panel-result. (We saw the cursor property setting assigned to this class above.)  Next, it's given a tabIndex of 0, which means it will be the first item in the list to receive focus in the event the Tab key is used to cycle through the list items. Third, the element is assigned a custom attribute (data-result-id = index). (The index variable is automatically updated on each iteration through the forEach loop, so each li element will get a unique data-result-id value.)  This will come into play momentarily when we look at the code that handles clicks on the list. Finally, the text of the li element is set to a concatenation of the name and farm count for the current county. The li element is then added to the DocumentFragment created just before the loop on line 70 using the appendChild() method.

After iterating through all of the counties returned by the query and creating a li element for each, the task is to plug those li elements into the ul element that was hard-coded into the page's HTML. This is accomplished by first getting a reference to that ul element (line 30), then using appendChild() again, this time to append the DocumentFragment to the ul. (Recall that the page initialized with a "Loading..." message; this text is cleared out before the list items are added by setting the element's innerHTML to an empty string.)

A couple of final important things happening in the loop through the query results is that a) each county graphic has its popupTemplate set to match the one assigned to the counties layer, and b) the graphic is added to the array stored in the variable graphics (created on line 52) using the array method push(), which adds the graphic to the end of the array.  

Handling clicks on the list items

The last part of the app to code is setting up a listener for clicks on the sidebar list. Line 85 uses the DOM method addEventListener() to trigger execution of a function called onListClickHandler() when the user clicks on the list. Looking at that function, the expression event.target returns a reference to the li element that was clicked. target.getAttribute("data-result-id") then gets the custom id value that was assigned to that li element.

The key to having the pop-up open over the correct county is that the data-result-id value matches the position of the county in the graphics array. On the first pass through, the query results loop assigned that county's list item a data-result-id of 0 and its graphic was added to the graphics array at position 0. On the second pass, that county's data-result-id was set to 1 and its graphic was added to the graphics array at position 1, etc.

Before we get to the opening of the pop-up, though, we have to look at line 90. This line is a bit tricky with its use of the logical operator &&. This operator is more commonly used in an if construct; for example, in situations where you want both condition A AND condition B to be true. Here it's being used in an assignment statement. The Mozilla Developer Network JavaScript tutorial does a pretty good job of explaining how logical operators work in this context, and I encourage you to read through the page if you're interested in understanding that line well.

The short (OK, not really all that short) summary is this: the expression resultId && graphics && graphics[parseInt(resultId, 10)] gets evaluated from left to right. If the resultId variable holds a null value (which it would if you didn't click on an li element), then the other pieces of the expression won't even be considered, and the result variable will be assigned a value of false. If resultId holds some number (which it would if you did click on an li element), then the first part of the expression will evaluate to true, and the next piece of the expression will be evaluated.

Similarly, if the graphics variable holds an empty array (e.g., no counties returned by the query), then the graphics piece of the expression will evaluate to false, the last part of the expression will be ignored, and result will be assigned false. If there are county graphics in the graphics variable, then the last part of the expression will be evaluated.

The last part of the statement uses the parseInt() method to convert the value from the data-result-id into an integer. (HTML attributes are always stored as strings, but we need the id value as a number.)  The 10 argument in parseInt(resultId,10) says that you want the parsing to be done in base 10 math. So basically, that expression is changing values like "1" to 1, "7" to 7, etc. The number returned by parseInt() then gets plugged into the square brackets. So, a resultId of "1" will ultimately yield the county graphic that was at position 1 in the graphics array, the resultId of "7" will yield the county graphic at position 7 in the array, etc. In those cases, the expression on line 90 will evaluate to a county graphic, which is then what is assigned to the result variable.

After all that, we finally get to the pop-up code. Line 92 first ensures that everything is OK with the click on the list. If so, then the Popup object associated with the MapView is opened using its open() method. Passed into the openPopup() method is the county graphic (stored in the result variable and used to set the Popup's features property) and the centroid of the geometry associated with that graphic (used to set the Popup's location property).

Phew! Hopefully, you were able to follow all of that. If not, don't hesitate to ask for help on the discussion forum.

8.4 Text Overlay

8.4 Text Overlay jed124

You may have situations, especially when your app requires user interaction with the map, that call for displaying some sort of instructions. In the example below, note the bit of text along the top of the map enclosed within a semi-transparent box.

See the Pen Text Overlay Demo by Jim Detwiler (@jimdetwiler) on CodePen.

This text box is created through two steps:

  1. In the HTML, a div element is defined and assigned an id. Within the div, the desired text is placed inside a p element.
  2. In the CSS, the div is placed through absolute positioning 10 pixels from the top of the page and 62 pixels from the left. A padding of 12 pixels is added to the left and right of the div so that the text isn’t rendered too close to the edge. The text is given a white color, while the background of the element is made black with a transparency value of 0.5. This allows the text to be displayed to the user without completely obstructing the view of the map underneath.

8.5 Custom Map Widgets

8.5 Custom Map Widgets jed124
Earlier in this lesson, you used the view.ui.add() method in MapView and SceneView to add API widgets to the map interface.  You can also use it to add your own custom widgets built with HTML text and form elements.

See the Pen Custom Map Widgets by Jim Detwiler (@jimdetwiler) on CodePen.

The example above shows a few ways to add your own HTML widgets to the map interface. Notice the use of Esri's CSS class "esri-widget" so that they match Esri's theme.  For simplicity, I've only included the code for creating and placing widgets in this example. Programming actions was described earlier in the lesson.

8.6 Populating a Dropdown List

8.6 Populating a Dropdown List jed124

Earlier we saw how to provide users with a set of options through a dropdown list (select element). As in that example, it is sometimes easy and appropriate to hard-code the list options into the page's HTML. However, there are also times when the list is quite long or changes over time. In such cases, it makes more sense to populate the list programmatically.

Also earlier in the lesson, we looked at a sample that involved querying earthquakes based on different attribute and spatial criteria. I want to return to that sample now because the well type dropdown list was constructed by identifying the unique values in one of the wells layer’s fields. If you’re going to populate your own lists "on the fly," you’ll want to implement similar logic.

See the Pen Untitled by Jim Detwiler (@jimdetwiler) on CodePen.

First, an empty select element (no child option elements) is defined in the HTML at the bottom of the code.

Within the JS code, a FeatureLayer containing the wells is created on lines 58-65. That layer is added to a new Map and the Map is associated with a new MapView. Then on lines 86-96 comes a chain of promises. The first is associated with the loading of the MapView. Within its callback function, its return object is set to the wells layer. It in turn has a promise defined such that when the layer is finished loading a Query object is created and used in a call to queryFeatures(). Because the Query has no filtering properties set, queryFeatures() returns all features from the wells layer.

Once the query returns its features, they are passed along to a function called getValues(), which is defined just below the promise chain. The getValues() function uses the map() method to iterate through all of the wells features, retrieving the values in the STATUS2 field and producing a new array containing those values. (The values in this array are the same ones you see in the well type dropdown list, though each value is in the array potentially many times.)

The array produced by the map() method in getValues() then gets passed along to the getUniqueValues() function. That function first creates a new empty array that will be used to store each unique value from the STATUS2 field just once. It uses the forEach() method to iterate over all of the values. Within the forEach loop, the idea is to check whether the current value is in the unique value array yet, add it to the array if it’s not, and skip over it if it is.

Looking at the if expression on lines 111-116, it is composed of three smaller expressions:

  • uniqueValues.length < 1
  • uniqueValues.indexOf(item) === -1
  • item !== ""

uniqueValues.length returns the number of items in the array.

uniqueValues.indexOf(item) returns the position of the first occurrence of item in the array. If item is not in the array, the expression will return -1.

The === operator may be new to you. Recall that a single = character is used for assigning values to variables or setting properties. When you want to test for equivalence between two entities, you need to use == or ===. The difference is that === requires a match in not only the values but also the data types, while == requires a match in just the values. For example, consider the following variables:

x = 0;
y = false;

And note how the following expressions evaluate:

if (x == y) { // this evaluates to true
if (x === y) { // this evaluates to false

So, the indexOf() condition in this example is written with extra caution to ensure the value being examined is truly not yet in the array.

The first two of these expressions are actually evaluated together (note the placement of parentheses) such that if the unique value array is empty or the item is not yet in the array, then the first part of the if condition should evaluate to true.

The last of the three expressions is then examined. It checks to make sure item is not an empty string (i.e., that the STATUS2 value wasn’t blank for the current record).

The use of the && operator means that the first part of the if condition:

uniqueValues.length < 1 || uniqueValues.indexOf(item) === -1

and the second part:

item !== ""

must both be true.

Given the setup of this loop, each value from the STATUS2 field will be added to the uniqueValues array exactly once.

That array is then passed along to the addToSelect() function. That function first uses the array sort() method on the values to put them in alphabetical order, then iterates through them using another forEach loop. In this loop, an option element is created using the createElement() method we saw earlier in the lesson, the text of the element is set to the current iteration value, and the option is added to the select element. Once all of the values have been processed by the loop, the list is fully populated.

8.7 Calcite Design System

8.7 Calcite Design System jed124

One recent development associated with the Maps SDK for JavaScript is the Calcite Design System.  This is a set of Esri resources, including a web component library, that simplify the development of rich user interfaces.  Web components are defined by the Mozilla Development Network as:

a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps.

If it wasn’t clear, we’re talking about custom HTML elements, which can be implemented in a similar way to native HTML elements.  To give an example, one commonly used Calcite component is the Panel, which can be instantiated in your HTML code like so:

    <calcite-panel>
    ...
    </calcite-panel>

An important concept to understand in dealing with web components is that of the slot, which is a placeholder for defining content associated with an element.  Slots are implemented in native HTML, too.  For example, the select element discussed earlier in the lesson is used to display dropdown lists:

    <select>
        <option value="psu">Penn State</option>
        <option value="osu">Ohio State</option>
        <option value="um">Michigan</option>
    </select>

The embedded child option elements in the code above can be said to be in the select element’s default slot.  Returning to the Calcite Panel component, it similarly has a default slot.  Here, we’re putting an h3 element in a Panel’s default slot:

    <calcite-panel>
        <h3>Layer Filter Options</h3>
    </calcite-panel>

In addition to the default slot, web components can be programmed with other named slots.  The Calcite Panel component has several, including, for example, “footer.”  As you may have guessed, this slot can be used to add content to the bottom of the Panel element.  Here, we’re putting a Calcite Button component in a Panel’s footer slot:

    <calcite-panel>
        <calcite-button slot="footer">Cancel</calcite-button>
    </calcite-panel>

In this section of the lesson, we'll see a few example apps that implement Calcite.  We'll start by walking step by step through the creation of a simple app built from some Calcite components.  Later, I’ll discuss a few other finished apps that demonstrate the implementation of some other components.  As part of the discussion, I’ll be referring to Esri’s Calcite Design System documentation.

8.7.1 Action Bar

8.7.1 Action Bar jed124

For this walkthrough, we’ll create an app that displays data from a WebMap and provides the ability to toggle layers on/off and change basemaps through a Calcite Action Bar.

A. Create a basic app and add references to Calcite

  1. Use Esri's Load a Basic WebMap Esri sample as a starting point, opening it in CodePen.  Feel free to replace the WebMap portal ID to one of your own, if you wish.
  2. Change the title of the document to First Calcite Walkthrough.
  3. Move the CSS and JS code to the appropriate CodePen windows.
  4. Add the following Calcite reference to your HTML, above the references to the Maps SDK for JS:

      <script type="module" src="https://js.arcgis.com/calcite-components/3.2.1/calcite.esm.js"></script>

B. Add a Shell component

Apps built using Calcite components typically have them embedded within a Shell component, and that's how we'll begin here. 

  1. First, access the documentation of this component by going to the Calcite documentation homepage, clicking the Components tab, scrolling down the alphabetical list of components on the left side of the page, then selecting Shell > Shell

    As its name implies, the Shell component is used as a kind of parent container for the other elements that make up your mapping application. 

    Like other components, the documentation includes an Overview of the component and some notes on its Usage.  It then provides an interactive Sample section intended to give an opportunity to see the component in action and to experiment with its properties, slots, variables, and events. 

    The Shell component documentation provides 3 samples at the time of writing.  If you click on the dropdown menu in the upper left of the UI, you should see that in addition to the default sample, there are also “Shell – with content” and “Shell – with content – bottom” variants.

    The Sample area of the page has a top and a bottom.  On the top, you’ll see the component in action.  On the bottom, you’ll see the code that produces the result on top (broken down into its HTML, CSS, and JS pieces).  Let’s look briefly at the code.

    As is often the case, the sample code for the Shell component includes a number of other components.  The Shell typically houses one or more Shell Panel components.  In fact, it has been configured with slots for displaying Shell Panels (panel-start, panel-end, panel-top, and panel-bottom).  In the sample, there is a calcite-shell-panel element in the panel-start slot and another in the panel-end slot.  The one in the panel-start slot contains an Action Bar component, which itself contains 3 Action components.  We’ll implement these same components momentarily.

    While we’re looking at the Code part of the Sample UI, note the buttons in the upper right that allow you to copy the code to your clipboard or to open the sample in CodePen. 

    Continuing past the Sample part of the page you’ll find documentation of the component’s Properties, Slots, and Styles

    With that orientation done, let’s get back to our walkthrough. 
  2. In the body of the document, add a Shell component and move the “viewDiv” inside of it:

        <calcite-shell content-behind>
            <div id="viewDiv"></div> 
        </calcite-shell>

    As noted in the Shell documentation, content-behind is a property that controls whether the Shell’s center content will be positioned behind any of its child Shell Panels.  The default setting of this property is false; here we’re setting it to true.

  3. Next, let’s add an h2 element to the Shell’s header slot.  Add this code inside the calcite-shell element, before “viewDiv”:

        <h2 id="header-title" slot="header">
            <!-- Populated at runtime -->
        </h2>
  4. Set the header’s margins (using CSS):

        #header-title {
          margin-left: 1rem;
          margin-right: 1rem;
        }
  5. Now add the following JS code to dynamically set the header once the WebMap has been loaded:

      webmap.when(() => {
        const title = webmap.portalItem.title;
        document.getElementById("header-title").textContent = title;
      });
  6. Go ahead and test your app.  The header should say “Accidental Deaths” if you stuck with the WebMap from the Esri sample.

C. Add a Shell Panel and Action Bar

Now let’s add a Shell Panel that will house the Action Bar with the desired functionality.

  1. Back in the HTML, add the Shell Panel between the h2 element and the “viewDiv” as follows:

        <calcite-shell-panel slot="panel-start" display-mode="float">
        </calcite-shell-panel>

    Note that this associates the Shell Panel with the Shell’s panel-start slot (mentioned briefly earlier) and sets its display-mode property.  You could navigate to the Shell Panel page in the Calcite documentation to see the values allowed for this property. 

  2. Next, let’s add the Action Bar to the Shell Panel.  Shell Panels are configured with an action-bar slot, so we'll add it to that slot:

        <calcite-action-bar slot="action-bar">
        </calcite-action-bar>
  3. Test the app again.  You should now see a narrow vertical strip on the left side of the page with a button for expanding/collapsing the strip.  (The Action Bar is empty because we haven’t added any Actions to it yet.)

D. Add an Action to the Action Bar

  1. Add an Action component for toggling layer visibility inside the Action Bar:

        <calcite-action data-action-id="layers" icon="layers" text="Layers">
        </calcite-action>
  2. Test the app again and note that you now have a button at the top of the Action Bar strip.  Expand the Action Bar and note the label Layers appears next to the button, which comes from the setting of the Action’s text property. 

    Lastly, note that the button’s icon comes from setting the Action’s icon property.  How do you know how to set that property?
  3. Return to the Calcite documentation and click on the Icons tab (upper right of the page).  On the Icons page you’ll find an alphabetical list of 1100+ available icons.  In this scenario, you might enter the word layers into the search box at the top of the page, which returns a much shorter list.  (Presumably the icons are tagged with keywords since some of the results don’t match on name.)  Scrolling down, you should see the “layers” icon used here.  Other icons could be suitable in its place.

    The button doesn’t actually do anything yet though.  What we’d like it to do is display a list of the layers in the WebMap along with buttons for toggling each layer on/off.  To produce that behavior, we’ll use a Panel component (different from a Shell Panel) to display a LayerList widget (discussed earlier in the lesson). 
  4. Add a Panel immediately after the Action Bar with the following code:

        <calcite-panel heading="Layers" data-panel-id="layers" hidden>
        </calcite-panel>

    data-panel-id is a custom attribute that we’ll use momentarily in our JS code to show/hide the Panel.

  5. As noted, we’re going to use a LayerList widget to provide the user with the ability to toggle layer visibility.  This widget won’t be associated with the Panel component directly, but with a div element embedded within the Panel.

    Within the calcite-panel element, add the div element:

        <div id="layers-container"></div>

    We assign the div an id so that we can easily wire it up to the LayerList widget we’re about to create.

  6. Shift to your JS code and add a reference to the LayerList module:

        const [MapView, WebMap, LayerList] = await $arcgis.import([
          "@arcgis/core/views/MapView.js",
          "@arcgis/core/WebMap.js",
          "@arcgis/core/widgets/LayerList.js"
        ]);    
    
  7. Next, create a new LayerList object immediately after the creation of the MapView:

        const layerList = new LayerList({ 
            view: view,
            selectionEnabled: true,
            container: "layers-container"
        });


    Note the setting of the container property to the div we created moments ago.

    At this point, the Action component doesn't actually perform any action.  To rectify that, we need to add some code that will process user clicks on the Action Bar.

E. Add a handler for clicks on the Action Bar

  1. Start by defining a variable to track the active widget (add this to the end of your existing JS code:

      let activeWidget;
  2. Next, define a handleActionBarClick function:

      const handleActionBarClick = ( event ) => {
        const target = event.target; 
        if (target.tagName !== "CALCITE-ACTION") {
          return;
        }
    
        if (activeWidget) {
          document.querySelector(`[data-action-id=${activeWidget}]`).active = false;
          document.querySelector(`[data-panel-id=${activeWidget}]`).hidden = true;
        }
    
        const nextWidget = target.dataset.actionId;
        if (nextWidget !== activeWidget) {
          document.querySelector(`[data-action-id=${nextWidget}]`).active = true;
          document.querySelector(`[data-panel-id=${nextWidget}]`).hidden = false;
          activeWidget = nextWidget;
        } else {
          activeWidget = null;
        }
      };
  3. And now configure a listener that triggers execution of the handleActionBarClick() function when the Action Bar is clicked:

      document.querySelector("calcite-action-bar").addEventListener("click", handleActionBarClick);
  4. Test the app, confirming that a click on the Layers Action opens a Panel showing the LayerList widget and that clicking it again hides the Panel.

    Let’s talk about what’s happening in the code just added.  First, note that the event listener uses the DOM’s querySelector() method to get a reference to the Action Bar.  This is a similar method to getElementById(), but more flexible.  Rather than being limited to referring to elements by their id attribute, you can also find them by tag name or class, much like you can refer to elements in your CSS code.  Here we’re finding the first element with the tag of "calcite-action-bar" and adding the event listener.

    Looking at handleActionBarClick(), it accepts the event object passed from addEventListener(), storing it in the event variable, then obtains a reference to the target of the click.  The first if block then checks to make sure that the target has a tagName of "CALCITE-ACTION".  If it does, that means an Action component was clicked.  If it does not, that means the user clicked on the Action Bar, but in an empty area rather than on an Action.  In that case, a return statement is used to exit out of the function.

    The second if block checks to see if there is an active widget associated with the Action Bar (i.e., that one of its Actions has been clicked previously and that an associated Panel component is currently visible).  If so, then a subsequent click on the Action should close the Panel.  And that is what the code block does, though some of the syntax used may be new to you.  The querySelector() method is used again, but note that the string supplied in parentheses is enclosed in backquote (also called backtick) characters.  This character is typically in the upper left of the keyboard, paired with the tilde character.  The backquote is used in JavaScript to define "string templates," one purpose of which is to insert values from variables into strings without the need for more complicated concatenation.  W3Schools has a discussion of string templates.

    The other aspect of the querySelector() statements that may require clarification is the use of square brackets.  This syntax is used to search for elements based on an attribute name.   Again, see W3Schools Query Selector.

    So, imagine that the "layers" Action has been clicked and its associated Panel is visible.  If the user clicks on that Action again, this block of code will be executed.  First, a query will be made for an element having an attribute setting of data-action-id=layers.  This will return the Action component and then set its active property to false.  (The Calcite documentation indicates that the component is highlighted when the active property is true, which I take to mean it has a blue outline.  I would expect that setting the property to false would cause that outline to disappear, but that’s not the behavior I’m seeing.  Setting the active property to false seemingly has no effect.)  The second querySelector() statement looks for an element with an attribute setting of data-panel-id=layers.  This will return the Panel component associated with the Action and then set its hidden property to true.  This causes the Panel to disappear.

    The next statement obtains a reference to the widget the user just clicked on.  Exactly how this works is a bit of a mystery to me, I’m afraid.  If the user clicks on a button on the Action Bar, then target will refer to an Action component.  The code then reads the dataset property and then the actionId of the object returned by the dataset property.  However, I see no dataset property listed in the Action component documentation, which is why I don’t fully understand what’s happening in this statement.  In any case, nextWidget will take on the name of the Action just clicked (e.g., "layers"). 

    Next comes an if block that checks whether nextWidget is the same as activeWidget.  If they are the same, that means the user clicked on an Action whose Panel was already visible.  It would have been hidden by the first part of the function and this part will simply set activeWIdget to null.  If nextWidget is not the same as activeWidget, then essentially the reverse of the logic described above occurs.  The just-clicked Action has its active property set to true and its associated Panel’s hidden property set to false.

    That was a lot of explanation for a relatively short block of code.  The good news is that adding other Actions to the Action Bar will be much easier.  Recall that the scenario called for the ability to change basemaps, so let’s build that in now.

F. Add a second Action to the Action Bar

  1. Add another Action component to the Action Bar beneath the Layers Action (in the HTML):

        <calcite-action data-action-id="basemaps" icon="basemap" text="Basemaps">
        </calcite-action>
  2. Further down in the HTML, add a Panel component to go with the Action:

        <calcite-panel heading="Basemaps" height-scale="l" data-panel-id="basemaps" hidden>        
            <div id="basemaps-container"></div>   
        </calcite-panel>
  3. Add a reference to the BasemapGallery module (in the JS):

        const [MapView, WebMap, LayerList, BasemapGallery] = await $arcgis.import([
          "@arcgis/core/views/MapView.js",
          "@arcgis/core/WebMap.js",
          "@arcgis/core/widgets/LayerList.js",
          "@arcgis/core/widgets/BasemapGallery.js"
        ]);    
    
  4. Instantiate a new BasemapGallery widget and associate it with the div embedded within the basemaps Panel component:

        const basemaps = new BasemapGallery({        
            view: view,        
            container: "basemaps-container"      
        });

G. Adjust view padding when Action Bar is toggled

One last thing we may want to do concerns what happens when the user expands the Action Bar (by clicking the arrows at the bottom of the bar). Note that the bar expands at the expense of the western edge of the MapView. This may not be a big deal, but an alternative is to increase the left padding of the MapView when the Action Bar is expanded.

  1. Add the following code to the end of the app’s JS block:

        let actionBarExpanded = false;
        document.addEventListener("calciteActionBarToggle", event => {
            actionBarExpanded = !actionBarExpanded;
            view.padding = {
              left: actionBarExpanded ? 135 : 50
            };
        });
    

    This code creates a variable to track whether or not the Action Bar is expanded and initializes it to false. It then configures a listener for the Action Bar’s calciteActionBarToggle event (see the Action Bar documentation). The function associated with the event is defined inline. It first flips the actionBarExpanded variable to its inverse. If it’s false, make it true; if it’s true, make it false. It then sets the view’s left-side padding to the expression actionBarExpanded ? 135 : 50. This expression uses the “ternary operator” (a shorthand for a longer if-else block) to assign a value of 135 if actionBarExpanded is true and 50 if it’s false.

  2. Run the app again and confirm that expanding the Action Bar causes the map to shift to the right.  You may want to remove the code block just added and test again to get a good grasp of what the code is doing.

With this walkthrough complete, let's have a look at a few other working examples that demonstrate some other useful Calcite components.

8.7.2 Tabs

8.7.2 Tabs jed124

As you've no doubt seen as an end user of graphical user interfaces, tabs are often provided for switching between parts of the interface.  Calcite provides a Tabs component for developers to implement this sort of design.  The Tabs component has a child Tab Nav component, which defines the navigation piece of the interface (i.e., the tab headings/labels).  The headings are defined using Tab Title components.  In addition to the Tab Nav, the Tabs component also contains multiple child Tab components (one for each Tab Title). 

Here's an example that demonstrates the implementation of the Tabs component:

See the Pen calcite tabs by Jim Detwiler (@jimdetwiler) on CodePen.

This app is used to present data from a set of related AGO web maps, each web map being displayed through a different tab.  Note in the HTML the use of the calcite-tabs, calcite-tab-nav, calcite-tab-title, and calcite-tab elements, corresponding to the components discussed above.  Also note that each calcite-tab contains a div that is used as a container for a WebMap.  The JS code is fairly straightforward.  A separate WebMap and MapView object is created for each race category that had a tab configured for it in the HTML.

The coding of this app is not particularly graceful as it contains a lot of copy/pasted lines in both the HTML and JS.  Here's a version of the app that handles the creation of the tabs more elegantly:

See the Pen calcite tabs array by Jim Detwiler (@jimdetwiler) on CodePen.

In this version, note the following: 

  • Only the Tabs component is defined in the HTML at design time; its child components are created dynamically by the JS.
  • The child components are created and added to the page using the DOM createElement() and appendChild() methods introduced earlier in the lesson.
  • An object -- stored in the groups constant -- is used to define the racial groups to be displayed by the app along with their portal IDs.  The group names are the object properties, while the portal IDs are the associated values.
  • A loop is used to iterate over each property/value pair in the groups object.
  • Within the loop, the key variable stores the group name on the current iteration.  Retrieving the portal ID for that group is done using the expression groups[key].
  • With this version of the app, adding other racial groups is as simple as adding the name and portal ID to the groups variable (as opposed to copying/modifying several lines in both the HTML and JS).

8.7.3 Date Picker

8.7.3 Date Picker jed124

Earlier in the lesson we saw that dates can be obtained from the user via the date input type built into HTML5. Another option for obtaining dates is Calcite's Date Picker component. This component is demonstrated in the CodePen below:

See the Pen calcite date picker by Jim Detwiler (@jimdetwiler) on CodePen.

Looking at the HTML, you should note that the two date widgets are created using calcite-input-date-picker elements. The scale="s" setting gives them a small size (with "m" and "l" being other options for that property).  Looking at the surrounding code, it begins with a similar configuration to the Action Bar example discussed earlier.  Everything in the body is enclosed within a Shell component, a div for the MapView goes in the Shell's default slot, and the div is followed by a Shell Panel component.  Unlike the Action Bar example, here the Shell Panel goes in the panel-end slot rather than panel-start.  

Within the Shell Panel is embedded a Panel component.  The heading property is set to show the Wildfire Viewer text at the top of the panel.  Next comes a Block component, which is used as an organizer for a set of related controls (here the Date Pickers and Button).  Nicely formatted headings are displayed at the top of the Block through the setting of its heading and description properties. Blocks are closed/collapsed by default, so the open property is set to display the Block content.  

Within the Block are the Date Pickers followed by a Button component.  id's are assigned to each of the elements so that they can be referenced in the JavaScript code.  

One last component is placed with the Block -- a Notice component.  This is used to display a nicely formatted message to the user on the number of fires found in the specified date range.  

In the JS code, the setDates() function is immediately called upon when the page loads. That function creates two Date objects: one representing today and the other 30 days prior to today. Those JS Date objects are used to set the widgets' initial values and constraints (min and max possible dates). This is in contrast to the earlier date picker example, in which those attributes were set to strings in yyyy-mm-dd format.

The rest of the app works exactly the same as the earlier date picker example.

And paralleling the earlier page that covered the HTML5 date picker, below is an example built on the 2019 wildfire layer in which the DateTextBox dijit's values and constraints have been hard-coded within the HTML.

See the Pen calcite date picker dynamic by Jim Detwiler (@jimdetwiler) on CodePen.

8.7.4 Combobox and Button

8.7.4 Combobox and Button jed124

Two other controls often found in user interfaces are the combo box and the button.  In the CodePen below, I've implemented Calcite's Combobox and Button components in a modification of the mountain peak filtering sample we saw earlier:

See the Pen calcite peaks by Jim Detwiler (@jimdetwiler) on CodePen.

In this version of the app, I've replaced the vanilla HTML select elements with calcite-combobox elements and the HTML option elements with calcite-combobox-item elements.  The Calcite Combobox's default behavior is to allow for multiple selections, which isn't really suitable for this scenario.  To produce the best result, we want to override the default behavior by setting the selection-mode, selection-display, and clear-disabled attributes.  Rather than repeat the same HTML attribute settings for each calcite-combobox, note that the settings are made using JS code.  The DOM's querySelectorsAll() method is used to get a reference to the calcite-combobox elements (as a NodeList).  This NodeList object is stored in a constant called comboBoxes.  A for loop is then used to iterate over each combobox, with the DOM's setAttribute() method used to set the three attributes noted above to desired values.  

Returning to the HTML, note that I've replaced the vanilla HTML button element with a calcite-button element.  One benefit to this change is the ability to configure an icon to appear alongside the button text, either before or after it.  Here I've added Calcite's query icon after the text by setting the icon-end attribute. 

Finally, note that I carried over the id values from the select and button elements in the Esri sample to the calcite-comboboxes and calcite-button elements in my modification.  Thus, the sample's doQuery() function needed no changes.

8.7.5 Other Calcite Components

8.7.5 Other Calcite Components jed124

We've really just scratched the surface of Calcite components that could be useful in developing geospatial apps. I encourage you to browse through the Calcite components documentation to see some of the other UI elements that are available. Here are a few that are particularly useful, in my opinion:

With that, we've reached the end of the lesson content on GUI development.  For this week's assignment, you'll return to your Assignment 7 scenario and develop a more user-friendly and informative version of that app.

8.8 Map Components

8.8 Map Components jed124

Paralleling their Calcite development, Esri has made a strong push to produce map-centric web components with the goal of simplifying the app development experience for 3rd-party developers. 

In this section of the lesson, we’ll dive into Esri’s map components, looking at their basic implementation, their documentation, how to combine them with objects from the Core API we’ve been dealing with thus far in the course, and finally looking specifically at the components that replace the deprecated widgets covered earlier in this lesson.

8.8.1 Intro to Map Components

8.8.1 Intro to Map Components jed124

Some background on web components technology can be found on Mozilla Developer. (Read as far as you like, but focus on the brief Concepts and Usage section at the top of the article.  Also note that we won't be creating web components as outlined by the 5 steps at the end of that section.  We'll just be consuming components created by Esri.)

Let's begin by examining a sample from Esri's Maps SDK for JS documentation.

  1. Go to the Sample Code area of the documentation, find the Intro to map components (2D) sample under the Get Started heading, and have a look at the code in the sandbox or CodePen. 

    Note that a map is added to the page through the use of a custom arcgis-map element.  No need to instantiate separate Map and MapView objects as required when working strictly with the Core API.  Initial map settings (basemap, zoom, and center) are applied as attributes on the HTML element.
  2. Adjust the zoom and center attributes to focus the map on your area of interest.
  3. Add zoom and home buttons to the UI using the Zoom and Home components:

    <arcgis-map basemap="topo-vector" zoom="4" center="15, 65">
          <arcgis-zoom position="top-left"></arcgis-zoom>
          <arcgis-home position="top-left"></arcgis-home>
    </arcgis-map>

    If you’d like to use the Map component to display a web map saved in ArcGIS Online, making that happen is as simple as setting its item-id attribute.

  4. Go into AGO, find your Jen and Barry’s web map from Lesson 1, copy its portal item ID, and use it to set the map's item-id.  Your web map will carry a basemap setting with it, so while setting the item-id attribute, you can also remove the basemap attribute:

    <arcgis-map basemap="topo-vector" item-id="<your web map id here>" zoom="6" center="-78, 42">

Awesome, huh?  You're probably wondering why we didn't talk about this sooner.  It's possible that as the course evolves, we'll shift the discussion of map components toward the beginning.  These coarse-grained elements can really make it easy to put together relatively simple apps.  However, if your app has much complexity to it, you're going to have to work with the finer-grained objects from the Core API.  The two parts of the SDK are not mutually exclusive, and we'll momentarily how to build an app that utilizes both map components and objects from the Core API.

Before we do that, here are a few important points to note in this map component sample:

  • You should add references to the Maps SDK for JS in the head of the HTML document, just as we've been doing.
  • The map components must be referenced separately (also in the head):

    <script type="module" src="https://js.arcgis.com/4.33/map-components"></script>
  • In the body of the doc, you no longer need a div that will serve as the container for a Core API Map object. The arcgis-map element will serve that purpose now. (If you want a 3D map instead, you can use arcgis-scene rather than arcgis-map.)
  • JavaScript code that builds upon the components should go in a script element in the page body as opposed to the head.

8.8.2 Integration with the Core API

8.8.2 Integration with the Core API jed124

Map components can simplify the basics of app development, but at some point you’re likely to need the more fine-grained objects made available through the Core API we’ve been working with during the class.  The Intro to map components (2D) sample provides a crosswalk of sorts for connecting map component code with Core API code.  The first step is to obtain a reference to the Map’s DOM element:     

     const viewElement = document.querySelector("arcgis-map");

From there, you can set up a listener for when the Map is ready for interaction:

      viewElement.addEventListener("arcgisViewReadyChange", (event) => {
        console.log("Map component is ready", event);
      });

Any code you want to build upon the map components can be embedded within this anonymous event listener. 

Picking up where we left off, let’s add Penn State buildings (which we worked with in Lesson 1) to the map as a FeatureLayer.  The first step will be to import that module. 

  1. Add the following to the top of the script element that holds your JS code:
          const [FeatureLayer] = await $arcgis.import(
            ["@arcgis/core/layers/FeatureLayer.js"]
          );
  2. Within the listener for the “arcgisViewReadyChange” event, instantiate a new FeatureLayer:
          const buildingsLyr = new FeatureLayer({
              url: "https://mapservices.pasda.psu.edu/server/rest/services/pasda/PSU_Campus/MapServer/1"
          });
    Creation of this FeatureLayer could also happen before getting the reference to the Map component.
  3. Finally, add the FeatureLayer to the map:

          viewElement.map.add(buildingsLyr);

Note that while the arcgis-map element is referred to as a Map component, adding one to your page actually creates a MapView from the Core API. Thus, to add a layer, you need to access the Map associated with the MapView through its map property (viewElement.map). In this last step, you could certainly store a reference to the Map instead of chaining:

           const map = viewElement.map;
           map.add(buildingsLyr);

Finally, a side note on $arcgis.import()… We’ve implemented this function with arrays of modules/variables, but you should be aware that it can also be implemented without arrays. For example, in this little walkthrough in which FeatureLayer is the only Core API module being imported, we could do the import as follows instead:

           const FeatureLayer = await $arcgis.import("@arcgis/core/layers/FeatureLayer.js");

You'll see single modules imported in this way in the documentation and I didn’t want the different syntax to throw you off.

8.8.3 Map Component Documentation

8.8.3 Map Component Documentation jed124

The documentation of the SDK's map components can be accessed by going to the documentation home page (https://developers.arcgis.com/javascript/latest/), clicking on the References tab, and then on the Map Components box.  Alternatively, if you happen to be looking at a Core API reference page, say MapView's, the Topics pane on the left side of the window will have its tree view expanded to show the core node.  If you scroll up to the top of the tree view and collapse the core node, you'll see that there are a few other nodes in the tree, all component-related, with map-components being one of them.  You can expand the map-components node to access a list of available components.

Click on the Map component to access its documentation.  The top of its page will look similar to a Core API class page, with info on how to import the component depending on whether you're accessing the SDK via ESM or CDN.  One nice aspect of map components, as shown on the Map component page, is that you don't need to import a separate module for each component the way that you must with Core API classes.  You just need to include the single reference to the map components library in your HTML doc's head and you're set to use any map component:
<script type="module" src="https://js.arcgis.com/4.33/map-components/"></script>

Much like a Core API class page, a map component page will include a section detailing its Properties, Methods, and Events.  It will also have a section on the component's Slots (which were discussed in the Calcite part of the lesson). 

The unique part of the component pages is the Demo section, which provides a mini-sandbox for seeing how the component works.  A live preview appears on the left, while a set of controls/properties/attributes appears on the right.  The control default values can be modified and the preview will update accordingly. 

The Map component demo shows a web map of NFL stadiums by default through the setting of the item-id control.  Wipe out that ID (the component will go blank), then assign a basemap setting of satellite (or any other valid basemap you wish).  Feel free to assign values to other controls, such as zoom or center.  (The center control requires a value in a form like -100, 40.) 

After you're done experimenting with the controls, click the Code tab along the top of the Demo GUI.  You should see the HTML code for the custom element you configured and can click the Copy button to copy the code to your clipboard.  Note that the "Control" settings made through the Demo GUI translate to HTML attribute settings.  (Interestingly, I'm seeing an item-id attribute without a string assigned to it after doing the customization I just recommended to you.  While the inclusion of this unset attribute won't hurt anything, it really makes more sense to just omit it if you're not setting it.)

The Demo GUI offers a number of buttons along the top that you're free to experiment with, though I'm not sure that I'd ever find a use for them myself.  There is a Reset controls button near the top of the Controls pane, allowing you to go back to its default settings.  You could also simply reload the documentation page altogether.  

One last point on the Demo sandbox is that the controls it shows will be the component properties that can be set to simple, scalar values (strings, numbers, and Booleans).  If you click on the Properties link to jump down to that section of the component's documentation, you should note that the properties that have something in the Attribute column correspond to the controls available in the Demo sandbox.  But you should keep in mind that those properties without something in the Attribute column are ones that you can set using JS code.  We'll see an example of doing that in the discussion of the Legend component later in this section.

8.8.4 BasemapToggle component

8.8.4 BasemapToggle component jed124

As noted earlier, Esri has deprecated the BasemapToggle widget in favor of the newer component by the same name.  The pen below shows the use of this component.  Note the setting of the position and next-basemap attributes.

  See the Pen   L8_BasemapToggle_component by Jim Detwiler (@jimdetwiler)   on CodePen.

8.8.5 BasemapGallery component

8.8.5 BasemapGallery component jed124

As we saw with the BasemapGallery widget example, in this example a BasemapGallery component is embedded within an Expand component. Note the setting of three attributes on the Expand component. The position attribute is self-explanatory. The mode can be either "floating", as shown here, or "drawer", which causes the basemaps to appear in a box filling much of the bottom of the window. The close-on-esc attribute controls whether the user can close the gallery by pressing the Esc key on the keyboard. As a Boolean attribute, you should simply refer to it by name (i.e., don't say close-on-esc="true") to set the attribute to true or omit it to set it to false.

You can have the gallery displayed when the map loads by setting the Boolean expanded attribute to true.

See the Pen BasemapGallery_component by Jim Detwiler (@jimdetwiler) on CodePen.

8.8.6 LayerList component

8.8.6 LayerList component jed124

The LayerList widget (covered earlier in the lesson) is not labeled as deprecated in the SDK documentation (as of Jul 2025).  Presumably that means the LayerList component lacks some important functionality.  In any case, the example below shows the component embedded within an Expand component, like the BasemapGallery example earlier.  One attribute you might consider setting is visibility-appearance, which allows for showing the user checkboxes for toggling layer visibility rather than the default eyeball icon.

See the Pen LayerList_component by Jim Detwiler (@jimdetwiler) on CodePen.

8.8.7 Legend component

8.8.7 Legend component jed124

As with the LayerList widget, Esri has not yet deprecated the Legend widget (as of Jul 2025). The example below demonstrates the use of the newer Legend component, which you should use over the widget unless it doesn't deliver some needed behavior.

See the Pen Legend Demo by Jim Detwiler (@jimdetwiler) on CodePen.

Looking at the code, the component has two attributes set: position and legend-style. The latter defaults to "classic", but can be set to "card" instead. The part of the code to pay particular attention to involves customizing the legend to show only the cities layer. As with the Legend widget covered earlier, the Legend component has a layerInfos property that can be used to specify which layers to depict. Looking at the documentation, you should note that there is no layer-infos attribute; this is sensible since it's not possible to specify the information associated with this property using a simple string. So this example is the first to demonstrate how to set a component property using JS as opposed to setting an attribute using HTML. The key is to use the same querySelector() DOM method that we've seen used to get a reference to the view element, but this time to get a reference to the legend element. With a reference to the element (component), its layerInfos property can be set.

8.8.8 ScaleBar component

8.8.8 ScaleBar component jed124

The pen below, modeled after the example for the ScaleBar widget, demonstrates the use of the ScaleBar component. Note that the style and unit are conveniently set using HTML attributes.

See the Pen ScaleBar demo - components by Jim Detwiler (@jimdetwiler) on CodePen.

This concludes the demonstration of map components built into Esri's Maps SDK for JS. There are many others and I recommend you take a few minutes to peruse them in the documentation. Some you may find to be of particular interest include (not an exhaustive list):

  • Area Measurement
  • Distance Measurement
  • Bookmarks: typically used to provide access to spatial bookmarks stored with a web map
  • Editor
  • Feature Table
  • Navigation Toggle: for 3D scene navigation
  • Swipe

Assignment

Assignment jed124

For this week's assignment, I want you to return to the scenario you selected for the Lesson 7 assignment.  Regardless of the scenario, I'd like you to follow these guidelines:

  • Instead of acquiring the user's input on what to map via the JavaScript prompt() method, use one of the more appropriate UI elements covered here in Lesson 8.
  • Include a sidebar that lists the features displayed on the map.  Clicks on items in the sidebar should open a popup over their associated features on the map.  (If you're looking for extra credit or just to challenge yourself, try displaying the sidebar information in a table.  For example, you might use the Calcite Table component, which can be used to provide a table with sortable and resizable columns, among other features.)
  • Include a user-friendly legend.
  • Include any other widgets or UI features that you think would enhance your app.

Deliverables

This project is one week in length. Please refer to the course Calendar, in Canvas, for the due date.

  1. Edit your e-Portfolio so that it includes a link to your map and post a link to your e-Portfolio through the Assignment 8 page.  (80 of 100 points)
  2. Beneath the map or on your e-Portfolio page, provide a short description of your app and how you approached solving this project. Reflect on what you learned from the lesson, what you found challenging, and include what resources you used to complete the assignment.  (20 of 100 points)
  3. Complete the Lesson 8 quiz.

Summary and Final Tasks

Summary and Final Tasks ksc17

In Lesson 8, you learned how your apps can be "taken to the next level" through the addition of many different types of user interface elements. I hope you'll come away from this lesson and associated assignment excited and thinking of ways you can apply what you've learned to your own work. You'll have an opportunity to do just that in your own final project in a couple of weeks. But first, you'll have one last lesson that focuses on building analytical capabilities into your web apps.