GEOG 863:
Web Application Development for Geospatial Professionals

8.6.1 Action Bar

PrintPrint

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 references to your HTML, above the references to the JS API:
        <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" /

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 window and add a reference to the LayerList module:
        require(["esri/views/MapView", "esri/WebMap", "esri/widgets/LayerList"],
    	  (MapView, WebMap, LayerList)
  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 require callback function):
      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 here:
    https://www.w3schools.com/js/js_string_templates.asp

    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:
    https://www.w3schools.com/jsref/met_document_queryselector.asp

    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):
        require(["esri/views/MapView", "esri/WebMap", "esri/widgets/LayerList", “esri/widgets/BasemapGallery”],
            (MapView, WebMap, LayerList, BasemapGallery)
  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.