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-noticeto 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-buttonthat 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
<bodytag. Also, add a> <mainsection.>
The <! 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 type="module">
</script>
</html>- To access Calcite Design System web components, add references to Calcite Components in the
<headelement.>
<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/3.3.3/calcite.esm.js" type="module"></script>
</head>
- Next, you will organize the contents of the application. In the
mainsection, add acalcite-shellandcalcite-panelwith attributes ofheadingandheading-levelto 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-noticeelements, each accompanied with an uniqueidattribute. 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 adivand place it in thetitleslot, which will be used later to populate the number of earthquake results using theidattribute.
<!-- 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
divwith aclassattribute to hold the earthquake results. Also, placecalcite-paginationin the panel'sfooterslot, and add apage-sizeattribute 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-containerCSS 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-filtercomponent'sitemsproperty 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 calciteevent to watch for changes to the filter'sFilter Change value. Then, go back to the first page by changing pagination'sstartandItem totalproperties. Lastly, create a conditional to add cards when the filter'sItems valueis 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'sopenproperty 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 totalfiltered earthquakes is greater than the specifiedItems pagevalue. Set theSize calcite-paginationcomponent'svisibilityCSS 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-cardcomponent(s) residing in thecard-containerclass 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"
scale="s"
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
createin theCard calciteevent listener for each feature that matches the filterFilter Change value. Also, clear thecardcontent, 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,
showand post the number of responses to theNumber Of Responses calcite-noticetitle. To ensure content is accessible to users, remove the initial notice'sopenattribute and set the results noticeopenproperty 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
showfunction call to theNumber Of Responses calciteevent listener's callback function. When no text is present in theFilter Change calcite-filter, set the filtered notice'sopenattribute 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
calciteevent so users can view subsequent filtered items when changing pages. Display a subset of thePagination Change filtered, starting at the pagination'sItems startproperty, and ending at the sum of theItem startandItem pageproperties.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 thecardcontents with any previous filtered results, and callContainer createin theCard calciteevent 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-containerclass using CSS grid layout,calcite-chip's color to display the earthquake magnitude, andcalcite-noticepositioning.
<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 {
--calcite-chip-border-color: var(--calcite-color-status-danger);
--calcite-chip-text-color: var(--calcite-color-status-danger);
}
calcite-notice {
position: relative;
margin: 0.75rem;
}
calcite-pagination {
width: 100vw;
justify-content: center;
}
</style>
Run the application
In CodePen, run your code to display the application.