Power Apps

Create Survey PCF Control Using Adaptive Cards Templating

Templating using Adaptive Cards has been in preview for more than a year and I have been itching to try them out so let’s do it!

 

Adaptive Cards

Description from the official site –  

Adaptive Cards are platform-agnostic snippets of UI, authored in JSON, that apps and services can openly exchange. When delivered to a specific app, the JSON is transformed into native UI that automatically adapts to its surroundings. It helps design and integrate light-weight UI for all major platforms and frameworks.

Learn more about it here – adaptivecards.io

The real beauty of Adaptive Cards is that the UI authoring experience is decoupled from the complicated design logic and language of the host application. By using the Adaptive Cards SDK, the host applications can take the same UI JSON and render the card consistently across multiple applications to provide a coherent experience to the end user. In this article we are going to look into hosting this in a model-driven Power Apps, using Adaptive Cards Templating in PCF control.

 

Adaptive Cards Templating

Before we dig deeper, let’s explore why we need templating using Adaptive Cards. Prior to templating the authored UI, JSON-serialized card object model, had data tightly coupled to its layout. This made the re-usability of UI difficult without the mechanism to regenerate the card with new data each time. With templating, there are now two JSONs; one for the UI and one for the data. The UI JSON, now abstracted as a template, allows for special placeholders to define the binding of the data. These are represented as open/close curly braces around data property names. When transformation takes place, the placeholders will be replaced with the actual data and the resulting card layout then can be rendered by the SDK. There are a lots of cool samples on the official site to demonstrate this

 

Model-Driven Survey App With Power Apps

To illustrate how this works, I’ve mocked up a very simple Survey app on Power Apps. Users will be able to setup survey templates with questions, which can then be used by individual surveys to be sent out for submission. When the survey record is published, the corresponding plugin will generate the data JSON and store it against the record. With the template JSON stored in the Adaptive Card Template entity, PCF control can retrieve the data and template JSON to host and render the card to the user. 

The example that I’ve setup results in survey card that looks like this in the PCF control.

 

Under the Hood – Entities

Here is an ER diagram to show the entity structure of the Survey app.

Survey Template – Survey template is where questions are configured. Idea is to set up the survey template once and have surveys reuse them as needed.

Question – Allows setting up question properties such as display order on the survey, data type for the answer and answer choices if any. Take special note of the Data Type attribute, this will drive the conditional checks to render the correct elements on the card later.

Survey – Individual survey of the survey template, which allows survey specific details such as publish date, participants, survey requests and survey status. This also holds the generated data JSON on publishing.

Survey Request – This captures survey requests sent to the participants. It will also be used to display the survey card in PCF control.

Adaptive Card Template – Store for adaptive cards template JSON.

 

Data JSON

The following data JSON is a serialization of the custom survey object model.

{
  "Title": "Work from Home Survey - Part 1",
  "Introduction": "It's a bit more than a week since we've started to work from home. Could you fill out a short survey to share your experience? Thanks and keep safe :)",
  "Items": [
    {
      "Id": "9dd41200-806e-ea11-a811-000d3ad0f227_label",
      "InputTypeName": "Label",
      "Text": "Do you like working from home?"
    },
    {
      "Id": "9dd41200-806e-ea11-a811-000d3ad0f227",
      "InputTypeName": "SingleChoice",
      "Choices": [
        { "Title": "Yes", "Value": "True" },
        { "Title": "No", "Value": "False" }
      ]
    },
    {
      "Id": "11e9d42b-806e-ea11-a811-000d3ad0f227_label",
      "InputTypeName": "Label",
      "Text": "Are you more productive?"
    },
    {
      "Id": "11e9d42b-806e-ea11-a811-000d3ad0f227",
      "InputTypeName": "SingleChoice",
      "Choices": [
        { "Title": "Yes", "Value": "True" },
        { "Title": "No", "Value": "False" }
      ]
    },
    {
      "Id": "40507e39-806e-ea11-a811-000d3ad0f227_label",
      "InputTypeName": "Label",
      "Text": "What do you miss the most?"
    },
    {
      "Id": "40507e39-806e-ea11-a811-000d3ad0f227",
      "InputTypeName": "SingleChoice",
      "Choices": [
        { "Title": "Interacting with colleagues", "Value": "0" },
        { "Title": "Meetings", "Value": "1" },
        { "Title": "Getting away from kids", "Value": "2" },
        { "Title": "Staff Cafe", "Value": "3" },
        { "Title": "Large Monitors", "Value": "4" }
      ]
    },
    {
      "Id": "95946455-806e-ea11-a811-000d3ad0f227_label",
      "InputTypeName": "Label",
      "Text": "What time do you start work now?"
    },
    {
      "Id": "95946455-806e-ea11-a811-000d3ad0f227",
      "InputTypeName": "Time"
    },
    {
      "Id": "dabd2a8d-806e-ea11-a811-000d3ad0f227_label",
      "InputTypeName": "Label",
      "Text": "Rate your experience from working from home?"
    },
    {
      "Id": "dabd2a8d-806e-ea11-a811-000d3ad0f227",
      "InputTypeName": "Number",
      "Placeholder": null
    },
    {
      "Id": "08448bad-806e-ea11-a811-000d3ad0f227_label",
      "InputTypeName": "Label",
      "Text": "Share your experience"
    },
    {
      "Id": "08448bad-806e-ea11-a811-000d3ad0f227",
      "InputTypeName": "Multiline",
      "MaxLength": 0,
      "Placeholder": null
    }
  ]
}

 

Template JSON

This template will be used to bind the data JSON above.

{
    "type": "AdaptiveCard",
    "version": "1.0",
    "body": [
        {
            "type": "Container",
            "items": [
                {
                    "type": "TextBlock",
                    "text": "{Title}",
                    "size": "Large",
                    "weight": "Bolder"
                },
                {
                    "type": "TextBlock",
                    "text": "{Introduction}",
                    "spacing": "ExtraLarge",
                    "wrap": true
                }
            ],
            "style": "default"
        },
        {
            "type": "Container",
            "$data": "{Items}",
            "items": [
                {
                    "$when": "{InputTypeName == 'Label'}",
                    "type": "TextBlock",
                    "text": "{Text}",
                    "weight": "Bolder",
                    "wrap": true
                },
                {
                    "$when": "{InputTypeName == 'Text'}",
                    "type": "Input.Text",
                    "placeholder": "{Placeholder}",
                    "id": "{Id}"
                },
                {
                    "$when": "{InputTypeName == 'Multiline'}",
                    "type": "Input.Text",
                    "placeholder": "{Placeholder}",
                    "isMultiline": true,
                    "id": "{Id}"
                },
                {
                    "$when": "{InputTypeName == 'Time'}",
                    "type": "Input.Time",
                    "id": "{Id}"
                },
                {
                    "$when": "{InputTypeName == 'Date'}",
                    "type": "Input.Date",
                    "id": "{Id}"
                },
                {
                    "$when": "{InputTypeName == 'Number'}",
                    "type": "Input.Number",
                    "id": "{Id}"
                },
                {
                    "$when": "{InputTypeName == 'SingleChoice'}",
                    "type": "Input.ChoiceSet",
                    "choices": [
                        {
                            "$data": "{Choices}",
                            "title": "{Title}",
                            "value": "{Value}"
                        }
                    ],
                    "id": "{Id}",
                    "style": "expanded"
                }
            ]
        }
    ],
    "actions": [
        {
            "type": "Action.Submit",
            "title": "Submit Survey"
        }
    ],
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
}

Notice the data placeholders in curly braces and the use of special binding scopes, such as $data and $when. Data context can be explicitly assigned to an element using the $data attribute. If the data context of an element is set to an array, then the element itself will be repeated for each item in the array. To conditionally check which element to show depending on the data, the $when attribute is used. When the condition is met, the element will be made visible, otherwise it will be dropped entirely. By putting these two concepts together, we can create a very simple version of form engine by binding an array of questions and selecting the correct element to show depending on the question data type.  

 

Build PCF Control With Adaptive Card Templating SDK

If you haven’t created a PCF control before, please read the instructions on the official Microsoft site.

To render the Adaptive Card using templating, we need to install two packages using npm:

npm install adaptivecards adaptivecards-templating

Don’t forget to import the modules:

import * as AdaptiveCards from "adaptivecards";
import * as ACData from "adaptivecards-templating";

To retrieve the data and template JSON, a few query related properties have been set up on the control manifest:

public async updateView(context: ComponentFramework.Context<IInputs>): Promise<void> {
	this._context = context;

	if (this._card) {
		this._container.removeChild(this._card);
	}

	if (this._context.updatedProperties.includes("responseJson")) {
		this.renderResponse('Thank you for submitting!')
	}
	else if (this._context.parameters.responseJson.raw) {
		this.renderResponse('This survey has already been submitted.');
	}
	else {
		const adaptiveCardTemplateJson = await this.getAdaptiveCardJson(
			this._context.parameters.templateRetrieveEntityType.raw, 
			this._context.parameters.templateRetrieveOptions.raw,
			this._context.parameters.templateRetrieveAttributeName.raw);

		const adaptiveCardDataJson = await this.getAdaptiveCardJson(
			this._context.parameters.dataRetrieveEntityType.raw,
			this._context.parameters.dataRetrieveOptions.raw,
			this._context.parameters.dataRetrieveAttributeName.raw);
			
		this.renderRequestCard(adaptiveCardTemplateJson, adaptiveCardDataJson);
	}
}

private async getAdaptiveCardJson(entityType: string, options: string, attributeName: string): Promise<any> {
	// @ts-ignore
	const entityId = this._context.page.entityId;
	const response = await this._context.webAPI.retrieveMultipleRecords(entityType, (options || "").replace("{entityId}", entityId));

	if (response && response.entities.length == 1) {
		const adaptiveCardJson = response.entities[0][attributeName];
		const json = JSON.parse(adaptiveCardJson);
		return json;
	}	
	return null;
}

Here is the code to transform and render the card:

private renderRequestCard(templateJson: any, dataJson: any) {
	if (!templateJson || !dataJson) return;
		
	const context = new ACData.EvaluationContext();
	context.$root = dataJson;
		
	const template = new ACData.Template(templateJson);
	const card = template.expand(context);
 
	const adaptiveCard = new AdaptiveCards.AdaptiveCard();
	adaptiveCard.parse(card);

	adaptiveCard.onExecuteAction = this.submitRequest.bind(this);

	this._card = adaptiveCard.render();
	this._container.appendChild(this._card);
}

 

Submit the Survey!

In order to submit the survey, Action.Submit button has been added to the card along with an event handler to capture the execute action command.

adaptiveCard.onExecuteAction = this.submitRequest.bind(this);

private submitRequest(action: AdaptiveCards.Action) {
	const submitAction = action as AdaptiveCards.SubmitAction;
	if (submitAction) {
		this._responseData = JSON.stringify(submitAction.data);
		this._notifyOutputChanged();
	}
}

public getOutputs(): IOutputs {
	return {
		responseJson: this._responseData
	};
}

This will trigger the output change event, which saves the submitted data back to the attribute bound on the PCF control. The submitted data will look like the sample below. Note that each guid represents the id of the question record.

{"9dd41200-806e-ea11-a811-000d3ad0f227":"True","11e9d42b-806e-ea11-a811-000d3ad0f227":"True","40507e39-806e-ea11-a811-000d3ad0f227":"0","95946455-806e-ea11-a811-000d3ad0f227":"08:00","dabd2a8d-806e-ea11-a811-000d3ad0f227":"7","08448bad-806e-ea11-a811-000d3ad0f227":"So far not too bad, although miss going outside!"}

I’ve written a plugin to parse the submitted data and generate answer records for the survey.

We won’t cover it in this post, but there are a few functionalities that can be added to take it to the next level, such as sending the survey to Microsoft teams. So that’s it, I hope you enjoyed reading about PCF control using Adaptive Cards Templating! Let me know your thoughts as this was my first blog post.