Use advanced features with Power Apps component framework

10 months ago 234
ARTICLE AD

Introduction to using React within a Power Apps component

Completed100 XP

10 minutes

React is a standardized client framework that is built for building user interfaces. It provides a declarative way to create interactive UIs and provides a mechanism to encapsulate components to make complex UIs that manage component states and have high interactivity. Because React is written in JavaScript, you can use this framework within a Power Apps component.

If you're new to React, go to React, which provides a wealth of tutorials and resources on how to build React components.

Prepare your computer for code components

To prepare your computer to build code components, follow these steps:

Install Npm (comes with Node.js) or Node.js (comes with npm). We recommend that you use a Long-Term Support (LTS) version.

Install Visual Studio Code.

Install Power Platform Tools extension.

Install the Build Tools for Visual Studio from Visual Studio Downloads.

Fluent UI

One of the many great developments from Microsoft has been its implementation of Fluent UI, a collection of UX frameworks that you can use to build fluent experiences that fit seamlessly into a broad range of Microsoft products. Using Fluent UI within your Power Apps code component is as simple as referencing its libraries, and it provides a React-specific version that you can use. For more information, see Fluent UI.

Implement a sample FacePile component

 Important

Download the FacePileComponent.zip zip file to use with this exercise. Extract the zip file.

In this example, you build a component that uses the FacePile Fluent UI component. The FacePile shows a list of faces or initials in a horizontal lookup, each circle representing a person.

A practical example of when you might use this lookup is to list contributors to an article or record, such as what you would see in Microsoft Learn, as shown in the following image.

Screenshot of FacePile Fluent UI component.

Create a new component project

To create a component project, follow these steps:

Create a directory where you build your component. In this sample, you place the component in C:\source\face-pile; however, you can create your own directory. To create your own directory, you use Visual Studio Code.

Start Visual Studio Code.

Select Terminal, select New Terminal, and switch the Terminal shell to the Command Prompt.

 Note

If you are not familiar with the Terminal in Visual Studio Code, select Terminal Basics

Create your source folder.

ConsoleCopy

md \source

Change the directory to your source folder.

ConsoleCopy

cd \source

From your source directory, create a directory named face-pile.

ConsoleCopy

md face-pile

Change to the directory you created.

ConsoleCopy

cd face-pile

You should now be in the new directory you created.

Initialize your component project by using Power Platform CLI with the following command:

ConsoleCopy

pac pcf init --namespace Learn --name ReactFacePile --template field --framework React

Install the project build tools by using the command npm install. You might see some warnings displayed; however, you can safely ignore them.

ConsoleCopy

npm install

Run the following command to open the project in Visual Studio Code.

ConsoleCopy

code -a .

The project should look like the following image.

Screenshot of Facepile U I in Visual Studio Code.

Implement your code component's logic

To implement your code component's logic, follow these steps:

Expand the ReactFacePile folder and open the ControlManifest.Input.xml file.

Locate the properties node and replace it with the following XML:

XMLCopy

" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;"><property name="numberOfFaces" display-name-key="numberOfFaces_Display_Key" description-key="numberOfFaces_Desc_Key" of-type="Whole.None" usage="bound" required="false" />

Locate the resources and uncomment the css and resx.

Screenshot of removing c s s comment in Visual Studio Code.

Make sure you still have the ControlManifest.Input.xml file selected and then select New Folder.

Name the new folder components.

Go to the folder where you extracted the downloaded FacePileComponent.zip file and open the FacePileComponent folder.

Drag the files inside the FacePileComponents folder and drop them in the components folder you created.

The components folder should now have two files.

Screenshot of removing two facepile folders.

Open the Index.ts file.

Replace import { HelloWorld, IHelloWorldProps } from "./HelloWorld"; with this snippet.

TypeScriptCopy

import { FacepileBasicExample, IFacepileBasicExampleProps } from "./components/Facepile" ;

After the imports, add the following constant.

TypeScriptCopy

const DEFAULT_NUMBER_OF_FACES = 3;

Add the snippet before the constructor.

TypeScriptCopy

private props: IFacepileBasicExampleProps = { numberFacesChanged: this.numberFacesChanged.bind(this), };

The changes you made should look like the following image.

Screenshot of changing the code in the index.ts file.

Locate the init method and add the following snippet after this.notifyOutputChanged = notifyOutputChanged; line

TypeScriptCopy

this.props.numberOfFaces = context.parameters.numberOfFaces.raw || DEFAULT_NUMBER_OF_FACES;

Replace the updateView method with the following method.

TypeScriptCopy

): React.ReactElement { if (context.updatedProperties.indexOf("numberOfFaces") > -1) { this.props.numberOfFaces = context.parameters.numberOfFaces.raw || DEFAULT_NUMBER_OF_FACES; } return React.createElement(FacepileBasicExample, this.props); } " style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;">public updateView(context: ComponentFramework.Context<IInputs>): React.ReactElement { if (context.updatedProperties.indexOf("numberOfFaces") > -1) { this.props.numberOfFaces = context.parameters.numberOfFaces.raw || DEFAULT_NUMBER_OF_FACES; } return React.createElement(FacepileBasicExample, this.props); }

The init and updateView methods should now look like the following image.

Screenshot of updated view code in index.ts file.

Replace the getOutputs method with the following method.

TypeScriptCopy

public getOutputs(): IOutputs { return { numberOfFaces: this.props.numberOfFaces, }; }

Add the following method after the destroy method.

TypeScriptCopy

private numberFacesChanged(newValue: number) { if (this.props.numberOfFaces !== newValue) { this.props.numberOfFaces = newValue; this.notifyOutputChanged(); } }

Select File and Save All your changes.

Add styling to your code component

To add styling to your code component, follow these steps:

Select the ControlManifest.Input.xml file and then select New Folder.

Name the new folder css.

Select the css folder you created and select New File.

Name the new file ReactFacePile.css.

Open the ReactFacePile.css file you created, and paste the following CSS snippet.

cssCopy

msFacepileExample { max-width: 300px; } .msFacepileExample .control { padding-top: 20px; } .msFacepileExample .ms-Dropdown-container, .msFacepileExample.ms-Slider { margin: 10px 0 10px 0; } .msFacepileExample .ms-Dropdown-container .ms-Label { padding-top: 0; } .msFacepileExample .ms-Checkbox { padding-top: 15px; } .exampleCheckbox { margin: 10px 0; } .exampleLabel { margin: 10px 0; }

Select File and Save your changes.

Select the ControlManifest.Input.xml file and select New Folder.

Name the new folder strings.

Go to the folder where you extracted the downloaded FacePileComponent.zip file and open the FacePileStrings folder.

Drag the ReactFacePile.1033.resx file and drop it in the strings folder you created.

The strings folder should now have the resx file.

Screenshot of showing the addition of the Facepile file.

Select File and Save your changes.

Go to the terminal and run this build command:

ConsoleCopy

npm run build

 Note

If you receive an error that JSX is not defined, open the file .eslintrc.json. On line 11: "ComponentFramework": true, add a comma and then a new line with "JSX": true. Save changes and repeat the npm run build command.

The build should complete successfully.

Test the components by running the following command.

ConsoleCopy

npm start

The test harness should open a new browser window.

The component should look like the following image.

Screenshot of the new component image.

Change the size of the container to 500 x 220 and move the slider to 5.

The component should now look like the following image. Close the test harness browser window.

Screenshot of the new component image with adjustments.

Close the test harness browser window.

Go back to the terminal and stop the watcher by pressing [CONTROL] + C.

Type Y and then press [ENTER].

For more information, go to Implementing the FacePile component.

Use the formatting API in a Power Apps component

Completed100 XP

10 minutes

Power Apps component framework exposes a formatting API that can be especially useful when you need to format various values in your application. This unit shows how to use this API by producing an HTML table to illustrate how various methods can be used.

Screenshot of HTML table to illustrate various methods and uses.

Initialize your component's project

To initialize your component's project, follow these steps:

Start Visual Studio Code.

Select Terminal, select New Terminal, and switch the Terminal shell to the Command Prompt.

Change directory to your source folder.

ConsoleCopy

cd \source

From your source directory, create a directory named Formatting-API.

ConsoleCopy

md Formatting-API

Run the following command to switch to the new directory.

ConsoleCopy

cd Formatting-API

Initialize the project by running the following command:

ConsoleCopy

pac pcf init --namespace SampleNamespace --name FormattingAPI --template field

Run npm install to load dependent libraries into your project.

ConsoleCopy

npm install

Open the project in Visual Studio Code by running the following command:

ConsoleCopy

code -a .

Implement your code component's logic

To implement your code component's logic, follow these steps:

Expand the FormattingAPI folder and open the ControlManifest.Input.xml file.

Replace the entire manifest with the following XML:

XMLCopy

" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;"><?xml version="1.0" encoding="utf-8" ?> <manifest> <control namespace="SampleNamespace" constructor="FormattingAPI" version="1.1.0" display-name-key="TS_FormattingAPI_Display_Key" description-key="TS_FormattingAPI_Desc_Key" control-type="standard"> <property name="controlValue" display-name-key="controlValue_Display_Key" description-key="controlValue_Desc_Key" of-type="SingleLine.Text" usage="bound" required="true" /> <resources> <code path="index.ts" order="1" /> <css path="css/TS_FormattingAPI.css" order="2" /> </resources> </control> </manifest>

You add the supporting files that are found in this manifest later.

Open the Index.ts file.

Above the constructor method, insert the following private variables:

TypeScriptCopy

void; // Reference to the div element that holds together all the HTML elements that you are creating as part of this control private divElement: HTMLDivElement; // Reference to HTMLTableElement rendered by control private _tableElement: HTMLTableElement; // Reference to the control container HTMLDivElement // This element contains all elements of your custom control example private _container: HTMLDivElement; // Reference to ComponentFramework Context object private _context: ComponentFramework.Context; // Flag if control view has been rendered private _controlViewRendered: Boolean; " style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;">// PCF framework delegate that will be assigned to this object that would be called whenever any update happens. private _notifyOutputChanged: () => void; // Reference to the div element that holds together all the HTML elements that you are creating as part of this control private divElement: HTMLDivElement; // Reference to HTMLTableElement rendered by control private _tableElement: HTMLTableElement; // Reference to the control container HTMLDivElement // This element contains all elements of your custom control example private _container: HTMLDivElement; // Reference to ComponentFramework Context object private _context: ComponentFramework.Context<IInputs>; // Flag if control view has been rendered private _controlViewRendered: Boolean;

Place the following logic inside of the init method:

TypeScriptCopy

this._notifyOutputChanged = notifyOutputChanged; this._controlViewRendered = false; this._context = context; this._container = document.createElement("div"); this._container.classList.add("TSFormatting_Container"); container.appendChild(this._container);

Add the following logic to the updateView method:

TypeScriptCopy

if (!this._controlViewRendered) { // Render and add HTMLTable to the custom control container element let tableElement: HTMLTableElement = this.createHTMLTableElement(); this._container.appendChild(tableElement); this._controlViewRendered = true; }

Add the following helper methods to generate your HTML table that will display formatted values after the destroy method:

TypeScriptCopy

/** * Helper method to create an HTML Table Row Element * @param key : string value to show in left column cell * @param value : string value to show in right column cell * @param isHeaderRow : true if method should generate a header row */ private createHTMLTableRowElement(key: string, value: string, isHeaderRow: boolean): HTMLTableRowElement { let keyCell: HTMLTableCellElement = this.createHTMLTableCellElement(key, "FormattingControlSampleHtmlTable_HtmlCell_Key", isHeaderRow); let valueCell: HTMLTableCellElement = this.createHTMLTableCellElement(value, "FormattingControlSampleHtmlTable_HtmlCell_Value", isHeaderRow); let rowElement: HTMLTableRowElement = document.createElement("tr"); rowElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlRow"); rowElement.appendChild(keyCell); rowElement.appendChild(valueCell); return rowElement; } /** * Helper method to create an HTML Table Cell Element * @param cellValue : string value to inject in the cell * @param className : class name for the cell * @param isHeaderRow : true if method should generate a header row cell */ private createHTMLTableCellElement(cellValue: string, className: string, isHeaderRow: boolean): HTMLTableCellElement { let cellElement: HTMLTableCellElement; if (isHeaderRow) { cellElement = document.createElement("th"); cellElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlHeaderCell " + className); let textElement: Text = document.createTextNode(cellValue); cellElement.appendChild(textElement); } else { cellElement = document.createElement("td"); cellElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlCell " + className); let textElement: Text = document.createTextNode(cellValue); cellElement.appendChild(textElement); } return cellElement; }

Add the following method, which contains sample usages of the formatting API, by saving them as table cells after the helper methods.

TypeScriptCopy

/** * Creates an HTML Table that showcases examples of basic methods available to the custom control * The left column of the table shows the method name or property that is being used * The right column of the table shows the result of that method name or property */ private createHTMLTableElement(): HTMLTableElement { // Create HTML Table Element let tableElement: HTMLTableElement = document.createElement("table"); tableElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlTable"); // Create header row for table let key: string = "Example Method"; let value: string = "Result"; tableElement.appendChild(this.createHTMLTableRowElement(key, value, true)); // Example use of formatCurrency() method // Change the default currency and the precision or pass in the precision and currency as additional parameters. key = "formatCurrency()"; value = this._context.formatting.formatCurrency(10250030); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example use of formatDecimal() method // Change the settings from user settings to see the output change its format accordingly key = "formatDecimal()"; value = this._context.formatting.formatDecimal(123456.2782); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example use of formatInteger() method // change the settings from user settings to see the output change its format accordingly. key = "formatInteger()"; value = this._context.formatting.formatInteger(12345); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example use of formatLanguage() method // Install additional languages and pass in the corresponding language code to see its string value key = "formatLanguage()"; value = this._context.formatting.formatLanguage(1033); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example of formatDateYearMonth() method // Pass a JavaScript Data object set to the current time into formatDateYearMonth method to format the data // and get the return in Year, Month format key = "formatDateYearMonth()"; value = this._context.formatting.formatDateYearMonth(new Date()); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example of getWeekOfYear() method // Pass a JavaScript Data object set to the current time into getWeekOfYear method to get the value for week of the year key = "getWeekOfYear()"; value = this._context.formatting.getWeekOfYear(new Date()).toString(); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); return tableElement; }

Select File and Save All your changes.

After you've made the updates, your completed class should look similar to the following example:

TypeScriptCopy

{ // PCF framework delegate that will be assigned to this object that would be called whenever any update happens. private _notifyOutputChanged: () => void; // Reference to the div element that holds together all the HTML elements that you are creating as part of this control private divElement: HTMLDivElement; // Reference to HTMLTableElement rendered by control private _tableElement: HTMLTableElement; // Reference to the control container HTMLDivElement // This element contains all elements of your custom control example private _container: HTMLDivElement; // Reference to ComponentFramework Context object private _context: ComponentFramework.Context; // Flag if control view has been rendered private _controlViewRendered: Boolean; /** * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. * Dataset values are not initialized here, use updateView. * @param context The entire property bag is available to control through the Context Object; It contains values as set up by the customizer and mapped to property names that are defined in the manifest and to utility functions. * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously. * @param state A piece of data that persists in one session for a single user. Can be set at any point in a control's life cycle by calling 'setControlState' in the Mode interface. * @param container If a control is marked control-type='starndard', it will receive an empty div element within which it can render its content. */ public init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement) { this._notifyOutputChanged = notifyOutputChanged; this._controlViewRendered = false; this._context = context; this._container = document.createElement("div"); this._container.classList.add("TSFormatting_Container"); container.appendChild(this._container); } /** * Called when any value in the property bag has changed. This includes field values, datasets, global values such as container height and width, offline status, control metadata values such as label, visible, and so on. * @param context The entire property bag that is available to control through the Context Object; It contains values as set up by the customizer that are mapped to names defined in the manifest and to utility functions */ public updateView(context: ComponentFramework.Context): void { if (!this._controlViewRendered) { // Render and add HTMLTable to the custom control container element let tableElement: HTMLTableElement = this.createHTMLTableElement(); this._container.appendChild(tableElement); this._controlViewRendered = true; } } /** * It is called by the framework prior to a control receiving new data. * @returns an object based on nomenclature that is defined in the manifest, expecting object[s] for property marked as "bound" or "output" */ public getOutputs(): IOutputs { return { }; } /** * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup, * such as canceling any pending remote calls, removing listeners, and so on. */ public destroy() { } /** * Helper method to create an HTML Table Row Element * @param key : string value to show in left column cell * @param value : string value to show in right column cell * @param isHeaderRow : true if method should generate a header row */ private createHTMLTableRowElement(key: string, value: string, isHeaderRow: boolean): HTMLTableRowElement { let keyCell: HTMLTableCellElement = this.createHTMLTableCellElement(key, "FormattingControlSampleHtmlTable_HtmlCell_Key", isHeaderRow); let valueCell: HTMLTableCellElement = this.createHTMLTableCellElement(value, "FormattingControlSampleHtmlTable_HtmlCell_Value", isHeaderRow); let rowElement: HTMLTableRowElement = document.createElement("tr"); rowElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlRow"); rowElement.appendChild(keyCell); rowElement.appendChild(valueCell); return rowElement; } /** * Helper method to create an HTML Table Cell Element * @param cellValue : string value to inject in the cell * @param className : class name for the cell * @param isHeaderRow : true if method should generate a header row cell */ private createHTMLTableCellElement(cellValue: string, className: string, isHeaderRow: boolean): HTMLTableCellElement { let cellElement: HTMLTableCellElement; if (isHeaderRow) { cellElement = document.createElement("th"); cellElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlHeaderCell " + className); let textElement: Text = document.createTextNode(cellValue); cellElement.appendChild(textElement); } else { cellElement = document.createElement("td"); cellElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlCell " + className); let textElement: Text = document.createTextNode(cellValue); cellElement.appendChild(textElement); } return cellElement; } /** * Creates an HTML Table that showcases examples of basic methods that are available to the custom control * The left column of the table shows the method name or property that is being used * The right column of the table shows the result of that method name or property */ private createHTMLTableElement(): HTMLTableElement { // Create HTML Table Element let tableElement: HTMLTableElement = document.createElement("table"); tableElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlTable"); // Create header row for table let key: string = "Example Method"; let value: string = "Result"; tableElement.appendChild(this.createHTMLTableRowElement(key, value, true)); // Example use of formatCurrency() method // Change the default currency and the precision or pass in the precision and currency as additional parameters. key = "formatCurrency()"; value = this._context.formatting.formatCurrency(10250030); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example use of formatDecimal() method // Change the settings from user settings to see the output change its format accordingly key = "formatDecimal()"; value = this._context.formatting.formatDecimal(123456.2782); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example use of formatInteger() method // change the settings from user settings to see the output change its format accordingly. key = "formatInteger()"; value = this._context.formatting.formatInteger(12345); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example use of formatLanguage() method // Install additional languages and pass in the corresponding language code to see its string value key = "formatLanguage()"; value = this._context.formatting.formatLanguage(1033); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example of formatDateYearMonth() method // Pass a JavaScript Data object set to the current time into formatDateYearMonth method to format the data // and get the return in Year, Month format key = "formatDateYearMonth()"; value = this._context.formatting.formatDateYearMonth(new Date()); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example of getWeekOfYear() method // Pass a JavaScript Data object set to the current time into getWeekOfYear method to get the value for week of the year key = "getWeekOfYear()"; value = this._context.formatting.getWeekOfYear(new Date()).toString(); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); return tableElement; } } " style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;">import {IInputs, IOutputs} from "./generated/ManifestTypes"; export class FormattingAPI implements ComponentFramework.StandardControl<IInputs, IOutputs> { // PCF framework delegate that will be assigned to this object that would be called whenever any update happens. private _notifyOutputChanged: () => void; // Reference to the div element that holds together all the HTML elements that you are creating as part of this control private divElement: HTMLDivElement; // Reference to HTMLTableElement rendered by control private _tableElement: HTMLTableElement; // Reference to the control container HTMLDivElement // This element contains all elements of your custom control example private _container: HTMLDivElement; // Reference to ComponentFramework Context object private _context: ComponentFramework.Context<IInputs>; // Flag if control view has been rendered private _controlViewRendered: Boolean; /** * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. * Dataset values are not initialized here, use updateView. * @param context The entire property bag is available to control through the Context Object; It contains values as set up by the customizer and mapped to property names that are defined in the manifest and to utility functions. * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously. * @param state A piece of data that persists in one session for a single user. Can be set at any point in a control's life cycle by calling 'setControlState' in the Mode interface. * @param container If a control is marked control-type='starndard', it will receive an empty div element within which it can render its content. */ public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement) { this._notifyOutputChanged = notifyOutputChanged; this._controlViewRendered = false; this._context = context; this._container = document.createElement("div"); this._container.classList.add("TSFormatting_Container"); container.appendChild(this._container); } /** * Called when any value in the property bag has changed. This includes field values, datasets, global values such as container height and width, offline status, control metadata values such as label, visible, and so on. * @param context The entire property bag that is available to control through the Context Object; It contains values as set up by the customizer that are mapped to names defined in the manifest and to utility functions */ public updateView(context: ComponentFramework.Context<IInputs>): void { if (!this._controlViewRendered) { // Render and add HTMLTable to the custom control container element let tableElement: HTMLTableElement = this.createHTMLTableElement(); this._container.appendChild(tableElement); this._controlViewRendered = true; } } /** * It is called by the framework prior to a control receiving new data. * @returns an object based on nomenclature that is defined in the manifest, expecting object[s] for property marked as "bound" or "output" */ public getOutputs(): IOutputs { return { }; } /** * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup, * such as canceling any pending remote calls, removing listeners, and so on. */ public destroy() { } /** * Helper method to create an HTML Table Row Element * @param key : string value to show in left column cell * @param value : string value to show in right column cell * @param isHeaderRow : true if method should generate a header row */ private createHTMLTableRowElement(key: string, value: string, isHeaderRow: boolean): HTMLTableRowElement { let keyCell: HTMLTableCellElement = this.createHTMLTableCellElement(key, "FormattingControlSampleHtmlTable_HtmlCell_Key", isHeaderRow); let valueCell: HTMLTableCellElement = this.createHTMLTableCellElement(value, "FormattingControlSampleHtmlTable_HtmlCell_Value", isHeaderRow); let rowElement: HTMLTableRowElement = document.createElement("tr"); rowElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlRow"); rowElement.appendChild(keyCell); rowElement.appendChild(valueCell); return rowElement; } /** * Helper method to create an HTML Table Cell Element * @param cellValue : string value to inject in the cell * @param className : class name for the cell * @param isHeaderRow : true if method should generate a header row cell */ private createHTMLTableCellElement(cellValue: string, className: string, isHeaderRow: boolean): HTMLTableCellElement { let cellElement: HTMLTableCellElement; if (isHeaderRow) { cellElement = document.createElement("th"); cellElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlHeaderCell " + className); let textElement: Text = document.createTextNode(cellValue); cellElement.appendChild(textElement); } else { cellElement = document.createElement("td"); cellElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlCell " + className); let textElement: Text = document.createTextNode(cellValue); cellElement.appendChild(textElement); } return cellElement; } /** * Creates an HTML Table that showcases examples of basic methods that are available to the custom control * The left column of the table shows the method name or property that is being used * The right column of the table shows the result of that method name or property */ private createHTMLTableElement(): HTMLTableElement { // Create HTML Table Element let tableElement: HTMLTableElement = document.createElement("table"); tableElement.setAttribute("class", "FormattingControlSampleHtmlTable_HtmlTable"); // Create header row for table let key: string = "Example Method"; let value: string = "Result"; tableElement.appendChild(this.createHTMLTableRowElement(key, value, true)); // Example use of formatCurrency() method // Change the default currency and the precision or pass in the precision and currency as additional parameters. key = "formatCurrency()"; value = this._context.formatting.formatCurrency(10250030); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example use of formatDecimal() method // Change the settings from user settings to see the output change its format accordingly key = "formatDecimal()"; value = this._context.formatting.formatDecimal(123456.2782); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example use of formatInteger() method // change the settings from user settings to see the output change its format accordingly. key = "formatInteger()"; value = this._context.formatting.formatInteger(12345); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example use of formatLanguage() method // Install additional languages and pass in the corresponding language code to see its string value key = "formatLanguage()"; value = this._context.formatting.formatLanguage(1033); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example of formatDateYearMonth() method // Pass a JavaScript Data object set to the current time into formatDateYearMonth method to format the data // and get the return in Year, Month format key = "formatDateYearMonth()"; value = this._context.formatting.formatDateYearMonth(new Date()); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); // Example of getWeekOfYear() method // Pass a JavaScript Data object set to the current time into getWeekOfYear method to get the value for week of the year key = "getWeekOfYear()"; value = this._context.formatting.getWeekOfYear(new Date()).toString(); tableElement.appendChild(this.createHTMLTableRowElement(key, value, false)); return tableElement; } }

Add styling to your code component

To add styling to your code component, follow these steps:

Create a new css subfolder under the FormattingAPI folder.

Create a new TS_FormattingAPI.css file inside the CSS subfolder.

Add the following style content to the TS_FormattingAPI.css file:

cssCopy

.SampleNamespace\.FormattingAPI { font-family: 'SegoeUI-Semibold', 'Segoe UI Semibold', 'Segoe UI Regular', 'Segoe UI'; } .SampleNamespace\.FormattingAPI .TSFormatting_Container { overflow-x: auto; } .SampleNamespace\.FormattingAPI .FormattingControlSampleHtmlTable_HtmlRow { background-color: #FFFFFF; } .SampleNamespace\.FormattingAPI .FormattingControlSampleHtmlTable_HtmlHeaderCell { text-align: center; } .SampleNamespace\.FormattingAPI .FormattingControlSampleHtmlTable_HtmlCell, .SampleNamespace\.FormattingAPI .FormattingControlSampleHtmlTable_HtmlHeaderCell { border: 1px solid black; padding-left: 3px; padding-right: 3px; } .SampleNamespace\.FormattingAPI .FormattingControlSampleHtmlTable_HtmlInputTextCell { border: 1px solid black; padding: 0px; } .SampleNamespace\.FormattingAPI .FormattingControlSampleHtmlTable_HtmlHeaderCell { font-weight: bold; font-size: 16px; } .SampleNamespace\.FormattingAPI .FormattingControlSampleHtmlTable_HtmlCell_Key { color: #1160B7; } .SampleNamespace\.FormattingAPI .FormattingControlSampleHtmlTable_HtmlCell_Value { color: #1160B7; text-align: center; }

Select File and Save All your changes.

Build and run your component

To build and run your component, follow these steps:

Build your solution by running the following command.

ConsoleCopy

npm run build

Upon a successful build, you can test your new formatting API component by running npm start.

ConsoleCopy

npm start

Close the test harness browser window.

Go back to the terminal and stop the watcher by holding [CONTROL] + C.

Type Y and then [ENTER].

For more information, see Implementing formatting API component.

Use the Microsoft Dataverse web API in a Power Apps component

Completed100 XP

20 minutes

A common scenario during component development is the requirement to interface with data in your solution's underlying Microsoft Dataverse. The Power Apps component framework exposes a Web API feature to achieve this requirement. This example illustrates how to perform various CRUD operations by using this feature.

 Note

The Web API component is only supported in model-driven apps.

Initialize your component's project

To initialize your component's project, follow these steps:

Start Visual Studio Code.

Select Terminal and select New Terminal and switch the Terminal shell to the Command Prompt.

Change directory to your source folder.

ConsoleCopy

cd \source

From your source directory, create a directory named TS-Web-API.

ConsoleCopy

md TS-Web-API

Run the following command to switch to the new directory.

ConsoleCopy

cd TS-Web-API

Initialize the project by running the following command:

ConsoleCopy

pac pcf init --namespace SampleNamespace --name TSWebAPI --template field

Run npm install to load dependent libraries into your project.

ConsoleCopy

npm install

Open the project in Visual Studio Code by running the following command:

ConsoleCopy

code -a .

Implement your code component's logic

To implement your code component's logic, follow these steps:

Expand the TSWebAPI folder and open the ControlManifest.Input.xml file.

Replace the entire manifest with the following XML:

XMLCopy

" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;"><?xml version="1.0" encoding="utf-8"?> <manifest> <control namespace="SampleNamespace" constructor="TSWebAPI" version="1.0.0" display-name-key="TS_WebAPI_Display_Key" description-key="TS_WebAPI_Desc_Display_Key" control-type="standard"> <property name="stringProperty" display-name-key="stringProperty_Display_Key" description-key="stringProperty_Desc_Key" of-type="SingleLine.Text" usage="bound" required="true" /> <resources> <code path="index.ts" order="1" /> <css path="css/TS_WebAPI.css" order="2" /> </resources> <feature-usage> <uses-feature name="WebAPI" required="true" /> </feature-usage> </control> </manifest>

You add the supporting files that are found in this manifest later.

Open the Index.ts file.

Above the constructor method, insert the following private variables to support rendering of your component:

TypeScriptCopy

; // Flag if control view has been rendered private _controlViewRendered: Boolean; // References to button elements that are rendered by example custom control private _createEntity1Button: HTMLButtonElement; private _createEntity2Button: HTMLButtonElement; private _createEntity3Button: HTMLButtonElement; private _deleteRecordButton: HTMLButtonElement; private _fetchXmlRefreshButton: HTMLButtonElement; private _oDataRefreshButton: HTMLButtonElement; // References to div elements that are rendered by the example custom control private _odataStatusContainerDiv: HTMLDivElement; private _resultContainerDiv: HTMLDivElement; " style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;">// Reference to the control container HTMLDivElement // This element contains all elements of our custom control example private _container: HTMLDivElement; // Reference to ComponentFramework Context object private _context: ComponentFramework.Context<IInputs>; // Flag if control view has been rendered private _controlViewRendered: Boolean; // References to button elements that are rendered by example custom control private _createEntity1Button: HTMLButtonElement; private _createEntity2Button: HTMLButtonElement; private _createEntity3Button: HTMLButtonElement; private _deleteRecordButton: HTMLButtonElement; private _fetchXmlRefreshButton: HTMLButtonElement; private _oDataRefreshButton: HTMLButtonElement; // References to div elements that are rendered by the example custom control private _odataStatusContainerDiv: HTMLDivElement; private _resultContainerDiv: HTMLDivElement;

Add the following private static variables above the constructor to indicate which entity/fields you're interfacing with in this example. If you want to try against different tables or columns, you can do so by changing their respective values.

TypeScriptCopy

// Name of entity to use for example Web API calls that are performed by this control private static _entityName: string = "account"; // Required field on _entityName of type 'single line of text' // Example Web API calls that are performed by the example custom control will set this field for new record creation examples private static _requiredAttributeName: string = "name"; // Value that the _requiredAttributeName field will be set to for new created records private static _requiredAttributeValue: string = "Web API Custom Control (Sample)"; // Name of currency field on _entityName to populate during record create // Example Web API calls that are performed by the example custom control will set and read this field private static _currencyAttributeName: string = "revenue"; // Friendly name of currency field (only used for control UI - no functional impact) private static _currencyAttributeNameFriendlyName: string = "annual revenue";

Place the following logic inside of the init method:

TypeScriptCopy

this._context = context; this._controlViewRendered = false; this._container = document.createElement("div"); this._container.classList.add("TSWebAPI_Container"); container.appendChild(this._container);

Add the following logic to the updateView method:

TypeScriptCopy

if (!this._controlViewRendered) { this._controlViewRendered = true; // Render Web API Examples this.renderCreateExample(); this.renderDeleteExample(); this.renderFetchXmlRetrieveMultipleExample(); this.renderODataRetrieveMultipleExample(); // Render result div to display output of Web API calls this.renderResultsDiv();}

Add the following helper methods to render the HTML elements in your component after the destroy method:

TypeScriptCopy

void): HTMLButtonElement { let button: HTMLButtonElement = document.createElement("button"); button.innerHTML = buttonLabel; if (buttonValue) { button.setAttribute("buttonvalue", buttonValue); } button.id = buttonId; button.classList.add("SampleControl_WebAPI_ButtonClass"); button.addEventListener("click", onClickHandler); return button; } /** * Helper method to create HTML Div Element * @param elementClassName : Class name of div element * @param isHeader : True if 'header' div - adds extra class and post-fix to ID for header elements * @param innerText : innerText of Div Element */ private createHTMLDivElement(elementClassName: string, isHeader: Boolean, innerText?: string): HTMLDivElement { let div: HTMLDivElement = document.createElement("div"); if (isHeader) { div.classList.add("SampleControl_WebAPI_Header"); elementClassName += "_header"; } if (innerText) { div.innerText = innerText.toUpperCase(); } div.classList.add(elementClassName); return div; } /** * Renders a 'result container' div element to inject the status of the example Web API calls */ private renderResultsDiv() { // Render header label for result container let resultDivHeader: HTMLDivElement = this.createHTMLDivElement("result_container", true, "Result of last action"); this._container.appendChild(resultDivHeader); // Div elements to populate with the result text this._resultContainerDiv = this.createHTMLDivElement("result_container", false, undefined); this._container.appendChild(this._resultContainerDiv); // Init the result container with a notification that the control was loaded this.updateResultContainerText("Web API sample custom control loaded"); } /** * Helper method to inject HTML into result container div * @param statusHTML : HTML to inject into result container */ private updateResultContainerText(statusHTML: string): void { if (this._resultContainerDiv) { this._resultContainerDiv.innerHTML = statusHTML; } } /** * Helper method to inject error string into result container div after failed Web API call * @param errorResponse : error object from rejected promise */ private updateResultContainerTextWithErrorResponse(errorResponse: any): void { if (this._resultContainerDiv) { // Retrieve the error message from the errorResponse and inject into the result div let errorHTML: string = "Error with Web API call:"; errorHTML += "
" errorHTML += errorResponse.message; this._resultContainerDiv.innerHTML = errorHTML; } } /** * Helper method to generate Label for Create Buttons * @param entityNumber : value to set _currencyAttributeNameFriendlyName field to for this button */ private getCreateRecordButtonLabel(entityNumber: string): string { return "Create record with " + TSWebAPI._currencyAttributeNameFriendlyName + " of " + entityNumber; } /** * Helper method to generate ID for Create button * @param entityNumber : value to set _currencyAttributeNameFriendlyName field to for this button */ private getCreateButtonId(entityNumber: string): string { return "create_button_" + entityNumber; } " style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;">/** * Helper method to create HTML button that is used for CreateRecord Web API Example * @param buttonLabel : Label for button * @param buttonId : ID for button * @param buttonValue : Value of button (attribute of button) * @param onClickHandler : onClick event handler to invoke for the button */ private createHTMLButtonElement(buttonLabel: string, buttonId: string, buttonValue: string | null, onClickHandler: (event?: any) => void): HTMLButtonElement { let button: HTMLButtonElement = document.createElement("button"); button.innerHTML = buttonLabel; if (buttonValue) { button.setAttribute("buttonvalue", buttonValue); } button.id = buttonId; button.classList.add("SampleControl_WebAPI_ButtonClass"); button.addEventListener("click", onClickHandler); return button; } /** * Helper method to create HTML Div Element * @param elementClassName : Class name of div element * @param isHeader : True if 'header' div - adds extra class and post-fix to ID for header elements * @param innerText : innerText of Div Element */ private createHTMLDivElement(elementClassName: string, isHeader: Boolean, innerText?: string): HTMLDivElement { let div: HTMLDivElement = document.createElement("div"); if (isHeader) { div.classList.add("SampleControl_WebAPI_Header"); elementClassName += "_header"; } if (innerText) { div.innerText = innerText.toUpperCase(); } div.classList.add(elementClassName); return div; } /** * Renders a 'result container' div element to inject the status of the example Web API calls */ private renderResultsDiv() { // Render header label for result container let resultDivHeader: HTMLDivElement = this.createHTMLDivElement("result_container", true, "Result of last action"); this._container.appendChild(resultDivHeader); // Div elements to populate with the result text this._resultContainerDiv = this.createHTMLDivElement("result_container", false, undefined); this._container.appendChild(this._resultContainerDiv); // Init the result container with a notification that the control was loaded this.updateResultContainerText("Web API sample custom control loaded"); } /** * Helper method to inject HTML into result container div * @param statusHTML : HTML to inject into result container */ private updateResultContainerText(statusHTML: string): void { if (this._resultContainerDiv) { this._resultContainerDiv.innerHTML = statusHTML; } } /** * Helper method to inject error string into result container div after failed Web API call * @param errorResponse : error object from rejected promise */ private updateResultContainerTextWithErrorResponse(errorResponse: any): void { if (this._resultContainerDiv) { // Retrieve the error message from the errorResponse and inject into the result div let errorHTML: string = "Error with Web API call:"; errorHTML += "<br />" errorHTML += errorResponse.message; this._resultContainerDiv.innerHTML = errorHTML; } } /** * Helper method to generate Label for Create Buttons * @param entityNumber : value to set _currencyAttributeNameFriendlyName field to for this button */ private getCreateRecordButtonLabel(entityNumber: string): string { return "Create record with " + TSWebAPI._currencyAttributeNameFriendlyName + " of " + entityNumber; } /** * Helper method to generate ID for Create button * @param entityNumber : value to set _currencyAttributeNameFriendlyName field to for this button */ private getCreateButtonId(entityNumber: string): string { return "create_button_" + entityNumber; }

Add the following onClick event handlers to trigger the various CRUD operations after the helper methods:

TypeScriptCopy

"; resultHtml += "
"; resultHtml += "id: " + id; resultHtml += "
"; resultHtml += "
"; resultHtml += TSWebAPI._requiredAttributeName + ": " + recordName; resultHtml += "
"; resultHtml += "
"; resultHtml += TSWebAPI._currencyAttributeName + ": " + currencyAttributeValue; thisRef.updateResultContainerText(resultHtml); }, function (errorResponse: any) { // Error handling code here - record failed to be created thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Event Handler for onClick of delete record button * @param event : click event */ private deleteButtonOnClickHandler(): void { // Invoke a lookup dialog to allow the user to select an existing record of type _entityName to delete var lookUpOptions: any = { entityTypes: [TSWebAPI._entityName] }; // store reference to 'this' so it can be used in the callback method var thisRef = this; var lookUpPromise: any = this._context.utils.lookupObjects(lookUpOptions); lookUpPromise.then( // Callback method - invoked after user has selected an item from the lookup dialog // Data parameter is the item selected in the lookup dialog (data: ComponentFramework.EntityReference[]) => { if (data && data[0]) { // Get the ID and entityType of the record that was selected by the lookup let id: string = data[0].id.guid; let entityType: string = data[0].etn!; // Invoke the deleteRecord method of the WebAPI to delete the selected record this._context.webAPI.deleteRecord(entityType, id).then( function (response: ComponentFramework.LookupValue) { // Record was deleted successfully let responseId: string = response.id; let responseEntityType: string = response.name!; // Generate HTML to inject into the result div to showcase the deleted record thisRef.updateResultContainerText("Deleted " + responseEntityType + " record with ID: " + responseId); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } }, (error: any) => { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(error); } ); } /** * Event Handler for onClick of calculate average value button * @param event : click event */ private calculateAverageButtonOnClickHandler(): void { // Build FetchXML to retrieve the average value of _currencyAttributeName field for all _entityName records // Add a filter to only aggregate on records that have _currencyAttributeName not set to null let fetchXML: string = ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API RetrieveMultipleRecords method to calculate the aggregate value this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, "?fetchXml=" + fetchXML).then( function (response: ComponentFramework.WebApi.RetrieveMultipleResponse) { // Retrieve multiple completed successfully -- retrieve the averageValue let averageVal: Number = response.entities[0].average_val; // Generate HTML to inject into the result div to showcase the result of the RetrieveMultiple Web API call let resultHTML: string = "Average value of " + TSWebAPI._currencyAttributeNameFriendlyName + " attribute for all " + TSWebAPI._entityName + " records: " + averageVal; thisRef.updateResultContainerText(resultHTML); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Event Handler for onClick of calculate record count button * @param event : click event */ private refreshRecordCountButtonOnClickHandler(): void { // Generate OData query string to retrieve the _currencyAttributeName field for all _entityName records // Add a filter to only retrieve records with _requiredAttributeName field which contains _requiredAttributeValue let queryString: string = "?$select=" + TSWebAPI._currencyAttributeName + "&$filter=contains(" + TSWebAPI._requiredAttributeName + ",'" + TSWebAPI._requiredAttributeValue + "')"; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API Retrieve Multiple call this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, queryString).then( function (response: any) { // Retrieve Multiple Web API call completed successfully let count1: number = 0; let count2: number = 0; let count3: number = 0; // Loop through each returned record for (let entity of response.entities) { // Retrieve the value of _currencyAttributeName field let value: Number = entity[TSWebAPI._currencyAttributeName]; // Check the value of _currencyAttributeName field and increment the correct counter if (value == 100) { count1++; } else if (value == 200) { count2++; } else if (value == 300) { count3++; } } // Generate HTML to inject into the fetch xml status div to showcase the results of the OData retrieve example let innerHtml: string = "Use above buttons to create or delete a record to see count update"; innerHtml += "
"; innerHtml += "
"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 100: " + count1; innerHtml += "
"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 200: " + count2; innerHtml += "
"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 300: " + count3; // Inject the HTML into the fetch xml status div if (thisRef._odataStatusContainerDiv) { thisRef._odataStatusContainerDiv.innerHTML = innerHtml; } // Inject a success message into the result div thisRef.updateResultContainerText("Record count refreshed"); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } " style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;">/** * Event Handler for onClick of create record button * @param event : click event */ private createButtonOnClickHandler(event: Event): void { // Retrieve the value to set the currency field to from the button's attribute let currencyAttributeValue: Number = parseInt( (event.srcElement! as Element)!.attributes.getNamedItem("buttonvalue")!.value ); // Generate unique record name by appending timestamp to _requiredAttributeValue let recordName: string = TSWebAPI._requiredAttributeValue + "_" + Date.now(); // Set the values for the attributes we want to set on the new record // If you want to set additional attributes on the new record, add to data dictionary as key/value pair var data: any = {}; data[TSWebAPI._requiredAttributeName] = recordName; data[TSWebAPI._currencyAttributeName] = currencyAttributeValue; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API to create the new record this._context.webAPI.createRecord(TSWebAPI._entityName, data).then( function (response: ComponentFramework.LookupValue) { // Callback method for successful creation of new record // Get the ID of the new record created let id: string = response.id; // Generate HTML to inject into the result div to showcase the fields and values of the new record that is created let resultHtml: string = "Created new " + TSWebAPI._entityName + " record with below values:" resultHtml += "<br />"; resultHtml += "<br />"; resultHtml += "id: " + id; resultHtml += "<br />"; resultHtml += "<br />"; resultHtml += TSWebAPI._requiredAttributeName + ": " + recordName; resultHtml += "<br />"; resultHtml += "<br />"; resultHtml += TSWebAPI._currencyAttributeName + ": " + currencyAttributeValue; thisRef.updateResultContainerText(resultHtml); }, function (errorResponse: any) { // Error handling code here - record failed to be created thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Event Handler for onClick of delete record button * @param event : click event */ private deleteButtonOnClickHandler(): void { // Invoke a lookup dialog to allow the user to select an existing record of type _entityName to delete var lookUpOptions: any = { entityTypes: [TSWebAPI._entityName] }; // store reference to 'this' so it can be used in the callback method var thisRef = this; var lookUpPromise: any = this._context.utils.lookupObjects(lookUpOptions); lookUpPromise.then( // Callback method - invoked after user has selected an item from the lookup dialog // Data parameter is the item selected in the lookup dialog (data: ComponentFramework.EntityReference[]) => { if (data && data[0]) { // Get the ID and entityType of the record that was selected by the lookup let id: string = data[0].id.guid; let entityType: string = data[0].etn!; // Invoke the deleteRecord method of the WebAPI to delete the selected record this._context.webAPI.deleteRecord(entityType, id).then( function (response: ComponentFramework.LookupValue) { // Record was deleted successfully let responseId: string = response.id; let responseEntityType: string = response.name!; // Generate HTML to inject into the result div to showcase the deleted record thisRef.updateResultContainerText("Deleted " + responseEntityType + " record with ID: " + responseId); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } }, (error: any) => { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(error); } ); } /** * Event Handler for onClick of calculate average value button * @param event : click event */ private calculateAverageButtonOnClickHandler(): void { // Build FetchXML to retrieve the average value of _currencyAttributeName field for all _entityName records // Add a filter to only aggregate on records that have _currencyAttributeName not set to null let fetchXML: string = "<fetch distinct='false' mapping='logical' aggregate='true'>"; fetchXML += "<entity name='" + TSWebAPI._entityName + "'>"; fetchXML += "<attribute name='" + TSWebAPI._currencyAttributeName + "' aggregate='avg' alias='average_val' />"; fetchXML += "<filter>"; fetchXML += "<condition attribute='" + TSWebAPI._currencyAttributeName + "' operator='not-null' />"; fetchXML += "</filter>"; fetchXML += "</entity>"; fetchXML += "</fetch>"; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API RetrieveMultipleRecords method to calculate the aggregate value this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, "?fetchXml=" + fetchXML).then( function (response: ComponentFramework.WebApi.RetrieveMultipleResponse) { // Retrieve multiple completed successfully -- retrieve the averageValue let averageVal: Number = response.entities[0].average_val; // Generate HTML to inject into the result div to showcase the result of the RetrieveMultiple Web API call let resultHTML: string = "Average value of " + TSWebAPI._currencyAttributeNameFriendlyName + " attribute for all " + TSWebAPI._entityName + " records: " + averageVal; thisRef.updateResultContainerText(resultHTML); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Event Handler for onClick of calculate record count button * @param event : click event */ private refreshRecordCountButtonOnClickHandler(): void { // Generate OData query string to retrieve the _currencyAttributeName field for all _entityName records // Add a filter to only retrieve records with _requiredAttributeName field which contains _requiredAttributeValue let queryString: string = "?$select=" + TSWebAPI._currencyAttributeName + "&$filter=contains(" + TSWebAPI._requiredAttributeName + ",'" + TSWebAPI._requiredAttributeValue + "')"; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API Retrieve Multiple call this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, queryString).then( function (response: any) { // Retrieve Multiple Web API call completed successfully let count1: number = 0; let count2: number = 0; let count3: number = 0; // Loop through each returned record for (let entity of response.entities) { // Retrieve the value of _currencyAttributeName field let value: Number = entity[TSWebAPI._currencyAttributeName]; // Check the value of _currencyAttributeName field and increment the correct counter if (value == 100) { count1++; } else if (value == 200) { count2++; } else if (value == 300) { count3++; } } // Generate HTML to inject into the fetch xml status div to showcase the results of the OData retrieve example let innerHtml: string = "Use above buttons to create or delete a record to see count update"; innerHtml += "<br />"; innerHtml += "<br />"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 100: " + count1; innerHtml += "<br />"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 200: " + count2; innerHtml += "<br />"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 300: " + count3; // Inject the HTML into the fetch xml status div if (thisRef._odataStatusContainerDiv) { thisRef._odataStatusContainerDiv.innerHTML = innerHtml; } // Inject a success message into the result div thisRef.updateResultContainerText("Record count refreshed"); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); }

Add the following helper methods to render the results of your CRUD operations within your component after the event handler methods:

TypeScriptCopy

/** * Renders example use of CreateRecord Web API */ private renderCreateExample() { // Create header label for Web API sample let headerDiv: HTMLDivElement = this.createHTMLDivElement("create_container", true, "Click to create " + TSWebAPI._entityName + " record"); this._container.appendChild(headerDiv); // Create button 1 to create a record with the revenue field set to 100 let value1: string = "100"; this._createEntity1Button = this.createHTMLButtonElement( this.getCreateRecordButtonLabel(value1), this.getCreateButtonId(value1), value1, this.createButtonOnClickHandler.bind(this)); // Create button 2 to create a record with the revenue field set to 200 let value2: string = "200"; this._createEntity2Button = this.createHTMLButtonElement( this.getCreateRecordButtonLabel(value2), this.getCreateButtonId(value2), value2, this.createButtonOnClickHandler.bind(this)); // Create button 3 to create a record with the revenue field set to 300 let value3: string = "300"; this._createEntity3Button = this.createHTMLButtonElement( this.getCreateRecordButtonLabel(value3), this.getCreateButtonId(value3), value3, this.createButtonOnClickHandler.bind(this)); // Append all button HTML elements to custom control container div this._container.appendChild(this._createEntity1Button); this._container.appendChild(this._createEntity2Button); this._container.appendChild(this._createEntity3Button); } /** * Renders example use of DeleteRecord Web API */ private renderDeleteExample(): void { // Create header label for Web API sample let headerDiv: HTMLDivElement = this.createHTMLDivElement("delete_container", true, "Click to delete " + TSWebAPI._entityName + " record"); // Render button to invoke DeleteRecord Web API call this._deleteRecordButton = this.createHTMLButtonElement( "Select record to delete", "delete_button", null, this.deleteButtonOnClickHandler.bind(this)); // Append elements to custom control container div this._container.appendChild(headerDiv); this._container.appendChild(this._deleteRecordButton); } /** * Renders example use of RetrieveMultiple Web API with OData */ private renderODataRetrieveMultipleExample(): void { let containerClassName: string = "odata_status_container"; // Create header label for Web API sample let statusDivHeader: HTMLDivElement = this.createHTMLDivElement(containerClassName, true, "Click to refresh record count"); this._odataStatusContainerDiv = this.createHTMLDivElement(containerClassName, false, undefined); // Create button to invoke OData RetrieveMultiple Example this._fetchXmlRefreshButton = this.createHTMLButtonElement( "Refresh record count", "odata_refresh", null, this.refreshRecordCountButtonOnClickHandler.bind(this)); // Append HTML elements to custom control container div this._container.appendChild(statusDivHeader); this._container.appendChild(this._odataStatusContainerDiv); this._container.appendChild(this._fetchXmlRefreshButton); } /** * Renders example use of RetrieveMultiple Web API with Fetch XML */ private renderFetchXmlRetrieveMultipleExample(): void { let containerName: string = "fetchxml_status_container"; // Create header label for Web API sample let statusDivHeader: HTMLDivElement = this.createHTMLDivElement(containerName, true, "Click to calculate average value of " + TSWebAPI._currencyAttributeNameFriendlyName); let statusDiv: HTMLDivElement = this.createHTMLDivElement(containerName, false, undefined); // Create button to invoke Fetch XML RetrieveMultiple Web API example this._oDataRefreshButton = this.createHTMLButtonElement( "Calculate average value of " + TSWebAPI._currencyAttributeNameFriendlyName, "odata_refresh", null, this.calculateAverageButtonOnClickHandler.bind(this)); // Append HTML Elements to custom control container div this._container.appendChild(statusDivHeader); this._container.appendChild(statusDiv); this._container.appendChild(this._oDataRefreshButton); }

After you've made the updates, your completed class should look similar to the following example:

TypeScriptCopy

{ // Reference to the control container HTMLDivElement // This element contains all elements of our custom control example private _container: HTMLDivElement; // Reference to ComponentFramework Context object private _context: ComponentFramework.Context; // Flag if control view has been rendered private _controlViewRendered: Boolean; // References to button elements that are rendered by example custom control private _createEntity1Button: HTMLButtonElement; private _createEntity2Button: HTMLButtonElement; private _createEntity3Button: HTMLButtonElement; private _deleteRecordButton: HTMLButtonElement; private _fetchXmlRefreshButton: HTMLButtonElement; private _oDataRefreshButton: HTMLButtonElement; // References to div elements that are rendered by the example custom control private _odataStatusContainerDiv: HTMLDivElement; private _resultContainerDiv: HTMLDivElement; // Name of entity to use for example Web API calls that are performed by this control private static _entityName: string = "account"; // Required field on _entityName of type 'single line of text' // Example Web API calls that are performed by the example custom control will set this field for new record creation examples private static _requiredAttributeName: string = "name"; // Value that the _requiredAttributeName field will be set to for new created records private static _requiredAttributeValue: string = "Web API Custom Control (Sample)"; // Name of currency field on _entityName to populate during record create // Example Web API calls that are performed by the example custom control will set and read this field private static _currencyAttributeName: string = "revenue"; // Friendly name of currency field (only used for control UI - no functional impact) private static _currencyAttributeNameFriendlyName: string = "annual revenue"; /** * Empty constructor. */ constructor() { } /** * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. * Data-set values are not initialized here, use updateView. * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions. * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously. * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface. * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content. */ public init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement): void { this._context = context; this._controlViewRendered = false; this._container = document.createElement("div"); this._container.classList.add("TSWebAPI_Container"); container.appendChild(this._container); } /** * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc. * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions */ public updateView(context: ComponentFramework.Context): void { if (!this._controlViewRendered) { this._controlViewRendered = true; // Render Web API Examples this.renderCreateExample(); this.renderDeleteExample(); this.renderFetchXmlRetrieveMultipleExample(); this.renderODataRetrieveMultipleExample(); // Render result div to display output of Web API calls this.renderResultsDiv();} } /** * It is called by the framework prior to a control receiving new data. * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output” */ public getOutputs(): IOutputs { return {}; } /** * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup. * i.e. cancelling any pending remote calls, removing listeners, etc. */ public destroy(): void { // Add code to cleanup control if necessary } /** * Helper method to create HTML button that is used for CreateRecord Web API Example * @param buttonLabel : Label for button * @param buttonId : ID for button * @param buttonValue : Value of button (attribute of button) * @param onClickHandler : onClick event handler to invoke for the button */ private createHTMLButtonElement(buttonLabel: string, buttonId: string, buttonValue: string | null, onClickHandler: (event?: any) => void): HTMLButtonElement { let button: HTMLButtonElement = document.createElement("button"); button.innerHTML = buttonLabel; if (buttonValue) { button.setAttribute("buttonvalue", buttonValue); } button.id = buttonId; button.classList.add("SampleControl_WebAPI_ButtonClass"); button.addEventListener("click", onClickHandler); return button; } /** * Helper method to create HTML Div Element * @param elementClassName : Class name of div element * @param isHeader : True if 'header' div - adds extra class and post-fix to ID for header elements * @param innerText : innerText of Div Element */ private createHTMLDivElement(elementClassName: string, isHeader: Boolean, innerText?: string): HTMLDivElement { let div: HTMLDivElement = document.createElement("div"); if (isHeader) { div.classList.add("SampleControl_WebAPI_Header"); elementClassName += "_header"; } if (innerText) { div.innerText = innerText.toUpperCase(); } div.classList.add(elementClassName); return div; } /** * Renders a 'result container' div element to inject the status of the example Web API calls */ private renderResultsDiv() { // Render header label for result container let resultDivHeader: HTMLDivElement = this.createHTMLDivElement("result_container", true, "Result of last action"); this._container.appendChild(resultDivHeader); // Div elements to populate with the result text this._resultContainerDiv = this.createHTMLDivElement("result_container", false, undefined); this._container.appendChild(this._resultContainerDiv); // Init the result container with a notification that the control was loaded this.updateResultContainerText("Web API sample custom control loaded"); } /** * Helper method to inject HTML into result container div * @param statusHTML : HTML to inject into result container */ private updateResultContainerText(statusHTML: string): void { if (this._resultContainerDiv) { this._resultContainerDiv.innerHTML = statusHTML; } } /** * Helper method to inject error string into result container div after failed Web API call * @param errorResponse : error object from rejected promise */ private updateResultContainerTextWithErrorResponse(errorResponse: any): void { if (this._resultContainerDiv) { // Retrieve the error message from the errorResponse and inject into the result div let errorHTML: string = "Error with Web API call:"; errorHTML += "
" errorHTML += errorResponse.message; this._resultContainerDiv.innerHTML = errorHTML; } } /** * Helper method to generate Label for Create Buttons * @param entityNumber : value to set _currencyAttributeNameFriendlyName field to for this button */ private getCreateRecordButtonLabel(entityNumber: string): string { return "Create record with " + TSWebAPI._currencyAttributeNameFriendlyName + " of " + entityNumber; } /** * Helper method to generate ID for Create button * @param entityNumber : value to set _currencyAttributeNameFriendlyName field to for this button */ private getCreateButtonId(entityNumber: string): string { return "create_button_" + entityNumber; } /** * Event Handler for onClick of create record button * @param event : click event */ private createButtonOnClickHandler(event: Event): void { // Retrieve the value to set the currency field to from the button's attribute let currencyAttributeValue: Number = parseInt( (event.srcElement! as Element)!.attributes.getNamedItem("buttonvalue")!.value ); // Generate unique record name by appending timestamp to _requiredAttributeValue let recordName: string = TSWebAPI._requiredAttributeValue + "_" + Date.now(); // Set the values for the attributes we want to set on the new record // If you want to set additional attributes on the new record, add to data dictionary as key/value pair var data: any = {}; data[TSWebAPI._requiredAttributeName] = recordName; data[TSWebAPI._currencyAttributeName] = currencyAttributeValue; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API to create the new record this._context.webAPI.createRecord(TSWebAPI._entityName, data).then( function (response: ComponentFramework.LookupValue) { // Callback method for successful creation of new record // Get the ID of the new record created let id: string = response.id; // Generate HTML to inject into the result div to showcase the fields and values of the new record that is created let resultHtml: string = "Created new " + TSWebAPI._entityName + " record with below values:" resultHtml += "
"; resultHtml += "
"; resultHtml += "id: " + id; resultHtml += "
"; resultHtml += "
"; resultHtml += TSWebAPI._requiredAttributeName + ": " + recordName; resultHtml += "
"; resultHtml += "
"; resultHtml += TSWebAPI._currencyAttributeName + ": " + currencyAttributeValue; thisRef.updateResultContainerText(resultHtml); }, function (errorResponse: any) { // Error handling code here - record failed to be created thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Event Handler for onClick of delete record button * @param event : click event */ private deleteButtonOnClickHandler(): void { // Invoke a lookup dialog to allow the user to select an existing record of type _entityName to delete var lookUpOptions: any = { entityTypes: [TSWebAPI._entityName] }; // store reference to 'this' so it can be used in the callback method var thisRef = this; var lookUpPromise: any = this._context.utils.lookupObjects(lookUpOptions); lookUpPromise.then( // Callback method - invoked after user has selected an item from the lookup dialog // Data parameter is the item selected in the lookup dialog (data: ComponentFramework.EntityReference[]) => { if (data && data[0]) { // Get the ID and entityType of the record that was selected by the lookup let id: string = data[0].id.guid; let entityType: string = data[0].etn!; // Invoke the deleteRecord method of the WebAPI to delete the selected record this._context.webAPI.deleteRecord(entityType, id).then( function (response: ComponentFramework.LookupValue) { // Record was deleted successfully let responseId: string = response.id; let responseEntityType: string = response.name!; // Generate HTML to inject into the result div to showcase the deleted record thisRef.updateResultContainerText("Deleted " + responseEntityType + " record with ID: " + responseId); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } }, (error: any) => { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(error); } ); } /** * Event Handler for onClick of calculate average value button * @param event : click event */ private calculateAverageButtonOnClickHandler(): void { // Build FetchXML to retrieve the average value of _currencyAttributeName field for all _entityName records // Add a filter to only aggregate on records that have _currencyAttributeName not set to null let fetchXML: string = ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; fetchXML += ""; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API RetrieveMultipleRecords method to calculate the aggregate value this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, "?fetchXml=" + fetchXML).then( function (response: ComponentFramework.WebApi.RetrieveMultipleResponse) { // Retrieve multiple completed successfully -- retrieve the averageValue let averageVal: Number = response.entities[0].average_val; // Generate HTML to inject into the result div to showcase the result of the RetrieveMultiple Web API call let resultHTML: string = "Average value of " + TSWebAPI._currencyAttributeNameFriendlyName + " attribute for all " + TSWebAPI._entityName + " records: " + averageVal; thisRef.updateResultContainerText(resultHTML); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Event Handler for onClick of calculate record count button * @param event : click event */ private refreshRecordCountButtonOnClickHandler(): void { // Generate OData query string to retrieve the _currencyAttributeName field for all _entityName records // Add a filter to only retrieve records with _requiredAttributeName field which contains _requiredAttributeValue let queryString: string = "?$select=" + TSWebAPI._currencyAttributeName + "&$filter=contains(" + TSWebAPI._requiredAttributeName + ",'" + TSWebAPI._requiredAttributeValue + "')"; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API Retrieve Multiple call this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, queryString).then() function (response: any) { // Retrieve Multiple Web API call completed successfully let count1: number = 0; let count2: number = 0; let count3: number = 0; // Loop through each returned record for (let entity of response.entities) { // Retrieve the value of _currencyAttributeName field let value: Number = entity[TSWebAPI._currencyAttributeName]; // Check the value of _currencyAttributeName field and increment the correct counter if (value == 100) { count1++; } else if (value == 200) { count2++; } else if (value == 300) { count3++; } } // Generate HTML to inject into the fetch xml status div to showcase the results of the OData retrieve example let innerHtml: string = "Use above buttons to create or delete a record to see count update"; innerHtml += "
"; innerHtml += "
"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 100: " + count1; innerHtml += "
"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 200: " + count2; innerHtml += "
"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 300: " + count3; // Inject the HTML into the fetch xml status div if (thisRef._odataStatusContainerDiv) { thisRef._odataStatusContainerDiv.innerHTML = innerHtml; } // Inject a success message into the result div thisRef.updateResultContainerText("Record count refreshed"); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Renders example use of CreateRecord Web API */ private renderCreateExample() { // Create header label for Web API sample let headerDiv: HTMLDivElement = this.createHTMLDivElement("create_container", true, "Click to create " + TSWebAPI._entityName + " record"); this._container.appendChild(headerDiv); // Create button 1 to create a record with the revenue field set to 100 let value1: string = "100"; this._createEntity1Button = this.createHTMLButtonElement( this.getCreateRecordButtonLabel(value1), this.getCreateButtonId(value1), value1, this.createButtonOnClickHandler.bind(this)); // Create button 2 to create a record with the revenue field set to 200 let value2: string = "200"; this._createEntity2Button = this.createHTMLButtonElement( this.getCreateRecordButtonLabel(value2), this.getCreateButtonId(value2), value2, this.createButtonOnClickHandler.bind(this)); // Create button 3 to create a record with the revenue field set to 300 let value3: string = "300"; this._createEntity3Button = this.createHTMLButtonElement( this.getCreateRecordButtonLabel(value3), this.getCreateButtonId(value3), value3, this.createButtonOnClickHandler.bind(this)); // Append all button HTML elements to custom control container div this._container.appendChild(this._createEntity1Button); this._container.appendChild(this._createEntity2Button); this._container.appendChild(this._createEntity3Button); } /** * Renders example use of DeleteRecord Web API */ private renderDeleteExample(): void { // Create header label for Web API sample let headerDiv: HTMLDivElement = this.createHTMLDivElement("delete_container", true, "Click to delete " + TSWebAPI._entityName + " record"); // Render button to invoke DeleteRecord Web API call this._deleteRecordButton = this.createHTMLButtonElement( "Select record to delete", "delete_button", null, this.deleteButtonOnClickHandler.bind(this)); // Append elements to custom control container div this._container.appendChild(headerDiv); this._container.appendChild(this._deleteRecordButton); } /** * Renders example use of RetrieveMultiple Web API with OData */ private renderODataRetrieveMultipleExample(): void { let containerClassName: string = "odata_status_container"; // Create header label for Web API sample let statusDivHeader: HTMLDivElement = this.createHTMLDivElement(containerClassName, true, "Click to refresh record count"); this._odataStatusContainerDiv = this.createHTMLDivElement(containerClassName, false, undefined); // Create button to invoke OData RetrieveMultiple Example this._fetchXmlRefreshButton = this.createHTMLButtonElement( "Refresh record count", "odata_refresh", null, this.refreshRecordCountButtonOnClickHandler.bind(this)); // Append HTML elements to custom control container div this._container.appendChild(statusDivHeader); this._container.appendChild(this._odataStatusContainerDiv); this._container.appendChild(this._fetchXmlRefreshButton); } /** * Renders example use of RetrieveMultiple Web API with Fetch XML */ private renderFetchXmlRetrieveMultipleExample(): void { let containerName: string = "fetchxml_status_container"; // Create header label for Web API sample let statusDivHeader: HTMLDivElement = this.createHTMLDivElement(containerName, true, "Click to calculate average value of " + TSWebAPI._currencyAttributeNameFriendlyName); let statusDiv: HTMLDivElement = this.createHTMLDivElement(containerName, false, undefined); // Create button to invoke Fetch XML RetrieveMultiple Web API example this._oDataRefreshButton = this.createHTMLButtonElement( "Calculate average value of " + TSWebAPI._currencyAttributeNameFriendlyName, "odata_refresh", null, this.calculateAverageButtonOnClickHandler.bind(this)); // Append HTML Elements to custom control container div this._container.appendChild(statusDivHeader); this._container.appendChild(statusDiv); this._container.appendChild(this._oDataRefreshButton); } " style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;">import { IInputs, IOutputs } from "./generated/ManifestTypes"; export class TSWebAPI implements ComponentFramework.StandardControl<IInputs, IOutputs> { // Reference to the control container HTMLDivElement // This element contains all elements of our custom control example private _container: HTMLDivElement; // Reference to ComponentFramework Context object private _context: ComponentFramework.Context<IInputs>; // Flag if control view has been rendered private _controlViewRendered: Boolean; // References to button elements that are rendered by example custom control private _createEntity1Button: HTMLButtonElement; private _createEntity2Button: HTMLButtonElement; private _createEntity3Button: HTMLButtonElement; private _deleteRecordButton: HTMLButtonElement; private _fetchXmlRefreshButton: HTMLButtonElement; private _oDataRefreshButton: HTMLButtonElement; // References to div elements that are rendered by the example custom control private _odataStatusContainerDiv: HTMLDivElement; private _resultContainerDiv: HTMLDivElement; // Name of entity to use for example Web API calls that are performed by this control private static _entityName: string = "account"; // Required field on _entityName of type 'single line of text' // Example Web API calls that are performed by the example custom control will set this field for new record creation examples private static _requiredAttributeName: string = "name"; // Value that the _requiredAttributeName field will be set to for new created records private static _requiredAttributeValue: string = "Web API Custom Control (Sample)"; // Name of currency field on _entityName to populate during record create // Example Web API calls that are performed by the example custom control will set and read this field private static _currencyAttributeName: string = "revenue"; // Friendly name of currency field (only used for control UI - no functional impact) private static _currencyAttributeNameFriendlyName: string = "annual revenue"; /** * Empty constructor. */ constructor() { } /** * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here. * Data-set values are not initialized here, use updateView. * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions. * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously. * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface. * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content. */ public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement): void { this._context = context; this._controlViewRendered = false; this._container = document.createElement("div"); this._container.classList.add("TSWebAPI_Container"); container.appendChild(this._container); } /** * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc. * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions */ public updateView(context: ComponentFramework.Context<IInputs>): void { if (!this._controlViewRendered) { this._controlViewRendered = true; // Render Web API Examples this.renderCreateExample(); this.renderDeleteExample(); this.renderFetchXmlRetrieveMultipleExample(); this.renderODataRetrieveMultipleExample(); // Render result div to display output of Web API calls this.renderResultsDiv();} } /** * It is called by the framework prior to a control receiving new data. * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output” */ public getOutputs(): IOutputs { return {}; } /** * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup. * i.e. cancelling any pending remote calls, removing listeners, etc. */ public destroy(): void { // Add code to cleanup control if necessary } /** * Helper method to create HTML button that is used for CreateRecord Web API Example * @param buttonLabel : Label for button * @param buttonId : ID for button * @param buttonValue : Value of button (attribute of button) * @param onClickHandler : onClick event handler to invoke for the button */ private createHTMLButtonElement(buttonLabel: string, buttonId: string, buttonValue: string | null, onClickHandler: (event?: any) => void): HTMLButtonElement { let button: HTMLButtonElement = document.createElement("button"); button.innerHTML = buttonLabel; if (buttonValue) { button.setAttribute("buttonvalue", buttonValue); } button.id = buttonId; button.classList.add("SampleControl_WebAPI_ButtonClass"); button.addEventListener("click", onClickHandler); return button; } /** * Helper method to create HTML Div Element * @param elementClassName : Class name of div element * @param isHeader : True if 'header' div - adds extra class and post-fix to ID for header elements * @param innerText : innerText of Div Element */ private createHTMLDivElement(elementClassName: string, isHeader: Boolean, innerText?: string): HTMLDivElement { let div: HTMLDivElement = document.createElement("div"); if (isHeader) { div.classList.add("SampleControl_WebAPI_Header"); elementClassName += "_header"; } if (innerText) { div.innerText = innerText.toUpperCase(); } div.classList.add(elementClassName); return div; } /** * Renders a 'result container' div element to inject the status of the example Web API calls */ private renderResultsDiv() { // Render header label for result container let resultDivHeader: HTMLDivElement = this.createHTMLDivElement("result_container", true, "Result of last action"); this._container.appendChild(resultDivHeader); // Div elements to populate with the result text this._resultContainerDiv = this.createHTMLDivElement("result_container", false, undefined); this._container.appendChild(this._resultContainerDiv); // Init the result container with a notification that the control was loaded this.updateResultContainerText("Web API sample custom control loaded"); } /** * Helper method to inject HTML into result container div * @param statusHTML : HTML to inject into result container */ private updateResultContainerText(statusHTML: string): void { if (this._resultContainerDiv) { this._resultContainerDiv.innerHTML = statusHTML; } } /** * Helper method to inject error string into result container div after failed Web API call * @param errorResponse : error object from rejected promise */ private updateResultContainerTextWithErrorResponse(errorResponse: any): void { if (this._resultContainerDiv) { // Retrieve the error message from the errorResponse and inject into the result div let errorHTML: string = "Error with Web API call:"; errorHTML += "<br />" errorHTML += errorResponse.message; this._resultContainerDiv.innerHTML = errorHTML; } } /** * Helper method to generate Label for Create Buttons * @param entityNumber : value to set _currencyAttributeNameFriendlyName field to for this button */ private getCreateRecordButtonLabel(entityNumber: string): string { return "Create record with " + TSWebAPI._currencyAttributeNameFriendlyName + " of " + entityNumber; } /** * Helper method to generate ID for Create button * @param entityNumber : value to set _currencyAttributeNameFriendlyName field to for this button */ private getCreateButtonId(entityNumber: string): string { return "create_button_" + entityNumber; } /** * Event Handler for onClick of create record button * @param event : click event */ private createButtonOnClickHandler(event: Event): void { // Retrieve the value to set the currency field to from the button's attribute let currencyAttributeValue: Number = parseInt( (event.srcElement! as Element)!.attributes.getNamedItem("buttonvalue")!.value ); // Generate unique record name by appending timestamp to _requiredAttributeValue let recordName: string = TSWebAPI._requiredAttributeValue + "_" + Date.now(); // Set the values for the attributes we want to set on the new record // If you want to set additional attributes on the new record, add to data dictionary as key/value pair var data: any = {}; data[TSWebAPI._requiredAttributeName] = recordName; data[TSWebAPI._currencyAttributeName] = currencyAttributeValue; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API to create the new record this._context.webAPI.createRecord(TSWebAPI._entityName, data).then( function (response: ComponentFramework.LookupValue) { // Callback method for successful creation of new record // Get the ID of the new record created let id: string = response.id; // Generate HTML to inject into the result div to showcase the fields and values of the new record that is created let resultHtml: string = "Created new " + TSWebAPI._entityName + " record with below values:" resultHtml += "<br />"; resultHtml += "<br />"; resultHtml += "id: " + id; resultHtml += "<br />"; resultHtml += "<br />"; resultHtml += TSWebAPI._requiredAttributeName + ": " + recordName; resultHtml += "<br />"; resultHtml += "<br />"; resultHtml += TSWebAPI._currencyAttributeName + ": " + currencyAttributeValue; thisRef.updateResultContainerText(resultHtml); }, function (errorResponse: any) { // Error handling code here - record failed to be created thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Event Handler for onClick of delete record button * @param event : click event */ private deleteButtonOnClickHandler(): void { // Invoke a lookup dialog to allow the user to select an existing record of type _entityName to delete var lookUpOptions: any = { entityTypes: [TSWebAPI._entityName] }; // store reference to 'this' so it can be used in the callback method var thisRef = this; var lookUpPromise: any = this._context.utils.lookupObjects(lookUpOptions); lookUpPromise.then( // Callback method - invoked after user has selected an item from the lookup dialog // Data parameter is the item selected in the lookup dialog (data: ComponentFramework.EntityReference[]) => { if (data && data[0]) { // Get the ID and entityType of the record that was selected by the lookup let id: string = data[0].id.guid; let entityType: string = data[0].etn!; // Invoke the deleteRecord method of the WebAPI to delete the selected record this._context.webAPI.deleteRecord(entityType, id).then( function (response: ComponentFramework.LookupValue) { // Record was deleted successfully let responseId: string = response.id; let responseEntityType: string = response.name!; // Generate HTML to inject into the result div to showcase the deleted record thisRef.updateResultContainerText("Deleted " + responseEntityType + " record with ID: " + responseId); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } }, (error: any) => { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(error); } ); } /** * Event Handler for onClick of calculate average value button * @param event : click event */ private calculateAverageButtonOnClickHandler(): void { // Build FetchXML to retrieve the average value of _currencyAttributeName field for all _entityName records // Add a filter to only aggregate on records that have _currencyAttributeName not set to null let fetchXML: string = "<fetch distinct='false' mapping='logical' aggregate='true'>"; fetchXML += "<entity name='" + TSWebAPI._entityName + "'>"; fetchXML += "<attribute name='" + TSWebAPI._currencyAttributeName + "' aggregate='avg' alias='average_val' />"; fetchXML += "<filter>"; fetchXML += "<condition attribute='" + TSWebAPI._currencyAttributeName + "' operator='not-null' />"; fetchXML += "</filter>"; fetchXML += "</entity>"; fetchXML += "</fetch>"; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API RetrieveMultipleRecords method to calculate the aggregate value this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, "?fetchXml=" + fetchXML).then( function (response: ComponentFramework.WebApi.RetrieveMultipleResponse) { // Retrieve multiple completed successfully -- retrieve the averageValue let averageVal: Number = response.entities[0].average_val; // Generate HTML to inject into the result div to showcase the result of the RetrieveMultiple Web API call let resultHTML: string = "Average value of " + TSWebAPI._currencyAttributeNameFriendlyName + " attribute for all " + TSWebAPI._entityName + " records: " + averageVal; thisRef.updateResultContainerText(resultHTML); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Event Handler for onClick of calculate record count button * @param event : click event */ private refreshRecordCountButtonOnClickHandler(): void { // Generate OData query string to retrieve the _currencyAttributeName field for all _entityName records // Add a filter to only retrieve records with _requiredAttributeName field which contains _requiredAttributeValue let queryString: string = "?$select=" + TSWebAPI._currencyAttributeName + "&$filter=contains(" + TSWebAPI._requiredAttributeName + ",'" + TSWebAPI._requiredAttributeValue + "')"; // store reference to 'this' so it can be used in the callback method var thisRef = this; // Invoke the Web API Retrieve Multiple call this._context.webAPI.retrieveMultipleRecords(TSWebAPI._entityName, queryString).then() function (response: any) { // Retrieve Multiple Web API call completed successfully let count1: number = 0; let count2: number = 0; let count3: number = 0; // Loop through each returned record for (let entity of response.entities) { // Retrieve the value of _currencyAttributeName field let value: Number = entity[TSWebAPI._currencyAttributeName]; // Check the value of _currencyAttributeName field and increment the correct counter if (value == 100) { count1++; } else if (value == 200) { count2++; } else if (value == 300) { count3++; } } // Generate HTML to inject into the fetch xml status div to showcase the results of the OData retrieve example let innerHtml: string = "Use above buttons to create or delete a record to see count update"; innerHtml += "<br />"; innerHtml += "<br />"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 100: " + count1; innerHtml += "<br />"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 200: " + count2; innerHtml += "<br />"; innerHtml += "Count of " + TSWebAPI._entityName + " records with " + TSWebAPI._currencyAttributeName + " of 300: " + count3; // Inject the HTML into the fetch xml status div if (thisRef._odataStatusContainerDiv) { thisRef._odataStatusContainerDiv.innerHTML = innerHtml; } // Inject a success message into the result div thisRef.updateResultContainerText("Record count refreshed"); }, function (errorResponse: any) { // Error handling code here thisRef.updateResultContainerTextWithErrorResponse(errorResponse); } ); } /** * Renders example use of CreateRecord Web API */ private renderCreateExample() { // Create header label for Web API sample let headerDiv: HTMLDivElement = this.createHTMLDivElement("create_container", true, "Click to create " + TSWebAPI._entityName + " record"); this._container.appendChild(headerDiv); // Create button 1 to create a record with the revenue field set to 100 let value1: string = "100"; this._createEntity1Button = this.createHTMLButtonElement( this.getCreateRecordButtonLabel(value1), this.getCreateButtonId(value1), value1, this.createButtonOnClickHandler.bind(this)); // Create button 2 to create a record with the revenue field set to 200 let value2: string = "200"; this._createEntity2Button = this.createHTMLButtonElement( this.getCreateRecordButtonLabel(value2), this.getCreateButtonId(value2), value2, this.createButtonOnClickHandler.bind(this)); // Create button 3 to create a record with the revenue field set to 300 let value3: string = "300"; this._createEntity3Button = this.createHTMLButtonElement( this.getCreateRecordButtonLabel(value3), this.getCreateButtonId(value3), value3, this.createButtonOnClickHandler.bind(this)); // Append all button HTML elements to custom control container div this._container.appendChild(this._createEntity1Button); this._container.appendChild(this._createEntity2Button); this._container.appendChild(this._createEntity3Button); } /** * Renders example use of DeleteRecord Web API */ private renderDeleteExample(): void { // Create header label for Web API sample let headerDiv: HTMLDivElement = this.createHTMLDivElement("delete_container", true, "Click to delete " + TSWebAPI._entityName + " record"); // Render button to invoke DeleteRecord Web API call this._deleteRecordButton = this.createHTMLButtonElement( "Select record to delete", "delete_button", null, this.deleteButtonOnClickHandler.bind(this)); // Append elements to custom control container div this._container.appendChild(headerDiv); this._container.appendChild(this._deleteRecordButton); } /** * Renders example use of RetrieveMultiple Web API with OData */ private renderODataRetrieveMultipleExample(): void { let containerClassName: string = "odata_status_container"; // Create header label for Web API sample let statusDivHeader: HTMLDivElement = this.createHTMLDivElement(containerClassName, true, "Click to refresh record count"); this._odataStatusContainerDiv = this.createHTMLDivElement(containerClassName, false, undefined); // Create button to invoke OData RetrieveMultiple Example this._fetchXmlRefreshButton = this.createHTMLButtonElement( "Refresh record count", "odata_refresh", null, this.refreshRecordCountButtonOnClickHandler.bind(this)); // Append HTML elements to custom control container div this._container.appendChild(statusDivHeader); this._container.appendChild(this._odataStatusContainerDiv); this._container.appendChild(this._fetchXmlRefreshButton); } /** * Renders example use of RetrieveMultiple Web API with Fetch XML */ private renderFetchXmlRetrieveMultipleExample(): void { let containerName: string = "fetchxml_status_container"; // Create header label for Web API sample let statusDivHeader: HTMLDivElement = this.createHTMLDivElement(containerName, true, "Click to calculate average value of " + TSWebAPI._currencyAttributeNameFriendlyName); let statusDiv: HTMLDivElement = this.createHTMLDivElement(containerName, false, undefined); // Create button to invoke Fetch XML RetrieveMultiple Web API example this._oDataRefreshButton = this.createHTMLButtonElement( "Calculate average value of " + TSWebAPI._currencyAttributeNameFriendlyName, "odata_refresh", null, this.calculateAverageButtonOnClickHandler.bind(this)); // Append HTML Elements to custom control container div this._container.appendChild(statusDivHeader); this._container.appendChild(statusDiv); this._container.appendChild(this._oDataRefreshButton); }

Add styling to your code component

To add styling to your code component, follow these steps:

Create a new css subfolder under the TSWebAPI folder.

Create a new TS_WebAPI.css file inside the CSS subfolder.

Add the following style content to the TS_WebAPI.css file:

XMLCopy

.SampleNamespace\.TSWebAPI { font-family: 'SegoeUI-Semibold', 'Segoe UI Semibold', 'Segoe UI Regular', 'Segoe UI'; color: #1160B7; } .SampleNamespace\.TSWebAPI .TSWebAPI_Container { overflow-x: auto; } .SampleNamespace\.TSWebAPI .SampleControl_WebAPI_Header { color: rgb(51, 51, 51); font-size: 1rem; padding-top: 20px; } .SampleNamespace\.TSWebAPI .result_container { padding-bottom: 20px; } .SampleNamespace\.TSWebAPI .SampleControl_WebAPI_ButtonClass { text-decoration: none; display: inline-block; font-size: 14px; cursor: pointer; color: #1160B7; background-color: #FFFFFF; border: 1px solid black; padding: 5px; text-align: center; min-width: 300px; margin-top: 10px; margin-bottom: 5px; display: block; }

Select File and Save All your changes.

Build and run your component

To build and run your component, follow these steps:

Build your solution by running the following command.

ConsoleCopy

npm run build

Upon a successful build, you can test your new formatting API component by running npm start.

ConsoleCopy

npm start

Close the test harness browser window.

Go back to the terminal and stop the watcher by pressing [CONTROL] + C.

Type Y and then press [ENTER].

To test the web API functionality, you need to publish and host the component in a Microsoft Power Platform environment. For more information on how to publish code components, see "Create a code component solution package" in the Build a Power Apps component module.

For more information, see Implementing Web API component.

Write a pop-up Power Apps component

Completed100 XP

15 minutes

Occasionally, it's required that you display a pop-up window to a user of your application. The Power Apps component framework exposes a pop-up API that allows you to achieve this requirement. The following example shows how to build a pop-up window that displays a loader graphic. In long-running operations, this method can help you achieve a satisfying user experience where the underlying UI is blocked from performing operations.

 Note

The Popup component is only supported in model-driven apps.

Initialize your component's project

To initialize your component's project, follow these steps:

Start Visual Studio Code.

Select Terminal, select New Terminal, and switch the Terminal shell to the Command Prompt.

Change the directory to your source folder.

ConsoleCopy

cd \source

From your source directory, create a directory named Popup-Component.

ConsoleCopy

md Popup-Component

Run the following command to switch to the new directory.

ConsoleCopy

cd Popup-Component

Initialize the project by running the following command:

ConsoleCopy

pac pcf init --namespace SampleNamespace --name PopupComponent --template field

Run npm install to load dependent libraries into your project.

ConsoleCopy

npm install

Open the project in Visual Studio Code by running the following command:

ConsoleCopy

code -a .

Implement your code component's logic

To implement your code component's logic, follow these steps:

Expand the PopupComponent folder and open the ControlManifest.Input.xml file.

Replace the entire manifest with the following XML:

XMLCopy

" style="box-sizing: inherit; outline-color: inherit; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 1em; direction: ltr; border-width: 0px; border-style: initial; line-height: 1.3571; display: block; position: relative;"><?xml version="1.0" encoding="utf-8" ?> <manifest> <control namespace="SampleNamespace" constructor="PopupComponent" version="0.0.1" display-name-key="PopupComponent_Display_Key" description-key="PopupComponent_Desc_Key" control-type="standard"> <!-- property node identifies a specific, configurable piece of data that the control expects from CDS --> <property name="sampleProperty" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type="SingleLine.Text" usage="bound" required="true" /> <resources> <code path="index.ts" order="1"/> <css path="css/loader.css" order="1" /> </resources> </control> </manifest>

You add the supporting files that are found in this manifest later.

Open the Index.ts file.

Above the export class method, insert the following interface method so that you can expose other methods provided by the pop-up API (popupStyle and shadowStyle):

TypeScriptCopy

interface Popup extends ComponentFramework.FactoryApi.Popup.Popup { popupStyle: object; shadowStyle: object; }

Add the following private variable above the constructor.

TypeScriptCopy

private _container: HTMLDivElement; private _popUpService: ComponentFramework.FactoryApi.Popup.PopupService;

Add the following logic to your component's init method:

TypeScriptCopy

this._container = document.createElement('div'); //============ content of our popup ============= let popUpContent = document.createElement('div'); popUpContent.setAttribute("class", "loader"); //============ our Popup object ============= let popUpOptions: Popup = { closeOnOutsideClick: true, content: popUpContent, name: 'loaderPopup', // unique popup name type: 1, // Root popup popupStyle: { "width": "100%", "height": "100%", "overflow": "hidden", "backgroundColor": "transparent", "display": "flex", "flexDirection": "column", "position": "absolute", "margin-top": 28 + "px" }, shadowStyle:{ position: "absolute", width: "100%", height: "100%" } }; this._popUpService = context.factory.getPopupService(); this._popUpService.createPopup(popUpOptions); container.appendChild(this._container); this._popUpService.openPopup('loaderPopup');

Add styling to your code component

To add styling to your code component, follow these steps:

Create a new css subfolder under the PopupComponent folder.

Create a new loader.css file inside the CSS subfolder.

Add the following style content to the loader.css file:

XMLCopy

.loader { border: 16px solid #f3f3f3; /* Light grey */ border-top: 16px solid #3498db; /* Blue */ border-radius: 50%; position: fixed; width: 120px; height: 120px; top:40%; left:50%; animation: spin 2s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }

Select File and Save All your changes.

Build and run your component

To build and run your component, follow these steps:

Build your solution by running the following command.

ConsoleCopy

npm run build

Upon a successful build, you can test your new Popup component by running npm start.

ConsoleCopy

npm start

Close the test harness browser window.

Go back to the terminal and stop the watcher by pressing [CONTROL] + C.

Type Y and then press [ENTER].

 Note

This loader component is bound to a text field. To use the component in a model-driven app, it might be beneficial for you to mark this field as hidden if you want to use it in a form.

To test the popup functionality, you need to publish and host the component in a Microsoft Power Platform environment. For more information on how to publish code components, see "Create a code component solution package" in the Build a Power Apps component module.

Check your knowledge

Completed200 XP

8 minutes

Answer the following questions to see what you've learned.

1. 

What is required to use React with your code component?

Copy the React framework files into your code component folder

Use the --framework React option on the Power Platform CLI

This option configures your code component to use React.

Manually edit package.json to include React

Manually edit ControlInput.Manifest.xml to include React.

2. 

Which method would you use to format a string into a specific language?

formatLocale

globalization.Localize

formatLanguage

formatLanguage is the method that you would use to format a string into a specific language.

setLanguage

3. 

Which is the correct XML element that specifies usage of the Web API in a component in a control’s manifest?

uses name="WebAPI" required="true"

uses-feature name="CommonDataServiceAPI" required="true"

uses-feature name="WebAPI" required="true"

The correct XML element that specifies usage of the Web API in a component in a control’s manifest is uses-feature name="WebAPI" required="true".

uses name="CommonDataServiceAPI" required="true"

4. 

What is the name of the API that you can use to overlay a pop-up window within a Power Apps Component?

ComponentFramework.Popup.Popup

ComponentFramework.FactoryApi.PopupComponent

PowerAppComponent.FactoryApi.Popup.Popup

ComponentFramework.FactoryApi.Popup.Popup

ComponentFramework.FactoryApi.Popup.Popup is the name of the API that you can use to overlay a pop-up window within a Power Apps Component.

Read Entire Article