ACE QuickViews and working with view navigators

Completed

The rendering of Adaptive Card Extensions (ACEs) is implemented using two different types of views: CardViews and QuickViews.

In this unit, you'll learn more about QuickViews, the navigators used in both types of views, and some common techniques and scenarios you can use in your Adaptive Cards.

QuickView

The QuickView for an ACE isn't initially visible when the ACE is rendered; the CardView is what's initially rendered and visible on the Viva Connections desktop and mobile experiences.

The QuickView is shown based on user interaction with the CardView. This can be when the ACE is selected with the mouse, or when one of the buttons on the CardView are selected.

While the CardView for the ACE is implemented in the ./[..]/cardView/CardView.ts file, the QuickViews are in the ./[..]/quickView folder. The initial QuickView.ts file created by the Yeoman generator contains three methods.

QuickView.template()

The template() method returns the JSON for an Adaptive Card that defines the rendering for the QuickView:

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

While the default Adaptive Card templates are found in the ./[..]/quickView/template folder, that's not a requirement. The method just needs to return the JSON that's used to implement the Adaptive Card for the QuickView.

QuickView.data()

Similar to the CardView, the data() method returns an object that's bound to the QuickView's template using Adaptive Card Templating. The properties on this object should match the expected properties you use within the QuickView's Adaptive Card implementation.

For example, the following data() method...

public get data(): IQuickViewData {
  return this.state.listItems[this.state.currentIndex]);
}

... returns an object that contains all the properties the currently selected item in the listItems array. These properties, including the list item's ID, Title, and Description property, are expected in the associated Adaptive Card template as you can see from the ${} notation used by Adaptive Cards:

{
  "type": "Container",
  "items": [
    {
      "type": "TextBlock",
      "text": "(${ID}) ${Title}",
      "horizontalAlignment": "Center",
      "size": "Medium",
      "weight": "Bolder",
      "wrap": true
    },
    {
      "type": "TextBlock",
      "text": "${Description}",
      "horizontalAlignment": "Center",
      "size": "Default",
      "wrap": true
    }
  ]
}

QuickView.onAction()

Finally, the onAction() method is called by the SharePoint Framework (SPFx) when an action is raised within the Adaptive Card rendering. Actions include Submit actions or when the ACE uses the geo-location or media selection capabilities supported by Viva Connections:

public onAction(action: IActionArguments): void {
  if (action.type !== 'Submit') { return ;}

  if (action.id === 'next') {
    let currentIndex = this.state.currentIndex;
    this.setState({ currentIndex: currentIndex + 1 });
  }
}

ACE view navigators

Another thing ACE developers must be familiar with are view navigators. View navigators are used by the ACE to keep track of all CardViews and QuickViews the ACE can use.

Registering CardViews and QuickViews

The two types of views must first be registered with their respective view navigators before they can be used. The registration involves setting the view's ID and the object that implements the view.

The main file that contains the class that defines the ACE, the *AdaptiveCardExtension.ts file, contains the IDs for all the views.

The ID for the CardViews isn't normally exported by the module because it's only referenced within the class defined in the same file:

const CARD_VIEW_REGISTRY_ID: string = 'SharePointRest_CARD_VIEW';

However, the IDs for QuickViews are exported from this module because they're referenced in the CardView and other QuickViews:

export const QUICK_VIEW_REGISTRY_ID: string = 'SharePointRest_QUICK_VIEW';

The registration of all views is done in the ACE's onInit() method. Notice how the following code passes in two parameters: the ID of the view and an instance of the view object:

import { CardView } from './cardView/CardView';
import { QuickView } from './quickView/QuickView';

// .. omitted for brevity

export default class SharePointRestAdaptiveCardExtension extends BaseAdaptiveCardExtension<
  ISharePointRestAdaptiveCardExtensionProps, ISharePointRestAdaptiveCardExtensionState> {
  // .. omitted for brevity

  public async onInit(): Promise<void> {
    // .. omitted for brevity

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

    // .. omitted for brevity
  }

  // .. omitted for brevity
}

Working with the view navigators

Both of the view navigators, the cardViewNavigator and quickViewNavigator are essentially stacks, or arrays that contain views. The top-most item on the stack is what's rendered by the SPFx ACE rendering engine unless a specific view is specified.

Let's look at how we can interact with the view navigator using some QuickView implementations. Consider the following scenario:

You want to display a QuickView that displays a form to collect information from a user. This can be triggered by a button on the CardView. So, the CardView.cardButtons() method could return a button that has an action.type set to QuickView. The view parameter for the button tells SPFx that registered QuickView to display. SPFx uses the ID provided to find and render the desired QuickView:

public get cardButtons(): [ICardButton] | [ICardButton, ICardButton] | undefined {
  return [{
    title: 'Book a Trip',
    action: {
      type: 'QuickView',
      parameters: { view: QUICK_VIEW_START_TRIP_REGISTRY_ID }
    }
  }];
}

Screenshot of a QuickView activation from a CardView.

The QuickView contains a few buttons. The first two buttons are actions that launch other QuickViews to collect additional information from the user:

export class StartTrip extends BaseAdaptiveCardView<
  ICampusShuttleAdaptiveCardExtensionProps,
  ICampusShuttleAdaptiveCardExtensionState,
  IStartTripData
> {
  // .. omitted for brevity

  public onAction(action: IActionArguments): void {
    if (action.type === 'Submit') {
      if (action.id === 'originLocation') {
        this.quickViewNavigator.push(QUICK_VIEW_SET_ORIGIN_REGISTRY_ID);
      } else if (action.id === 'destinationLocation') {
        this.quickViewNavigator.push(QUICK_VIEW_SET_DESTINATION_REGISTRY_ID);
      } else if (action.id === 'save') {
        (async () => {
          await upsertListItem(this.context, this.properties.listId, this.state.currentTrip);
          this.quickViewNavigator.push(QUICK_VIEW_SAVE_TRIP_REGISTRY_ID);
        })();
      }
    }
  }
}

For example, if the user selects the Select / set trip destination button, it launches the destination selector QuickView by calling the this.quickViewNavigator.push() method. That causes the ACE rendering engine to replace the contents of the QuickView using the different QuickView:

Screenshot of the QuickView destination selector.

After completing the form, when the user selects the Save destination location button, the onAction() handler for that QuickView first updates the component's state and then pops the current QuickView off the navigator, causing the initial form QuickView to be at the top and to be re-rendered:

public onAction(action: IActionArguments | IGetLocationActionArguments): void {
  const currentTrip = this.state.currentTrip;

  // logic to handle updating the trip destination object
  //    based on the selection in the form

  this.setState({ currentTrip: currentTrip });
  this.quickViewNavigator.pop();
}

When the user selects the Save trip button on the initial form, the QuickView displaying a save confirmation is displayed using the QUICK_VIEW_SAVE_TRIP_REGISTRY_ID ID:

Screenshot of the QuickView save trip confirmation.

Finally, when the user selects the final Close button, the entire QuickView is closed using the close() method on the navigator:

{
  "schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  "type": "AdaptiveCard",
  "version": "1.2",
  "body": [{
    "type": "TextBlock", "text": "${title}"
  }],
  "actions": [{
    "type": "Action.Submit",
    "id": "close", "title": "Close"
  }]
}
public onAction(action: IActionArguments): void {
  if (action.id === 'close') {
    this.quickViewNavigator.close();
  }
}

Defer loading of QuickViews

Developers can implement complex ACEs using multiple QuickViews to create sophisticated interactions for their users. However, maybe not all QuickViews will be used by most users. In that case, it makes sense to defer loading of the QuickView only when the ACE tries to load it, limiting the payload that the user has to download or wait to be initialized by the page rendering all the ACEs.

To implement this, remove the import statement for the QuickView that's normally found at the top of the ACE's component file:

import { QuickView } from './quickView/QuickView';

Then, update the quickViewNavigator.register() method call with the following code that defers the loading of the component only when it's initiated by the SPFx ACE runtime:

// this.quickViewNavigator.register(QUICK_VIEW_REGISTRY_ID, () => new QuickView());
this.quickViewNavigator.register(
  QUICK_VIEW_REGISTRY_ID,
  () => import('./quickView/QuickView').then((c) => new c.QuickView())
);

Summary

In this unit, you learned more about QuickViews, the navigators used in both types of views, and some common techniques and scenarios you can use in your Adaptive Cards.