A Few Finishing Touches
There are a few things left to improve the user experience in our app. In this section, you will make minor changes to enhance the app's look and feel.
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.
Fetching the Number of Records
When the app starts, it takes a fraction of a second to reach the backend and retrieve the number of contact records to display. You may still see an "(undefined)" label while the data loads. We can quickly fix this by showing a "loading..." message while the contact data is retrieved from the database.
Create a code-behind file for Main.xmlui
(remember, you must name it Main.xmlui.xs
). Add a new function, countLabel
to Main.xmlui.xs
:
function countLabel(cat) {
return contactCounts.inProgress ? "loading..." : contactCounts.value[cat];
}
This function takes a name (the name of the contact section) as its argument and retrieves the corresponding number of contact categories.
The "true" value of the contactCounts.inProgress
property indicates whether the DataSource
(with the contactCounts
id) retrieving the contact counts is currently fetching data.
Update the task count expression in the menu item labels to leverage the new, improved display:
<!-- Omitted -->
<NavPanel>
<NavLink label="Dashboard" to="/" />
<NavLink label="All Contacts ({countLabel('all')})" to="/contacts/all" />
<NavLink label="Overdue Reviews ({countLabel('overdue')})" to="/contacts/overdue"/>
<NavLink label="Today's Reviews ({countLabel('today')})" to="/contacts/today"/>
<NavLink label="Upcoming Reviews ({countLabel('upcoming')})" to="/contacts/upcoming"/>
<NavLink label="Completed Reviews ({countLabel('completed')})" to="/contacts/completed"/>
</NavPanel>
<!-- Omitted -->
Now, instead of "undefined", labels display "loading...":

Displaying List Sections
The List
component supports the grouping of list items according to a specified attribute.
It has a groupBy
property that enables this grouping.
The List
identifies each unique value of the attribute given to groupBy
and groups the items accordingly.
In the previous section, you created a getSections
function within Main.xmlui.xs
. Let's use the same function in the Contact.xmlui.xs
code-behind file; append the definition of getSections
and a few other functions to it:
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 extendWithSection(contact) {
return { ...contact, section: getSection(contact) };
}
function loading(contacts, message) {
return contacts.inProgress ? "loading..." : message;
}
The extendWithSection
function takes a particular contact and adds a new property named section
. If you do not precisely understand the { ...contact, section: getSection(contact) }
expression, just accept that it does its job.
The loading
function displays the "loading..." text while the data is fetched from the backend. When the data is received, it shows the message
passed.
To display the section information, add the groupBy
property to the List
and define the template to display section headers:
<Component name="Contacts">
<DataSource id="categories" url="/api/categories" />
<List data="{$props.data.map(extendWithSection)}"
groupBy="section">
<property name="groupHeaderTemplate">
<HStack paddingHorizontal="$space-normal" paddingVertical="$space-tight">
<Text
variant="subtitle"
value="{$group.key} ({loading(contacts, $group.items.length ?? 0)})" />
</HStack>
</property>
<!-- Unchanged -->
</List>
</Component>
The list now uses the contact records extended with the section
property (mapping the contacts with the extendWithSection
function).
In this code snippet, the groupBy
property of List
names the list's property for grouping items; it is the newly calculated section
property of a contact record.
The highlighted groupHeaderTemplate
area is the markup that represents the header of a particular item group. The list passes the special $group
context property to the template. Its value is an object with key
holding the grouping value; items
store the items within the particular group.
Now, When you display all contacts, you can see that items are sectioned:

When you do not declare a group template, the items are still grouped, but the list does not indicate the group boundaries. Besides groupHeaderTemplate
, List
provides a groupFooterTemplate
. With this property, you can declare a markup for the footer of a section.
Expanding and Collapsing Sections
The sections of a list can be expanded or collapsed. Unless you declare otherwise, all sections are expanded by default. Using the $group
context property, you can query the state of the section (isExpanded
) and use the toggle
function to expand or collapse the section.
Update the List
definition in Contacts
:
<Component name="Contacts">
<DataSource id="categories" url="/api/categories" />
<List data="{$props.data.map(extendWithSection)}"
groupBy="section">
<property name="groupHeaderTemplate">
<HStack
paddingHorizontal="$space-normal"
paddingVertical="$space-tight"
verticalAlignment="center"
onClick="$group.toggle">
<Text
variant="subtitle"
value="{$group.key} ({loading(contacts, $group.items.length ?? 0)})" />
<SpaceFiller />
<Icon
name="{$group.isExpanded ? 'chevrondown' : 'chevronright'}"
size="md" />
</HStack>
</property>
<!-- Unchanged -->
</List>
</Component>
The paddingHorizontal
and paddingVertical
properties of HStack
(XMLUI calls them layout properties) define some spacing around the content within HStack
. The $space-normal
and $space-tight
layout values refer to theme settings (they come from the app's current theme). Using these values ensures that your app accommodates the theme changes.
Now, you can expand or collapse sections by clicking them. The following figure shows the Completed section in its collapsed state:

Section Ordering
The order of sections displayed above could be more helpful using a different ordering. With the help of the defaultGroups
property of the List
, you can define the order of sections.
Modify the List
accordingly:
<!-- Omitted -->
<List
items="{contacts.value.map(t => ({...t, section: getSection(t)}))}"
defaultGroups="{['Overdue', 'Today', 'Upcoming', 'No Due Date', 'Completed']}"
groupBy="section">
<!-- Omitted -->
</List>
<!-- Omitted -->
You can see the updated section order:

When you visit a filtered page, only the corresponding section is displayed, like in the following figure:

So far, you have displayed only read-only data. In the following section, you will create, edit, and delete contact data.