Working with Data

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 that DataSource 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>