PCF Components: Build & Lifecycle
Build custom visual controls with TypeScript and React using the Power Apps Component Framework. Understand the component lifecycle, configure the manifest, and implement component interfaces.
Building custom controls
Think of PCF components like LEGO Technic pieces.
Standard LEGO bricks (built-in controls) build most things. But sometimes you need a motor, a gear, or a custom piece that does not exist in the standard set. PCF (Power Apps Component Framework) lets you build those custom pieces using TypeScript and React.
Your custom component follows a lifecycle: it is initialised (born), updated (data changes), asked for output (what value to save), and eventually destroyed (form closes). Understanding this lifecycle is the key to building components that work reliably.
The PCF component lifecycle
Every component goes through four phases:
| Method | When It Runs | What You Do |
|---|---|---|
| init | Component first loads | Set up DOM elements, event listeners, initial state. Called ONCE. |
| updateView | Data changes (from the platform) | Re-render the component with new data. Called MANY times. |
| getOutputs | Platform requests current value | Return the current value(s) to save to Dataverse. Called when user interacts. |
| destroy | Component is removed | Clean up event listeners, timers, memory. Called ONCE. |
// Simplified PCF component structure
export class StarRating implements ComponentFramework.StandardControl<IInputs, IOutputs> {
private container: HTMLDivElement;
private currentValue: number;
private notifyOutputChanged: () => void;
// Called ONCE when the component loads
public init(
context: ComponentFramework.Context<IInputs>,
notifyOutputChanged: () => void,
state: ComponentFramework.Dictionary,
container: HTMLDivElement
): void {
this.container = container;
this.notifyOutputChanged = notifyOutputChanged;
this.currentValue = context.parameters.ratingValue.raw || 0;
this.renderStars();
}
// Called when data changes from the platform
public updateView(context: ComponentFramework.Context<IInputs>): void {
this.currentValue = context.parameters.ratingValue.raw || 0;
this.renderStars();
}
// Called when the platform needs the current value
public getOutputs(): IOutputs {
return { ratingValue: this.currentValue };
}
// Called when the component is removed
public destroy(): void {
// Clean up event listeners, timers, etc.
}
private renderStars(): void {
// Render star HTML based on this.currentValue
// When user clicks a star: this.currentValue = n;
// Then call: this.notifyOutputChanged();
}
}
The notifyOutputChanged pattern
The notifyOutputChanged callback is critical. When a user interacts with your component (clicks a star, types in a field), you:
- Update your internal state (
this.currentValue = newValue) - Call
this.notifyOutputChanged()to tell the platform βmy value changedβ - The platform calls
getOutputs()to retrieve the new value - The platform saves the value to the bound field
If you forget to call notifyOutputChanged, the platform never knows the value changed and nothing saves. This is a common bug and exam topic.
The component manifest
The manifest (ControlManifest.Input.xml) defines your componentβs contract with the platform:
<?xml version="1.0" encoding="utf-8" ?>
<manifest>
<control namespace="NovaSoft"
constructor="StarRating"
version="1.0.0"
display-name-key="Star Rating"
description-key="A star rating control"
control-type="standard">
<!-- Bound property (the value this component reads/writes) -->
<property name="ratingValue"
display-name-key="Rating Value"
of-type="Whole.None"
usage="bound"
required="true" />
<!-- Input property (configuration, not saved to Dataverse) -->
<property name="maxStars"
display-name-key="Maximum Stars"
of-type="Whole.None"
usage="input"
required="false"
default-value="5" />
<resources>
<code path="index.ts" order="1" />
<css path="css/StarRating.css" order="1" />
</resources>
</control>
</manifest>
Key manifest concepts
| Element | Purpose | Example |
|---|---|---|
| namespace | Unique identifier for your org | NovaSoft |
| constructor | Class name in index.ts | StarRating |
| property (bound) | Data field the component reads/writes | ratingValue β saves to Dataverse |
| property (input) | Configuration value (not saved) | maxStars β set by the form designer |
| control-type | standard (field) or virtual (no bound property) | standard |
| data-set | For dataset components (grids) | Replaces property for collection binding |
| Feature | Field Component | Dataset Component |
|---|---|---|
| Binds to | A single column (field) | A view or collection of records |
| Manifest | Uses <property> with usage='bound' | Uses <data-set> element |
| Use case | Star rating, colour picker, toggle | Custom grid, chart, kanban board |
| Context data | context.parameters.fieldName | context.parameters.dataSet |
| Host | Form field, canvas control | Grid/subgrid, canvas gallery |
Scenario: Marcus designs a PCF manifest
Marcus at NovaSoft is building a colour-coded priority badge component. His manifest decisions:
- Bound property:
priorityValue(Whole.None) β reads the priority number from Dataverse - Input property:
colorScheme(SingleLine.Text) β lets admins configure βwarmβ or βcoolβ colours - Input property:
showLabel(TwoOptions) β toggle whether the priority name shows alongside the badge - No data-set β this is a field-level component (binds to one value)
The manifest defines the contract; the index.ts implements the visual rendering.
Marcus builds a PCF component for a progress bar. When the user drags the slider, the component should update the bound Dataverse field. The component renders correctly but the value never saves. What is the most likely cause?
π¬ Video coming soon
Next up: PCF Components: Package, Deploy & Advanced Features β getting your component from code to production, plus Device and Web API features.