Build a user interface (UI) with Calcite Design System. This tutorial will provide an overview of Calcite Components, which can be replicated and scaled to solve complex problems. You will:
- Learn how to execute a query to access attributes from a feature service.
- Add a
calcite-notice
to dynamically populate the number of records found in the query. - Dynamically add
calcite-card
's to display individual records from the query.- Create a
calcite-button
that links to a record's location in Map Viewer.
- Create a
This tutorial leverages vanilla JavaScript, but these concepts and methods of interaction are applicable across frameworks. For a tutorial that includes a map, visit Create a mapping app.
Prerequisites
Steps
Create a new pen
- Go to CodePen to create a new pen for your application.
Construct the HTML
- In CodePen > HTML, add HTML to create a page with a
<body>
tag. Also, add a<main>
section.
The <!DOCTYPE html>
tag is not required in CodePen. If you are using a different editor or running the page on a local server, be sure to add this tag to the top of your HTML page.
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Calcite Components: Filter recent earthquakes</title>
</head>
<style>
</style>
<body>
<main>
</main>
</body>
<script>
</script>
</html>
- To access Calcite Design System web components, add references to Calcite Components in the
<head>
element.
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Calcite Components: Filter recent earthquakes</title>
<script src="https://js.arcgis.com/calcite-components/2.12.1/calcite.esm.js" type="module"></script>
<link rel="stylesheet" href="https://js.arcgis.com/calcite-components/2.12.1/calcite.css" />
</head>
- Next, you will organize the contents of the application. In the
main
section, add acalcite-shell
andcalcite-panel
with attributes ofheading
andheading-level
to contain the earthquake results.
The heading
and heading-level
attributes arrange page hierarchy throughout your application to support a larger audience. Since this is the first header on the page, set the heading-level
value to "1".
<calcite-shell>
<!-- Panel to display records -->
<calcite-panel heading="Earthquake results" heading-level="1" description="Search by location to display results">
</calcite-panel>
</calcite-shell>
Showcase the records
You will continue building HTML content, which the user will have the ability to adjust. The content will change as the user interacts with the application.
- In the
calcite-panel
, add acalcite-filter
, which will be used to query the earthquakes feature service.
<!-- Filter records -->
<calcite-filter placeholder="Try searching Alaska"></calcite-filter>
- Next, add two
calcite-notice
elements, each accompanied with an uniqueid
attribute. The first will provide guidance on the app's initial state and be set asopen
. The second will supply on-demand feedback to the user.
<calcite-shell>
<!-- Panel to display records -->
<calcite-panel heading="Earthquake results" heading-level="1" description="Search by location to display results">
<!-- Filter records -->
<calcite-filter placeholder="Try searching Alaska"></calcite-filter>
<!-- Provide details of the app's initial state -->
<calcite-notice id="initial-note" open icon="information">
<div slot="title">Try searching a place of interest</div>
<div slot="message">Results will display when text is entered.</div>
</div>
</calcite-notice>
<!-- An open property will be added to display the number of filtered records -->
<calcite-notice id="note">
</calcite-notice>
</calcite-panel>
</calcite-shell>
- In the second
calcite-notice
, add adiv
and place it in thetitle
slot, which will be used later to populate the number of earthquake results using theid
attribute.
<!-- An open property will be added to display the number of filtered records -->
<calcite-notice id="note">
<div id="number-records" slot="title">
<!-- Content is automatically generated -->
</div>
</calcite-notice>
- Next, add a
div
with aclass
attribute to hold the earthquake results. Also, placecalcite-pagination
in the panel'sfooter
slot, and add apage-size
attribute to specify the number of items to display per page.
<!-- Container with Cards -->
<div class="card-container">
<!-- Content is automatically generated -->
</div>
<!-- Pagination -->
<calcite-pagination slot="footer" page-size="12" style="visibility:hidden" ></calcite-pagination>
Query the data
You will add JavaScript functionality to query an earthquakes feature service via a search term entered by a user.
- First, create constant variables referencing the
calcite-filter
,calcite-pagination
, bothcalcite-notice
's, and thecard-container
CSS class to reference the elements later on.
const filterElement = document.querySelector("calcite-filter");
const paginationElement = document.querySelector("calcite-pagination");
const initialNoticeElement = document.getElementById("initial-note");
const noticeElement = document.getElementById("note");
const cardContainer = document.querySelector(".card-container");
- Next, query the earthquakes service using the Fetch API. When successful, parse the response with
Response.json()
, and themap()
method to capture the features.
/* Fetch the earthquakes feature service */
fetch("https://services9.arcgis.com/RHVPKKiFTONKtxq3/ArcGIS/rest/services/USGS_Seismic_Data_v1/FeatureServer/0/query?where=1%3D1&outFields=*&f=json")
.then((response) => response.json())
.then(({features}) => features.map(({attributes}) => attributes))
Filter the results
Next, with the response parsed, you will filter and display the user-defined results.
- Filter results using a named function expression,
init
. Set theFilter calcite-filter
component'sitems
property using the constant variable defined in the step above.
/* Fetch the earthquakes feature service */
fetch("https://services9.arcgis.com/RHVPKKiFTONKtxq3/ArcGIS/rest/services/USGS_Seismic_Data_v1/FeatureServer/0/query?where=1%3D1&outFields=*&f=json")
.then((response) => response.json())
.then(({features}) => features.map(({attributes}) => attributes))
.then((attributes) => initFilter(attributes));
/* Filter the results to display */
const initFilter = (items) => {
filterElement.items = items;
};
- In
init
, add a listener for theFilter calcite
event to watch for changes to the filter'sFilter Change value
. Then, go back to the first page by changing pagination'sstart
andItem total
properties. Lastly, create a conditional to add cards when the filter'sItems value
is not falsey.
The filter's filtered
property contains all of the items when there is no value
. To create search functionality, you need to only add cards when the user has entered a query term.
/* Filter the results to display */
const initFilter = (items) => {
filterElement.items = items;
document.addEventListener("calciteFilterChange", () => {
paginationElement.startItem = 1;
paginationElement.totalItems = 0;
// When a Filter value is present
// Create Cards, update Pagination, and number of responses
if (filterElement.value) {
// If additional pages are populated, display Pagination
if (paginationElement.totalItems > paginationElement.pageSize) {
paginationElement.style.visibility = "visible";
}
});
};
- When there is no text present in the
calcite-filter
, set the initial notice'sopen
property totrue
.
/* Filter the results to display */
const initFilter = (items) => {
filterElement.items = items;
document.addEventListener("calciteFilterChange", () => {
paginationElement.startItem = 1;
paginationElement.totalItems = 0;
paginationElement.style.visibility = "hidden";
// When a Filter value is present
// Create Cards, update Pagination, and number of responses
if (filterElement.value) {
// If additional pages are populated, display Pagination
if (paginationElement.totalItems > paginationElement.pageSize) {
paginationElement.style.visibility = "visible";
}
} else {
// If no text is present in the Filter, display the initial notice
initialNoticeElement.open = true;
}
});
};
- Next, within
calcite
, you will display the pagination component when theFilter Change total
filtered earthquakes is greater than the specifiedItems page
value. Set theSize calcite-pagination
component'svisibility
CSS property to"hidden"
. When there is more than one page of results, you will change the CSS property's value to"visible"
.
/* Filter the results to display */
const initFilter = (items) => {
filterElement.items = items;
document.addEventListener("calciteFilterChange", () => {
paginationElement.startItem = 1;
paginationElement.totalItems = 0;
paginationElement.style.visibility = "hidden";
// When a Filter value is present
// Create Cards, update Pagination, and number of responses
if (filterElement.value) {
// If additional pages are populated, display Pagination
if (paginationElement.totalItems > paginationElement.pageSize) {
paginationElement.style.visibility = "visible";
}
} else {
// If no text is present in the Filter, display the initial notice
initialNoticeElement.open = true;
}
});
};
Display the earthquakes
To display the earthquakes you will store each result's attributes in a calcite-card
. You will also add a calcite-button
, which, when accessed, will open the earthquake location in Map Viewer.
- Place the filtered earthquake(s) into
calcite-card
component(s) residing in thecard-container
class with a named function expression,create
.Card
/* Create Cards and their content */
const createCard = (item) => {
const headingName = item.place.replace(/[;']/g,"");
// Populate Card content
if (cardContainer.childElementCount < paginationElement.pageSize) {
const cardString =
`<calcite-card id="card-${item.OBJECTID}">
<span slot="heading">
<b>${item.place}</b>
</span>
<span slot="description">
Occurred on: ${new Date(item.eventTime)}
</span>
<calcite-chip
appearance="outline-fill"
scale="s"
kind="inverse"
icon="graph-time-series"
>
Magnitude: ${item.mag}
</calcite-chip>
<calcite-button
label="Open ${headingName} in map"
icon-end="launch"
slot="footer-end"
target="_blank"
width="full"
href="https://www.arcgis.com/apps/mapviewer/index.html?` +
`marker=${item.longitude};${item.latitude};` + // Marker (lng, lat)
`4326;` + // Coordinate system
headingName + `;` +
`;` + // Marker image
`Magnitude: ${item.mag}&` +
`center=${item.longitude};${item.latitude}&` +
`level=6"
>
Open in map
</calcite-button>
</calcite-card>`;
const cardElement = document
.createRange()
.createContextualFragment(cardString);
cardContainer.appendChild(cardElement);
}
};
- To create the cards, call
create
in theCard calcite
event listener for each feature that matches the filterFilter Change value
. Also, clear thecard
content, which contains previous filter results.Container
/* Filter the results to display */
const initFilter = (items) => {
filterElement.items = items;
document.addEventListener("calciteFilterChange", () => {
paginationElement.startItem = 1;
paginationElement.totalItems = 0;
paginationElement.style.visibility = "hidden";
cardContainer.innerHTML = "";
// When a Filter value is present
// Create Cards, update Pagination, and number of responses
if (filterElement.value) {
filterElement.filteredItems.forEach((item) => createCard(item));
paginationElement.totalItems = filterElement.filteredItems.length;
// If additional pages are populated, display Pagination
if (paginationElement.totalItems > paginationElement.pageSize) {
paginationElement.style.visibility = "visible";
}
} else {
// If no text is present in the Filter, display the initial notice
initialNoticeElement.open = true;
}
});
};
- Next, you will display the number of earthquake results for the user. Create a function,
show
and post the number of responses to theNumber Of Responses calcite-notice
title. To ensure content is accessible to users, remove the initial notice'sopen
attribute and set the results noticeopen
property totrue
.
/* Display the number of responses in a Notice */
function showNumberOfResponses(responseNumber) {
const note = document.getElementById("note");
const numberRecordsNote = document.getElementById("number-records");
// If 0 responses, add "Sorry" to the Notice text
// Add the Notice color and icon
if (responseNumber === 0) {
responseNumber = `Sorry, ${responseNumber}`
note.kind = "danger";
note.icon = "exclamation-mark-triangle";
} else {
note.kind = "brand";
note.icon = "information";
}
// Hide the initial notice
initialNoticeElement.removeAttribute("open");
// Notice text
numberRecordsNote.innerHTML = `${responseNumber} records found.`;
noticeElement.open = true;
}
-
Similar to creating the cards, add the
show
function call to theNumber Of Responses calcite
event listener's callback function. When no text is present in theFilter Change calcite-filter
, set the filtered notice'sopen
attribute tofalse
, and thecalcite-pagination
's visibility to"hidden"
.Use dark colors for code blocks /* Filter the results to display */ const initFilter = (items) => { filterElement.items = items; document.addEventListener("calciteFilterChange", () => { paginationElement.startItem = 1; paginationElement.totalItems = 0; // Prevent display if no Filter value is present noticeElement.open = false; paginationElement.style.visibility = "hidden"; cardContainer.innerHTML = ""; // When a Filter value is present // Create Cards, update Pagination, and number of responses if (filterElement.value) { filterElement.filteredItems.forEach((item) => createCard(item)); paginationElement.totalItems = filterElement.filteredItems.length; showNumberOfResponses(filterElement.filteredItems.length); // If additional pages are populated, display Pagination if (paginationElement.totalItems > paginationElement.pageSize) { paginationElement.style.visibility = "visible"; } } else { // If no text is present in the Filter, display the initial notice initialNoticeElement.open = true; } }); };
-
Add a listener for pagination's
calcite
event so users can view subsequent filtered items when changing pages. Display a subset of thePagination Change filtered
, starting at the pagination'sItems start
property, and ending at the sum of theItem start
andItem page
properties.Size Use dark colors for code blocks /* Update Cards when interacting with Pagination */ document.addEventListener("calcitePaginationChange", ({ target }) => { const displayItems = filterElement.filteredItems.slice( target.startItem - 1, target.startItem - 1 + target.pageSize ); });
-
Lastly, you will update the cards when interacting with
calcite-pagination
. Clear thecard
contents with any previous filtered results, and callContainer create
in theCard calcite
event listener.Pagination Change Use dark colors for code blocks /* Update Cards when interacting with Pagination */ document.addEventListener("calcitePaginationChange", ({ target }) => { cardContainer.innerHTML = ""; const displayItems = filterElement.filteredItems.slice( target.startItem - 1, target.startItem - 1 + target.pageSize ); displayItems.forEach((item) => createCard(item)); });
Add styling
The application's functionality is now complete. Make final design tweaks to the interface using CSS styling.
- Add styling to the
card-container
class using CSS grid layout,calcite-chip
's color to display the earthquake magnitude, andcalcite-notice
positioning.
<style>
.card-container {
margin: 0.75rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: 1rem;
justify-content: space-evenly;
}
calcite-chip {
color: var(--calcite-color-status-danger);
}
calcite-notice {
position: relative;
margin: 0.75rem;
}
</style>
Run the application
In CodePen, run your code to display the application.