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.