Deep dive into Power Apps Component Framework – Part 4 : Walkthrough to create your first PCF (based on a field)!
In this new episode, we will simply build step by step a PCF bound to a field! Before continuing to read this article, I advise you to read the four latest articles:
- Prelude : Getting started with PowerApps Component Framework
- Episode 1: First Steps!
- Episode 2: Focus on … Control Manifest file!
- Episode 3: Focus on … Architecture & Component’s life cycle!
The objective will therefore to create a component using the knowledge and principles mentioned in these articles.
Note that this component is not necessarily very clean but will allow you to fully understand how to build one!
Defining need and scope
I was recently able to work for a client who had implemented Data Quality for account creation. By using a Business Process Flow, the user was guided, but external calls were made to retrieve information from the customer.
Data Quality is one of the key words when implementing a Business App, whether in PreGoLive or PostGoLive. Data must be properly cleaned to be imported, but future records must be consistent to increase the added value of CRM.
In this case, users still had to enter a minimum amount of information so that the external service could return several possible records or only one in the case that a unique identifier was entered.
In this case, the unique identifier was the VAT number. This number is unique to each EU countries and that identifies a taxable person (business) or non-taxable legal entity that is registered for VAT.
The problem was therefore to allow validation of the entry entered, to check if this number exists, before calling the external service.
So I started to find out about the structure of this code and I quickly realized that there were much specificity per country and that it would therefore be very constraining to develop a simple JavaScript dealing with all the countries of Europe.
I searched on internet for JS libraries and found several! The problem is that finally they were doing a whole test only on the structure of the number but nothing proved to us that it was valid. Even if the structural check fulfilled the specifications well, I wondered if we could not go further…
And that’s when I noticed that a SOAP Web Service was made available by… the European Commission! What better way to check our VAT number!
So here is the initial principle of the component:
The user will enter text in a field, a call to this web service will be made and depending on the answer the validity or not of the number will be displayed.
The SOAP Web Service called VIES is available at this address.
You can also be interested in the other Web Services offered: https://ec.europa.eu/taxation_customs/online-services-and-databases-taxation_en.
Web Service
For the SOAP service you may send your XML request by using the following WSDL link:
http://ec.europa.eu/taxation_customs/vies/checkVatTestService.wsdl
We notice several things, we see that there are (1) specific statecodes to manage errors which will be useful to us!
The parameters of the CheckVat request (2).
And the message that interests us (3)!
We quickly understand that it is the checkVat function that will interest us and that we will have to give it two parameters:
- VAT Number
- Country Code
This can easily be verified by using online tools such as https://wsdlbrowser.com/ and inserting the WSDL link.
So first I tried to build the right query by calling the Web Service, note that we must use a CORS proxy to reach VIES from the browser, like this one for example: https://cors-anywhere.herokuapp.com/https://example.com
.
This makes a call to https://example.com
with origin header set to the one that satisfies CORS policy requirements, and https://cors-anywhere.herokuapp.com
returns us the result. Simple yet elegant solution.
The problem is that we’re going to rely on a 3rd party…
So I created a codepen to test my request using a known VAT Number: FR02432610426!
Yes, it’s the one from Avanade France!
See the Pen VAT Validation by Allan De Castro (@allan-de-castro) on CodePen.
My surprise was that in the result we had much more information than I imagined (it was actually detailed in the wsdl definition but I didn’t pay attention…):
- Valid: boolean to indicates if the VAT Number is valid or not
- Name: The name of the organization
- Address: The address of the organization
This is quite interesting because we will be able to use it to make Data Quality and bring back into our CRM the right name and address!
VAT Number Validator PCF
We will therefore create a PCF bound to a field and which will take 3 other parameters:
- vatNumberField: Field containing the VAT Number.
- isVatNumberValid: Boolean field filled-in with the output validity of the VAT Number.
- companyName: Field that will be used to insert the name of the found company.
- companyAddress: Field that will be used to insert the address of the found company.
- displayDialog: Used to specify whether you want to display error message(s) in the following cases (it’s an enum property).
Apart from the parameter isVatNumberValid which will be a boolean (so a TwoOptions in the manifest) and the displayDialog, the others will be of string type, we prefer to use a type group so that they can be linked all types of text fields:
- SingleLine.Text
- SingleLine.TextArea
- Multiple
Since we will have the country reference, I thought it was rather interesting to get a piece of code from my previous PCF which displays the country’s flags. We will therefore have to declare all flags in the manifest file. This leads us to the following definition:
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="VATNumberValidatorNameSpace" constructor="VATNumberValidator" version="1.0.0" display-name-key="VATNumberValidator" description-key="VATNumberValidator allows you to validate the entry of a VAT Number by checking it with the European Commission and retrieving, depending on the configuration, the address and the name of the company." control-type="standard" preview-image="img/preview.png">
<property name="vatNumberfield" display-name-key="VAT Number Field" description-key="Field containing the VAT Number." of-type-group="textFields" usage="bound" required="true" />
<property name="isVatNumberValid" display-name-key="VAT number validity field" description-key="Boolean field filled-in with the output validity of the VAT Number." of-type="TwoOptions" usage="bound" required="false" />
<property name="companyName" display-name-key="Company Name" description-key="Field that will be used to insert the name of the found company." of-type-group="textFields" usage="bound" required="false" />
<property name="companyAddress" display-name-key="Company Address" description-key="Field that will be used to insert the address of the found company." of-type-group="textFields" usage="bound" required="false" />
<property name="displayDialog" display-name-key="displayDialog" description-key="Specifies whether you want to display error message(s) in the following cases." usage="input" of-type="Enum" required="false">
<value name="NotFound" display-name-key="displayNotFound" description-key="Display message only if no result found.">NotFound</value>
<value name="ApiError" display-name-key="displayApiError" description-key="Display message only if service is unavailable or crashed." >ApiError</value>
<value name="Both" display-name-key="displayBoth" description-key="Display message only if service is unavailable or crashed and if no result found." default="true">Both</value>
</property>
<type-group name="textFields">
<type>SingleLine.Text</type>
<type>SingleLine.TextArea</type>
<type>Multiple</type>
</type-group>
<resources>
<code path="index.ts" order="1"/>
<css path="css/VATNumberValidator.css" order="1" />
<img path='img/preview.png' />
<img path='img/warning.png' />
<img path='img/loading.gif' />
<img path='img/at.png' />
<img path='img/be.png' />
<img path='img/bg.png' />
<img path='img/cy.png' />
<img path='img/cz.png' />
<img path='img/de.png' />
<img path='img/dk.png' />
<img path='img/ee.png' />
<img path='img/el.png' />
<img path='img/es.png' />
<img path='img/fi.png' />
<img path='img/fr.png' />
<img path='img/gb.png' />
<img path='img/hr.png' />
<img path='img/hu.png' />
<img path='img/ie.png' />
<img path='img/it.png' />
<img path='img/lt.png' />
<img path='img/lu.png' />
<img path='img/lv.png' />
<img path='img/mt.png' />
<img path='img/nl.png' />
<img path='img/pl.png' />
<img path='img/pt.png' />
<img path='img/ro.png' />
<img path='img/se.png' />
<img path='img/si.png' />
<img path='img/sk.png' />
</resources>
</control>
</manifest>
Note that we obviously create a folder for the CSS and images.
Now let’s try to imagine the logic of this component!
During the initialization of the component we will have to create the html element of the field (give it CSS classes), retrieve the different parameters, specify the onChange event of this element which will be able to replay our logic to check the VAT Number input.
We will therefore initialize private properties to make it cleaner
private _context: ComponentFramework.Context<IInputs>;
private _container: HTMLDivElement;
private _notifyOutputChanged: () => void;
private _vatNumberElement: HTMLInputElement;
private _vatNumberTypeElement: HTMLElement;
private _vatNumberChanged: EventListenerOrEventListenerObject;
private _vatNumber: string;
private _companyName: string;
private _companyAddress: string;
private _isValid: boolean;
private _displayDialog: string;
That will allow us to code our init function in this way:
Note that the vatNumberChanged function will be detailed later.
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement) {
this._context = context;
this._container = container;
this._notifyOutputChanged = notifyOutputChanged;
this._vatNumberChanged = this.vatNumberChanged.bind(this);
this._vatNumber = this._context.parameters.vatNumberfield == null || this._context.parameters.vatNumberfield.raw == null ? "" : this._context.parameters.vatNumberfield.raw.trim();
this._companyName = this._context.parameters.companyName == null || this._context.parameters.companyName.raw == null ? "" : this._context.parameters.companyName.raw;
this._companyAddress = this._context.parameters.companyAddress == null || this._context.parameters.companyAddress.raw == null ? "" : this._context.parameters.companyAddress.raw;
this._displayDialog = this._context.parameters.displayDialog == null || this._context.parameters.displayDialog.raw == null ? "" : this._context.parameters.displayDialog.raw;
this._container = document.createElement("div");
container.appendChild(this._container);
this._vatNumberElement = document.createElement("input");
this._vatNumberElement.setAttribute("type", "text");
if (this._vatNumber.length > 0)
this._vatNumberElement.setAttribute("title", this._vatNumber);
else
this._vatNumberElement.setAttribute("title", "Select to enter data");
this._vatNumberElement.setAttribute("class", "pcfvatinputcontrol");
this._vatNumberElement.addEventListener("change", this._vatNumberChanged);
this._vatNumberTypeElement = document.createElement("img");
this._vatNumberTypeElement.setAttribute("class", "pcfvatimagecontrol");
this._vatNumberTypeElement.setAttribute("height", "24px");
this._container.appendChild(this._vatNumberElement);
this._container.appendChild(this._vatNumberTypeElement);
this.SecurityEnablement(this._vatNumberElement);
}
During the execution it is necessary to not forget to check the visibility of the component but also to make sure your PCF component handles read-only and field-level security!
You can read this very good article by Scott Durow: It’s time to add some finishing touches to your PCF controls!
To manage more easily the notion of security I preferred to make it a function that takes as parameter our HTML Element, so the reuse of this code will be simpler.
/**
* Used to implement the security model about the targeted field
* @param _vatNumberElement 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.
*/
private SecurityEnablement(_vatNumberElement: HTMLInputElement): void {
let readOnly = this._context.mode.isControlDisabled; // If the form is diabled because it is inactive or the user doesn't have access isControlDisabled is set to true
// When a field has FLS enabled, the security property on the attribute parameter is set
let masked = false;
if (this._context.parameters.vatNumberfield.security) {
readOnly = readOnly || !this._context.parameters.vatNumberfield.security.editable;
masked = !this._context.parameters.vatNumberfield.security.readable;
}
if (masked)
this._vatNumberElement.setAttribute("placeholder", "*******");
else
this._vatNumberElement.setAttribute("placeholder", "Insert a VAT Number..");
if (readOnly)
this._vatNumberElement.readOnly = true;
else
this._vatNumberElement.readOnly = false;
}
Now, if you have a good understanding of the life cycle of a component, we will have to write our updateView function!
We will retrieve the value of the field, check its consistency (length in particular), check id the value in the input is really different from the real data, then insert it in our input element and finally call our logic function.
public updateView(context: ComponentFramework.Context<IInputs>): void {
let visible = this._context.mode.isVisible;
if (visible) {
this._vatNumber = this._context.parameters.vatNumberfield == null || this._context.parameters.vatNumberfield.raw == null ? "" : this._context.parameters.vatNumberfield.raw.trim();
if (this._vatNumber != null && this._vatNumber.length > 0 && this._vatNumber != this._vatNumberElement.value) {
this._vatNumberElement.value = this._vatNumber;
this.CheckVatNumber();
}
}
}
Now we will have to determine our logic with the call to the WebService which will be contained in the CheckVatNumber function!
/**
* Used to query the SOAP services and set the result to the appropriate fields.
*/
private CheckVatNumber(): void {
this.findAndSetImage("loading","gif");
if (this._vatNumberElement.value.length > 0) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('POST', 'https://cors-anywhere.herokuapp.com/http://ec.europa.eu/taxation_customs/vies/services/checkVatService', false);
// build SOAP request
var soadpRequest: string = "<?xml version='1.0' encoding='UTF-8'?>" +
"<SOAP-ENV:Envelope xmlns:ns0='urn:ec.europa.eu:taxud:vies:services:checkVat:types'" +
" xmlns:ns1='http://schemas.xmlsoap.org/soap/envelope/'" +
" xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'" +
" xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/'>" +
"<SOAP-ENV:Header/><ns1:Body><ns0:checkVat>" +
"<ns0:countryCode>" + this._vatNumberElement.value.slice(0, 2).toUpperCase() + "</ns0:countryCode>" +
"<ns0:vatNumber>" + this._vatNumberElement.value.slice(2) + "</ns0:vatNumber>" +
"</ns0:checkVat></ns1:Body></SOAP-ENV:Envelope>";
let isValid: boolean = false;
var _this = this;
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState == 4) {
if (xmlhttp.status == 200) {
var parser: DOMParser, xmlDoc: any;
parser = new DOMParser();
xmlDoc = parser.parseFromString(xmlhttp.responseText, "text/xml");
if (xmlDoc.getElementsByTagName("valid")[0] != undefined && xmlDoc.getElementsByTagName("valid")[0].childNodes[0].nodeValue === "true") {
_this._isValid = true;
if (xmlDoc.getElementsByTagName("name")[0] != undefined)
_this._companyName = xmlDoc.getElementsByTagName("name")[0].childNodes[0].nodeValue == null || xmlDoc.getElementsByTagName("name")[0].childNodes[0].nodeValue == undefined ? "" : <string>xmlDoc.getElementsByTagName("name")[0].childNodes[0].nodeValue;
if (xmlDoc.getElementsByTagName("address")[0] != undefined)
_this._companyAddress = xmlDoc.getElementsByTagName("address")[0].childNodes[0].nodeValue == null || xmlDoc.getElementsByTagName("address")[0].childNodes[0].nodeValue == undefined ? "" : <string>xmlDoc.getElementsByTagName("address")[0].childNodes[0].nodeValue;
_this.findAndSetImage(_this._vatNumberElement.value.slice(0, 2).toLowerCase(),"png");
}
else {
_this._isValid = false;
if (_this._displayDialog === "Both" || _this._displayDialog === "NotFound")
_this._context.navigation.openAlertDialog({ text: "No result found for the following VAT Number: " + _this._vatNumberElement.value });
_this.findAndSetImage("warning","png");
}
}
else {
if (_this._displayDialog === "Both" || _this._displayDialog === "ApiError")
_this._context.navigation.openAlertDialog({ text: "Problem with the remote service, status: " + xmlhttp.status });
_this.findAndSetImage("warning","png");
}
}
}
// Send the POST request
xmlhttp.setRequestHeader('Content-Type', 'text/xml');
xmlhttp.send(soadpRequest);
}
}
To summarize, this function will first set a loading image, retrieve the value of the input, design our request with the right parameters and send it!
If we encounter an error with the remote server we will display the error in a dialog box and display a warning image, according to the parameters specified in displayDialog.
If the result of the query is negative we will indicate that the VAT Number is not valid, display a message in a dialog box, according to the parameters specified in displayDialog, and display a warning image.
In case the result is positive, we will parse the answer to retrieve the different elements like the address, the name of the company etc… And in that case we will be able to use the flag of the country!
Note that the code is not really very clean and that we could go further and manage the different return statuses for example!
When changing the value of the input field, the vatNumberChanged function is called. Note that I only notify of a change if the value has really changed so there will not be another call to the updateView function! And when the notification takes place, the update view function will not replay the logic and the call because, as said above, a test is present.
/**
* Called when a change is detected in the phone number input.
*/
private vatNumberChanged(): void {
this._vatNumberElement.value = this._vatNumberElement.value.trim().replace(" ", "").toUpperCase();
this.CheckVatNumber();
if (this._vatNumber != this._vatNumberElement.value) {
this._vatNumber = this._vatNumberElement.value;
this._vatNumberElement.setAttribute("title", this._vatNumber);
this._notifyOutputChanged();
}
}
The final key: send the information back to the application using the getOutputs function of the framework.
public getOutputs(): IOutputs {
return {
vatNumberfield: this._vatNumber,
isVatNumberValid: this._isValid,
companyName: this._companyName,
companyAddress: this._companyAddress
};
}
Now all we have to do is insert the right CSS:
.pcfvatinputcontrol
{
border-color: transparent;
padding-right: 0.5rem;
padding-left: 0.5rem;
padding-bottom: 0px;
padding-top: 0px;
color: rgb(0,0,0);
box-sizing: border-box;
border-style: solid;
border-width: 1px;
line-height: 2.5rem;
font-weight:600;
font-size: 1rem;
height: 2.5rem;
margin-right: 0px;
margin-left: 0px;
text-overflow: ellipsis;
width: 80%
}
.pcfvatinputcontrol:hover
{
border-color: black;
}
.pcfvatimagecontrol
{
padding-left: 5px;
vertical-align: middle;
}
Okay now we only have the final touch left! Add a preview image for the component!
As you may have noticed in the manifest file, I declared a resource, called preview, in addition to those of the flags. So I was able to add it in the node <control> of the manifest.
The complete code of this component is available here : VATNumberValidator!
Demo
Video is available here (download it before trying to play it) : https://github.com/allandecastro/VATNumberValidator/blob/master/video.webm?raw=true
Improvements
Obviously, this component is far from being perfect but is here to demonstrate once again the usefulness of the Power Apps Component Framework, to guide you a little more on the creation of a component but also to show you that the complexity of a component can quickly be complicated to manage!
To improve this component, we could:
- Add a parameter to decide whether to clear the field in case of an error or result not found (only if in creation otherwise we keep the old value?).
- Add a parameter to decide whether to put the address and name in PascalCase format.
- Manage the different status codes sent back by the remote service.
- Manage messages in several languages.
- Add offline management to avoid trying to make a call if you are offline context.client.isOffline().
- Create the component using React.
I hope you enjoyed this blog post and that it will help you get started
This was originally posted here.
*This post is locked for comments