Introduction to creating a code component

Completed

Power Apps provides a ton of functionality out of the box for app makers to build their apps, but sometimes there's a need to build a piece of UI that isn't included. Replacing a percentage value with a gauge, rendering a bar code rather than an ID, or replacing existing controls with controls that have more functionality—like a drag-and-drop grid view. You can also wrap the Power Apps component framework around existing components you have written in other web frameworks, like React or Angular.

Creating these components allows you to use the full breadth of the modern web development ecosystem: the libraries, frameworks, and other tools you may be familiar with, and packages that capability into a form that allows app makers to build apps with your code, as if it were an out of the box part of the platform.

Note

You can find other sample components—including those that use Angular or React—in our documentation.

Custom Power Apps components are frequently referred to as code components because they require custom code to implement them. They consist of three elements: a manifest, an implementation, and resources. In the following exercise, you will write your own custom code component: a hello world message that will look like the following image.

Screenshot of the hello pcf custom code component.

This component will listen to changes coming from the host app, and allow the user to make changes that are then pushed to the host app. The following steps will help you build this component.

Install Power Apps CLI

To get Microsoft Power Apps CLI, follow these steps:

  1. Install Npm (comes with Node.js) or Node.js (comes with npm). We recommend that you use LTS (Long-Term Support) version 10.15.3 or higher.

  2. Install .NET Framework 4.6.2 Developer Pack.

  3. If you don't already have Visual Studio 2017 or later, follow one of these options:

  4. Install Microsoft Power Apps CLI.

  5. To take advantage of all the latest capabilities, update the Power Apps CLI tooling to the latest version by using this command: pac install latest

Create a new component project

To create a new component project, follow these steps:

  1. Create a directory where you'll build your component. In this sample, you'll place the component in C:\source\hello-pcf; however, you can create your own directory. To create your own directory, you'll use a command prompt. From your source directory, create a directory named hello-pcf and then go to that directory through cd hello-PCF:

    Screenshot of command prompt used to create your directory.

  2. Initialize your component project by using Power Apps CLI through the following command:

    pac pcf init --namespace SampleNamespace --name HelloPCF --template field
    

    The following image shows an example of the output that you should see.

    Example of output you should see after initializing your project.

  3. Install the project build tools by using the command npm install. You might see some warnings displayed; however, you can ignore them.

    Example of command npm install warnings.

  4. Open your new project in a developer environment. You can use Visual Studio, Visual Studio Code, or any other environment that you prefer. In this example, you'll use Visual Studio Code, which provides a way to open a new window from a directory in a command prompt through the command code. 

    Screenshot Visual Studio Code with the new project opened.

Update your code component's manifest

Update the manifest file to accurately represent your control.

  1. Change the version, display-name-key, and description-key properties that are found in the HelloPCF node ControlManifest.Input.xml file to more meaningful values. In this example, you'll change the properties to 1.0.0, Hello PCF, and Says hello, respectively:

      <control namespace="SampleNamespace" constructor="HelloPCF" version="1.0.0" display-name-key="Hello PCF" description-key="Says hello" control-type="standard">
    
  2. Replace the sample property with your own custom Name property.

    <property name="Name" display-name-key="Name" description-key="A name" of-type="SingleLine.Text" usage="bound" required="true" />
    
  3. Update the <resources> node to include a reference to a CSS file named hello-pcf.css that you'll create. Place this file into a CSS folder. The node will look like the following example:

    <css path="css/hello-pcf.css" order="1" />
    
  4. After you've made updates, save the changes. Your manifest file should look similar to the following example:

    <?xml version="1.0" encoding="utf-8" ?>
    <manifest>
      <control namespace="SampleNamespace" constructor="HelloPCF" version="1.0.0" display-name-key="Hello PCF" description-key="Says hello" control-type="standard">
        <property name="Name" display-name-key="Name" description-key="A name" of-type="SingleLine.Text" usage="bound" required="true" />
        <resources>
          <code path="index.ts" order="1"/>
          <css path="css/hello-pcf.css" order="1" />
        </resources>
      </control>
    </manifest>
    

Add styling to your code component

To add styling to your code component, follow these steps:

  1. Create a new CSS subfolder under the HelloPCF folder.

  2. Create a new hello-pcf.css file inside the CSS subfolder.

  3. Add the following style content to the hello-pcf.css file:

    .SampleNamespace\.HelloPCF {
      font-size: 1.5em;
    }
    
  4. Save the hello-pcf.css file.

Build your code component

Before you can implement your component logic, you need to run a build on your component. This makes sure that the right TypeScript types are generated to match the properties in your ControlManifest.xml document.

Go back to your command prompt and build your project by using the following command.

npm run build

Screenshot of the command prompt used to build your project using the npm run build command.

The component is compiled into the out/controls/HelloPCF directory. The build artifacts include:

  • bundle.js - Bundled component source code.

  • ControlManifest.xml - Actual component manifest file that is uploaded to the Microsoft Dataverse organization.

Implement your code component's logic

To implement your code component's logic, follow these steps:

  1. Open index.ts in Visual Studio Code or your preferred code editor.

  2. Above the constructor method, insert the following private variables:

    // The PCF context object
    private context: ComponentFramework.Context<IInputs>;
    
    // The wrapper div element for the component
    private container: HTMLDivElement;
    
    // The callback function to call whenever your code has made a change to a bound or output property
    private notifyOutputChanged: () => void;
    
    // Flag to track if the component is in edit mode or not
    private isEditMode: boolean;
    
    // Tracks the event handler so we can destroy it when done
    private buttonClickHandler: EventListener;
    
    // Tracking variable for the name property
    private name: string | null;
    
  3. Replace the init method with the following logic:

    public init(
      context: ComponentFramework.Context<IInputs>,
      notifyOutputChanged: () => void,
      state: ComponentFramework.Dictionary,
      container: HTMLDivElement
    ) {
      // Track all the things
      this.context = context;
      this.notifyOutputChanged = notifyOutputChanged;
      this.container = container;
      this.isEditMode = false;
      this.buttonClickHandler = this.buttonClick.bind(this);
    
      // Create the span element to hold the hello message
      const message = document.createElement("span");
      message.innerText = `Hello ${this.isEditMode ? "" : context.parameters.Name.raw}`;
    
      // Create the textbox to edit the name
      const textbox = document.createElement("input");
      textbox.type = "text";
      textbox.style.display = this.isEditMode ? "block" : "none";
      if (context.parameters.Name.raw) {
        textbox.value = context.parameters.Name.raw;
      }
    
      // Wrap the two above elements in a div to box out the content
      const messageContainer = document.createElement("div");
      messageContainer.appendChild(message);
      messageContainer.appendChild(textbox);
    
      // Create the button element to switch between edit and read modes
      const button = document.createElement("button");
      button.textContent = this.isEditMode ? "Save" : "Edit";
      button.addEventListener("click", this.buttonClickHandler);
    
      // Add the message container and button to the overall control container
      this.container.appendChild(messageContainer);
      this.container.appendChild(button);
    }
    
  4. Add a buttonClick method below init with the following logic:

    // The event handler for the button's click event
    public buttonClick() {
      // Get our controls via DOM queries
      const textbox = this.container.querySelector("input")!;
      const message = this.container.querySelector("span")!;
      const button = this.container.querySelector("button")!;
    
      // If not in edit mode, copy the current name value to the textbox
      if (!this.isEditMode) {
        textbox.value = this.name ?? "";
      } else if (textbox.value != this.name) {
        // if in edit mode, copy the textbox value to name and call the motify callback
        this.name = textbox.value;
        this.notifyOutputChanged();
      }
    
      // flip the mode flag
      this.isEditMode = !this.isEditMode; 
    
      // Set up the new output based on changes
      message.innerText = `Hello ${this.isEditMode ? "" : this.name}`;
    
      textbox.style.display = this.isEditMode ? "inline" : "none";
      textbox.value = this.name ?? "";
    
      button.textContent = this.isEditMode ? "Save" : "Edit";
    }
    
  5. Replace the updateView method with the following logic:

    public updateView(context: ComponentFramework.Context<IInputs>): void {
      // Checks for updates coming in from outside
      this.name = context.parameters.Name.raw;
      const message = this.container.querySelector("span")!;
      message.innerText = `Hello ${this.name}`;
    }
    
  6. Replace getOuptuts with the following method:

    public getOutputs(): IOutputs {
      return {
        // If our name variable is null, return undefined instead
        Name: this.name ?? undefined
      };
    }
    
  7. Replace the destroy method with the following logic:

    public destroy() {
      // Remove the event listener we created in init
      this.container.querySelector("button")!.removeEventListener("click", this.buttonClickHandler);
    }
    
  8. After you've made the updates, your index.ts file should look similar to the following example:

    import {IInputs, IOutputs} from "./generated/ManifestTypes";
    
    export class HelloPCF implements ComponentFramework.StandardControl<IInputs, IOutputs> {
      // The PCF context object
      private context: ComponentFramework.Context<IInputs>;
    
      // The wrapper div element for the component
      private container: HTMLDivElement;
    
      // The callback function to call whenever your code has made a change to a bound or output property
      private notifyOutputChanged: () => void;
    
      // Flag to track if the component is in edit mode or not
      private isEditMode: boolean;
    
      // Tracks the event handler so we can destroy it when done
      private buttonClickHandler: EventListener;
    
      // Tracking variable for the name property
      private name: string | null;
    
      /**
      * Empty constructor.
      */
      constructor()
      {
    
      }
    
      /**
      * Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
      * Data-set values are not initialized here, use updateView.
      * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
      * @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
      * @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
      * @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
      */
      public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container:HTMLDivElement)
      {
        // Track all the things
        this.context = context;
        this.notifyOutputChanged = notifyOutputChanged;
        this.container = container;
        this.isEditMode = false;
        this.buttonClickHandler = this.buttonClick.bind(this);
    
        // Create the span element to hold the hello message
        const message = document.createElement("span");
        message.innerText = `Hello ${this.isEditMode ? "" : context.parameters.Name.raw}`;
    
        // Create the textbox to edit the name
        const textbox = document.createElement("input");
        textbox.type = "text";
        textbox.style.display = this.isEditMode ? "block" : "none";
        if (context.parameters.Name.raw) {
          textbox.value = context.parameters.Name.raw;
        }
    
        // Wrap the two above elements in a div to box out the content
        const messageContainer = document.createElement("div");
        messageContainer.appendChild(message);
        messageContainer.appendChild(textbox);
    
        // Create the button element to switch between edit and read modes
        const button = document.createElement("button");
        button.textContent = this.isEditMode ? "Save" : "Edit";
        button.addEventListener("click", this.buttonClickHandler);
    
        // Add the message container and button to the overall control container
        this.container.appendChild(messageContainer);
        this.container.appendChild(button);
      }
    
      // The event handler for the button's click event
      public buttonClick() {
        // Get our controls via DOM queries
        const textbox = this.container.querySelector("input")!;
        const message = this.container.querySelector("span")!;
        const button = this.container.querySelector("button")!;
    
        // If not in edit mode, copy the current name value to the textbox
        if (!this.isEditMode) {
          textbox.value = this.name ?? "";
        } else if (textbox.value != this.name) {
          // if in edit mode, copy the textbox value to name and call the motify callback
          this.name = textbox.value;
          this.notifyOutputChanged();
        }
    
        // flip the mode flag
        this.isEditMode = !this.isEditMode; 
    
        // Set up the new output based on changes
        message.innerText = `Hello ${this.isEditMode ? "" : this.name}`;
    
        textbox.style.display = this.isEditMode ? "inline" : "none";
        textbox.value = this.name ?? "";
    
        button.textContent = this.isEditMode ? "Save" : "Edit";
      }
    
      /**
      * Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
      * @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
      */
      public updateView(context: ComponentFramework.Context<IInputs>): void
      {
        // Checks for updates coming in from outside
        this.name = context.parameters.Name.raw;
        const message = this.container.querySelector("span")!;
        message.innerText = `Hello ${this.name}`;
      }
    
      /** 
      * It is called by the framework prior to a control receiving new data. 
      * @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
      */
      public getOutputs(): IOutputs
      {
        return {
          // If our name variable is null, return undefined instead
          Name: this.name ?? undefined
        };
      }
    
      /** 
      * Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
      * i.e. cancelling any pending remote calls, removing listeners, etc.
      */
      public destroy(): void
      {
        // Remove the event listener we created in init
        this.container.querySelector("button")!.removeEventListener("click", this.buttonClickHandler);
      }
    }
    

Rebuild and run your code component

To rebuild and run your code component, follow these steps:

  1. Now that your component's logic is implemented, go back to your command prompt to rebuild it by using this command:

    npm run build
    

    In command prompt rebuild your component using the command.

  2. Run your component in Node's test harness by running npm start. You can also enable watch mode to ensure that any changes to the following assets are made automatically without having to restart the test harness by using the npm start watch command.

    • index.ts file.

    • ControlManifest.Input.xml file.

    • Imported libraries in index.ts.

    • All resources listed in the manifest file

    screenshot of the npm start watch command.

  3. Observe your control in the test harness by going to the hosting address in a browser window (the window likely popped up for you automatically, but you can also reference the address as found in the command window, too).

    PowerApps component framework test environment