Introduction to creating a code component

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 linear slider control that will look like the following image.

linear slider

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\pcf-samples; 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 LinearComponent and then go to that directory through cd LinearComponent:

    navigate to your directory

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

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

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

    initialized component project

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

    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. 

    open with visual studio code

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 TSLinearInputComponent node ControlManifest.Input.xml file to more meaningful values. In this example, you'll change the properties to 1.0.0, Linear Input Component, and Allows you to enter the numeric values using the visual slider, respectively:

      <control namespace="SampleNameSpace" constructor="TSLinearInputComponent" version="1.0.0" display-name-key="Linear Input Component" description-key="Allows you to enter the numeric values using the visual slider." control-type="standard">
    
  2. Replace the sample property with your own custom slider property. You will be using a custom type group called numbers in the property that you'll need to add.

       <property name="sliderValue" display-name-key="sliderValue_Display_Key" description-key="sliderValue_Desc_Key" of-type-group="numbers" usage="bound" required="true" />
    
  3. Insert the following node above your property to indicate the types of attributes that can use this control:

         <type-group name="numbers">
         <type>Whole.None</type>
         <type>Currency</type>
         <type>FP</type>
         <type>Decimal</type>
       </type-group>
    
  4. Update the node to include a reference to a CSS file named TS_LinearInputComponent.css that you'll create. Place this file into a CSS folder. The node will look like the following example:

    <css path="css/TS_LinearInputComponent.css" order="1" />
    
  5. 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="TSLinearInputComponent" version="1.0.0" display-name-key="Linear Input Component" description-key="Allows you to enter the numeric values using the visual slider." control-type="standard">
       <type-group name="numbers">
         <type>Whole.None</type>
         <type>Currency</type>
         <type>FP</type>
         <type>Decimal</type>
        </type-group>
       <property name="sliderValue" display-name-key="sliderValue_Display_Key" description-key="sliderValue_Desc_Key" of-type-group="numbers" usage="bound" required="true" />
      <resources>
        <code path="index.ts" order="1" />
        <css path="css/TS_LinearInputComponent.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 TSLinearInputComponent folder.

  2. Create a new TS_LinearInputComponent.css file inside the CSS subfolder.

  3. Add the following style content to the TS_LinearInputComponent.css file:

    .SampleNamespace\.TSLinearInputComponent input[type=range].linearslider {
      margin: 1px 0;
      background: transparent;
      -webkit-appearance: none;
      width: 100%;
      padding: 0;
      height: 24px;
      -webkit-tap-highlight-color: transparent
    }
    
    .SampleNamespace\.TSLinearInputComponent input[type=range].linearslider:focus {
      outline: none;
    }
    
    .SampleNamespace\.TSLinearInputComponent input[type=range].linearslider::-webkit-slider-runnable-track {
      background: #666;
      height: 2px;
      cursor: pointer
    }
    
    .SampleNamespace\.TSLinearInputComponent input[type=range].linearslider::-webkit-slider-thumb {
      background: #666;
      border: 0 solid #f00;
      height: 24px;
      width: 10px;
      border-radius: 48px;
      cursor: pointer;
      opacity: 1;
      -webkit-appearance: none;
      margin-top: -12px
    }
    
    .SampleNamespace\.TSLinearInputComponent input[type=range].linearslider::-moz-range-track {
      background: #666;
      height: 2px;
      cursor: pointer
    }
    
    .SampleNamespace\.TSLinearInputComponent input[type=range].linearslider::-moz-range-thumb {
      background: #666;
      border: 0 solid #f00;
      height: 24px;
      width: 10px;
      border-radius: 48px;
      cursor: pointer;
      opacity: 1;
      -webkit-appearance: none;
      margin-top: -12px
    }
    
    .SampleNamespace\.TSLinearInputComponent input[type=range].linearslider::-ms-track {
      background: #666;
      height: 2px;
      cursor: pointer
    }
    
    .SampleNamespace\.TSLinearInputComponent input[type=range].linearslider::-ms-thumb {
      background: #666;
      border: 0 solid #f00;
      height: 24px;
      width: 10px;
      border-radius: 48px;
      cursor: pointer;
      opacity: 1;
      -webkit-appearance: none;
    }
    
  4. Save the TS_LinearInputComponent.css file.

Build your code component

Before you can implement your component logic, build your component so that the appropriate types are generated, as specified in your ControlManifest.xml document.

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

npm run build

command.

build your project

The component is compiled into the out/controls/TSLinearInputComponent folder. The build artifacts include:

  • bundle.js - Bundled component source code.

  • ControlManifest.xml - Actual component manifest file that is uploaded to the Common Data Service organization.

Implement your code component's logic

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

  1. Open index.ts in Visual Studio or something similar.

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

    // Value of the field is stored and used inside the component
      private _value: number;
      // Power Apps component framework delegate which will be assigned to this object which would be called whenever any update happens.
      private _notifyOutputChanged: () => void;
      // label element created as part of this component
      private labelElement: HTMLLabelElement;
      // input element that is used to create the range slider
      private inputElement: HTMLInputElement;
      // reference to the component container HTMLDivElement
      // This element contains all elements of our code component example
      private _container: HTMLDivElement;
      // reference to Power Apps component framework Context object
      private _context: ComponentFramework.Context<IInputs>;
      // Event Handler 'refreshData' reference
      private _refreshData: EventListenerOrEventListenerObject;
    
  3. Replace the init method with the following logic:

    public init(
        context: ComponentFramework.Context<IInputs>,
        notifyOutputChanged: () => void,
        state: ComponentFramework.Dictionary,
        container: HTMLDivElement
      ) {
        this._context = context;
        this._container = document.createElement("div");
        this._notifyOutputChanged = notifyOutputChanged;
        this._refreshData = this.refreshData.bind(this);
        // creating HTML elements for the input type range and binding it to the function which refreshes the component data
        this.inputElement = document.createElement("input");
        this.inputElement.setAttribute("type", "range");
        this.inputElement.addEventListener("input", this._refreshData);
        //setting the max and min values for the component.
        this.inputElement.setAttribute("min", "1");
        this.inputElement.setAttribute("max", "1000");
        this.inputElement.setAttribute("class", "linearslider");
        this.inputElement.setAttribute("id", "linearrangeinput");
        // creating a HTML label element that shows the value that is set on the linear range component
        this.labelElement = document.createElement("label");
        this.labelElement.setAttribute("class", "TS_LinearRangeLabel");
        this.labelElement.setAttribute("id", "lrclabel");
        // retrieving the latest value from the component and setting it to the HTML elements.
        this._value = context.parameters.sliderValue.raw
          ? context.parameters.sliderValue.raw
          : 0;
        this.inputElement.value =
          context.parameters.sliderValue.formatted
            ? context.parameters.sliderValue.formatted
            : "0";
    
        this.labelElement.innerHTML = context.parameters.sliderValue.formatted
          ? context.parameters.sliderValue.formatted
          : "0";
        // appending the HTML elements to the component's HTML container element.
        this._container.appendChild(this.inputElement);
        this._container.appendChild(this.labelElement);
        container.appendChild(this._container);
      }
    
  4. Add the refreshData code below the previous code:

    /**
    * Updates the values to the internal value variable we are storing and also updates the html label that displays the value
    * @param context : The "Input Properties" containing the parameters, component metadata and interface functions
    */
    
    public refreshData(evt: Event): void {
    this._value = (this.inputElement.value as any) as number;
    this.labelElement.innerHTML = this.inputElement.value;
    this._notifyOutputChanged();
    }
    
  5. Replace the updateView method with the following logic:

      public updateView(context: ComponentFramework.Context<IInputs>): void {
        // storing the latest context from the control.
        this._value = context.parameters.sliderValue.raw
          ? context.parameters.sliderValue.raw
          : 0;
        this._context = context;
        this.inputElement.value =
    
          context.parameters.sliderValue.formatted
            ? context.parameters.sliderValue.formatted
            : "";
    
        this.labelElement.innerHTML = context.parameters.sliderValue.formatted
          ? context.parameters.sliderValue.formatted
          : "";
      }
    
  6. Replace getOuptuts with the following method:

      public getOutputs(): IOutputs {
        return {
          sliderValue: this._value
        };
    }
    
  7. Replace the destroy method with the following logic:

    public destroy() {
            this.inputElement.removeEventListener("input", this._refreshData);
    }
    
  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 TSLinearInputComponent
      implements ComponentFramework.StandardControl<IInputs, IOutputs> {
      // Value of the field is stored and used inside the component
      private _value: number;
      // Power Apps component framework delegate which will be assigned to this object which would be called whenever any update happens.
      private _notifyOutputChanged: () => void;
      // label element created as part of this component
      private labelElement: HTMLLabelElement;
      // input element that is used to create the range slider
      private inputElement: HTMLInputElement;
      // reference to the component container HTMLDivElement
      // This element contains all elements of our code component example
      private _container: HTMLDivElement;
      // reference to Power Apps component framework Context object
      private _context: ComponentFramework.Context<IInputs>;
      // Event Handler 'refreshData' reference
      private _refreshData: EventListenerOrEventListenerObject;
    
      constructor() {}
    
      public init(
        context: ComponentFramework.Context<IInputs>,
        notifyOutputChanged: () => void,
        state: ComponentFramework.Dictionary,
        container: HTMLDivElement
      ) {
        this._context = context;
        this._container = document.createElement("div");
        this._notifyOutputChanged = notifyOutputChanged;
        this._refreshData = this.refreshData.bind(this);
        // creating HTML elements for the input type range and binding it to the function which refreshes the component data
        this.inputElement = document.createElement("input");
        this.inputElement.setAttribute("type", "range");
        this.inputElement.addEventListener("input", this._refreshData);
        //setting the max and min values for the component.
        this.inputElement.setAttribute("min", "1");
        this.inputElement.setAttribute("max", "1000");
        this.inputElement.setAttribute("class", "linearslider");
        this.inputElement.setAttribute("id", "linearrangeinput");
        // creating a HTML label element that shows the value that is set on the linear range component
        this.labelElement = document.createElement("label");
        this.labelElement.setAttribute("class", "TS_LinearRangeLabel");
        this.labelElement.setAttribute("id", "lrclabel");
        // retrieving the latest value from the component and setting it to the HTML elements.
        this._value = context.parameters.sliderValue.raw
          ? context.parameters.sliderValue.raw
          : 0;
        this.inputElement.value =
          context.parameters.sliderValue.formatted
            ? context.parameters.sliderValue.formatted
            : "0";
    
        this.labelElement.innerHTML = context.parameters.sliderValue.formatted
          ? context.parameters.sliderValue.formatted
          : "0";
        // appending the HTML elements to the component's HTML container element.
        this._container.appendChild(this.inputElement);
        this._container.appendChild(this.labelElement);
        container.appendChild(this._container);
      }
    
      /**
       * Updates the values to the internal value variable we are storing and also updates the html label that displays the value
       * @param context : The "Input Properties" containing the parameters, component metadata and interface functions
       */
    
      public refreshData(evt: Event): void {
        this._value = (this.inputElement.value as any) as number;
        this.labelElement.innerHTML = this.inputElement.value;
        this._notifyOutputChanged();
      }
    
      public updateView(context: ComponentFramework.Context<IInputs>): void {
        // storing the latest context from the control.
        this._value = context.parameters.sliderValue.raw
          ? context.parameters.sliderValue.raw
          : 0;
        this._context = context;
        this.inputElement.value =
    
          context.parameters.sliderValue.formatted
            ? context.parameters.sliderValue.formatted
            : "";
    
        this.labelElement.innerHTML = context.parameters.sliderValue.formatted
          ? context.parameters.sliderValue.formatted
          : "";
      }
    
      public getOutputs(): IOutputs {
        return {
          sliderValue: this._value
        };
      }
    
      public destroy() {
        this.inputElement.removeEventListener("input", this._refreshData);
      }
    }
    

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

rebuild your component

  1. 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

      manifest file

  2. 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