7.3 Clickable Sidebar List
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.
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 40. 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 popup not being anchored in the correct location.
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.
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.