Introduction
The Power Platform is revolutionizing low-code development, but for advanced needs, PCF (Power Apps Component Framework) controls let you inject native TypeScript code into your Canvas or Model-Driven Power Apps. Imagine an interactive counter: a dynamic label displaying user input, a button that increments a counter, and a Power Fx output to bind to other controls. This advanced tutorial guides you step by step to create, test, and deploy such a control in 2026.
Why is this essential? PCF controls push low-code boundaries: optimal performance, JS library integration, dataset awareness for custom grids. PAC CLI makes automation professional. By the end, you'll bookmark this guide for your enterprise projects. Estimated time: 45 min. Prepare your dev environment.
Prerequisites
- Power Apps account with a developer environment (free at make.powerapps.com).
- Node.js 18+ and npm installed.
- Visual Studio Code with Power Platform Tools extensions.
- Power Platform CLI (PAC CLI) v1.12+.
- Advanced knowledge of TypeScript, Power Fx, and Dataverse.
Install PAC CLI
npm install -g @microsoft/powerplatform-cli --unsafe-perm=true
pac --version
pac auth create --url https://<votre-environnement>.crm.dynamics.comThis PowerShell script installs PAC CLI globally, checks the version, and creates an authentication profile for your Power Apps environment. Replace with your Dataverse instance. The --unsafe-perm flag avoids npm permission errors; authenticate via browser.
Initialize the PCF Project
With PAC CLI ready, initialize an empty PCF project. This generates the boilerplate structure: index.ts, manifest XML, package.json. We'll customize it for a CounterLabel control: text input, clickable button, numeric output (counter).
Initialize and Configure package.json
mkdir pcf-counterlabel && cd pcf-counterlabel
pac pcf init --namespace Contoso --name CounterLabel --template field
npm install
npm install --save-dev typescript@5.5.4
cat > package.json << 'EOF'
{
"name": "pcf-counterlabel",
"version": "0.0.1",
"description": "Advanced Counter Label PCF",
"private": true,
"main": "CounterLabel.js",
"scripts": {
"build": "gulp build",
"watch": "gulp watch",
"bundle": "gulp bundle",
"clean": "gulp clean",
"extension": "gulp extension"
},
"dependencies": {
"@microsoft/powerapps-component-framework": "~2.0.10"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/powerapps-component-framework": "~2.0.10",
"del": "^7.1.0",
"eslint": "^8.57.0",
"gulp": "^4.0.2",
"gulp-eslint": "^6.0.0",
"gulp-sourcemaps": "3.0.0",
"gulp-typedoc": "^2.2.1",
"gulp-typescript": "^6.0.0-alpha.1",
"typescript": "5.5.4",
"webpack": "^5.94.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0"
}
}
EOFThis bash script initializes the project with the field template, installs dependencies, and replaces package.json for 2026 compatibility (TypeScript 5.5, PCF 2.0). The template generates base files; we'll override them next. Run npm run build to verify.
Define the Manifest XML
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="Contoso" constructor="CounterLabel" version="0.0.1" display-name-key="CounterLabel" description-key="Dynamic counter label control" control-type="standard" preview-image="~/assets/images/Control.png">
<resources>
<code path="index.ts" order="1"/>
<css path="CounterLabel.css" order="1" />
<resx path="strings/CounterLabel.1033.resx" version="1.0" />
</resources>
<data-set name="sampleDataSet" display-name-key="DataSet" />
<property name="inputText" display-name-key="Text_Input" description-key="The text to display" of-type="SingleLine.Text" usage="input" required="true" />
<property name="counterValue" display-name-key="Counter_Value" description-key="Output counter" of-type="Whole.Number" usage="output" />
<feature-usage>
<uses-feature name="BoardingAvailable" required="true" />
</feature-usage>
<control-manifest-schemas>
<schema version="1.0" name="ControlManifestSchema" />
</control-manifest-schemas>
</control>
</manifest>This manifest defines the control with inputText input (string), counterValue output (number), and optional dataset. usage="output" allows assignment via Power Fx like CounterLabel_1.counterValue. Add a preview PNG in assets/images. Version for ALM.
Implement the Core TypeScript Code
The heart of the PCF is index.ts: lifecycle hooks (init, updateView, destroy), context for properties, notifications for outputs. Our control displays inputText + ' (' + count + ')', the button increments count and notifies the output.
Write the Complete index.ts
import { IInputs, IOutputs } from './generated/ManifestTypes';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
export class CounterLabel implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private _container: HTMLDivElement;
private _count: number = 0;
private _notifyOutputChanged: () => void;
private _rootElement: React.ComponentElement<any, any>;
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: any, container: HTMLDivElement): void {
this._container = document.createElement('div');
this._container.style.width = '100%';
this._container.style.height = '100%';
this._container.className = 'counter-container';
this._notifyOutputChanged = notifyOutputChanged;
container.appendChild(this._container);
this.renderReact();
}
public updateView(context: ComponentFramework.Context<IInputs>): void {
this._rootElement = React.createElement(CounterLabelComponent, {
inputText: context.parameters.inputText.formatted ? context.parameters.inputText.formatted : '',
count: this._count,
onIncrement: () => {
this._count++;
this._notifyOutputChanged();
}
});
ReactDOM.render(this._rootElement, this._container);
}
public getOutputs(): IOutputs {
return {
counterValue: this._count
};
}
public destroy(): void {
ReactDOM.unmountComponentAtNode(this._container);
}
private renderReact(): void {
this.updateView({} as any);
}
}
interface CounterLabelProps {
inputText: string;
count: number;
onIncrement: () => void;
}
const CounterLabelComponent: React.FC<CounterLabelProps> = ({ inputText, count, onIncrement }) => (
<div style={{ padding: '10px', fontFamily: 'Segoe UI', border: '1px solid #ccc', borderRadius: '4px' }}>
<label>{inputText} ({count})</label>
<button onClick={onIncrement} style={{ marginLeft: '10px', padding: '5px 10px' }}>
+1
</button>
</div>
);This code uses React for rendering (optional but professional), handles updateView for reactivity, and getOutputs() exposes the counter. notifyOutputChanged() triggers Power Fx updates. Full lifecycle prevents memory leaks. Add CSS for advanced styles.
Add CSS Styles
.counter-container {
display: flex;
align-items: center;
font-size: 14px;
}
.counter-container button {
background-color: #0078d4;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.2s;
}
.counter-container button:hover {
background-color: #106ebe;
}
@media (max-width: 480px) {
.counter-container {
flex-direction: column;
}
}Responsive CSS for mobile-first design with hover effects. Loaded via manifest, scoped to the container. Use Tailwind via CDN if needed, but vanilla for performance. Test in F12 dev tools.
Build and Test Locally
npm run build
pac solution init --publisher-name "Contoso" --publisher-prefix "contoso"
pac solution add-reference --path %CD%
npm run start
# Ouvrez https://localhost:8181 pour test harnessBuild compiles TS to JS and initializes an unmanaged solution. npm run start launches the webpack dev server with a test harness for live property testing. Debug JS in the browser. Once ready, use pac solution clone for managed.
Integrate into a Canvas Power App
Steps: 1. Use pac pcf push to import to production. 2. Create a Canvas app, add Custom Control > CounterLabel. 3. Bind inputText to TextInput1.Text, counterValue to Label2 via Power Fx: CounterLabel1_1.counterValue. Test clicks!
Power Fx Formulas for App Binding
// Label2.Text
CounterLabel1.counterValue
// Update inputText
UpdateContext({
varInput: TextInput1.Text
});
CounterLabel1.inputText = varInput
// Flow trigger on count > 5
If(CounterLabel1.counterValue > 5, Notify("High count!"))These Power Fx formulas bind inputs/outputs. UpdateContext refreshes input. Integrate with Power Automate flows via variable triggers. Scalable for datasets: iterate over collections.
Deploy the Solution
pac solution build --enhanced
mkdir bin/for%20package
copy %CD%/out/* bin/for%20package/
pac solution pack --zip-file bin/Contoso.CounterLabel.zip
pac solution import --path bin/Contoso.CounterLabel.zipEnhanced build for production, packs into a managed ZIP. Import auto-publishes the control. Use Azure DevOps CI/CD for ALM. Version ZIPs as artifacts.
Best Practices
- Performance: Use
context.mode.isOfflinefor IndexedDB caching; avoid heavy libraries. - Accessibility: Add ARIA labels, keyboard navigation (Enter for increment).
- Dataset-aware: Extend to
reacttemplate for grids; paginate withcontext.parameters.sampleDataSet. - Security: Validate inputs client-side; no secrets in code.
- Testing: Add Jest; mock
contextfor unit tests.
Common Errors to Avoid
- Forgetting
notifyOutputChanged(): outputs won't sync with Power Fx. - Ignoring
destroy(): memory leaks in multi-screen apps. - Malformed manifest: validate XML with VS Code schema.
- Building without
enhanced: missing debug symbols in production.
Next Steps
Dive into PCF datasets for custom grids (docs.microsoft.com/power-apps/developer). Integrate D3.js for charts. Check our Power Platform Learni trainings: advanced PL-400 certification. Example GitHub repo: github.com/learni-dev/pcf-counterlabel.