Blog Post

Modern Work App Consult Blog
17 MIN READ

Build a Viva Connections card to display stock prices - Part 2: the card

Matteo Pagani's avatar
Matteo Pagani
Icon for Microsoft rankMicrosoft
Mar 21, 2023

Welcome back to our series of posts about building a Viva Connections card to display the stock price of a company! In the previous post we built the backend which provides the data we need to display. The backend was built using an Azure Function, in companion with the Azure Cache for Redis service to improve performance and reduce the number of API calls against Alpha Vantage, the free service we have chosen to retrieve stock information.

 

In this post, we're going to build the actual Viva Connections card, using the SharePoint AdaptiveCard extension modelAs a reminder, this is the look & feel of the actual card we're going to build:

 

 

SharePoint Adaptive Cards supports three types of templates. In our case, we're going to choose the one called Primary Text, which supports displaying a title, a primary text, and a description. As you can notice from the screenshot, the card doesn't offer too much space to display information. This is where the Quick View feature comes into play. When we click or tap on the See more button, we'll open a panel that will be rendered using an adaptive card and that we can use to display more information and more detailed content.

 

Now that we have understood what we're going to build, let's start to build it!

 

Setting up the project

Viva Connections is a Teams application, but it's part of the SharePoint ecosystem. The Viva Connections dashboard, in fact, is hosted on the same intranet SharePoint used by the company. As such, the developer story for Viva Connections is based on SharePoint and the same type of extensions you can build for SharePoint (like web parts) can be used also for Viva Connections. However, there's one type of extension that was recently added that is tailored specifically for Viva Connections, which is the SharePoint Adaptive Card. The name comes from the fact that parts of the layout of the card can be defined using Adaptive Cards, a popular technology in the Microsoft 365 ecosystem to render UI components starting from a JSON template.

 

On this blog, I already walked you through the steps required to create a SharePoint Adaptive Card extension on Windows, so you can head to the article to discover all the requirements you need.

 

In this blog post, we'll assume you have satisfied all the requirements, so just open a terminal, create a folder where to store your project and type the following command:

 

yo @microsoft/sharepoint

You will start a wizard that will guide you through the creation of the project. Make sure to choose the following values:

  • What is your solution name?: Choose the name you prefer, I used viva-connections-stocks
  • Which type of side client-component to create?: Adaptive Card Extension
  • Which template do you want to use?: Primary Text Card template
  • What is your Adaptive Card Extension name?: Viva Connections Stocks

Yeoman will generate a starting project for us. Let's explore it.

 

Building the SharePoint Adaptive Card

A SharePoint Adaptive Card project is made by multiple files, but there are four of them which are important (keep in mind that the name might change, since it's generated based on the name you have chosen for the project):

  • The StocksAdaptiveCardExtension.manifest.json file, which describes the card. It contains valuable information like the name, the version, and the properties.
  • StocksAdaptiveCardExtension.ts, which is the entry point of the card. This file includes a class that inherits from BaseAdaptiveCardExtension and it takes care of initializing the card. This is where we're going to load the stocks information from our Azure Function.
  • CardView.ts, which is the definition of the card. This file includes a class that inherits from BasePrimaryTextCardView (since we chose the Primary Text template; if we would have picked a different template, it would be a different base class) and it's where we supply the data that we want to display in the card (the stock information).
  • QuickView.ts, which is the definition of the adaptive card displayed when you click or tap on the card. This file includes a class that inherits from BaseAdaptiveCardView and, similarly to the CardView, it's where we supply the data that we want to display in the quick view.

You can use any type of editor to work on this project, but I highly suggest Visual Studio Code, since it offers a built-in debugging experience, which will come especially useful when it will be time to start testing our Viva Connections card.

 

Let's break down the development of the card in little steps, so that hopefully it will be easier for you to follow the process.

 

Loading the data

The entry point of the card, which is implemented by the StocksAdaptiveCardExtension.ts file, is the place where a Viva Connections card must load the data we want to display. This class, in fact, implements a function called onInit(), which is triggered when the card is loaded in the dashboard. This is the right place to load and initialize the data.

Before starting to fetch the data, however, we need a way to represent the stock information coming from the Azure Function. The default template is based on TypeScript, so why not leverage the strongly typed support that this language brings over JavaScript? Let's add a new file to our project and call it IStockPrice.ts, then copy the following definition:

 
export interface IStockPrice {
    openingPrice: number;
    closingPrice: number;
    highestPrice: number;
    lowestPrice: number;
    volume: number;
    time: Date;
    companyName: string;
    exchange: string;
    symbol: string;
}

This interface is the equivalent of the StockPrice class we have defined in our Azure Function, and it matches the JSON payload we're going to receive from our backend. Now that we have an interface, we must introduce the concept of state in a Viva Connections card. If you have experience with React, you'll find yourself at home, since it's the same concept and it's even implemented with the same APIs. State is the place where you store data that is persisted across the lifecycle of a card. It's the best place to store our data, since we can read its content from every file included in the project. This means that we can store the stock information in the onInit() function of the StocksAdaptiveCardExtension.ts file and then read it in the CardView.ts file.

 

The StocksAdaptiveCardExtension.ts file already includes a definition for the state which, however, is empty by default. We must add a new property to store the stock information, using the interface we have just created:

 
export interface IStocksAdaptiveCardExtensionState {
  stock: IStockPrice;
}

Now we can move on and define now a new function called fetchData(), which uses the fetch() API to retrieve the stock price from our Azure Function (we'll use the Microsoft one as a reference).

 

public async fetchData(): Promise<void> { 

    const url = "https://myfunction.azurewebsites.com/api/stockPrices/MSFT";

    let response = await fetch(url);

    const json = await response.text();
    const parsedJson: IStockPrice = JSON.parse(json) as IStockPrice;

    this.setState({stock: parsedJson});
}

The function is quite straightforward. First, we retrieve the JSON payload that contains all the information about the Microsoft stock; then we turn into a string, calling the text() function and we use JSON.parse() to convert the string into an IStockPrice object. Finally, we store it into the stock property of the state, by calling this.setState().

Now, we just need to call this function inside the onInit()  method:

 
public async onInit(): Promise<void> {
  
  this.state = { 
    stock: null
  };

  
  await this.fetchData();
  

  this.cardNavigator.register(CARD_VIEW_REGISTRY_ID, () => new CardView());
  this.quickViewNavigator.register(QUICK_VIEW_REGISTRY_ID, () => new QuickView());

  return Promise.resolve();
}

The onInit() function takes care of two additional tasks: first, before working with the state, we must initialize it. In our scenario, we don't have a default value, so we just set the stock property in the state to null. Second, we must register the cards and the quick view we want to render. The card is registered into the cardNavigator collection, while the quick view is registered in the quickViewNavigator collection. The default template includes a single card and a single quick view. A more complex project might provide multiple cards (for example, one for the medium and one for the large size) and multiple quick views (for example, triggered by different actions).

 

Now that we have the stock data, we must display it. Let's head to the CardView.ts file: we'll find a function called data(), which implements an interface that changes based on the template we have chosen. In our case, it's called IPrimaryTextCardParameters and it offers three properties, one for each of the elements that we can customize in the card: the title, the primary text, and the description.

 

Thanks to the state, we can set some of these properties with the stock information we have previously downloaded:

 

public get data(): IPrimaryTextCardParameters {
  return {
    description: "MSFT",
    primaryText: this.state.stock.openingPrice.toString(),
    title: this.properties.title
  };
}

The description property has a fixed value, the MSFT symbol; title is taken from the card properties (we'll get back to it later); primaryText is taken from the state. Thanks to the this.state keyword, we can access the IStockPrice object we have previously stored. Specifically, we are displaying on the card the current price of the stock, which is stored in the openingPrice property.

 

That's all we need to implement the basic experience. We're ready to test this first step.

 

Testing the Viva Connections card

SharePoint offers a feature called SharePoint Workbench to test SharePoint extensions, including Adaptive Card ones. It's a special environment, hosted on our SharePoint tenant, which can connect to our local machine and load an extension hosted locally. This tool simplifies the testing experience; otherwise, we would need each time to create a new SharePoint package, upload it to the administrator portal, test the work, identify the issues, make changes, and repeat the process again.

 

Before getting to the SharePoint workbench, however, we must launch the Azure Functions which we built the last time. However, it isn't enough. We must also use a tool like Ngrok to expose our local machine to a public URL, so that the Azure Functions can be accessed also by the SharePoint workbench. Since it runs on our SharePoint website, in fact, it's not able to connect to an API exposed on localhost. Once you have ngrok setup on your machine, launch it with the following command:

 
ngrok http 7071

You will get back a URL like https://<random-value>.ngrok.io. Make sure to include it in the code, by assigning it to the url property of the fetchData() function.

Now you can go back to the Viva Connections card project. Inside the .vscode folder, you will find a file called launch.json, which defines the debugging profile. You will find a url property, with the following setting:

 

"url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx",

Replace the first part of the URL with the real URL of your SharePoint domain. Now you must perform two operations in Visual Studio Code:

  • In the top menu, click on Terminal → New Terminal to open a terminal on the current folder. Run the following command:

    powershell
     
    gulp serve --nobrowser
    

    This command will run a local server that will host the Adaptive Card extension and it will enable the Live Reload feature, so that you can make changes to the code without having to redeploy the project.

  • Move to the Debug tab from the left sidebar and press F5. Visual Studio Code will launch a browser on the SharePoint workbench, and you will have to login with a valid SharePoint account.

This is how the SharePoint Workbench looks like:

 

 

When you click on +, you will see a series of components that you can add to test them. You will see a special section called Local, with the name of your project. Click on it to add it to the workbench. If you have done everything correctly, you will see a card with the Microsoft stock price.

 

 

 

In case, instead, you get an error, you can leverage the developers' tools in the browser (press F12 in Edge or Chrome) to see what's going on. It's likely that the first time you will experience a CORS failure: since the SharePoint website is hosted on a different domain than your Azure Functions, the request is blocked by default. While you're in development, you can overcome this problem by adding the CORS entry under the Host section in the local.settings.json file of your Azure Function:

 

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
  },
  "Host": {
    "CORS": "*"
  }
}

This configuration will open the usage of the API from any client app. You can also choose to restrict it, by adding the domain of your SharePoint website (like https://contoso.sharepoint.com).

 

Everything works, but the current implementation has a lot of limitations. We have hardcoded information, like the URL of our Azure Function or the stock price we want to track. This means that we would need to make changes to the code every time we want to track a stock different than Microsoft or the URL of our backend changes. Let's fix that by adding properties.

 

Adding properties

When you have added the Stocks card in the SharePoint Workbench, you have noticed that a panel opened on the right, which gave you the option to customize the title and to choose the size of the card. These are properties: the administrator who configures the Viva Connections dashboard can define them and, in code, you can read their value so that you can customize the behavior of the card. We have already seen an example of the property in action. Let's look again at the data() function implemented in the CardView.ts file:

 

 

public get data(): IPrimaryTextCardParameters {
  return {
    description: this.state.stock.symbol, 
    primaryText: this.state.stock.openingPrice.toString(),
    title: this.properties.title
  };
}

The title of the card is taken from the this.properties collection, which means that the administrator who configures the dashboard can customize the title without needing to ask the developer to make changes.

 

Let's use the same approach to add two other properties: one called stockSymbol, to store the company we want to track; one called apiUrl, to store the URL of our backend.

The first place where you must define properties is the manifest file. You will find a section called properties, where you can list them with their default value. This is how we must change it:

 
"properties": {
  "title": "Stocks",
  "stockSymbol": "",
  "apiUrl": ""
},

title was already included, with a default value of Stocks. We add stockSymbol and apiUrl and we use an empty string as default value, since we don't have a way to guess which will be the stock symbol to track or the backend URL.

The next step is to update the IStocksAdaptiveCardExtensionProps interface, included in the StocksAdaptiveCardExtension.ts file, which describes the properties, in a similar way to what we did before with the IStocksAdaptiveCardExtensionState interface for the state. This is how we must change it:

 
export interface IStocksAdaptiveCardExtensionProps {
  title: string;
  stockSymbol: string;
  apiUrl: string;
}

We are simply adding two new properties, stockSymbol and apiUrl, which type is string.

Now that we have our properties, we must change the fetchData() function we have previously created so that the API URL and the stock symbol are taken from the properties, instead of being hardcoded:

 
public async fetchData(): Promise<void> { 

    const url = this.properties.apiURL + "/api/stockPrices/" + this.properties.stockSymbol;

    let response = await fetch(url);

    const json = await response.text();
    const parsedJson: IStockPrice = JSON.parse(json) as IStockPrice;

    this.setState({stock: parsedJson});
}

There's one last step to complete the task. We have defined two new properties, but we haven't added a way to set them. The panel that is displayed when you add a card to the dashboard, in fact, isn't automatically generated by SharePoint, but it's managed by a file included in the project (the one called StocksPropertyPane.ts, in our case). Inside this file, you use TypeScript objects to define the various fields. Let's take a look:

 
import { IPropertyPaneConfiguration, PropertyPaneTextField } from '@microsoft/sp-property-pane';
import * as strings from 'StocksAdaptiveCardExtensionStrings';

export class StocksPropertyPane {
  public getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: { description: strings.PropertyPaneDescription },
          groups: [
            {
              groupFields: [
                PropertyPaneTextField('title', {
                  label: strings.TitleFieldLabel
                }),
                PropertyPaneTextField('stockSymbol', {
                  label: strings.StockCodeLabel
                }),
                PropertyPaneTextField('apiUrl', {
                  label: strings.ApiUrlLabel
                })
              ]
            }
          ]
        }
      ]
    };
  }
}

This class is very flexible, since you can define an arbitrary number of properties, group them into different sections, etc. For the sake of simplicity, in our case we are displaying all the properties into a unique group. Each property is mapped to a specific PropertyPane object. In our scenario, all the properties are strings, so they are rendered with a PropertyPaneTextField object, which generates a text input. The SharePoint Framework includes many other types of objects, like PropertyPaneDropdown or PropertyPaneCheckbox, to support several types of parameters, like boolean or numbers.

 

When we initialize the PropertyPaneTextField object, we must specify two information:

  • The name of the property we want to bind to the field.
  • The label we want to display as header of the field. By default, SharePoint Adaptive Card extensions are built with localization in mind, so labels aren't hardcoded, but they're taken from an interface included in the project called IStocksAdaptiveCardExtensionStrings and defined in the mystring.d.ts file. This interface loads the strings from a localization file: by default, the project includes only English as a language, so you'll find the strings in the en-us.js file.

The StocksPropertyPane.ts file already included a field to support the title property. As we have seen in the previous snippet, we just must add two new fields to support the stockSymbol and apiUrl properties.

 

This was the last required step to support properties. There's only one catch that we must keep in mind: properties, by default, are loaded only when the card is initialized. This means that, with the current implementation, when you drag the card into the dashboard and you start changing properties, you won't see the changes until you refresh the page. If you want to make the card more dynamic, you can use the onPropertyPaneFieldChanged event, which is available in the StocksAdaptiveCardExtension.ts file. Let's look at the implementation:

 

protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
  if (propertyPath === 'stockSymbol' || propertyPath === 'apiUrl' && newValue !== oldValue) {
    if (newValue) {
      void this.fetchData();
    } 
  }
}

Using the propertyPath parameter, we check which property was changed. In our case, we want to detect changes in the stockSymbol or apiUrl properties. If the administrator simply changes the title of the card, in fact, we don't need to retrieve new data from our backend. If one of these two properties has changed and there's indeed a new value, we call again the fetchData() function we have previously defined to query our backend, using the new API URL or the new stock symbol.

 

Now that we have completed the implementation, we can use the SharePoint Workbench again to test our project. This time, when you add the Stocks card to the dashboard, the property pane on the right should include two new fields other than the title one:

 

 

Now set the two fields with a valid stock symbol and with the URL generated by Ngrok: the card should automatically update with the data coming from our Azure Function.

 

Implementing the quick view

The last feature we're going to implement today is the quick view. We don't have to do much in code, since the default template already implements the quick view the way we need: when the user clicks on the button on the card, we want to open the quick view to display more details about the stock.

Let's open the CardView.ts file and let's look at how the button is implemented:

 

public get cardButtons(): [ICardButton] | [ICardButton, ICardButton] | undefined {
  return [
    {
      title: strings.QuickViewButton,
      action: {
        type: 'QuickView',
        parameters: {
          view: QUICK_VIEW_REGISTRY_ID
        }
      }
    }
  ];
}

The file includes a function called cardButtons(), which can return zero, one or two ICardButton objects. A card, in fact, doesn't need to have a button (you can also manage the click on the card itself) but, if you want to have them, you can have only one when the card is displayed in medium size and two when the card is displayed in large size.

 

A card button supports various types of interactions. The Quick View panel is a built-in one, so you just need to supply a label for the button (using the title property) and, as action type, the fixed QuickView value. Then, as parameter, you must supply the view property with the identifier of the quick view you want to display, among the ones that have been registered inside the quickViewNavigator collection.

 

This is about triggering the quick view. But how is a quick view defined? Let's look at the QuickView.ts file. The basic structure is the same as the CardView one. It includes, in fact, a data() function which we must use to supply the information we want to display on the card. However, this time the function implements the IQuickViewData interface, which is generic. Unlike with the basic card (which supports only three templates, each of them with a limited number of parameters), we have lot of flexibility with an adaptive card, so we can supply an undefined number of parameters.

 

The core implementation of the QuickView file, however, is the template() function, in which we must supply the JSON that defines the Adaptive Card. In the default template, this JSON is defined in a dedicated file called QuickViewTemplate.json, so the function simply uses the require() JavaScript method to load the content of that file:

 

public get template(): ISPFxAdaptiveCard {
  return require('./template/QuickViewTemplate.json');
}

The default project contains a basic template, which isn't a good fit, however, for our scenario. The easiest way to generate a better template is to leverage the Adaptive Card Designer, a tool which you can use to visually design your card and get the corresponding JSON. Additionally, the template gallery includes a stock template, which is perfect for our needs. This is the definitive version of the template, modified for our needs:

 

 

{
  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  "type": "AdaptiveCard",
  "version": "1.2",
  "body": [
      {
          "type": "Container",
          "items": [
              {
                  "type": "TextBlock",
                  "text": "${companyName}",
                  "size": "Medium",
                  "wrap": true
              },
              {
                  "type": "TextBlock",
                  "text": "${primaryExchange}: ${symbol}",
                  "isSubtle": true,
                  "spacing": "None",
                  "wrap": true
              },
              {
                "type": "TextBlock",
                "text": "${time}",
                "wrap": true
            }
          ]
      },
      {
          "type": "Container",
          "spacing": "None",
          "items": [
              {
                  "type": "ColumnSet",
                  "columns": [
                      {
                          "type": "Column",
                          "width": "stretch",
                          "items": [
                              {
                                  "type": "TextBlock",
                                  "text": "${formatNumber(latestPrice, 2)} ",
                                  "size": "ExtraLarge",
                                  "wrap": true
                              }
                          ]
                      },
                      {
                          "type": "Column",
                          "width": "auto",
                          "items": [
                              {
                                  "type": "FactSet",
                                  "facts": [
                                      {
                                          "title": "High",
                                          "value": "${high} "
                                      },
                                      {
                                          "title": "Low",
                                          "value": "${low} "
                                      }
                                  ]
                              }
                          ]
                      }
                  ]
              }
          ]
      }
  ]
}

We won't go through the details of implementing an adaptive card, you can refer to the official documentation. One thing I want to highlight, however, is that you can notice how some values aren't hardcoded, but there's a special string that acts as a placeholder, like ${companyName} or ${symbol}. These are parameters, which are replaced at runtime with the parameters we supply to the data() function of the QuickView file. This is how our data() function looks like:

 

public get data(): IQuickViewData {
  return {
    subTitle: strings.SubTitle,
    title: strings.Title,
    companyName: this.state.stock.companyName,
    symbol: this.state.stock.symbol,
    latestPrice: this.state.stock.openingPrice,
    high: this.state.stock.highestPrice,
    low: this.state.stock.lowestPrice,
    primaryExchange: this.state.stock.exchange,
    time: this.state.stock.time
  };
}

Thanks to the this.state keyword, we can access the stock object we have previously stored and retrieve the various information we want to display, like the company name, the symbol, or the latest price. The properties names match the placeholders we have placed in the JSON template of the Adaptive Card.

 

We're good to test our code now. Press F5 again in Visual Studio Code to launch the SharePoint workbench, add the Stocks card to the dashboard and set up its properties. Now, if you click on the See more button on the card, you should see the adaptive card being rendered, which displays a bit more information about the stock than the one you see in the card:

 

Wrapping up

In this second part of the series we have built our Viva Connections card. We didn't just build a card to display stock prices but, in the process, we have learned the basics on how to build a Viva Connections card. Everything we have learned, in fact, can be easily reapplied to display any type of data, coming from any type of 3rd party service.

 

In the next and final post of the series, we'll talk about deployment and security. The current implementation, in fact, has a security flaw: anyone with the URL of our Azure Function will be able to call the API and get the stock data. We'll learn how to protect the API, so that only authorized users who are logged in into Teams and Viva Connections can use it.

 

In the meantime, remember that the full code of the final solution is available on GitHub.

 

Happy coding!

Updated Mar 21, 2023
Version 1.0
No CommentsBe the first to comment

Share