Create a virtual PCF Control

This post provides a walkthrough of building a lightweight virtual React based PCF control (Figure 1). This particular control supports two display modes: a ‘No Unknown Yes’ slider and a ‘Percentage’ slider, configurable at design time

Figure 1

Configure the Control

Before creating the control, we’ll first walk through how it can be configured as a ‘No Unknown Yes’ slider

1.) Add a whole number column named cpl_employed to the form (Figure 2)

Figure 2

2.) Add the pcf control to the cpl_employed column (Figure 3)

Figure 3

Create the Control

The first step in creating the control is to execute the following line. This creates the control’s scaffolding. Note:

  • -ns: Namespace
  • -n: Name
  • -t: Template type. This can either be ‘field’ or ‘dataset’
  • -npm: Initialise npm. Will setup package.json and run npm install to download the necessary dependencies
  • -fw: Framework
pac pcf init -ns ContosoPcfControls -n SliderControl -t field -npm -fw react

Next, the control is updated with code based on references 1 & 2 (listed at the bottom of this post)

The resulting code for the completed control is as follows:

SliderControl
     - ControlManifest.Input.xml
     - index.ts
     - SliderComponent.tsx
     css
           - SliderControl.css
.eslintrc.json
.gitignore
package.json
package-lock.json
pcfconfig.json
README.md
SliderControl.pcfproj
tsconfig.json

The following code snippet shows the ControlManifest.Input.xml file. This serves as the cornerstone of the PCF control. It defines the control’s capabilities and ensures it is recognised by the Power Apps runtime. In essence, this file acts as the contract between the control and the Power Platform

Key aspects to note:

  • Line 4 – Declares the control as a virtual control
  • Lines 45 & 46 – References the platform provided React and Fluent UI libraries. This allows the control to leverage the shared resources instead of bundling them into the control’s bundle.js
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
  <control namespace="ContosoPcfControls" constructor="SliderControl" version="0.0.10" display-name-key="Contoso Slider Control" 
      description-key="Contoso Slider Control description" control-type="virtual" >
	  <!--external-service-usage node declares whether this 3rd party PCF control is using external service or not, if yes, this 
	     control will be considered as premium and please also add the external domain it is using.
    If it is not using any external service, please set the enabled="false" and DO NOT add any domain below. The "enabled" will be false by default.
    Example1:
      <external-service-usage enabled="true">
        <domain>www.Microsoft.com</domain>
      </external-service-usage>
    Example2:
      <external-service-usage enabled="false">
      </external-service-usage>
    -->
	  <external-service-usage enabled="false">
		  <!--UNCOMMENT TO ADD EXTERNAL DOMAINS
      <domain></domain>
      <domain></domain>
      -->
	  </external-service-usage>
	  <!-- property node identifies a specific, configurable piece of data that the control expects from CDS -->

	  <property name="value" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type="Whole.None" 
	     usage="bound" required="true"/>
	  <property name="configuration" display-name-key="Configuration_Name" description-key="Configuration_Desc" of-type="Enum" 
	     default-value="0" usage="input" required="true">
		  <value name="NoUnknownYes" display-name-key="No Unknown Yes" description-key="NoUnknownYes_Desc">0</value>
		  <value name="Percentage" display-name-key="Percentage" description-key="LivingArrangementPercentage_Desc">1</value>
	  </property>

	  <!--
      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"/>
		  <platform-library name="React" version="16.8.6" />
		  <platform-library name="Fluent" version="8.29.0" />
          <css path="css/SliderControl.css" order="1" />
		  <!-- UNCOMMENT TO ADD MORE RESOURCES
          <resx path="strings/ChoicesPickerReact.1033.resx" version="1.0.0" />
      -->
	  </resources>
	  <!-- UNCOMMENT TO ENABLE THE SPECIFIED API
    <feature-usage>
      <uses-feature name="Device.captureAudio" required="true" />
      <uses-feature name="Device.captureImage" required="true" />
      <uses-feature name="Device.captureVideo" required="true" />
      <uses-feature name="Device.getBarcodeValue" required="true" />
      <uses-feature name="Device.getCurrentPosition" required="true" />
      <uses-feature name="Device.pickFile" required="true" />
      <uses-feature name="Utility" required="true" />
      <uses-feature name="WebAPI" required="true" />
    </feature-usage>
    -->
  </control>
</manifest>

The following code snippet shows the index.ts file. It defines the lifecycle methods that define how the control behaves, renders and interacts with the Power Platform.

Key aspects to note:

Line 33 – Performs a destructurng assignment to extract control parameters, as defined in the manifest.

  • value = 1 (Corresponds to the value stored in the bound field cpl_employed)
  • configuration = 0 (Maps to the ‘NoUnknownYes’ display mode)

Line 35 – Evaluates whether the input parameters are present by checking if the condition is truthy

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import * as React from "react";
import { SliderComponent } from "./SliderComponent";

export class SliderControl implements ComponentFramework.ReactControl<IInputs, IOutputs> {
	private notifyOutputChanged: () => void;
	private selectedValue: number | undefined;

	/**
	 * 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): void {
		this.notifyOutputChanged = notifyOutputChanged;
	}

	/**
	 * 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>): React.ReactElement {
		const { value, configuration } = context.parameters;

		if (value && configuration) {
			return React.createElement(SliderComponent, {configuration: configuration.raw, value: value.raw, onChange: this.onChange});
		}
		return React.createElement("div");
	}

	private onChange = (newValue: number | undefined): void => {
		this.selectedValue = newValue;
		this.notifyOutputChanged();
	};

	/**
	 * 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 { value: this.selectedValue } as IOutputs;
	}

	/**
	 * 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
	}
}

The following code snippet shows the SliderComponent.tsx file. Key aspects to note:

Line 20 – React.memo (Definition from https://react.dev/reference/react/memo – Wrap a component in memo to get a memozied version of that component. This memoized version of your component will usually not be re-rendered when its parent component is re-rendered as long as its props have not changed.)

Line 25 – React.useCallback (Definition from https://react.dev/reference/react/useCallback – Call useCallback at the top level of your component to cache a function definition between re-renders)

import { Slider } from "@fluentui/react";
import * as React from "react";

enum NoUnknownYes {
    No = 0,
    Unknown = 1,
    Yes = 2
}
enum SliderType {
    NoUnknownYes = '0',
    Percentage = '1'
}

export interface SliderComponentProps {
    value: number | null;
    configuration: string | null;
    onChange: (newValue: number | undefined) => void;
}

export const SliderComponent = React.memo((props: SliderComponentProps) => {
    const { value, configuration, onChange } = props;
    const valueKey = value ?? undefined;
    const sliderValueFormat = (value: number) => `${value}%`;

    const onChangeSlider = React.useCallback(
        (value: number): void => {
            onChange(value);
        },
        [onChange]
    );

    if (configuration === SliderType.NoUnknownYes) {
        return (
            <div className="slider">
                <div className="nounknownyes">
                    <div className="No">No</div>
                    <div className="Unknown">Unknown</div>
                    <div className="Yes">Yes</div>
                </div>
                <Slider min={NoUnknownYes.No} max={NoUnknownYes.Yes} step={1} defaultValue={NoUnknownYes.Unknown} showValue={false} value={valueKey} onChange={onChangeSlider} vertical={false} />
            </div>
        );
    }
    else if (configuration === SliderType.Percentage) {
        return (
            <div className="slider">
                <Slider max={100} valueFormat={sliderValueFormat} showValue value={valueKey} onChange={onChangeSlider} />
            </div>
        );
    }
    else
        return null;
});
SliderComponent.displayName = "SliderComponent";

The code snippet below is the SliderControl.css

.slider {
    min-width: 100%;
    max-width: 100%;

}
.nounknownyes {
    display: flex;
    justify-content: space-between;
    flex-direction: row;
}

The code snippet below is package.json

{
  "name": "pcf-project",
  "version": "1.0.0",
  "description": "Project containing your PowerApps Component Framework (PCF) control.",
  "scripts": {
    "build": "pcf-scripts build",
    "clean": "pcf-scripts clean",
    "lint": "pcf-scripts lint",
    "lint:fix": "pcf-scripts lint fix",
    "rebuild": "pcf-scripts rebuild",
    "start": "pcf-scripts start",
    "refreshTypes": "pcf-scripts refreshTypes"
  },
  "dependencies": {
    "@fluentui/react": "^8.29.0",
    "react-dom": "^16.8.6"
  },
  "devDependencies": {
    "@microsoft/eslint-plugin-power-apps": "^0.2.6",
    "@types/node": "^18.8.2",
    "@types/powerapps-component-framework": "^1.3.4",
    "@typescript-eslint/eslint-plugin": "^5.39.0",
    "@typescript-eslint/parser": "^5.39.0",
    "eslint": "^8.24.0",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-promise": "^6.0.1",
    "eslint-plugin-react": "^7.31.8",
    "pcf-scripts": "^1",
    "pcf-start": "^1",
    "react": "^16.14.0",
    "typescript": "^4.8.4"
  }
}

The code snippet below is tsconfig.json

{
    "extends": "./node_modules/pcf-scripts/tsconfig_base.json",
    "compilerOptions": {
        "typeRoots": ["node_modules/@types"],
    }
}

Test the control

Referring to Figure 4 & 5, the slider is moved from ‘Unknown’ to ‘Yes’

Figure 4
Figure 5

Lifecycle of the control

Figure 6 – Sequence in which the control is executed
Figure 7

Appendix

This section describes how to test (very limited testing in this case) the slider component within a React app (Figure 8)

React App

Figure 8

1.) Create the React app

In the ‘Developer PowerShell’ of Visual Studio, type

  • npx create-react-app my-react-app –template typescript
  • cd my-react-app
  • npm run build
  • npm start
Figure 9
Figure 10

2.) Add the slider control to the newly created react app by adding or updating the following files (refer to Figures 11 thru to 15)

  • SliderComponent.tsx
  • App.tsx
  • App.css
  • package.json
  • tsconfig.json

Then in the ‘Developer PowerShell’ of Visual Studio, execute the commands

  • npm install
  • npm start

Note: if the following error is generated, make sure the case of each folder structure matches

[eslint] Plugin “react” was conflicted between “package.json >> eslint-config-react-app >> C:\Users\dminer\source\repos\products-framework\code\Pcf.Controls\my-react-app\node_modules\eslint-config-react-app\base.js and “BaseConfig >> C:\Users\dminer\source\repos\products-framework\Code\Pcf.controls\my-react-app\node_modules\eslint-config-react-app\base.js

Figure 11 – SliderComponent.tsx with newly added line 54
Figure 12 – App.css with newly added lines 40 to 49
Figure 13 – App.tsx updated with new contents
Figure 14 – package.json with the fluent references added on lines 19 & 20
Figure 15 – tsconfig.json

Further reading

Create and deploy a standard pcf control

References

1.) https://github.com/microsoft/PowerApps-Samples/tree/master/component-framework/ChoicesPickerReactControl/ChoicesPickerReact

2.) https://developer.microsoft.com/en-us/fluentui#/controls/web/slider

3.) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

4.) https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/if…else

5.) https://react.dev/reference/react/createElement

6.) Powerful Devs Conference 1:54:48

7.) https://create-react-app.dev/docs/getting-started

8.) Virtual React PCF Code Components

9.) Building awesome PCF’s using React Hooks – David Rivard

10.) Hooked to PCF – React Hooks in PCF Development – Power Apps Development Bootcamp

11.) https://learn.microsoft.com/en-us/training/modules/get-started-power-apps-component-framework/?%3FWT.mc_id=M365-MVP-5002876