Working with data
Real apps consume data. XMLUI provides a straightforward way to reach data from the backend and display them in the UI. It also lets you execute actions that change backend data.
This section will teach you how to access backend APIs through XMLUI components. In addition to the data access techniques, you will learn about a few data-aware components, such as List
and Table
.
The data
property
Each component in the framework (including the built-in and reusable components ) may have a data
property. XMLUI specially handles this property:
- First, it gets the property values; if it is an expression, it evaluates it.
- If the property value is a string, the framework handles it as a URL and fetches the data from it. The retrieved data is passed to the component when the fetch is completed.
- If the value is a
DataSource,
the framework fetches the data from thatDataSource
and passes it to the component when the fetch is completed. - In other cases, the value is passed to the component.
As you learned, property changes trigger UI refresh, so when the data
property's value gets the actual data, the UI is updated accordingly.
Using data
with Explicit Value
When you pass an explicit (non-string) value to the data
property, the framework directly passes it to the component, which renders it.
Here is a sample demonstrating this case:
<App>
<List data="{['one', 'two', 'three']}">
<Card title="{$item}" />
</List>
</App>
This code passes an array of strings to data
. A List
component displays each item in data
according to the template described by its children. The $item
context value in the template represents the current item to display.
Using data
with a valid URL
When you pass a string value to data
, XMLUI considers it a URL and fetches the data from it. When the fetch is complete, the UI is refreshed accordingly.
<App>
<List data="https://api.spacexdata.com/v4/launchpads">
<Card title="{$item.name}" />
</List>
</App>
This markup uses the endpoint in data
to fetch launchpad information from the SpaceX API.
Using data
with a non-functional URL
When you use a string value in data
but not a valid URL or the data fetch results in an error, the data
is set to an empty value. The following example demonstrates this case:
<App var.myData="{[]}">
<Button label="Fetch Data" onClick="myData = 'bla-bla-bla'" />
<List data="{myData}">
<Card title="{$item}" />
</List>
</App>
By default, the value passed to data
is an empty list (the UI displays "No data available"); however, when you click the button that sets the data
property to an invalid URL ("bla-bla-bla"), an error is raised while the framework fetches the data from that URL.
When you click the button again, the value of data
does not change (it remains "bla-bla-bla"), so the framework does not trigger the fetch again.
Using data
with a DataSource
You can bind the data
property to a DataSource
instance. Similarly to a single URL, DataSource
fetches the data from that URL.
<App>
<DataSource id="rocketsData" url="https://api.spacexdata.com/v4/rockets" />
<List data="{rocketsData}">
<Card title="{$item.rocket_name}" subtitle="{$item.success_rate_pct}%" />
</List>
</App>
You can refer to the DataSource
instance with its identifier, rocketsData
.
This sample has the same effect as using a URL in the data
property. However, when you need additional configuration to issue a fetch request (for example, changing the GET
method or adding some custom headers), DataSource
provides that flexibility over a single string URL.
Note that a DataSource
identifier can be shared among multiple components, intrinsic and/or user-defined.
<App>
<DataSource id="rocketsData" url="https://api.spacexdata.com/v4/rockets" />
<List data="{rocketsData}">
<Card title="{$item.rocket_name}" subtitle="{$item.success_rate_pct}%" />
</List>
<MyCustomRocketDisplay source="{rocketsData} />
</App>
<Component name="MyCustomRocketDisplay">
<Items data="{$props.source}">
<Text>
{$item.name}, {$item.type}, {$item.first_flight}
</Text>
</Items>
</Component>
Using DataSource
Though using the data
property is extremely easy, there are situations when you need more control over the fetched data than just automatically getting it. Here are a few of them:
- You need to extract some part of the response and consider that part as the data you want to display.
- You need to transform the data from the backend before displaying it.
- You want to know that the data is being fetched to indicate that the fetch operation is in progress.
- Initiating the fetch request is more complex than issuing a GET request with the specified URL; for example, you must pass some information in the request header.
The DataSource
component provides more control of the fetch operation with its methods and state information.
This samples in this section use XMLUI's emulated API feature.
Accessing the Data
You can use DataSource
with any component (even if that one does not handle data natively). When the fetch is complete, the value
property represents retrieved data.
In the following example, you use the value
property to set the contents of H3
:
<App>
<DataSource id='fruits' url='/api/fruits'/>
<HStack verticalAlignment="center">
<H3>These are the {fruits.value.length} fruits:</H3>
<Items data="{fruits}">
<Badge value="{$item}" />
</Items>
</HStack>
</App>
Refreshing the data
DataSource
exposes a method, refetch
, which you can use imperatively to re-query the data. The following markup demonstrates using it:
<App>
<DataSource id='coords' url='/api/coords'/>
<Button label="Refresh" onClick="coords.refetch()" />
<Text>Satelite position: x={coords.value.x}, y={coords.value.y}</Text>
</App>
Periodic Polling
DataSource
allows you to periodically poll the backend for data. Use the pollIntervalInSeconds
property to set this period, as the following example shows:
<App var.pollCount="{0}">
<DataSource
id='rand'
url='/api/random'
pollIntervalInSeconds="3"
onLoaded="pollCount++"/>
<H3>Poll temperature in every 3 seconds (#{pollCount})</H3>
<Text>Current random temperature: {rand.value}</Text>
</App>
Besides getting the data, this app uses the loaded
event of DataSource
to count the number of polls.
In-Progress State
You can quickly determine if a data fetch operation is in progress. DataSource
offers a property, inProgress,
which indicates whether the data is currently being fetched. The following sample shows using this property:
<App var.pollCount="{0}">
<DataSource id='orders' url='/api/monthly-total' />
<Button onClick="orders.refetch()">Query #of total orders</Button>
<Text when="{orders.inProgress}">Calculating...</Text>
<H2 when="{!orders.inProgress}">Total Orders: {orders.value}</H2>
</App>
Binding Component Properties to DataSource
When you bind a DataSource
component instance to a property, XMLUI starts the data fetch and changes the component property as soon as the retrieval has been completed.
The following sample demonstrates this scenario:
<App>
<DataSource id='orders' url='/api/title-info' />
<Card title="{orders.value.title}" subtitle="{orders.value.author}" />
</App>
Adding Header information
Some API endpoints may ask for a particular header. You can send it with the request using the headers
property of DataSource
.
In the following example, the backend requires a header named x-api-key
with the value of 1111
to accept the request.
<App>
<DataSource id='orders' url='/api/title-info-header' headers="{{ 'x-api-key': '1111' }}" />
<Card title="{orders.value.title}" subtitle="{orders.value.author}" />
</App>
Pop out this example in the playground and check what happens when you change or remove the headers
property.
Selecting the Result
Sometimes, the response of a data fetch operation contains extra information that we do not consider part of the data. With the resultSelector
property of a DataSource, you can declare the segment of the response to be taken into account as the actual data.
For example, let's assume we have this response:
{
"info": [
{"name": "New York", "population": 8336817},
{"name": "Los Angeles", "population": 3979576},
{"name": "Chicago", "population": 2693976},
{"name": "Houston", "population": 2320268},
{"name": "Phoenix", "population": 1680992},
],
"meta": {
"total": 10,
"page": 1,
"per_page": 10
}
}
The actual data is in the "info" property; "meta" is additional, and we do not want to display it. We can tighten the response to the actual data with resultSelector
:
<App>
<DataSource id='cities' url='/api/cities' resultSelector="info" />
<VStack>
<H3>Cities</H3>
<Table data="{cities}">
<Column header="Name" bindTo="name" />
<Column header="Population" bindTo="population" />
</Table>
</VStack>
</App>
Transforming the Result in the Displaying Component
An intuitive option is to transform the data retrieved from a DataSource
in the component displaying the data.
The following example orders a list of friends' data by descending age:
<App>
<DataSource id='friends' url='/api/friends' />
<VStack>
<H3>My Friens:</H3>
<Items data="{friends.value.toSorted((a, b) => b.age - a.age)}">
<H4>{$item.age}: {$item.name}</H4>
</Items>
</VStack>
</App>
Though this technique is seducing, you must apply the transform in every component that intends to display the data. Should you change that transform (let's say from descending to ascending order), you should maintain each occurrence.
Transforming the Result with transformResult
DataSource
has a transformResult
property you can use to define a data transforms operation.
Let's assume we have this hotel review information retrieved from the backend:
[
{ "hotel": "Golden Beach Hotel", "reviews": [5, 3, 3, 4] },
{ "hotel": "Sunset Hotel", "reviews": [2, 3, 3, 4, 4, 4, 5] },
{ "hotel": "Blue Lagoon Hotel", "reviews": [5, 5, 5, 5] },
{ "hotel": "Green Garden Hotel", "reviews": [1, 2, 3, 2, 2] }
]
We are interested in the average value of reviews. With transformResult
, we can transform the entire resultset and add a new field to each hotel row with the average review points:
<App>
<DataSource
id='reviews'
url='/api/reviews'
transformResult="{(data) => data.map(d => ({...d, avg: avg(d.reviews, 2)}))}" />
<VStack>
<H3>Reviews</H3>
<Table data="{reviews}">
<Column header="Hotel" bindTo="hotel" />
<Column header="#of Reviews" bindTo="reviews.length" />
<Column header="Avg Review" bindTo="avg" />
</Table>
</VStack>
</App>
Transforming the Result via a Helper Variable
You can introduce a new (declarative) variable and define its value as a transformation of the DataSource value. Let's assume we have reviews about cafes and want to display them in descending order, the cafe with the best review at the top.
Let's declare the sortedReviews
variable in code-behind:
var sortedReviews = reviews.value
.map(c => ({...c, avg: avg(c.reviews, 2)}))
.toSorted((a, b) => b.avg - a.avg);
XMLUI does not allow you to invoke a JavaScript array's sort()
function, as that sorts the data in place. Use the toSorted()
function with the same signature, which creates a new array with sorted data.
Due to XMLUI's reactive nature, sortedReviews
is evaluated every time the data is fetched (as reviews.value
changes). In the Table
component's data
property, you refer to the sortedReviews
variable to ensure the transformed value is displayed.
<App>
<DataSource
id='reviews' url='/api/cafe-reviews' />
<VStack>
<H3>Reviews</H3>
<Table data="{sortedReviews}">
<Column header="Cafe" bindTo="cafe" />
<Column header="#of Reviews" bindTo="reviews.length" />
<Column header="Avg Review" bindTo="avg" />
</Table>
</VStack>
</App>
Persisting Data
In contrast to fetching data, where the framework can be smart about when to initiate the data fetch operation, persisting data requires an explicit user (or system) trigger to invoke a related API endpoint.
XMLUI has an APICall
component that manages API endpoint invocations that persist data (or cause other state changes at the backend).
An APICall
component requires a URL and an operation method (such as POST, PUT, DELETE, etc.) to do its job. It must also be configured with other details to convey the request information between the UI and the backend.
Besides managing the request-response protocol, APICall
provides a few UI services:
- It can ask you for confirmation and cancel the operation on refusing it.
- You can check if an operation is in progress.
- It may display toast messages (if you ask so) when the operation is completed.
- You can define a toast message to show while the operation is in progress.
- Other components may ask the APICall whether it is in progress.
- You can define event handlers for handling successful/failed operations.
Using APICall to Persist Data
The following sample demonstrates how to use APICall
to send a request with a particular body. The sample allows you to add a new fruit to a list; you send the new fruit's name in the request body.
<App>
<DataSource id='myFruits' url='/api/my-fruits' />
<HStack>
<TextBox id="newFruit" placeholder="Enter a new fruit" width="50%" />
<Button enabled="{newFruit.value.trim()}" label="Add">
<event name="click">
<APICall
method="post"
url="/api/my-fruits"
body="{newFruit.value}"
onSuccess="newFruit.setValue('')" />
</event>
</Button>
</HStack>
<HStack wrapContent="true">
<Items data="{myFruits}">
<Badge value="{$item.name}"/>
</Items>
</HStack>
</App>
Observe the onSuccess
event handler: the code deletes the textbox after completing the API call.
Confirming and Notifications
APICall
allows you to request user confirmation before issuing a particular call. The operation continues if the answer is affirmative; otherwise, it will not be sent. You can also display a notification message when the operation is completed.
The following example demonstrates these features:
<App>
<List data="/api/components">
<HStack padding="$padding-tight" verticalAlignment="center">
<H3 width="20%">{$item.name}</H3>
<Button label="Delete" size="xs">
<event name="click">
<APICall
url="/api/components/{$item.id}"
method="delete"
confirmTitle="Delete a Component"
confirmMessage="Are you sure you want to remove '{$item.name}' from your list?"
completedNotificationMessage="{$item.name} component deleted." />
</event>
</Button>
</HStack>
</List>
</App>
Using APICall Imperatively
Besides using APICall
in event handlers, you can use this component imperatively through its component identifier. This section shows you a few scenarios of this imperative usage.
Fetching Data with APICall
Though you primarily use DataSource
to fetch data, you can also use APICall
. The following example demonstrates how to do this.
<App var.launchpadData="{[]}">
<APICall id="launchpads" url="https://api.spacexdata.com/v4/launchpads" method="get" />
<Button
id="fetchButton"
enabled="{!fetchButton.clickInProgress}"
onClick="launchpadData = launchpads.execute()">
Fetch Launchpads
</Button>
<List data="{launchpadData}">
<Card subtitle="{$item.full_name}" />
</List>
</App>
This app displays the data stored in the launchpadData
variable, initialized to an empty array. When you click the button, it triggers the fetch (get) operation defined by the APICall
invoking the execute()
method through the APICall
's identifier, launchpads
. The execute method retrieves the fetched data and displays it by assigning it to launchpadData
.
Passing a Parameter to APICall
You can pass parameters to an APICall
component and use that parameter to prepare the request. The following sample passes an identifier (of a rocket) in the execute
methods. The APICall
accesses that through its $param
context value:
<App
var.rocketData="">
<APICall id="rocket" url="https://api.spacexdata.com/v4/rockets/{$param}" method="get" />
<Button
id="fetchButton"
enabled="{!fetchButton.clickInProgress}"
onClick="rocketData = rocket.execute('5e9d0d95eda69955f709d1eb')">
Fetch Falcon 1
</Button>
<Card when="{rocketData}" title="{rocketData.name}" subtitle="{rocketData.description}" />
</App>
Passing Multiple Parameters
As the following sample demonstrates, you can pass multiple parameters to APICall
when invoking the execute method.
<App>
<DataSource id='myNames' url='/api/my-names' />
<HStack>
<APICall
id="addNameApi"
url="/api/my-names/{$params[0]}"
body="{$params[1]}"
method="post" />
<TextBox id="newName" placeholder="Enter a new name" width="50%" />
<Button
enabled="{newName.value.trim()}"
label="Add"
onClick="
addNameApi.execute(myNames.value.length + 1, newName.value.trim());
newName.setValue('');
"/>
</HStack>
<HStack wrapContent="true">
<Items data="{myNames}">
<Badge value="{$item.name}"/>
</Items>
</HStack>
</App>
When the APICall
receives multiple parameters, you can access only the first parameter with the $param
context value. Other parameters can be accessed with indexing through the $params
value, such as $params[1]
for the second, $params[2]
for the third (and so on) parameters. $params[0]
is the same as $param
.
Data-Aware Components
XMLUI comes with several data-aware components. In this section, you will learn about them.
Items
The Items
component maps sequential data into component instances, representing each data item as a particular component.
Learn more about this component in the Items
reference documentation.
The following sample uses the data
property to define the source of backend data displayed in the component:
<App>
<Items data="https://api.spacexdata.com/v3/rockets">
<Image height="80px" width="110px" fit="cover" src="{$item.flickr_images[0]}"/>
</Items>
</App>
The Items
component does not use virtualization; it maps each data item into a component.
Thus, passing many items to a component instance will use many resources and slow down your app.
If you plan to work with many items (more than a few dozen), use the List
and Table
components instead.
Items also can be used when you provide a list of options for components such as Select
:
<App>
<Select id="landpads">
<Items data="https://api.spacexdata.com/v4/landpads">
<Option label="{$item.full_name}" value="{$item.name}" />
</Items>
</Select>
<Text>Selected ID: {landpads.value ?? '(none)'}</Text>
</App>
List
The List
component is a robust layout container that renders associated data items as a list of components. List
is virtualized; it renders only items visible in the viewport.
Learn more about this component in the List
reference documentation.
The following sample demonstrates using List
:
<App>
<List data="https://api.spacexdata.com/v4/ships">
<HStack padding="$padding-tight">
<Text variant="strong" width="30%">{$item.name}</Text>
<Text width="15%">Built: {$item.year_built ?? '(unknown)'}</Text>
<Image when="{$item.image}" height="80px" width="110px" fit="cover" src="{$item.image}"/>
<Text when="{!$item.image}" width="*">No image available</Text>
</HStack>
</List>
</App>
You can order the items on the list with its orderBy
property, which names a field and a sorting direction. Observe how the following sample uses a DataSource
and maps the result into a new data array to create the launchCount
field:
<App>
<DataSource id="shipsData" url="https://api.spacexdata.com/v4/ships" />
<List
data="{shipsData.value.map(s => ({...s, launchCount: s.launches.length ?? 0}))}"
orderBy="{{ field: 'launchCount', direction: 'desc' }}">
<HStack padding="$padding-tight">
<Text variant="strong" width="30%">{$item.name}</Text>
<Text width="15%">Launches: {$item.launchCount}</Text>
<Image when="{$item.image}" height="80px" width="110px" fit="cover" src="{$item.image}"/>
<Text when="{!$item.image}" width="*">No image available</Text>
</HStack>
</List>
</App>
As you expect, the list displays ships in descending order by the number of their launches.
As the following sample demonstrates, a List
can group its items according to a particular field. You can optionally define a section header and footer for the list.
<App>
<List data="https://api.spacexdata.com/v4/ships"
groupBy="type">
<HStack padding="$padding-tight">
<Text variant="strong" width="30%">{$item.name}</Text>
<Text width="15%">Type: {$item.type}</Text>
<Image when="{$item.image}" height="80px" width="110px" fit="cover" src="{$item.image}"/>
<Text when="{!$item.image}" width="*">No image available</Text>
</HStack>
<property name="groupHeaderTemplate">
<Card title="{$group.key}" />
</property>
</List>
</App>
Table
Table
is a component that displays cells organized into rows and columns. The Table
component is virtualized so it only renders visible cells.
Learn more about this component in the Table
reference documentation.
The following sample demonstrates using Table
. You can use Column
components to specify table column templates.
<App>
<Table data="https://api.spacexdata.com/v4/rockets">
<Column header="Image" width="100px">
<Image height="80px" width="110px" fit="cover" src="{$item.flickr_images[0]}"/>
</Column>
<Column header="Name" bindTo="name" width="110px">
<Text variant="strong">{$item.name}</Text>
</Column>
<Column header="Description" bindTo="description" width="*">
<Text maxLines="5">{$item.description}</Text>
</Column>
</Table>
</App>