ODC with Loop Components - Card Interaction

Stefan WeberStefan Weber
8 min read

In the last part of this series, we implemented a refresh pattern to keep our Adaptive Card-based Loop Components updated. These steps set the stage for the next phase, where we add user interaction to our auctions. Specifically, we want to enable users to bid on items.

Our implementation should allow the following:

  • Users can bid on an item until the auction is closed from the web application.

  • The card should show the highest bid.

  • Closed auctions display a “Closed” badge next to the title and do not allow further bids.

Demo Application

This article series includes a demo application called "ODC with Loop Components Demo," available on ODC Forge. Be sure to download the version of the application that matches each article in this series.

For this article, you need to install Version 0.3 from ODC Forge.

  • In the ODC Portal, go to Forge - All Assets.

  • Search for "ODC with Loop Components Demo".

  • Click on the Asset (Do not click on Install on the tile!).

  • Switch to the Version tab and click on Install next to Version 0.3.

Version 0.3 depends on other Forge components:

  • OAuthTokenExchange - An external logic library that helps retrieve access tokens easily. In this tutorial however we use an action to retrieve the discovery document from Microsoft Entra only.

  • LiquidFluid - An external logic library that merges data with templates based on the Shopify Liquid Templating language.

  • UriParser - An external logic library that parses a given URI/URL.

After updating the demo application in your environment, you can try it out right away because the App Manifest—the configuration file that added the app to Microsoft Teams—has not changed. Note that only new Loop components will refresh, while all previously added cards in a channel will stay the same.

Recap

Let's quickly recap what we covered in the last part of the series. We implemented a refresh pattern that keeps an Adaptive Card-based Loop Component up to date. This means a card triggers a Universal Action, which then returns an updated card.

We achived this by adding a refresh object to the Adaptive Card template

{
    ...,        
    "refresh": {
        "action": {
        "type": "Action.Execute",
        "title": "Refresh",
        "verb": "refreshAuctionCard",
        "data": {
            "url": "{{webUrl}}"
        }
        }
    },
    ....
}

The definition above sends an Activity of type Invoke to the messaging endpoint when executed. We observed that the action can be identified by the verb in our messaging endpoint implementation, and we can pass extra data using a data object.

The messaging endpoint processes the request and returns a complete Adaptive Card, wrapped in a status response template that looks like this:

{
    "statusCode": 200,
    "type": "application/vnd.microsoft.card.adaptive",
    "value": <Adaptive Card Content>
}

The changes we made in the previous step also laid the foundation for handling other actions besides the refresh action.

What we build

We are going to modify our implementation so that when a user pastes a running auction into a Teams channel, it will look like this.

A running auction should have a green "Active" badge next to the title. Below the description, the user should be able to enter an amount and place a bid.

After placing the bid, the card should immediately update to show the highest bid for the auction next to the description.

If an auction is closed from the web application, the card will render or update to look like this:

Closed auctions display a red badge “Closed” next to the title and the highest bid next to the description. Closed auction do not have an input field and “Place Bid” button.

In addition closed auction cards will not update anymore.

Let's now look at the changes needed to make this happen.

Entities

In ODC Studio, switch to Data - Entities - Database.

The Auction entity now has an additional attribute called IsActive, which shows whether an auction is running (true) or completed (false).

The Bid entity keeps track of all user bids for an auction. For this demo, it's quite simple, as we are only storing the amount.

Templates

Go to Data - Resources - Templates.

Download the AuctionCard template locally and inspect it with a text editor.

Declaring Variables

At the top of the template are some Liquid instructions to define additional variables based on data merged from the application.

{% if isActive %}
  {% assign status = "Active" %}
  {% assign statusStyle = "Good" %}
{% else %}
  {% assign status = "Closed" %}
  {% assign statusStyle = "Attention" %}
{% endif %}

{% if bid %}
  {% assign price = bid %}
  {% assign priceColor = "Good" %}
{% else %}
  {% assign priceColor = "Default" %}
{% endif %}

The first If statement sets the status and statusStyle variables based on the isActive boolean value from the application. These variables are used to show the badge next to the title.

The second If statement checks if there is a bid value, which exists only if there has been at least one bid. If there is a bid, it updates the price variable (part of the merged data) with the bid amount.

Conditional Rendering

Throughout the template, you will notice several If conditions that wrap parts of the template. For example, the refresh object should only be displayed if the auction is still running.

{% if isActive %}
  "refresh": {
    "action": {
      "type": "Action.Execute",
      "title": "Refresh",
      "verb": "refreshAuctionCard",
      "data": {
        "url": "{{webUrl}}"
      }
    }
  },
{% endif %}

Inspect the entire template to identify which parts are displayed only when an auction is still active.

Form and Action

The most interesting part of the template is where we display the input field and the button for placing a bid.

{
  "type": "Container",
  "items": [
    {
      "type": "Input.Number",
      "placeholder": "Enter your bidding..",
      "id": "bid",
      "isRequired": true,
      "errorMessage": "Please enter an amount"
    }
  ]
}

The above part renders a Container with a single number input field (type of Input.Number).

It's important to note that the id of an input field becomes the attribute name when we submit the form using the Place Bid button.

💡
More details about Adaptive Cards elements can be found in the official documentation at Welcome - Adaptive Cards.
"actions": [
    {
      "type": "Action.Execute",
      "id": "bidAction",
      "verb": "placeBidding",
      "title": "Place Bid",
      "data": {
        "url": "{{webUrl}}"
      }
    }
]

All card actions must be included in the actions array. Our template has one action of type Action.Execute. You'll notice that this is quite similar to the refresh action, and it is. Like the refresh action, it has a verb—which we use to identify the action taken in our messaging endpoint—and the same data object that passes the full URL to the auction item.

When a user enters an amount into the Input.Number field and clicks the “Place Bid” button, all input field data, along with the data defined at the action level, will be added to the data object sent to the messaging endpoint. In our case, the data object sent to the messaging endpoint will look like this.

"data": {
  "url": "https://<YOUR ODC STAGE>/ODCwithLoopComponentsDemo/AuctionDetail?AuctionId=12f521f9-5137-4489-be8c-9172673b710b",
  "bid": 650.00
}

Take your time to inspect the template and the RenderAuctionCard server action in Logic - Server Actions - Loop.

Deserialize Activity

In Logic - Server Actions - Util inspect DeserializeAdaptiveCardActionActivity and the InvokeAdaptiveCardActionActivity structure in Data - Structures - Messaging.

In addition to the Url data item, this structure includes an extra attribute called Bid, which deserializes the bid value sent to the messaging endpoint.

Messaging Endpoint

In Logic - Integrations - REST - MessagingEndpoint, open the Messages endpoint. In the previous part of the series, the second switch statement "Verb" had only one condition for the refresh action. We now add a second condition for the placeBidding verb defined in the AuctionCard template.

The MessagingAuctionBidAction server action processes the deserialized Activity and returns an updated card.

Bid Action

Switch to Logic - Server Actions - Messaging and review the MessagingAuctionBidAction server action.

You will note that this action is almost identical to the refresh action with the following exceptions

  • Auction_AddBidding - Saves the received bid in the Bid entity.

  • RenderAuctionCard - Displays the updated Auction card with the user's bid. (see note below)

💡
Important note: The demo does not verify if the bid is higher than the current highest bid. I wanted to keep the demo as simple as possible. Feel free to add extra checks as you see fit.

Try

Paste an auction link from the demo application into a Microsoft Teams conversation. This will first display the auction card.

Next, change the auction price in the demo application.

In Microsoft Teams, click on a different conversation, then return to the conversation where you pasted the link. The card will update to show the new price you entered.

💡
Note that cards do not update while you remain in the conversation. They automatically refresh only when you re-enter the conversation.

Place a bid and see if the card updates with the new value.

Close a bid in the web application and check if the card updates in the channel.

Summary

In this tutorial, we explored how to add form elements to an Adaptive Card-based Loop Component and submit form data to the messaging endpoint using a card action.

I hope you enjoyed it. Please leave a comment with your feedback or any questions you have.

0
Subscribe to my newsletter

Read articles from Stefan Weber directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Stefan Weber
Stefan Weber

As a seasoned Senior Director at Telelink Business Services EAD, a leading IT full-service provider headquartered in Sofia, Bulgaria, I lead the charge in our Application Services Practice. In this role, I spearhead the development of tailored software solutions using no-code/low-code platforms and cutting-edge cloud-ready/cloud-native solutions based on the Microsoft .NET stack. Throughout my diverse career, I've accumulated a wealth of experience in various capacities, both technically and personally. The constant desire to create innovative software solutions led me to the world of Low-Code and the OutSystems platform. I remain captivated by how closely OutSystems aligns with traditional software development, offering a seamless experience devoid of limitations. While my managerial responsibilities primarily revolve around leading and inspiring my teams, my passion for solution development with OutSystems remains unwavering. My personal focus extends to integrating our solutions with leading technologies such as Amazon Web Services, Microsoft 365, Azure, and more. In 2023, I earned recognition as an OutSystems Most Valuable Professional, one of only 80 worldwide, and concurrently became an AWS Community Builder.