Create Custom Controls using PowerApp Component Framework
So many new features came up in April release but the one I was very excited about was creating custom controls using PowerApp Component Framework. If you are not sure what custom control is please read this article.
I am documenting my experience of developing the custom controls (end-to-end) in this blog.
Prepare your development environment
Installations
- Install npm from here. NTS or Current – any version of npm will do
- If you don’t have Visual Studio 2017 then download and install VS2017
- Download and Install PowerApp CLI on your machine from here
Currently PowerApps CLI is supported only on Windows 10
Create & configure PowerApp Component Framework components
- Create or identify your working folder. In my case it is “C:\Users\dnaglekar\Source\Workspaces\xxx\PowerAppCustomControl”
- Open Visual Studio command prompt
- Navigate to you recently created folder (step 1) using
cdcommand. In my case it iscd C:\Users\dnaglekar\Source\Workspaces\xxx\PowerAppCustomControl - Run the
paccommand to create new component project. Command is as follows:pac pcf init --namespace [Your Namespace] --name [Your Component Name] --template [Component Type]
In my case, following is the
paccommand:pac pcf init --namespace DanzMaverick --name Tags --template field
As of today there are two component types: field and dataset.
- Now we want to retrieve all project dependencies. To do that we want to run the following command:
npm install
Develop custom control
Now that the project is ready; navigate in your project folder. You can use any development tool to implement your custom control. I have used Visual Studio 2017. Below are the steps to be followed to successfully develop your control.
Following will be the file structure you will find after all project dependencies are installed.

As you can see there is one folder named “Tags” (you will see different folder as per the name of your control), that was the name of the control I gave in my pac command. Navigate inside that particular folder and you will find the following structure.

Configure Manifest file
ControlManifest.Input.xml
This is a very important file. It contains the component’s definition. Open this file in your favorite developer interface.
First thing you would need to change in this file is the definition of the custom control. You will see an example configuration with a tag named “control” – shown below:
<control namespace="DanzMaverick" constructor="Tags" version="0.0.1" display-name-key="Control_Display_Key" description-key="Control_Desc_Key" control-type="standard"]
- namespace: this was provided earlier in the
paccommand - constructor: this is the name of your control that you had provided in the
paccommand - version: change it if needed; else keep what is
- display-name-key: change it to be the display name (no spaces) of your custom control
- description-key: change it to be the description of your custom control that you want to show in D365
- control-type: do not change this
Next you will find is a tag named “property” – shown below:
<property name="sampleProperty" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type="SingleLine.Text" usage="bound" required="true" />
Following are the attributes of property tag:
- name: change it to be the name of your control
- display-name-key: change it to be the display name (no spaces) of your custom control
- description-key: change it to be the description of your custom control that you want to show in D365
- of-type: if your control is going to support only single data-type then use of-type attribute.
Valid values are:- Currency
- DateAndTime.DateAndTime
- DateAndTime.DateOnly
- Decimal
- Enum
- FP
- Multiple
- Optionset
- SingleLine.Email
- SingleLine.Phone
- SingleLine.Text
- SingleLine.TextArea
- SingleLine.Ticker
- SingleLine.URL
- TwoOptions
- Whole.None
More information about of-type values is here
- of-type-group: if your control is going to support multiple data-type then use of-type-group attribute. If you use this attribute then you have to define the type-group tag and the name of that type-group should be mentioned here.
Below is a sample for defining a type-group:<type-group name="numbers"> <type>Whole.None</type> <type>Currency</type> <type>FP</type> <type>Decimal</type> </type-group>
After you configured control and property tags; you should move towards the end of manifest file. You’ll find resource tag. This tag lists three type of child tags; code, css & resx.
- code: it contains the relative path to the typescript file that will contain the code for our custom control
- css: it contains the relative path of the css file that our custom control will use while rendering the controls
- resx: it contains the relative path of resx file that will contain localized string
Make sure that if you are referencing relative path; it is correct
My final manifest file looks like below:
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="DanzMaverick" constructor="Tags" version="0.0.1" display-name-key="DanzMaverick.Tags" description-key="Takes your comma separated string and converts it into Tags" control-type="standard">
<!-- property node identifies a specific, configurable piece of data that the control expects from CDS -->
<type-group name="forTags">
<type>SingleLine.Text</type>
<type>SingleLine.TextArea</type>
</type-group>
<property name="Tags" display-name-key="Tags" description-key="Tags" of-type-group="forTags" usage="bound" required="true" />
<!--
Property node's of-type attribute can be of-type-group attribute.
Example:
<type-group name="numbers">
<type>Whole.None</type>
<type>Currency</type>
<type>FP</type>
<type>Decimal</type>
</type-group>
<property name="sampleProperty" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type-group="numbers" usage="bound" required="true" />
-->
<resources>
<code path="index.ts" order="1"/>
<css path="css/index.css" order="1" />
<!-- UNCOMMENT TO ADD MORE RESOURCES
<css path="css/Tags.css" order="1" />
<resx path="strings/Tags.1033.resx" version="1.0.0" />
-->
</resources>
</control>
</manifest>
Implement logic in typescript
First open the index.ts file in your favorite development tool. I am using Visual Studio 2017. You will find following methods:
- init: this will be the first method that system will invoke. All your design should happen in this method
- updateView: this method is invoked when property bag is changed; which includes fields, data-sets, global variables such as height and/or width
- getOutputs: this method is called prior to receiving any data
- destroy: add your cleanup code here
In my case as I am creating a control to read a comma-separated value in the text field and create tag-like elements. So follow along with manipulating the index.ts file.
HTML Elements
Let’s start by adding HTML elements we need. As we are writing a .ts file we do not have HTML tags (per say). But using the power of typescript we are going to create dynamic HTML elements in the form of variables and later use them in the init function to add it on the control.
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class Tags implements ComponentFramework.StandardControl {
/**
* Variables for HTML element
*/
private tagsElement: HTMLElement;
private spaceElement: HTMLElement;
private refreshButton: HTMLElement;
I have added an element for my tags and space. I have also added a button for refresh. I am going for the following look & feel.

Global Variables
Now that I have decided what all HTML elements I need; let’s decide all variables I may need during the implementation process. I have decided I will need a variable to store the value that will contain a comma-separated string. So adding to the above code tagsString will be the variable that will contain the attribute value.
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class Tags implements ComponentFramework.StandardControl {
/**
* Variables for HTML element
*/
private tagsElement: HTMLElement;
private spaceElement: HTMLElement;
private refreshButton: HTMLElement;
/**
* Variables for Properties
*/
private tagsString: string;
As I said it is not an HTML so we have to define variable for all. Hence, let’s define variable for event listener for the “refresh button”. Adding to the same code and expanding it – “refreshClicked” will be my event listener when “Refresh” button is clicked.
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class Tags implements ComponentFramework.StandardControl {
/**
* Variables for HTML element
*/
private tagsElement: HTMLElement;
private spaceElement: HTMLElement;
private refreshButton: HTMLElement;
private divElement: HTMLElement;
/**
* Variables for Properties
*/
private tagsString: string;
/**
* Variables for Event Listener
*/
private refreshClicked: EventListenerOrEventListenerObject;
We also need some local variable to capture the input parameter from init function. Expanding the code; it looks like below.
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class Tags implements ComponentFramework.StandardControl {
/**
* Variables for HTML element
*/
private tagsElement: HTMLElement;
private spaceElement: HTMLElement;
private refreshButton: HTMLElement;
private divElement: HTMLElement;
/**
* Variables for Properties
*/
private tagsString: string;
/**
* Variables for Event Listener
*/
private refreshClicked: EventListenerOrEventListenerObject;
/**
* Local Variables
*/
private localContext: ComponentFramework.Context;
private localNotifyOutputChanged: () => void;
private localContainer: HTMLDivElement;
init() function
In this function, let’s first initialize local variables from the input parameters. Then create a functions to handle events; in my case, I have created refreshClick() function. Now, bind the event handlers to the variables defined for event listening.
From here onward the logic to add the HTML elements may defer based on the logic, in my case, I am adding refresh button first to the body of the control; to do so I have written the below code.
// Refresh button
this.refreshButton = document.createElement("button");
this.refreshButton.setAttribute("type", "button");
this.refreshButton.setAttribute("value", "Refresh");
this.refreshButton.setAttribute("class", "btn btn-default btn-sm glyphicon glyphicon-refresh");
this.refreshButton.addEventListener("click", this.refreshClick);
// Add elements to container
this.localContainer.appendChild(this.refreshButton);
Here, I initialized my variable as input tag using document.createElement. Then I defined some attributes to that tag; in this case, type & value. Later, I added the event listener to that element. Finally, I add my element to the div container.
Now, I read the attribute value bound to the control from the context, split the values based on comma and in the for-loop I initialize my tags tag as span HTML element and set class attributes on that element.
Below is my entire init() function
/**
* 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='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) {
// Init local variables
this.localContext = context;
this.localNotifyOutputChanged = notifyOutputChanged;
this.localContainer = container;
// Register EventHandler
this.refreshClicked = this.refreshClick.bind(this);
// Refresh button
this.refreshButton = document.createElement("button");
this.refreshButton.setAttribute("type", "button");
this.refreshButton.setAttribute("value", "Refresh");
this.refreshButton.setAttribute("class", "btn btn-default btn-sm glyphicon glyphicon-refresh");
this.refreshButton.addEventListener("click", this.refreshClick);
// Add elements to container
this.localContainer.appendChild(this.refreshButton);
// CRM attributes bound to the control properties.
// @ts-ignore
var crmTagStringsAttribute = context.parameters.Tags.attributes.LogicalName;
// @ts-ignore
var crmTagStringsAttributeValue = Xrm.Page.getAttribute(crmTagStringsAttribute).getValue();
var data = crmTagStringsAttributeValue.split(",");
for (var i in data) {
// Create controls
// Tag element
this.tagsElement = document.createElement("span");
this.tagsElement.setAttribute("class", "badge badge-pill badge-primary");
var ele = this.localContainer.appendChild(this.tagsElement);
ele.innerHTML = data[i];
// Space element
this.spaceElement = document.createElement("span");
var space = this.localContainer.appendChild(this.spaceElement);
space.innerHTML = " ";
}
}
Below is my refreshClick() function
/**
* Custom Event Handlers
*/
public refreshClick(evnt: Event): void {
this.localNotifyOutputChanged();
}
updateView() function
In here, we need to refresh the HTML elements based on the new data entered on the screen.
Below is my updateView() function
/**
* 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 {
// Add code to update control view
// CRM attributes bound to the control properties.
// @ts-ignore
var crmTagStringsAttribute = this._context.parameters.Tags.attributes.LogicalName;
// @ts-ignore
var crmTagStringsAttributeValue = Xrm.Page.getAttribute(crmTagStringsAttribute).getValue();
var data = crmTagStringsAttributeValue.split(",");
// Delete all elements first
var tagCollection = document.getElementsByTagName("span");
for (var tagIndex in tagCollection) {
this.localContainer.removeChild(tagCollection[tagIndex]);
}
// Add new tags
for (var i in data) {
// Create controls
// Tag element
this.tagsElement = document.createElement("span");
this.tagsElement.setAttribute("class", "badge badge-pill badge-primary");
var ele = this.localContainer.appendChild(this.tagsElement);
ele.innerHTML = data[i];
// Space element
this.spaceElement = document.createElement("span");
var space = this.localContainer.appendChild(this.spaceElement);
space.innerHTML = " ";
}
}
Use // @ts-ignore to use Xrm.Page module
My entire index.ts code file
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class Tags implements ComponentFramework.StandardControl {
/**
* Variables for HTML element
*/
private tagsElement: HTMLElement;
private spaceElement: HTMLElement;
private refreshButton: HTMLElement;
private divElement: HTMLElement;
/**
* Variables for Properties
*/
private tagsString: string;
/**
* Variables for Event Listener
*/
private refreshClicked: EventListenerOrEventListenerObject;
/**
* Local Variables
*/
private localContext: ComponentFramework.Context;
private localNotifyOutputChanged: () => void;
private localContainer: HTMLDivElement;
/**
* 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='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) {
// Init local variables
this.localContext = context;
this.localNotifyOutputChanged = notifyOutputChanged;
this.localContainer = container;
// Register EventHandler
this.refreshClicked = this.refreshClick.bind(this);
// Refresh button
this.refreshButton = document.createElement("button");
this.refreshButton.setAttribute("type", "button");
this.refreshButton.setAttribute("value", "Refresh");
this.refreshButton.setAttribute("class", "btn btn-default btn-sm glyphicon glyphicon-refresh");
this.refreshButton.addEventListener("click", this.refreshClick);
// Add elements to container
this.localContainer.appendChild(this.refreshButton);
// CRM attributes bound to the control properties.
// @ts-ignore
var crmTagStringsAttribute = context.parameters.Tags.attributes.LogicalName;
// @ts-ignore
var crmTagStringsAttributeValue = Xrm.Page.getAttribute(crmTagStringsAttribute).getValue();
var data = crmTagStringsAttributeValue.split(",");
for (var i in data) {
// Create controls
// Tag element
this.tagsElement = document.createElement("span");
this.tagsElement.setAttribute("class", "badge badge-pill badge-primary");
var ele = this.localContainer.appendChild(this.tagsElement);
ele.innerHTML = data[i];
// Space element
this.spaceElement = document.createElement("span");
var space = this.localContainer.appendChild(this.spaceElement);
space.innerHTML = " ";
}
}
/**
* 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 {
// Add code to update control view
// CRM attributes bound to the control properties.
// @ts-ignore
var crmTagStringsAttribute = this._context.parameters.Tags.attributes.LogicalName;
// @ts-ignore
var crmTagStringsAttributeValue = Xrm.Page.getAttribute(crmTagStringsAttribute).getValue();
var data = crmTagStringsAttributeValue.split(",");
// Delete all elements first
var tagCollection = document.getElementsByTagName("span");
for (var tagIndex in tagCollection) {
this.localContainer.removeChild(tagCollection[tagIndex]);
}
// Add new tags
for (var i in data) {
// Create controls
// Tag element
this.tagsElement = document.createElement("span");
this.tagsElement.setAttribute("class", "badge badge-pill badge-primary");
var ele = this.localContainer.appendChild(this.tagsElement);
ele.innerHTML = data[i];
// Space element
this.spaceElement = document.createElement("span");
var space = this.localContainer.appendChild(this.spaceElement);
space.innerHTML = " ";
}
}
/**
* 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 {
Tags: this.tagsString
};
}
/**
* 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 {
// remove the event handlers.
this.refreshButton.removeEventListener("click", this.refreshClick);
}
/**
* Custom Event Handlers
*/
public refreshClick(evnt: Event): void {
this.localNotifyOutputChanged();
}
}
Customize UI using CSS
As you may have observed I have used class attributes on my HTML tags but that would not render unless I add the class definitions in the .css file
.badge-primary {
color: #fff;
background-color: #007bff;
}
.badge-pill {
padding-right: 0.6em;
padding-left: 0.6em;
border-radius: 10rem;
}
.badge {
display: inline-block;
padding: 0.25em 0.4em;
font-size: 75%;
font-weight: bold;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
}
button {
text-rendering: auto;
color: initial;
letter-spacing: normal;
word-spacing: normal;
text-transform: none;
text-indent: 0px;
text-shadow: none;
display: inline-block;
text-align: start;
margin: 0em;
font: 400 13.3333px Arial;
}
.btn-sm {
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
}
.btn-default {
color: #333;
background-color: #fff;
border-color: #ccc;
}
.btn {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-size: 14px;
font-weight: 400;
line-height: 1.42857143;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-ms-touch-action: manipulation;
touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
}
.glyphicon {
position: relative;
top: 1px;
display: inline-block;
font-family: 'Glyphicons Halflings';
font-style: normal;
font-weight: 400;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.glyphicon-refresh:before {
content: "\e031";
}
Build the project
Again, open Visual Studio command prompt and navigate to the folder where the index.ts file resides using cd command.
To build the package, run npm run build command as shown below.

If the build is succeeded, then we can test it by running the control in a browser. To do so, you will need to use npm start command as shown below. In my case, as init() function needs an input to load I have hard-coded some default values to test.

When testing in the browser; screen might look something like this. In my case as I do not have any output sent back I don’t see anything in there but if you have output sent back from the control you’ll see your output variables values as well.

You can use debugger in your index.ts file and using F12 you can do a step-by-step debugging
Create a solution package for D365 CE
For this create a new directory inside your control folder. In my case I have created a folder named “deployment” inside the “Tags” folder.

Now, use cd command to navigate inside this newly created folder and run the following command to create a new solution project for D365 CE.
pac solution init --publisherName [publisher name] --customizationPrefix [publisher prefix]
For example, in my case the command was
pac solution init --publisherName DanzMaverick --customizationPrefix dm
Once the solution project is created we need to add the component into this solution. To do this, we need to use the following command. The path needs to be where the project file (pcfproj) resides
pac solution add-reference --path [path or relative path of your PowerApps component framework project on disk]
In my case the command was
pac solution add-reference --path C:\Users\dnaglekar\Source\Workspaces\xxx\PowerAppCustomControl

Once the above command is executed, you’ll see deployment.cdsproj created.

We now have to run few more commands to create the .zip file we need for importing the solution in D365 CE. To do so, we need to execute msbuild /t:restore command followed by msbuild command.
After running both the command, if you navigate to your “deployment” folder (bin->debug) you should see a .zip file created.

Import this solution zip file in any of your favorite D365 CE instance and publish customization.
Custom Control in D365 CE
Let’s look at how to configure the custom control in D365 CE. I have created a custom attribute called “Tag” on “Account” entity which contains comma-separated value. I am going to customized the form to add a control to this attribute.

Once the form is customized and published; let’s navigate to the Account form and see the custom control in action.

Based on the data it created several tags. A basic custom control to give you a glimpse of what we can do with it.
Please leave a feedback or comment below. Thank you
This was originally posted here.

Like
Report
*This post is locked for comments