What is the PowerApps Component Framework?
The PowerApps Component Framework (PCF) enables developers to create custom, reusable components for model-driven apps, canvas apps, and Power Pages. PCF controls provide enhanced user experiences beyond what's possible with out-of-the-box controls, allowing you to build rich, interactive components using modern web technologies like TypeScript and React. official PCF documentation.
Why Build PCF Controls?
- Enhanced UX: Create intuitive interfaces that standard controls can't provide
- Reusability: Build once, deploy across multiple apps and environments
- Modern Development: Use TypeScript, React, and modern tooling
- Full Control: Complete control over rendering and behavior
- Integration: Access to Web API for data operations within components
Development Environment Setup
Prerequisites
Install the following tools to start building PCF controls:
- Node.js: LTS version (16.x or later)
- Visual Studio Code: Recommended IDE with TypeScript support
- Power Platform CLI: Command-line tools for PCF development
- .NET SDK: Required for building solution packages
# Install Power Platform CLI via npm
npm install -g pac
# Or download from Microsoft
https://learn.microsoft.com/en-us/power-platform/developer/cli/introduction
Creating Your First PCF Project
Initialize a new PCF control project using the CLI:
# Create project directory mkdir MyCustomControl cd MyCustomControl # Initialize PCF component pac pcf init --namespace Contoso --name RatingControl --template field # Install dependencies npm install # Start development server with hot reload npm start watch
PCF Control Types
Field Controls
Field controls bind to a single field and replace the default field rendering. Ideal for custom input experiences.
- Star rating controls
- Color pickers
- Rich text editors
- Custom date/time pickers
- Slider controls
Dataset Controls
Dataset controls display collections of records with custom rendering. Perfect for grid replacements and visualizations.
- Custom grids with advanced filtering
- Card-based record displays
- Timeline visualizations
- Kanban boards
- Calendar views
Anatomy of a PCF Control
ControlManifest.Input.xml
The manifest file defines control properties, data bindings, and resources:
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<control namespace="Contoso" constructor="RatingControl"
version="1.0.0" display-name-key="Rating Control"
description-key="A custom star rating control"
control-type="standard">
<!-- Bound property (the field value) -->
<property name="ratingValue" display-name-key="Rating"
of-type="Whole.None" usage="bound" required="true"/>
<!-- Configuration properties -->
<property name="maxStars" display-name-key="Maximum Stars"
of-type="Whole.None" usage="input" required="false"
default-value="5"/>
<property name="starColor" display-name-key="Star Color"
of-type="SingleLine.Text" usage="input" required="false"
default-value="#FFD700"/>
<!-- Resources -->
<resources>
<code path="index.ts" order="1"/>
<css path="css/RatingControl.css" order="1"/>
<resx path="strings/RatingControl.1033.resx" version="1.0.0"/>
</resources>
</control>
</manifest>Index.ts - Control Implementation
The main TypeScript file implements the control lifecycle:
import { IInputs, IOutputs } from "./generated/ManifestTypes";
export class RatingControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private container: HTMLDivElement;
private notifyOutputChanged: () => void;
private currentValue: number;
private maxStars: number;
private starColor: string;
// Called when control is initialized
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this.container = container;
this.notifyOutputChanged = notifyOutputChanged;
this.maxStars = context.parameters.maxStars.raw || 5;
this.starColor = context.parameters.starColor.raw || "#FFD700";
this.currentValue = context.parameters.ratingValue.raw || 0;
this.renderControl();
}
// Called when any bound property changes
public updateView(context: ComponentFramework.Context<IInputs>): void {
this.currentValue = context.parameters.ratingValue.raw || 0;
this.renderControl();
}
// Returns the current value to Dataverse
public getOutputs(): IOutputs {
return {
ratingValue: this.currentValue
};
}
// Cleanup when control is removed
public destroy(): void {
// Clean up event listeners, timers, etc.
}
private renderControl(): void {
this.container.innerHTML = "";
const wrapper = document.createElement("div");
wrapper.className = "rating-container";
for (let i = 1; i <= this.maxStars; i++) {
const star = document.createElement("span");
star.className = i <= this.currentValue ? "star filled" : "star";
star.innerHTML = "★";
star.style.color = i <= this.currentValue ? this.starColor : "#ccc";
star.onclick = () => this.setRating(i);
wrapper.appendChild(star);
}
this.container.appendChild(wrapper);
}
private setRating(value: number): void {
this.currentValue = value;
this.notifyOutputChanged();
this.renderControl();
}
}Building with React
React Framework Support
PCF supports React for building complex, state-driven components:
# Initialize with React template pac pcf init --namespace Contoso --name ReactControl --template field --framework react # This creates a React-ready structure with: # - React and ReactDOM dependencies # - HelloWorld.tsx component # - Proper TypeScript configuration
React Component Example
// RatingComponent.tsx
import * as React from "react";
interface IRatingProps {
value: number;
maxStars: number;
starColor: string;
onChange: (value: number) => void;
disabled?: boolean;
}
export const RatingComponent: React.FC<IRatingProps> = ({
value, maxStars, starColor, onChange, disabled
}) => {
const [hoverValue, setHoverValue] = React.useState<number | null>(null);
const handleClick = (rating: number) => {
if (!disabled) {
onChange(rating);
}
};
return (
<div className="rating-component">
{[...Array(maxStars)].map((_, index) => {
const starValue = index + 1;
const isFilled = starValue <= (hoverValue ?? value);
return (
<span
key={index}
className={`star ${isFilled ? 'filled' : ''} ${disabled ? 'disabled' : ''}`}
style={{ color: isFilled ? starColor : '#ccc', cursor: disabled ? 'default' : 'pointer' }}
onClick={() => handleClick(starValue)}
onMouseEnter={() => !disabled && setHoverValue(starValue)}
onMouseLeave={() => setHoverValue(null)}
>
★
</span>
);
})}
</div>
);
};Integrating React in index.ts
import * as React from "react";
import * as ReactDOM from "react-dom";
import { RatingComponent } from "./RatingComponent";
export class RatingControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private container: HTMLDivElement;
private notifyOutputChanged: () => void;
private currentValue: number;
public init(context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement): void {
this.container = container;
this.notifyOutputChanged = notifyOutputChanged;
this.renderReactComponent(context);
}
public updateView(context: ComponentFramework.Context<IInputs>): void {
this.renderReactComponent(context);
}
private renderReactComponent(context: ComponentFramework.Context<IInputs>): void {
ReactDOM.render(
React.createElement(RatingComponent, {
value: context.parameters.ratingValue.raw || 0,
maxStars: context.parameters.maxStars.raw || 5,
starColor: context.parameters.starColor.raw || "#FFD700",
disabled: context.mode.isControlDisabled,
onChange: (value: number) => {
this.currentValue = value;
this.notifyOutputChanged();
}
}),
this.container
);
}
public destroy(): void {
ReactDOM.unmountComponentAtNode(this.container);
}
}Dataset Controls
Working with Datasets
Dataset controls receive collections of records to render:
// Manifest for dataset control <data-set name="dataSet" display-name-key="Records"> <property-set name="name" display-name-key="Name" of-type="SingleLine.Text"/> <property-set name="status" display-name-key="Status" of-type="OptionSet"/> <property-set name="dueDate" display-name-key="Due Date" of-type="DateAndTime.DateOnly"/> </data-set>
// Accessing dataset in TypeScript
public updateView(context: ComponentFramework.Context<IInputs>): void {
const dataset = context.parameters.dataSet;
if (!dataset.loading) {
const records: any[] = [];
dataset.sortedRecordIds.forEach(recordId => {
const record = dataset.records[recordId];
records.push({
id: recordId,
name: record.getFormattedValue("name"),
status: record.getFormattedValue("status"),
dueDate: record.getValue("dueDate"),
// Open record in form
openRecord: () => dataset.openDatasetItem(record.getNamedReference())
});
});
// Render records
this.renderGrid(records);
}
// Handle paging
if (dataset.paging.hasNextPage) {
// Show "Load More" button
}
}Accessing Platform Features
Web API Access
PCF controls can make Web API calls for additional data operations:
// Retrieve related records
const webApi = context.webAPI;
const result = await webApi.retrieveMultipleRecords(
"contact",
"?$select=fullname,emailaddress1&$filter=parentcustomerid eq " + accountId
);
result.entities.forEach(contact => {
console.log(contact.fullname);
});
// Create a record
const newTask = {
subject: "Follow up",
description: "Created from PCF control",
"regardingobjectid_account@odata.bind": "/accounts(" + accountId + ")"
};
const created = await webApi.createRecord("task", newTask);
console.log("Created task: " + created.id);Navigation and Dialogs
// Open a form
context.navigation.openForm({
entityName: "contact",
entityId: contactId,
openInNewWindow: false
});
// Show confirmation dialog
const confirmResult = await context.navigation.openConfirmDialog({
title: "Confirm Delete",
text: "Are you sure you want to delete this record?",
confirmButtonLabel: "Delete",
cancelButtonLabel: "Cancel"
});
if (confirmResult.confirmed) {
// Proceed with delete
}
// Show alert
await context.navigation.openAlertDialog({
title: "Success",
text: "Record saved successfully!"
});Packaging and Deployment
Building the Solution
Package your control into a Dataverse solution for deployment:
# Build the control npm run build # Create solution project cd .. pac solution init --publisher-name Contoso --publisher-prefix con # Add the PCF control to solution pac solution add-reference --path ./MyCustomControl # Build the solution msbuild /t:build /restore # Output: bin/Debug/YourSolution.zip
Deployment Options
- Manual Import: Upload solution.zip via make.powerapps.com
- Power Platform CLI: pac solution import --path solution.zip
- Azure DevOps: Automated deployment via pipelines
- Solution Packager: Include in larger solution packages
Best Practices
Performance Optimization
- Minimize DOM manipulations; use virtual DOM (React) when possible
- Debounce frequent updates to notifyOutputChanged()
- Lazy load heavy dependencies
- Use CSS for animations instead of JavaScript
- Implement proper cleanup in destroy() method
Accessibility
- Support keyboard navigation (Tab, Enter, Arrow keys)
- Include ARIA attributes for screen readers
- Ensure sufficient color contrast
- Provide focus indicators
- Test with accessibility tools
Testing
- Use the test harness (npm start watch) during development
- Write unit tests for business logic
- Test in multiple browsers
- Test on mobile devices
- Validate behavior when disabled or read-only
Conclusion
PCF controls unlock tremendous potential for creating rich, tailored user experiences in Power Apps. By leveraging modern web development practices with TypeScript and React, you can build sophisticated components that integrate seamlessly with the Power Platform. Start with simple field controls to learn the fundamentals, then progress to complex dataset controls as you gain experience.