Through this point in the course, we've worked with many of the most commonly used parts of Esri's JavaScript API. 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.
At the successful completion of this lesson, you should be able to:
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.)
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.
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.
|
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. |
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.
There are four key steps in implementing this widget:
The four steps are annotated in the code sample below:
const map = new Map({ basemap: "topo-vector" // STEP 1 }); const view = new MapView({ container: "viewDiv", map: map, center: [-86.049, 38.485], zoom: 3 }); const toggle = new BasemapToggle({ view: view, // STEP 2 nextBasemap: "hybrid" // STEP 3 }); view.ui.add(toggle, "top-right"); // STEP 4
This widget provides the user a set of basemap options (with thumbnail previews) to choose from. The simplest implementation, as seen in this sample [1] involves creating a BasemapGallery object, associating it with the appropriate View, and adding it to the desired position in the UI.
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 [2], 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, [3]a cartography and visualization company based in San Francisco, and delivered in partnership with Stadia [4], 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: '© <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> © <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> © <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:
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 [8](a company based in Madrid). I want to draw your attention to a couple of important points:
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. This sample demonstrates the Home widget [9].
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. This is another widget that is straightforward to implement, so I'll again refer you to an Esri sample of the LayerList widget [10].
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:
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 [13] 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).
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", "non-metric" or "dual". The style property can be set to "ruler" or "line". Here's an example that shows a map with two ScaleBar widgets:
The only difference I can see between the two styles is that the ruler style has the labels appear above the line while the line style has them below the line. The Esri ScaleBar sample [15] shows that setting the unit to dual will produce a line with the metric label on top and the non-metric label on bottom.
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.
Before getting to the examples, please work through the HTML Forms tutorial at w3schools [16]. 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.
This Esri sample [17] 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 [18] 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 [19] 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.
As you saw in the w3schools tutorial, a dropdown list is created in HTML using a select element. This Esri sample [20] 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.
The next sample [21] 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.
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.
Some noteworthy aspects of this app:
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" [24], 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 [25], which was introduced at v4.12 of Esri's API.
Initial setup
Two divs are defined in the HTML on lines 25 and 27 of the HTML to serve as placeholders for the two Slider widgets. The widget objects themselves are created early in the JS code, on lines 25-53. 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 224-226, which specifies that a click on the button should trigger execution of a function called queryEarthquakes(). That function (which begins on line 228) 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.
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:
Looking at the HTML at the bottom of the example, you should note the following:
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.)
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 which shows counties in Jen & Barry’s world that meet the 500-farm criterion. FYI, this example builds on an earlier one [29] and is modeled after this Esri sample [30].
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:
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 40. Some important differences in this version of the farmQuery are:
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 52-89). 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 23), 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 50) 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 91 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 popup 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 popup though, we have to look at line 97. 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 [32] 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 line 97 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 97 will evaluate to a county graphic, which is then what is assigned to the result variable.
After all that, we finally get to the popup code. Line 100 first ensures that everything was 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 open() 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.
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.
This text box is created through two steps:
Earlier in this lesson, you used the view.ui.add() [34] 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. This example [35] shows a few ways to add your own HTML widgets to the map interface and notice that I've used the Esri's CSS class "esri-widget" so they match the Esri's theme. For simplicity, I've only included the code for creating and placing widgets in this example, but programming actions was described earlier in the lesson.
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.
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 57-65. That layer is added to a new Map and the Map is associated with a new MapView. Then on lines 90-101 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 119-124, it is composed of three smaller expressions:
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.
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 (https://developer.mozilla.org/en-US/docs/Web/API/Web_components [37]).
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:
https://developers.arcgis.com/calcite-design-system/ [38]
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.
<script src="https://js.arcgis.com/calcite-components/2.6.0/calcite.esm.js" type="module"></script> <link rel="stylesheet" href="https://js.arcgis.com/calcite-components/2.6.0/calcite.css" /
Apps built using Calcite components typically have them embedded within a Shell component, and that's how we'll begin here.
<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.
<h2 id="header-title" slot="header"> <!-- Populated at runtime --> </h2>
#header-title { margin-left: 1rem; margin-right: 1rem; }
webmap.when(() => { const title = webmap.portalItem.title; document.getElementById("header-title").textContent = title; });
Now let’s add a Shell Panel that will house the Action Bar with the desired functionality.
<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.
<calcite-action-bar slot="action-bar"> </calcite-action-bar>
<calcite-action data-action-id="layers" icon="layers" text="Layers"> </calcite-action>
<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.
<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.
require(["esri/views/MapView", "esri/WebMap", "esri/widgets/LayerList"], (MapView, WebMap, LayerList)
const layerList = new LayerList({ view: view, selectionEnabled: true, container: "layers-container" });
let activeWidget;
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; } };
document.querySelector("calcite-action-bar").addEventListener("click", handleActionBarClick);
<calcite-action data-action-id="basemaps" icon="basemap" text="Basemaps"> </calcite-action>
<calcite-panel heading="Basemaps" height-scale="l" data-panel-id="basemaps" hidden> <div id="basemaps-container"></div> </calcite-panel>
require(["esri/views/MapView", "esri/WebMap", "esri/widgets/LayerList", “esri/widgets/BasemapGallery”], (MapView, WebMap, LayerList, BasemapGallery)
const basemaps = new BasemapGallery({ view: view, container: "basemaps-container" });
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.
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.
With this walkthrough complete, let's have a look at a few other working examples that demonstrate some other useful Calcite components.
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:
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:
In this version, note the following:
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 [44] component. This component is demonstrated in the CodePen below:
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 [46] component. The heading property is set to show the Wildfire Viewer text at the top of the panel. Next comes a Block [47] 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 [48] 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.
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 [50] and Button [51] components in a modification of the mountain peak filtering sample we saw earlier:
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 attribute settings for each calcite-combobox, note that the settings are made in the JS block, at the beginning of the require() callback function. 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.
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 [53] 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.
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:
This project is one week in length. Please refer to the course Calendar, in Canvas, for the due date.
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.
Links
[1] https://developers.arcgis.com/javascript/latest/sample-code/widgets-basemapgallery/
[2] http://leaflet-extras.github.io/leaflet-providers/preview/index.html
[3] https://stamen.com/
[4] https://stadiamaps.com
[5] https://codepen.io/jimdetwiler/pen/YerNNg
[6] https://codepen.io/jimdetwiler
[7] https://codepen.io
[8] https://carto.com/
[9] http://developers.arcgis.com/javascript/latest/sample-code/widgets-home/index.html
[10] http://developers.arcgis.com/javascript/latest/sample-code/widgets-layerlist/index.html
[11] https://codepen.io/jimdetwiler/pen/eVGgEy
[12] https://codepen.io/jimdetwiler/pen/wyrgPG
[13] http://developers.arcgis.com/javascript/latest/sample-code/widgets-legend/index.html
[14] https://codepen.io/jimdetwiler/pen/xYXgYy
[15] https://developers.arcgis.com/javascript/latest/sample-code/sandbox/index.html?sample=widgets-scalebar
[16] https://www.w3schools.com/html/html_forms.asp
[17] https://developers.arcgis.com/javascript/latest/sample-code/elevation-query-points/
[18] https://www.w3schools.com/jsref/met_document_getelementbyid.asp
[19] http://www.w3schools.com/jsref/met_document_addeventlistener.asp
[20] https://developers.arcgis.com/javascript/latest/sample-code/layers-scenelayer-filter-query/
[21] https://developers.arcgis.com/javascript/latest/sample-code/sandbox/?sample=query
[22] https://codepen.io/jimdetwiler/pen/LYeRqqp
[23] https://www.w3schools.com/html/html_form_attributes.asp
[24] https://www.w3schools.com/howto/howto_js_rangeslider.asp
[25] https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Slider.html
[26] https://codepen.io/jimdetwiler/pen/WNdGmNK
[27] https://codepen.io/jimdetwiler/pen/qBMvZzN
[28] https://codepen.io/jimdetwiler/pen/VwGRrMr
[29] https://codepen.io/jimdetwiler/pen/OzePRb
[30] https://developers.arcgis.com/javascript/latest/sample-code/sandbox/index.html?sample=featurelayerview-query
[31] https://codepen.io/jimdetwiler/pen/jZGypw
[32] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators
[33] https://codepen.io/jimdetwiler/pen/NWXbWYo
[34] https://developers.arcgis.com/javascript/latest/api-reference/esri-views-ui-DefaultUI.html#add
[35] https://codepen.io/JimmyKroon/details/gOPMZjd
[36] https://codepen.io/jimdetwiler/pen/BabGZGa
[37] https://developer.mozilla.org/en-US/docs/Web/API/Web_components
[38] https://developers.arcgis.com/calcite-design-system/
[39] https://developers.arcgis.com/javascript/latest/sample-code/webmap-basic/
[40] https://www.w3schools.com/js/js_string_templates.asp
[41] https://www.w3schools.com/jsref/met_document_queryselector.asp
[42] https://codepen.io/jimdetwiler/pen/oNOGbXZ
[43] https://codepen.io/jimdetwiler/pen/pomNYRL
[44] https://developers.arcgis.com/calcite-design-system/components/date-picker/
[45] https://codepen.io/jimdetwiler/pen/qBwVYqb
[46] https://developers.arcgis.com/calcite-design-system/components/panel/
[47] https://developers.arcgis.com/calcite-design-system/components/block/
[48] https://developers.arcgis.com/calcite-design-system/components/notice/
[49] https://codepen.io/jimdetwiler/pen/LYoJzzZ
[50] https://developers.arcgis.com/calcite-design-system/components/combobox/
[51] https://developers.arcgis.com/calcite-design-system/components/button/
[52] https://codepen.io/jimdetwiler/pen/pomWVYW
[53] https://developers.arcgis.com/calcite-design-system/components/
[54] https://developers.arcgis.com/calcite-design-system/components/accordion/
[55] https://developers.arcgis.com/calcite-design-system/components/menu/
[56] https://developers.arcgis.com/calcite-design-system/components/tree/
[57] https://developers.arcgis.com/calcite-design-system/components/color-picker/
[58] https://developers.arcgis.com/calcite-design-system/components/dropdown/
[59] https://developers.arcgis.com/calcite-design-system/components/progress/