Introduction
La Power Platform révolutionne le low-code, mais pour des besoins avancés, les contrôles PCF (Power Apps Component Framework) permettent d'injecter du code TypeScript natif dans vos Power Apps Canvas ou Model-Driven. Imaginez un compteur interactif : un label dynamique affichant une entrée utilisateur, un bouton incrémentant un compteur, et une sortie Power Fx pour binder à d'autres contrôles. Ce tutoriel avancé vous guide pas à pas pour créer, tester et déployer un tel contrôle en 2026.
Pourquoi c'est crucial ? Les PCF étendent les limites du low-code : performances optimales, intégration JS libraries, dataset-aware pour grids custom. Avec PAC CLI, l'automatisation est pro. À la fin, vous bookmarkederez ce guide pour vos projets enterprise. Durée estimée : 45 min. Préparez votre environnement dev.
Prérequis
- Compte Power Apps avec environnement développeur (gratuit sur make.powerapps.com).
- Node.js 18+ et npm installés.
- Visual Studio Code avec extensions Power Platform Tools.
- Power Platform CLI (PAC CLI) v1.12+.
- Connaissances avancées en TypeScript, Power Fx et Dataverse.
Installer PAC CLI
npm install -g @microsoft/powerplatform-cli --unsafe-perm=true
pac --version
pac auth create --url https://<votre-environnement>.crm.dynamics.comCe script PowerShell installe globalement PAC CLI, vérifie la version et crée un profil d'authentification pour votre environnement Power Apps. Remplacez par votre instance Dataverse. Le flag --unsafe-perm évite les erreurs de permissions npm ; authentifiez-vous via browser.
Initialiser le projet PCF
Une fois PAC CLI prêt, initialisez un projet PCF vide. Cela génère la structure boilerplate : index.ts, manifest XML, package.json. Nous customiserons pour un contrôle CounterLabel : entrée texte, bouton cliquable, sortie numérique (compteur).
Initialiser et configurer 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"
}
}
EOFCe bash init le projet avec template field, installe deps et remplace package.json pour compat 2026 (TypeScript 5.5, PCF 2.0). Le template génère les fichiers de base ; nous les overriderons ensuite. Lancez npm run build pour vérifier.
Définir le 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>Ce manifest définit le contrôle avec entrée inputText (string), sortie counterValue (number), et dataset optionnel. usage="output" permet d'assigner via Power Fx comme CounterLabel_1.counterValue. Ajoutez preview PNG dans assets/images. Versionnez pour ALM.
Implémenter le code TypeScript core
Le cœur du PCF est index.ts : lifecycle hooks (init, updateView, destroy), context pour properties, notifications pour outputs. Notre contrôle affiche inputText + ' (' + count + ')', bouton incrémente count et notifie sortie.
Écrire index.ts complet
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>
);Ce code utilise React pour render (optionnel mais pro), gère updateView pour reactivity, getOutputs() expose le compteur. notifyOutputChanged() trigger Power Fx updates. Lifecycle complet évite memory leaks. Ajoutez CSS pour styles avancés.
Ajouter les styles CSS
.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;
}
}CSS responsive pour mobile-first, hover effects. Chargé via manifest, scope au container. Utilisez Tailwind si besoin via CDN, mais vanilla pour perf. Testez en F12 devtools.
Build et test local
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 compile TS→JS, init solution unmanaged. npm run start lance webpack dev server avec harness pour tester properties live. Debuggez JS en browser. Une fois OK, pac solution clone pour managed.
Intégrer dans une Power App Canvas
Étapes : 1. pac pcf push pour importer en prod. 2. Créez une app Canvas, ajoutez Custom Control > CounterLabel. 3. Bind inputText à TextInput1.Text, counterValue à Label2 via Power Fx : CounterLabel1_1.counterValue. Testez clics !
Power Fx pour binding dans l'app
// 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!"))Ces formules Power Fx bindent entrée/sortie. UpdateContext refresh input. Intégrez à flows Power Automate via trigger sur variable. Scalable pour datasets : itérez sur collection.
Déployer la 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.zipBuild enhanced pour prod, pack en ZIP managed. Import auto publie le contrôle. Utilisez CI/CD Azure DevOps pour ALM. Versionnez ZIP en artifacts.
Bonnes pratiques
- Perf : Utilisez
context.mode.isOfflinepour cache IndexedDB ; évitez heavy libs. - Accessibilité : Ajoutez ARIA labels, keyboard nav (Enter pour increment).
- Dataset-aware : Étendez à
reacttemplate pour grids ; paginez aveccontext.parameters.sampleDataSet. - Sécurité : Validez inputs client-side ; pas de secrets en code.
- Tests : Ajoutez Jest ; mock
contextpour unit tests.
Erreurs courantes à éviter
- Oublier
notifyOutputChanged(): outputs ne sync pas avec Power Fx. - Ignorer
destroy(): leaks mémoire en app multi-écrans. - Manifest malformé : validez XML avec VS Code schema.
- Build sans
enhanced: debug symbols manquants en prod.
Pour aller plus loin
Plongez dans PCF datasets pour custom grids (docs.microsoft.com/power-apps/developer). Intégrez D3.js pour charts. Découvrez nos formations Power Platform Learni : certification PL-400 avancée. Repo GitHub exemple : github.com/learni-dev/pcf-counterlabel.