Build a PCF Text Control

March 15th, 2020

🕓 8 minutes

I’m going to build a standard text control using the PowerApps Component Framework (PCF). The idea here is to replicate the out-of-box (OOB) text control available in model-driven apps as best I can.

Note: Custom controls built using PCF can be used in both model and canvas apps. I will only be considering model driven apps for now with a view to investigating canvas apps in the future.

This should help me understand the basic lifecycle methods provided by the [PCF] framework. I’ll discuss them as needed.

📃Requirements:

  1. Display the PCF control as a text input.
  2. Load the initial field value into the PCF control.
  3. Update the PCF control with the field value changes.
  4. Update the field when the PCF control value changes.

Before I get started, I’ll assume you have the boilerplate PCF control project structure setup. You can find a short guide I wrote on that here.

1. Display the PCF control as a text input

To do this, we need to create the text input DOM element. This should be done in the init method of the index.ts file. This method is called by the framework with the context and other parameters being passed in when your control is loading.

First, add this member to the controls class in index.ts.

1
private _inputElement: HTMLInputElement;

Next we create this input element from the init method as below.

1
2
3
4
// Create HTML text input element
this._inputElement = document.createElement("input");
this._inputElement.setAttribute("type", "text");
this._inputElement.setAttribute("placeholder", "---");

The container parameter passed in is a reference to the parent element the control is rendered in. Thats important because now, we add the below line to the init method to append the newly created _inputElement as a child to our container parameter which again, references the root element our control can render in.

1
2
// Add the text input to the DOM
container.appendChild(this._inputElement);

Lets test the progress so far by running npm run start. The test harness should spin up and you should see a standard text input in the control area. You can edit the value of the text input but nothing happens, yet.

Explanation

  • We made _inputElement a class member because we will want to access it later through another lifecycle method.
  • The init method is called once by the framework when loading which makes for a good place to initialize the things that will be reused in the component. More info here in the docs.

2. Load the initial field value into the PCF control

When the control is loading, a context object containing the initial value of the underlying field is passed into the init method. We need to extract that value and use it to populate the value of our input element. Add this new property as a class member to hold the value.

1
private _inputValue: string;

Next, in the init method, add these lines above where we appending the input to the container to extract the parameter passed in, and set the input elements value to it.

1
2
3
// Extract the input value and update the input element
this._inputValue = context.parameters.inputValue.raw || "";
this._inputElement.value = this._inputValue;

Note: If you’ve just been following along, you’ll get the below error. Without going into any extraordinary detail here, basically the ManifestTypes.d.ts file in the 📁generated folder does not contain a definition for inputValue.

1
Property 'inputValue' does not exist on type 'IInputs'

To fix this, [you guessed it] we need to update our manifest file which in effect (well, on the next build) will update our ManifestTypes.d.ts file. See below, we need to update the property node of the manifest file to specify inputValue as the name of our bound property. For everything you need to know about the manifest file, see here for the official documentation.

1
<property name="inputValue" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type="SingleLine.Text" usage="bound" required="true" />

Run npm run build and the manifest definition file will be updated and the errors should be gone. You’ll see the property on the IInputs type in the ManifestTypes.d.ts file.
Test the PCF control by running npm run start. In the test harness, the inputValue property in the data inputs on the right hand side should be loaded into the text input on load. Try editing the value in the data input and refreshing the page as below.

Image

This covers the on load scenario but as you have observed, the PCF control isn’t updated when a change takes places after the initial load. We’ll cover that next.

3. Update the PCF control with the field value changes

When the underlying field value changes, the framework calls the updateView method on our PCF control class. Like the init method, it passed in a context object that again, holds the value of the field, be it changed or not. Following the same flow as the init method, lets extract that value, update our classes _inputValue property, and update the value of our _inputElement. Add the below to the updateView method.

1
2
3
// Extract the input value and update the input element
this._inputValue = context.parameters.inputValue.raw || "";
this._inputElement.value = this._inputValue;

Test this and note that when the inputValue data input changes, the PCF control updates with the new value, as below. Magic! 🔮

Image

4. Update the field when the PCF control value changes

We need to notify the framework when ever a change has occurred to our PCF controls value so the framework can update the underlying record property. To do this we make use of the notifyOutputChanged method that is passed into the ìnit method of our control class. This methods in turn triggers the invocation of our getOutputs method which should return the latest value.

When do we call notifyOutputChanged

In most scenario’s, it makes sense to let the framework know we have changes we’d like persisted to the records property when ever we get them. However, the OOB text control does this on the blur event of the input. So, for that reason, we’ll invoke it when the PCF controls input raised the blur event. To get started, store the notifyOutputChanged method as a class member.

1
private _notifyOutputChanged: void;

Assign it in the init method.

1
this._notifyOutputChanged = notifyOutputChanged;

Next, created the below class function to intercept the on change event, store the new value, and notify the framework there are changes to be retrieved.

1
2
3
4
public onBlur = (event: Event): void => {
this._inputValue = this._inputElement.value;
this._notifyOutputChanged();
}

Using the above function, register an on blur event handler on the input element. This can be added just before you append the input element to the container in the init method.

1
2
// Attach on change event handler
this._inputElement.addEventListener("blur", this.onChange);

Finally, update the getOutputs method to return the latest input value.

1
2
3
4
5
public getOutputs(): IOutputs {
return {
inputValue: this._inputValue
};
}

Running the PCF control in the test harness now should allow you to update the PCF control value, lose focus on the input, and see the changes in the data input property on the right.

Image

Make it look pretty

You will have noticed that the styling isn’t anything special and certainly doesn’t match the control styling of other OOB controls. To fix that, add the below CSS to a file named styles.css in the 📁css folder. The folder should be in the same directory as index.ts, but if not, just create it.

Show CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
.textInputControl {
text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
white-space: normal;
word-break: normal;
font-style: normal;
font-variant: normal;
font-family: 'SegoeUI', 'Segoe UI';
margin-left: 0px;
margin-right: 0px;
width: 100%;
height: 2.5rem;
text-overflow: ellipsis;
box-sizing: border-box;
border-style: solid;
padding-top: 0px;
padding-bottom: 0px;
font-weight: 600;
font-size: 1rem;
color: rgb(0, 0, 0);
border-width: 1px;
padding-left: 0.5rem;
padding-right: 0.5rem;
line-height: 2.5rem;
border-color: transparent;
outline: none !important;
}

.textInputControl:hover{
border-color: rgb(102, 102, 102);
border-width: 1px;
border-style: solid;
margin-right: 0px;
margin-left: 0px;
padding-bottom: 0px;
padding-top: 0px;
padding-right: 0.5rem;
padding-left: 0.5rem;
color: rgb(0, 0, 0);
font-weight: 400;
font-size: 1rem;
text-overflow: ellipsis;
width: 100%;
line-height: 2.5rem;
height: 2.5rem;
box-sizing: border-box;
background-color: rgb(255, 255, 255);
}

Next, add the CSS file as a resource in the manifest file as below. Simply add the below line to the resources node.

1
<css path="styles.css" order="1" />

Add the textInputControl CSS class to the input element in the init method using the below line.

1
this._inputElement.setAttribute("class", "textInputControl");

Test once again and the style should be more of less the same as the OOB control.

Result

Source

You can get the source code/solutions for the control so far here on my GitHub. I’ve included some instructions on building and deploying the control also in the readme.

Moving forward

I plan to make some changes to this simple text control to handle some other behaviors that a true production ready control should. Including:

  • Complete styling
  • Handling read-only states
  • Handling security