Filter Data
You can continue working with the code you completed in the previous tutorial section. Alternatively, you can download the code from here and carry on.
In the previous article, you created and customized a list of contacts. That list displayed all contacts retrieved from the database. However, you often want to display only a subset of them, such as contacts with their review overdue. In this article, you will learn how you can filter data.
Technically, you have two different options for data filtering in a client-server scenario:
- You call a particular API with the filter condition to retrieve only the requested data.
- You get the data from the backend and filter it on the client side.
You may combine these two options: query some pre-filtered data from the backend and apply further filtering in the UI.
In this article, you will implement the filtering functionality with both options.
Create Filter Pages
Add a few new menu items with their corresponding pages to the app definition in the Main.xmlui
file. Change the /contacts
URL representing all contacts to /contacts/all
. Set up each page to display only the related filter's name:
<App
layout="vertical-sticky"
name="Contact List Tutorial"
logo="resources/logo.svg"
logo-dark="resources/logo-dark.svg">
<AppHeader>
<H2>Contact Management</H2>
<SpaceFiller />
<ToneChangerButton />
</AppHeader>
<NavPanel>
<NavLink label="Dashboard" to="/" />
<NavLink label="All Contacts" to="/contacts/all" />
<NavLink label="Overdue Reviews" to="/contacts/overdue"/>
<NavLink label="Today's Reviews" to="/contacts/today"/>
<NavLink label="Upcoming Reviews" to="/contacts/upcoming"/>
<NavLink label="Completed Reviews" to="/contacts/completed"/>
</NavPanel>
<Pages>
<Page url="/">Dashboard</Page>
<Page url="/contacts/all">
<!-- Unchanged -->
</Page>
<Page url="/contacts/overdue">Overdue</Page>
<Page url="/contacts/today">Today</Page>
<Page url="/contacts/upcoming">Upcoming</Page>
<Page url="/contacts/completed">Completed</Page>
</Pages>
<Footer>
Powered by XMLUI
</Footer>
</App>
The URLs specified in the NavLink
components will navigate to the page determined by the matching Page
component.
For example, selecting the "Overdue Reviews menu" item displays the following:

Filter on the Client
First, let's focus on filtering the data on the client side. In this scenario, you will fetch all the data from the backend but filter it before assigning it to the displayed list. All lists use the same logic; the only difference is how they filter the contact data.
Using a DataSource
Previously, you used a URL, /api/contacts
to display the contact data in a List
:
<App>
<!-- Omitted -->
<Page url="/contacts/all">
<List data="/api/contacts">
<!-- Omitted -->
</List>
</Page>
<!-- Omitted -->
</App>
With the new app pages, you will use the same URL five times and filter the data by different criteria. Instead of writing the same URL five times, outsource the fetch mechanism to a DataSource
component. Harness it with an id (contactsDs
), and refer to this id in the list's data
property with the following steps:
Add a DataSource
declaration to Main.xmlui
before the <Pages>
tag:
<App
layout="vertical-sticky"
name="Contact List Tutorial"
logo="resources/logo.svg"
logo-dark="resources/logo-dark.svg">
<!-- Omitted -->
<DataSource id="contactsDs" url="/api/contacts" />
<Pages>
<!-- Omitted -->
</App>
Update the contacts page to use this DataSource
(refer to the identifier of DataSource
):
<App>
<!-- Omitted -->
<Page url="/contacts/all">
<List data="{contactsDs}">
<!-- Omitted -->
</List>
</Page>
<!-- Omitted -->
</App>
When you visit the All Contacts page, the UI displays the same list as before.

Earlier, you learned that XMLUI handles the data
property in a special way: when its value is evaluated as a string, the framework considers it a URL and fetches the data from there.
Here, you can learn another specialty of handling data
. When its value is evaluated to a DataSource
component, that data source is used to fetch the data from the backend.
DataSource
allows you granular control over how a web request is sent to the backend; in addition to the URL, you can configure other parameters, including headers, the request method, the body, and a few others.
So, in the <List data="{contactsDs}">
tag, the contactsDs
expression is evaluated to a DataSource
instance (as contactsDs
is the identifier of that data source). This way, the framework knows how to set the list's data to display.
A Reusable Contacts
Component
So far, you have used the markup to display the contact list only within a single Page
definition. However, as you continue to define pages with filtered information, you will leverage the same markup in four more pages. Though the copy-and-paste approach would work, it would create a maintenance nightmare. Every modification in the list's view would require five updates in the markup.
It's time to create a reusable component for the contact list and use that to remove the maintenance pain.
First, create a new file, Contacts.xmlui
in the components
folder, and add this piece of code:
<Component name="Contacts">
</Component>
This component is empty; fill it with content! Copy the contents of the All contacts page (omit the wrapping Page
tag):
<Component name="Contacts">
<List data="{contactsDs}">
<Card var.categoryName="{getCategoryNameById($item.categoryId)}">
<HStack verticalAlignment="center">
<Checkbox initialValue="{$item.reviewCompleted}" />
<VStack width="*" gap="0">
<Text variant="strong">{$item.fullName}</Text>
<Text>{$item.comments}</Text>
</VStack>
<HStack verticalAlignment="center" horizontalAlignment="end">
<Badge when="{categoryName}" value="{categoryName}" colorMap="{categoriesColorMap}"/>
<Text>{smartFormatDate($item.reviewDueDate)}</Text>
</HStack>
</HStack>
</Card>
</List>
</Component>
You are almost done. However, as we filter each data list according to different criteria, the Contacts
component should not use the data source with all contact records. It should either receive the filtered data or a filter condition.
In this example, you will use the first approach (using the filtered data), as the component's responsibility is to display the list. We do not want to mix the display logic with the filtering.
Change the List
component to use the data received through a property:
<Component name="Contacts">
<List data="{$props.data}">
<!-- Unchanged -->
</List>
</Component>
The Contacts
component passes the array of contact items (received in its data
property) to the underlying list, which displays them as before.
The $props
context value represents the properties the Contacts
component receives. Thus, $props.data
is the data
property passed to a Contacts
instance.
Update the Contact Page
(the one with the /contacts/all
URL) in Main.xmlui
to use the new Contacts
component:
<!-- Omitted -->
<Pages>
<Page url="/">Dashboard</Page>
<Page url="/contacts/all">
<Contacts data="{contactsDs}" />
</Page>
<!-- Omitted -->
</Pages>
<!-- Omitted -->
You can check that your app behaves just like before. Ooops, the category badges are not displayed!
Fix Categories
We moved the contact list markup into the Contacts
component but forgot about the UI logic, which is in the Main.xmlui.xs
file. This logic is responsible for the category badges. Let's fix this issue!
Categories use the Datasource
with the categories
id in Main.xmlui
(you find it before the<Pages>
tag). Move this DataSource
into Contacts.xmlui
:
<Component name="Contacts">
<DataSource id="categories" url="/api/categories" />
<!-- Unchanged -->
</Component>
Create a code-behind file for Contacts.xmlui
(put it into the components
folder and name it Contacts.xmlui.xs
). Copy the contents of Main.xmlui.xs
into the new Contacts.xmlui.xs
file:
// Create a color map for all categories
var categoriesColorMap = toHashObject(categories.value, "name", "color");
// Resolve category name by id
function getCategoryNameById(categoryId) {
const category = findByField(categories.value, "id", categoryId);
return category ? category.name : "";
}
Now, when refreshing the app in the browser, you should see the category badges again.
Define Filters
The Contacts
component displays the data it receives, so the UI logic must pass the appropriate filtered data to each Contacts
instance. You will prepare the pages to show filtered contacts.
Change the content of Main.xmlui.xs
code-behind file to this code (yes, you can remove the previous code entirely):
function getSection(contact) {
if (contact.reviewCompleted) return "Completed";
if (!contact.reviewDueDate) return "No Due Date";
if (isToday(contact.reviewDueDate)) return "Today";
return getDate(contact.reviewDueDate) < getDate() ? "Overdue" : "Upcoming";
}
function filterBySection(contacts, section) {
return contacts.filter((contact) => getSection(contact) === section);
}
var allContacts = contactsDs.value;
var overdueContacts = filterBySection(allContacts, "Overdue");
var todayContacts = filterBySection(allContacts, "Today");
var upcomingContacts = filterBySection(allContacts, "Upcoming");
var completedContacts = filterBySection(allContacts, "Completed");
The getSection(contact)
function returns a particular contact's section, one of these values according to the state of its review process: "Completed"
, "No Due Date"
, "Today"
, "Overdue"
, or "Upcoming"
.
The filterBySection(contacts, section)
function retrieves only the contacts
in the specified section
.
You may not understand precisely how the
contacts.filter((contact) => getSection(contact) === section);
code snippet works; just accept that it does its job.
The allContacts
, overdueContacts
, todayContacts
, upcomingContacts
, and completedContacts
variables store the filtered contact lists.
Update all remaining contact pages in Main.xmlui
to use Contacts
with filtered data:
<App>
<!-- Omitted -->
<Pages>
<Page url="/">Dashboard</Page>
<Page url="/contacts/all">
<Contacts data="{allContacts}" />
</Page>
<Page url="/contacts/overdue">
<Contacts data="{overdueContacts}" />
</Page>
<Page url="/contacts/today">
<Contacts data="{todayContacts}" />
</Page>
<Page url="/contacts/upcoming">
<Contacts data="{upcomingContacts}" />
</Page>
<Page url="/contacts/completed">
<Contacts data="{completedContacts}" />
</Page>
</Pages>
<!-- Omitted -->
</App>
Now, contact filtering works as expected. The list of today's reviews contains only contacts belonging to that category.

Display Contact Counts
From a UX point of view, it would be good to display the number of contacts in a particular section with the menu items to indicate, for example, that there are some reviews to be done today.
Update the NavLabel
components in Main.xmlui
:
<!-- Omitted -->
<NavPanel>
<NavLink label="Dashboard" to="/" />
<NavLink label="All Contacts ({allContacts.length})" to="/contacts/all" />
<NavLink label="Overdue Reviews ({overdueContacts.length})" to="/contacts/overdue"/>
<NavLink label="Today's Reviews ({todayContacts.length})" to="/contacts/today"/>
<NavLink label="Upcoming Reviews ({upcomingContacts.length})" to="/contacts/upcoming"/>
<NavLink label="Completed Reviews ({completedContacts.length})" to="/contacts/completed"/>
</NavPanel>
<!-- Omitted -->
When you run the app, it displays the contact record counts in the menu item labels:

Filter on the Server
So far, the app has used a single API endpoint, /api/contacts
to receive all contact records.
However, the API has other endpoints to get filtered data, as the following URLs indicate:
/api/contacts/overdue
/api/contacts/today
/api/contacts/upcoming
/api/contacts/completed
In this section, you will change the code to get filtered contact data from the backend.
As you leverage five different URLs to get the data, you do not need the DataSource
referring to the /api/contacts
URL (its id is contactsDs
). Remove this DataSource
from Main.xmlui
. Change the data
properties of Contacts
instances to use the corresponding URLs:
<!-- Omitted -->
<Pages>
<Page url="/">Dashboard</Page>
<Page url="/contacts/all">
<Contacts data="/api/contacts" />
</Page>
<Page url="/contacts/overdue">
<Contacts data="/api/contacts/overdue" />
</Page>
<Page url="/contacts/today">
<Contacts data="/api/contacts/today" />
</Page>
<Page url="/contacts/upcoming">
<Contacts data="/api/contacts/upcoming" />
</Page>
<Page url="/contacts/completed">
<Contacts data="/api/contacts/completed" />
</Page>
</Pages>
<!-- Omitted -->
When you refresh the app in the browser, you can test that data filtering still works. However, this time, it happens on the backend. Nonetheless, the item counts in the menu labels show "undefined", because the counters were calculated through the DataSource
component you just removed.

If you still see the item counters, you forgot to remove the DataSource
component from Main.xmlui
.
Let's fix this! The API has an endpoint, /api/contactcounts
,
retrieving the task category counts in an object with properties all
, overdue
, today
, upcoming
, and completed
,
each representing a particular contact section.
Add a new DataSource
to the Main.xmlui
file to fetch the contact counts:
<!-- After AppHeader -->
<DataSource id="contactCounts" url="/api/contactcounts" />
<NavPanel>
<!-- Omitted -->
</NavPanel>
<!-- Omitted -->
Update the menu item labels to use the contact counts retrieved from the server:
<!-- Omitted -->
<NavPanel>
<NavLink label="Dashboard" to="/" />
<NavLink label="All Contacts ({contactCounts.value.all})" to="/contacts/all" />
<NavLink label="Overdue Reviews ({contactCounts.value.overdue})" to="/contacts/overdue"/>
<NavLink label="Today's Reviews ({contactCounts.value.today})" to="/contacts/today"/>
<NavLink label="Upcoming Reviews ({contactCounts.value.upcoming})" to="/contacts/upcoming"/>
<NavLink label="Completed Reviews ({contactCounts.value.completed})" to="/contacts/completed"/>
</NavPanel>
<!-- Omitted -->
With this step, the contact counts are displayed again (refresh the browser to see the changes):

When you filtered the data on the client, you used a Main.xmlui.xs
code-behind file with several helper functions and variables. Since you no longer leverage them, you can remove the Main.xmlui.xs
file.
Now that you learned how to retrieve and filter data on the client or the backend site, it is time to add a few finishing touches to the app. The next article will show you how.