← Back to list

From Field to Functionality: Creating a Button-Enhanced PCF Control

Introduction

Shakishnavi · 2025-06-04 04:10 · 1 claps · 2.8 min read
#pcf #powerapps #model-driven-app #pcf-control
Open on Medium ↗

From Field to Functionality: Creating a Button-Enhanced PCF Control

Introduction

In the world of Power Apps, sometimes default field controls just don’t cut it. In this tutorial-style blog, we’ll explore how to create a Power Apps Component Framework (PCF) control that adds a button to a numeric field — specifically, an age field that increments and decrements when clicked.

This guide is ideal for beginners to PCF and covers the entire journey: from manifest setup to deploying your control in a model-driven app.

Why This Use Case?

Buttons aren’t native to input fields in Dataverse forms. By creating a PCF control, we:

  • Enhance user experience with an intuitive UI.
  • Learn the PCF lifecycle.
  • Understand binding data and deploying to model-driven apps.

Step 1: Prerequisites

Ensure you have the following installed:

  • Node.js (more than version 14.0)
  • Power Platform CLI (pac)
  • Visual Studio (with MSBuild)
  • Power Apps environment with a Model-Driven App

Install the PCF project template globally:

npm install -g pac pcf-scripts

Step 2: Create the PCF Control

pac pcf init --namespace MyNamespace --name AgeIncremental --template field
cd AgeIncremental
npm install

Here we are going to create an age field with increment and decrement buttons, We need to modify the generated ControlManifest.Input.xml file property with the relevant datatype accordingly:

<property name="sampleProperty" display-name-key="Property_Display_Key" description-key="Property_Desc_Key" of-type="Whole.None" usage="bound" required="true" />

Step 3: Write the TypeScript Logic

In index.ts, update the class to render the field with both + and - buttons:

import { IInputs, IOutputs } from "./generated/ManifestTypes";

export class numberControl implements ComponentFramework.StandardControl<IInputs, IOutputs> {
    private _input: HTMLInputElement;
    private _notifyOutputChanged: () => void;
    private _container: HTMLDivElement;
    private _value: number;

    constructor() {
        // Empty
    }

    public init(
        context: ComponentFramework.Context<IInputs>,
        notifyOutputChanged: () => void,
        state: ComponentFramework.Dictionary,
        container: HTMLDivElement
    ): void {
        this._value = context.parameters.sampleProperty.raw ?? 0;
        this._notifyOutputChanged = notifyOutputChanged;
        this._container = container;

        this._input = document.createElement("input");
        this._input.type = "number";
        this._input.value = this._value.toString();
        this._input.style.margin = "0 8px";
        this._input.onchange = () => {
            this._value = parseInt(this._input.value) || 0;
            this._notifyOutputChanged();
        };

        const incrementButton = document.createElement("button");
        incrementButton.textContent = "+";
        incrementButton.onclick = () => {
            this._value++;
            this._input.value = this._value.toString();
            this._notifyOutputChanged();
        };

        const decrementButton = document.createElement("button");
        decrementButton.textContent = "-";
        decrementButton.onclick = () => {
            this._value--;
            this._input.value = this._value.toString();
            this._notifyOutputChanged();
        };

        this._container.appendChild(decrementButton);
        this._container.appendChild(this._input);
        this._container.appendChild(incrementButton);
    }

    public updateView(context: ComponentFramework.Context<IInputs>): void {
        const newValue = context.parameters.sampleProperty.raw ?? 0;
        if (newValue !== this._value) {
            this._value = newValue;
            this._input.value = this._value.toString();
        }
    }

    public getOutputs(): IOutputs {
        return {
            sampleProperty: this._value
        };
    }

    public destroy(): void {
        // Optional cleanup code
    }
}

Step 4: Build and Package

npm run build
mkdir Solution
cd Solution
pac solution init --publisher-name developer --publisher-prefix dev
pac solution add-reference --path "../" (use the full path of the project)
# From inside the Solution folder:
msbuild /t:Restore
msbuild

Find the solution .zip under bin\Debug.

Step 5: Import into Power Apps

  1. Navigate to make.powerapps.com.
  2. Go to Solutions > Import.
  3. Upload the .zip file.
  4. Open your Model-Driven form.
  5. Select your field (bound to sampleProperty) > Click Change Control > Add your PCF control.

Output:

Common Issues and Fixes

❌ Control Not Showing in “Change Control” List

  • Ensure your property has usage="bound".
  • The field type in Dataverse must match the type in the manifest.
  • You must import it via a solution, not just build locally.

❌ TypeScript Errors: Property 'sampleProperty' does not exist on type 'IInputs'

  • Your manifest defines the property. Run npm run build again to regenerate typings.

❌ MSBuild Not Found

  • Ensure Visual Studio is installed with the .NET Desktop Build Tools.
  • Use the Developer Command Prompt or install MSBuild standalone.

With just a bit of TypeScript and XML, we’ve created a more interactive input field using PCF. This blog demonstrated how to bind a property and use simple UI logic for a + and — button.


메타데이터
post_id
c7aebf7dfde4
slug
from-field-to-functionality-creating-a-button-enhanced-pcf-control-c7aebf7dfde4
url
https://medium.com/@shakishnavi27/from-field-to-functionality-creating-a-button-enhanced-pcf-control-c7aebf7dfde4
canonical_url
https://medium.com/@shakishnavi27/from-field-to-functionality-creating-a-button-enhanced-pcf-control-c7aebf7dfde4
author_url
https://medium.com/@shakishnavi27
status
ok
fetched_at
2026-06-12 22:02:08