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:

  1. You call a particular API with the filter condition to retrieve only the requested data.
  2. 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:

Main.xmlui
<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:


Task menus

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:

Main.xmlui
<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:

Main.xmlui
<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):

Main.xmlui
<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.


Contact predicates

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:

Contacts.xmlui
<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):

Contacts.xmlui
<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:

Contacts.xmlui
<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:

Main.xmlui
<!-- 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:

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:

Contacts.xmlui.xs
// 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):

Main.xmlui.xs
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:

Main.xmlui
<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.


Today's Reviews

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:

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:


Completed tasks

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:

Main.xmlui
<!-- 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.


Completed tasks
📔

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:

Main.xmlui
<!-- 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:

Main.xmlui
<!-- 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):


Completed tasks

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.