Building a PCF Todo List Control: A Complete Tutorial!

Meta Description: Learn how to build a custom Power Apps Component Framework (PCF) Todo List control using React, TypeScript, and Fluent UI. Complete step-by-step guide with code examples, deployment instructions, and best practices.

Keywords: PCF development, Power Apps Component Framework, React PCF control, TypeScript PCF, Fluent UI, Power Apps custom control, PCF tutorial, Power Apps development, custom control development

Reading Time: ~25 minutes | Difficulty: Intermediate | Last Updated: October 2025

Quick Summary

This comprehensive tutorial will teach you how to build a fully functional Power Apps Component Framework (PCF) Todo List control from scratch. By the end of this guide, you'll have:

Built a custom PCF control using React and TypeScript
Integrated with Power Apps data using datasets and context
Implemented CRUD operations for todo management
Added interactive features like selection and navigation
Deployed your control to a Power Apps environment

What You'll Learn:

  • PCF architecture and development fundamentals
  • React component development for Power Apps
  • Data binding and context management
  • Fluent UI integration and responsive design
  • Solution packaging and deployment strategies

Prerequisites: Basic knowledge of React, TypeScript, and Power Apps concepts.

Table of Contents

  1. Understanding PCF Fundamentals
  2. Creating the Starter Template
  3. Building UI with Static Data
  4. Adding Dynamic Data with Context
  5. Implementing Record Navigation
  6. Adding Task Completion Functionality
  7. Implementing Todo Selection
  8. Deployment and Testing
  9. Frequently Asked Questions (FAQ)
  10. Conclusion

Understanding PCF Fundamentals

What is Power Apps Component Framework (PCF)?

Power Apps Component Framework (PCF) is Microsoft's official development framework for creating custom controls in Power Apps, Power Pages, and model-driven applications. PCF enables developers to build sophisticated, interactive components using modern web technologies including React, TypeScript, and CSS.

PCF serves as a crucial bridge between Power Apps' no-code/low-code environment and traditional web development, allowing developers to extend platform capabilities with custom functionality that isn't available out-of-the-box.

Key Benefits of PCF Development:

  • Enhanced User Experience: Create custom UI components tailored to specific business needs
  • Modern Web Technologies: Leverage React, TypeScript, and modern CSS frameworks
  • Seamless Integration: Native integration with Power Apps data and APIs
  • Reusability: Build once, deploy across multiple Power Apps environments
  • Performance: Optimized rendering and data handling for enterprise applications

💡 Pro Tip: If you're new to Power Apps development, check out our Power Apps fundamentals guide before diving into PCF controls. Understanding the platform basics will accelerate your PCF learning curve.

Key PCF Concepts

Understanding these core concepts is essential for successful PCF development:

  1. Manifest File (ControlManifest.Input.xml):

    • Defines control metadata, properties, and capabilities
    • Specifies data sources and external service requirements
    • Configures control behavior and appearance settings
  2. Context Object:

    • Provides access to Power Apps data and APIs
    • Enables communication between your control and the Power Apps environment
    • Contains user information, device capabilities, and utility functions
  3. Dataset:

    • Represents entity records bound to the control
    • Provides methods for data manipulation and record selection
    • Handles data refresh and filtering operations
  4. Properties:

    • Configurable inputs that app makers can set
    • Enable customization without code changes
    • Support various data types (text, numbers, booleans, etc.)
  5. Lifecycle Methods:

    • init(): Control initialization and setup
    • updateView(): Renders the control UI
    • getOutputs(): Returns control output values
    • destroy(): Cleanup when control is removed

🔗 Related: We'll implement these lifecycle methods in detail in the Building UI with Static Data section.

PCF Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Power Apps    │    │   PCF Control   │    │   React UI      │
│   Environment   │◄──►│   (index.ts)    │◄──►│   Components    │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                              │
                              ▼
                       ┌─────────────────┐
                       │   Context API   │
                       │   (Data Access) │
                       └─────────────────┘

Creating the Starter Template

Prerequisites for PCF Development

Before building your first PCF control, ensure you have the following development environment set up:

Required Software:

  • Node.js (v14 or later) – JavaScript runtime for building and testing
  • npm or yarn – Package manager for dependencies
  • Power Apps CLI – Microsoft's official CLI for PCF development
  • Visual Studio Code (recommended) – IDE with excellent TypeScript and React support

Optional but Recommended:

  • Git – Version control for your PCF projects
  • Power Apps Developer Plan – Free environment for testing your controls
  • Fluent UI React – Microsoft's design system components

Step 1: Install Power Apps CLI

The Power Apps CLI is the essential tool for PCF development. Install it globally to access PCF commands from any directory:

npm install -g @microsoft/powerapps-cli

What this command does:

  • Installs the Power Apps CLI globally on your system
  • Enables PCF project creation, building, and testing commands
  • Provides access to solution packaging and deployment tools
  • Includes the test harness for local development and debugging

Step 2: Create a New PCF Project

Create your first PCF control project using the Power Apps CLI. This command sets up a complete development environment:

pac pcf init --namespace rtn --name TodoList --template dataset --framework react

Command Parameters Explained:

  • --namespace rtn: Sets your organization's namespace prefix
  • --name TodoList: Defines the control name (becomes rtn.TodoList)
  • --template dataset: Creates a data-bound control template
  • --framework react: Sets up React with TypeScript support

Project Structure Created:

  • TypeScript configuration files
  • React component templates
  • Manifest file for control definition
  • Build and test scripts
  • Fluent UI integration setup

📁 File Structure: The generated project includes all necessary files for PCF development. We'll explore the key files in the Building UI with Static Data section.

Step 3: Install Dependencies

npm install

Step 4: Install Additional Dependencies

For our Todo List control, we only need to install Tailwind CSS:

npm install tailwindcss@^4.1.11 @tailwindcss/cli@^4.1.11

The React framework and Fluent UI components are already included when using the --framework react flag, so we only need to add the styling framework. It's like getting a starter kit that actually includes the essentials for once.

Building UI with Static Data

Step 1: Create the Todo Model

Create TodoList/models/todo.model.ts:

export interface Todo {
  readonly id: string;
  readonly name: string;
  readonly description: string;
  isCompleted: boolean;
}

// Static data for initial development
export const staticTodos: Todo[] = [
  {
    id: "1",
    name: "Learn PCF Development",
    description:
      "Understand the fundamentals of Power Apps Component Framework",
    isCompleted: false,
  },
  {
    id: "2",
    name: "Build Todo Control",
    description: "Create a custom todo list control using React and TypeScript",
    isCompleted: true,
  },
  {
    id: "3",
    name: "Deploy to Power Apps",
    description: "Package and deploy the control to Power Apps environment",
    isCompleted: false,
  },
];

Step 2: Create the Todo Card Component

Create TodoList/components/todo-card.tsx:

import * as React from "react";
import {
  Card,
  CardFooter,
  CardHeader,
  Subtitle1,
} from "@fluentui/react-components";
import { Todo } from "../models/todo.model";

interface ITodoCard {
  todo: Todo;
}

export const TodoCard = ({ todo }: ITodoCard) => {
  return (
    <Card className="!flex-row gap-2 items-start">
      <div className="flex-grow flex flex-col gap-2">
        <CardHeader header={<Subtitle1>{todo.name}</Subtitle1>} />
        <CardFooter>{todo.description}</CardFooter>
      </div>
    </Card>
  );
};

Code Explanation:

  • Imports: We import React and Fluent UI components for the card layout, plus our Todo model
  • Interface: ITodoCard defines the props this component expects (just a todo object)
  • Component: The TodoCard function receives a todo prop and renders it using Fluent UI components
  • Layout: Uses a Card with flexbox layout (!flex-row gap-2 items-start) for horizontal arrangement
  • Content: Displays the todo name in the header and description in the footer
  • Styling: Uses Tailwind classes for responsive layout and spacing

This component displays a basic todo item with just the name and description. We'll add more functionality in later steps.

Step 3: Create the Main Todo List Component

Create TodoList/rtn-todo-list.tsx:

import * as React from "react";
import { TodoCard } from "./components/todo-card";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import { staticTodos } from "./models/todo.model";
import { Todo } from "./models/todo.model";

export const RtnTodoList = () => {
  const [todos, setTodos] = React.useState<Todo[]>(staticTodos);

  return (
    <FluentProvider theme={webLightTheme}>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 !p-4">
        {todos.map((todo) => (
          <TodoCard key={todo.id} todo={todo} />
        ))}
      </div>
    </FluentProvider>
  );
};

Code Explanation:

  • Imports: React for state management, TodoCard component, Fluent UI provider and theme, plus our static data and Todo type
  • State: useState hook manages the todos array, initialized with our static data
  • Provider: FluentProvider wraps the entire component to provide Fluent UI theming
  • Grid Layout: Uses Tailwind's responsive grid system:
    • grid-cols-1: 1 column on mobile
    • md:grid-cols-2: 2 columns on medium screens
    • lg:grid-cols-3: 3 columns on large screens
  • Mapping: todos.map() renders each todo as a TodoCard component
  • Key Prop: Each TodoCard gets a unique key prop (todo.id) for React's reconciliation

We're using a responsive grid layout here because apparently mobile-first design is still a thing. The component manages the state for todos and renders them in a nice grid.

Step 4: Update the Main Control Class

Update TodoList/index.ts:

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { RtnTodoList } from "./rtn-todo-list";
import * as React from "react";

export class TodoList
  implements ComponentFramework.ReactControl<IInputs, IOutputs>
{
  private notifyOutputChanged: () => void;

  constructor() {
    // Empty constructor
  }

  public init(
    context: ComponentFramework.Context<IInputs>,
    notifyOutputChanged: () => void,
    state: ComponentFramework.Dictionary
  ): void {
    this.notifyOutputChanged = notifyOutputChanged;
  }

  public updateView(
    context: ComponentFramework.Context<IInputs>
  ): React.ReactElement {
    return React.createElement(RtnTodoList, {});
  }

  public getOutputs(): IOutputs {
    return {};
  }

  public destroy(): void {
    // Add code to cleanup control if necessary
  }
}

Code Explanation:

  • Imports: TypeScript interfaces for inputs/outputs, our React component, and React itself
  • Class Declaration: TodoList implements the PCF ReactControl interface with typed inputs and outputs
  • Constructor: Empty constructor (PCF handles initialization)
  • init(): Called when the control is first created:
    • Stores the notifyOutputChanged callback for later use
    • Context contains all the data and APIs from Power Apps
  • updateView(): Called whenever the control needs to re-render:
    • Creates and returns our React component
    • This is where the magic happens – PCF renders our React UI
  • getOutputs(): Returns any output values (empty for now)
  • destroy(): Cleanup method called when control is removed

This is the main control class that Power Apps will instantiate. It's like the conductor of an orchestra, but the orchestra is your React components.

Step 5: Set Up Tailwind CSS

Create TodoList/styles/input.css:

@import "tailwindcss";

Code Explanation:

  • @import: Imports all Tailwind CSS utilities and components
  • Simple Setup: This single line gives us access to all Tailwind classes

Update package.json to include CSS build script:

{
  "scripts": {
    "watch:css": "tailwindcss -i ./TodoList/styles/input.css -o ./TodoList/styles/output.css --watch"
  }
}

Code Explanation:

  • Script Name: watch:css – a custom npm script for CSS compilation
  • Input File: -i ./TodoList/styles/input.css – reads from our input CSS file
  • Output File: -o ./TodoList/styles/output.css – writes compiled CSS to output file
  • Watch Mode: --watch – automatically recompiles when input changes

Tailwind CSS is like having a utility belt for styling. Instead of writing custom CSS, you just add classes to your HTML elements.

Step 6: Update the Manifest to Include CSS

Update TodoList/ControlManifest.Input.xml to include the CSS resource:

<resources>
  <code path="index.ts" order="1"/>
  <platform-library name="React" version="16.14.0" />
  <platform-library name="Fluent" version="9.46.2" />
  <css path="styles/output.css" order="1" />
</resources>

Code Explanation:

  • resources: Container for all files the control needs to load
  • code: Points to our main TypeScript entry point (index.ts)
  • platform-library: Pre-installed libraries (React and Fluent UI)
  • css: Our compiled Tailwind CSS file (styles/output.css)
  • order: Loading priority (1 = highest priority)

This tells Power Apps to include our compiled CSS file when the control loads.

Step 7: Test the Static UI

npm run start

This will start the test harness where you can see your control with static data.

Adding Dynamic Data with Context

Step 1: Update the Manifest

Update TodoList/ControlManifest.Input.xml to include property definitions:

<?xml version="1.0" encoding="utf-8" ?>
<manifest>
  <control namespace="rtn" constructor="TodoList" version="1.0.0" display-name-key="TodoList" description-key="TodoList description" control-type="virtual">
    <external-service-usage enabled="false">
    </external-service-usage>

    <data-set name="dataSet" display-name-key="DataSet">
    </data-set>

    <property name="name_logicalname" display-name-key="Name Logical Name" description-key="Todo item name" of-type="SingleLine.Text" usage="input" required="true" />
    <property name="description_logicalname" display-name-key="Description Logical Name" description-key="Todo item description" of-type="SingleLine.Text" usage="input" required="true" />
    <property name="iscompleted_logicalname" display-name-key="Is Completed Logical Name" description-key="Whether the todo item is completed" of-type="SingleLine.Text" usage="input" required="true" />
    <property name="entity_logicalname" display-name-key="Entity Logical Name" description-key="The logical name of the entity" of-type="SingleLine.Text" usage="input" required="true" />

    <resources>
      <code path="index.ts" order="1"/>
      <platform-library name="React" version="16.14.0" />
      <platform-library name="Fluent" version="9.46.2" />
      <css path="styles/output.css" order="1" />
    </resources>

    <feature-usage>
      <uses-feature name="WebAPI" required="true" />
    </feature-usage>
  </control>
</manifest>

Code Explanation:

  • control: Main control definition with namespace, constructor name, and version
  • data-set: Defines a dataset that will contain entity records from Power Apps
  • property: Input parameters that users configure in Power Apps:
    • name_logicalname: Field name for todo title
    • description_logicalname: Field name for todo description
    • iscompleted_logicalname: Field name for completion status
    • entity_logicalname: The entity type (e.g., "rtn_todo")
  • feature-usage: Declares we need WebAPI access for CRUD operations

This XML file is like a contract between your control and Power Apps. It tells Power Apps what your control can do and what data it needs.

Step 2: Generate TypeScript Types

npm run refreshTypes

This generates TodoList/generated/ManifestTypes.d.ts with proper TypeScript interfaces.

Step 3: Update the Todo Model for Dynamic Data

Update TodoList/models/todo.model.ts:

import type { IInputs } from "../generated/ManifestTypes";

export interface Todo {
  readonly id: string;
  readonly name: string;
  readonly description: string;
  isCompleted: boolean;
}

export function createTodoFromRecord(
  record: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord,
  inputs: IInputs
): Todo {
  return {
    id: record.getRecordId(),
    name: record.getValue(inputs.name_logicalname.raw!) as string,
    description: record.getValue(inputs.description_logicalname.raw!) as string,
    isCompleted: record.getValue(inputs.iscompleted_logicalname.raw!) === "1",
  };
}

Code Explanation:

  • Import: Gets TypeScript interfaces from generated manifest types
  • Todo Interface: Defines our data structure with readonly properties (except isCompleted)
  • createTodoFromRecord Function: Converts Power Apps records to Todo objects:
    • record.getRecordId(): Gets the unique ID from the Power Apps record
    • record.getValue(): Retrieves field values using logical names from inputs
    • inputs.name_logicalname.raw!: Gets the configured field name (e.g., "rtn_name")
    • === "1": Converts Power Apps boolean (1/0) to JavaScript boolean

This function converts Power Apps records into our Todo objects. It's like a translator between Power Apps data and our React components.

Step 4: Update the Todo Card Component for Dynamic Data

Update TodoList/components/todo-card.tsx to work with dynamic data:

import * as React from "react";
import {
  Card,
  CardFooter,
  CardHeader,
  Subtitle1,
  Button,
} from "@fluentui/react-components";
import { createTodoFromRecord } from "../models/todo.model";
import { CheckmarkFilled, DismissFilled } from "@fluentui/react-icons";
import type { IInputs } from "../generated/ManifestTypes";

interface ITodoCard {
  context: ComponentFramework.Context<IInputs>;
  record: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
}

export const TodoCard = ({ record, context }: ITodoCard) => {
  const todo = createTodoFromRecord(record, context.parameters);
  const [todoState, setTodoState] = React.useState(todo);

  return (
    <Card className="!flex-row gap-2 items-start">
      <div className="flex-grow flex flex-col gap-2">
        <CardHeader
          action={
            <Button
              icon={
                todoState.isCompleted ? (
                  <CheckmarkFilled color="green" />
                ) : (
                  <DismissFilled color="red" />
                )
              }
              onClick={() => {}} // Placeholder for now
            />
          }
          header={<Subtitle1>{todo.name}</Subtitle1>}
        />
        <CardFooter>{todo.description}</CardFooter>
      </div>
    </Card>
  );
};

Code Explanation:

  • Interface Update: Now receives context and record instead of a static todo object
  • Data Conversion: createTodoFromRecord() converts Power Apps record to Todo object
  • State Management: useState tracks the todo state for UI updates
  • Dynamic Icons: Shows green checkmark for completed, red X for incomplete todos
  • Placeholder Action: onClick={() => {}} is a placeholder for completion functionality
  • Context Access: context.parameters provides access to all configured properties

The component now works with real Power Apps data instead of static objects.

Step 5: Update the Main Component to Use Context

Update TodoList/rtn-todo-list.tsx:

import * as React from "react";
import { TodoCard } from "./components/todo-card";
import { FluentProvider, webLightTheme } from "@fluentui/react-components";
import type { IInputs } from "./generated/ManifestTypes";

export interface IRtnTodoList {
  context: ComponentFramework.Context<IInputs>;
}

export const RtnTodoList = ({ context }: IRtnTodoList) => {
  const sortedIds = context.parameters.dataSet.sortedRecordIds;
  const records = sortedIds.map((id) => context.parameters.dataSet.records[id]);

  return (
    <FluentProvider theme={webLightTheme}>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 !p-4">
        {records.map((record) => (
          <TodoCard
            key={record.getRecordId()}
            record={record}
            context={context}
          />
        ))}
      </div>
    </FluentProvider>
  );
};

Code Explanation:

  • Interface: IRtnTodoList defines that the component receives a context prop
  • Data Access: context.parameters.dataSet provides access to Power Apps records
  • Sorted Records: sortedRecordIds gives us records in the order Power Apps wants them displayed
  • Record Mapping: sortedIds.map() converts IDs to actual record objects
  • Props Passing: Each TodoCard receives the record and context for data access
  • No State: Removed local state since we're now using Power Apps data directly

The component now renders real data from Power Apps instead of static todos. The context object gives us access to the dataset and all the records.

Step 6: Update the Control Class

Update TodoList/index.ts:

import { IInputs, IOutputs } from "./generated/ManifestTypes";
import { RtnTodoList, IRtnTodoList } from "./rtn-todo-list";
import * as React from "react";

export class TodoList
  implements ComponentFramework.ReactControl<IInputs, IOutputs>
{
  private notifyOutputChanged: () => void;

  constructor() {
    // Empty
  }

  public init(
    context: ComponentFramework.Context<IInputs>,
    notifyOutputChanged: () => void,
    state: ComponentFramework.Dictionary
  ): void {
    this.notifyOutputChanged = notifyOutputChanged;
  }

  public updateView(
    context: ComponentFramework.Context<IInputs>
  ): React.ReactElement {
    const props: IRtnTodoList = {
      context,
    };
    return React.createElement(RtnTodoList, props);
  }

  public getOutputs(): IOutputs {
    return {};
  }

  public destroy(): void {
    // Add code to cleanup control if necessary
  }
}

Code Explanation:

  • Import Update: Now imports IRtnTodoList interface for proper typing
  • Props Object: Creates a props object with the context to pass to our React component
  • Context Passing: The context contains all Power Apps data and APIs
  • React.createElement: Creates the React component with the context as props
  • Type Safety: Uses TypeScript interfaces to ensure proper prop types

The control now passes the Power Apps context to our React component, giving it access to real data.

Implementing Record Navigation

Step 1: Update the Todo Card Component

Update TodoList/components/todo-card.tsx to include navigation functionality:

import * as React from "react";
import {
  Card,
  CardFooter,
  CardHeader,
  Subtitle1,
  Button,
} from "@fluentui/react-components";
import { createTodoFromRecord } from "../models/todo.model";
import {
  ArrowRightFilled,
  CheckmarkFilled,
  DismissFilled,
} from "@fluentui/react-icons";
import type { IInputs } from "../generated/ManifestTypes";

interface ITodoCard {
  context: ComponentFramework.Context<IInputs>;
  record: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
}

export const TodoCard = ({ record, context }: ITodoCard) => {
  const todo = createTodoFromRecord(record, context.parameters);
  const [todoState, setTodoState] = React.useState(todo);

  const goToDetailsPage = async () => {
    await context.navigation.openForm({
      entityName: context.parameters.entity_logicalname.raw!,
      entityId: todoState.id,
    });
  };

  return (
    <Card className="!flex-row gap-2 items-start">
      <div className="flex-grow flex flex-col gap-2">
        <CardHeader
          action={
            <Button
              icon={
                todoState.isCompleted ? (
                  <CheckmarkFilled color="green" />
                ) : (
                  <DismissFilled color="red" />
                )
              }
              onClick={() => {}} // Placeholder for now
            />
          }
          header={<Subtitle1>{todo.name}</Subtitle1>}
        />
        <CardFooter
          action={
            <Button icon={<ArrowRightFilled />} onClick={goToDetailsPage} />
          }
        >
          {todo.description}
        </CardFooter>
      </div>
    </Card>
  );
};

Code Explanation:

  • Navigation Function: goToDetailsPage is an async function that opens the record's detail form
  • context.navigation.openForm(): Power Apps API to open forms with parameters:
    • entityName: Uses the configured entity logical name from inputs
    • entityId: Uses the current record's ID to open the specific record
  • Navigation Button: Arrow button in CardFooter triggers the navigation when clicked

The navigation functionality allows users to click the arrow button and open the full record details in Power Apps.

Adding Task Completion Functionality

Step 1: Implement Toggle Complete Function

Update TodoList/components/todo-card.tsx:

export const TodoCard = ({ record, context, selected }: ITodoCard) => {
  const todo = createTodoFromRecord(record, context.parameters);
  const [todoState, setTodoState] = React.useState(todo);

  const toggleComplete = async () => {
    const newValue = !todoState.isCompleted;
    setTodoState((value) => ({ ...value, isCompleted: newValue }));

    // Update the record in Power Apps
    await context.webAPI.updateRecord(
      context.parameters.entity_logicalname.raw!,
      record.getRecordId(),
      {
        [context.parameters.iscompleted_logicalname.raw!]: newValue,
      }
    );

    // Refresh the dataset to reflect changes (data doesn't update itself you know...)
    context.parameters.dataSet.refresh();
  };

  return (
    <Card className="!flex-row gap-2 items-start">
      <div className="flex-grow flex flex-col gap-2">
        <CardHeader
          action={
            <Button
              icon={
                todoState.isCompleted ? (
                  <CheckmarkFilled color="green" />
                ) : (
                  <DismissFilled color="red" />
                )
              }
              onClick={toggleComplete}
            />
          }
          header={<Subtitle1>{todo.name}</Subtitle1>}
        />
        <CardFooter
          action={
            <Button icon={<ArrowRightFilled />} onClick={goToDetailsPage} />
          }
        >
          {todo.description}
        </CardFooter>
      </div>
    </Card>
  );
};

Code Explanation:

  • Optimistic Update: setTodoState() immediately updates the UI before server response for better user experience
  • WebAPI Call: context.webAPI.updateRecord() updates the record in Power Apps
  • Entity Name: Uses configured entity logical name from inputs
  • Record ID: Gets the specific record ID to update
  • Field Update: Updates the completion field using the configured logical name
  • Dataset Refresh: context.parameters.dataSet.refresh() refreshes the data to reflect changes
  • Button Click: onClick={toggleComplete} connects the button to the function

Key Features

  • Immediate Feedback: Users see the change right away, which feels responsive
  • Error Handling: If the update fails, you can revert the optimistic update
  • Flexibility: The control works with any entity as long as the logical names are configured correctly

Implementing Todo Selection

Step 1: Update the Todo Card Component Interface

First, update the interface to include the selected prop:

interface ITodoCard {
  context: ComponentFramework.Context<IInputs>;
  record: ComponentFramework.PropertyHelper.DataSetApi.EntityRecord;
  selected: boolean;
}

Code Explanation:

  • selected: boolean: New prop that indicates whether this todo is currently selected
  • Interface Update: Extends the existing interface to support selection state

Step 2: Add Selection Logic

Update TodoList/components/todo-card.tsx to include the selection functionality:

import {
  Card,
  CardFooter,
  CardHeader,
  Subtitle1,
  Button,
  Checkbox,
} from "@fluentui/react-components";

export const TodoCard = ({ record, context, selected }: ITodoCard) => {
  const todo = createTodoFromRecord(record, context.parameters);
  const [todoState, setTodoState] = React.useState(todo);

  const handleSelect = (id: string) => {
    const selectedIds = context.parameters.dataSet.getSelectedRecordIds();
    if (selectedIds.includes(id)) {
      context.parameters.dataSet.setSelectedRecordIds(
        selectedIds.filter((selectedId) => selectedId !== id)
      );
    } else {
      context.parameters.dataSet.setSelectedRecordIds([...selectedIds, id]);
    }
  };

  return (
    <Card className="!flex-row gap-2 items-start">
      <Checkbox
        checked={selected}
        onChange={() => handleSelect(todoState.id)}
      />
      <div className="flex-grow flex flex-col gap-2">
        <CardHeader
          action={
            <Button
              icon={
                todoState.isCompleted ? (
                  <CheckmarkFilled color="green" />
                ) : (
                  <DismissFilled color="red" />
                )
              }
              onClick={toggleComplete}
            />
          }
          header={<Subtitle1>{todo.name}</Subtitle1>}
        />
        <CardFooter
          action={
            <Button icon={<ArrowRightFilled />} onClick={goToDetailsPage} />
          }
        >
          {todo.description}
        </CardFooter>
      </div>
    </Card>
  );
};

Code Explanation:

  • Checkbox Import: Added Checkbox component from Fluent UI
  • handleSelect Function: Manages selection state using Power Apps dataset API:
    • getSelectedRecordIds(): Gets currently selected record IDs
    • setSelectedRecordIds(): Updates the selection state
    • Toggle logic: Adds ID if not selected, removes if already selected
  • Checkbox Component: Renders a checkbox that reflects the selection state
  • Layout Update: Checkbox is positioned at the start of the card layout

Step 3: Update the Main Component

Update TodoList/rtn-todo-list.tsx to pass the selected state:

export const RtnTodoList = ({ context }: IRtnTodoList) => {
  const selectedIds = context.parameters.dataSet.getSelectedRecordIds();
  const sortedIds = context.parameters.dataSet.sortedRecordIds;
  const records = sortedIds.map((id) => context.parameters.dataSet.records[id]);

  return (
    <FluentProvider theme={webLightTheme}>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 !p-4">
        {records.map((record) => (
          <TodoCard
            key={record.getRecordId()}
            record={record}
            context={context}
            selected={selectedIds?.includes(record.getRecordId())}
          />
        ))}
      </div>
    </FluentProvider>
  );
};

Code Explanation:

  • selectedIds: Gets the array of currently selected record IDs from the dataset
  • Selection Check: selectedIds?.includes(record.getRecordId()) checks if the current record is selected
  • Props Passing: Each TodoCard receives the selected boolean prop based on selection state
  • Optional Chaining: ?. safely handles cases where selectedIds might be undefined
  • Real-time Updates: Selection state updates automatically when users interact with checkboxes

This selection logic enables multi-select functionality with visual feedback through checkboxes that toggle the selection state.

Deployment and Testing

Step 1: Build the Control

npm run build

Code Explanation:

  • Build Command: Compiles TypeScript, bundles React components, and processes CSS
  • Output: Creates the control bundle in the out/controls/TodoList/ directory
  • Bundle Contents: Includes all JavaScript, CSS, and manifest files needed for deployment

This creates the control bundle in the out/controls/TodoList/ directory.

Step 2: Create Solution Package

Create a new folder for the solution and initialize it:

mkdir TodoListControl
cd TodoListControl
pac solution init --publisher-name "YourPublisher" --publisher-prefix "rtn"
pac solution add-reference --path ..

Code Explanation:

  • mkdir: Creates a separate folder for the solution package
  • cd: Navigates into the solution folder
  • pac solution init: Initializes a new solution with publisher details
  • pac solution add-reference: Links the solution to our PCF control project
  • –path ..: References the parent directory where our control is located

Step 3: Build Solution

dotnet build

Code Explanation:

  • dotnet build: Compiles the solution and creates the deployment package
  • Output: Generates a .zip file in the bin/Debug/ directory
  • Package Contents: Includes the PCF control and solution metadata for import into Power Apps

Step 4: Import to Power Apps

  1. Go to Power Apps maker portal
  2. Navigate to Apps → Solutions
  3. Import the generated solution file
  4. Add the control to a form or view

Step 5: Configure Control Properties

When adding the control to a form:

  1. DataSet: Bind to your todo entity
  2. Name Logical Name: Set to your name field (e.g., rtn_name)
  3. Description Logical Name: Set to your description field (e.g., rtn_description)
  4. Is Completed Logical Name: Set to your completion field (e.g., rtn_iscompleted)
  5. Entity Logical Name: Set to your entity name (e.g., rtn_todo)

Best Practices and Tips

1. Error Handling

const toggleComplete = async () => {
  try {
    const newValue = !todoState.isCompleted;
    setTodoState((value) => ({ ...value, isCompleted: newValue }));

    await context.webAPI.updateRecord(
      context.parameters.entity_logicalname.raw!,
      record.getRecordId(),
      {
        [context.parameters.iscompleted_logicalname.raw!]: newValue,
      }
    );

    context.parameters.dataSet.refresh();
  } catch (error) {
    console.error("Failed to update todo:", error);
    // Revert optimistic update
    setTodoState(todo);
  }
};

Always handle errors gracefully. Your users will thank you.

2. Performance Optimization

  • Use React.memo() for components that don't need frequent re-renders
  • Implement proper loading states
  • Use useCallback() for event handlers

3. Accessibility

  • Add proper ARIA labels
  • Ensure keyboard navigation works
  • Use semantic HTML elements

4. Testing

  • Test with different data scenarios (because users will always find ways to break your code)
  • Verify error handling
  • Test on different screen sizes (because apparently not everyone uses a 27-inch monitor)

Frequently Asked Questions (FAQ)

What is PCF in Power Apps?

PCF (Power Apps Component Framework) is Microsoft's development framework for creating custom controls in Power Apps, Power Pages, and model-driven applications using modern web technologies like React and TypeScript.

Do I need to know React to build PCF controls?

While not strictly required, knowledge of React significantly enhances your PCF development capabilities. The framework supports React components and provides excellent TypeScript integration.

Can PCF controls work with external APIs?

Yes, PCF controls can integrate with external APIs through the WebAPI feature. You can make HTTP requests to external services and consume data from various sources.

How do I debug PCF controls?

Use the built-in test harness (npm run start) for local debugging. The Power Apps CLI provides comprehensive debugging tools and browser developer tools integration.

Are PCF controls responsive?

Yes, PCF controls are fully responsive and can adapt to different screen sizes. Use CSS frameworks like Tailwind CSS or Fluent UI for responsive design.

Can I use third-party libraries in PCF?

Yes, you can include third-party libraries, but they must be bundled with your control. Consider bundle size and licensing requirements when selecting libraries.

Conclusion

This comprehensive tutorial has guided you through building a complete PCF Todo List control from concept to deployment. You've mastered:

Core PCF Concepts:

  • Understanding PCF architecture and lifecycle methods
  • Working with manifest files and context objects
  • Implementing data binding with datasets

Development Skills:

  • Creating React components with TypeScript
  • Integrating Fluent UI and Tailwind CSS
  • Building responsive, accessible user interfaces

Advanced Features:

  • Dynamic data integration with Power Apps
  • CRUD operations and record navigation
  • Multi-select functionality and state management
  • Error handling and performance optimization

Deployment & Best Practices:

  • Solution packaging and deployment
  • Configuration management
  • Testing and debugging strategies

Your PCF control is now production-ready and can be extended with advanced features like filtering, bulk operations, and custom theming.

Next Steps for Advanced PCF Development

Immediate Enhancements:

  • Add filtering and search capabilities
  • Implement bulk operations (delete, complete multiple)
  • Add drag-and-drop reordering functionality
  • Create custom themes and styling options

Advanced Features:

  • Add unit tests with Jest and React Testing Library
  • Implement offline support with data caching
  • Add accessibility features (ARIA labels, keyboard navigation)
  • Create reusable component libraries

Performance Optimization:

  • Implement virtual scrolling for large datasets
  • Add lazy loading for improved performance
  • Optimize bundle size and loading times
  • Add error boundaries and fallback UI

Integration Opportunities:

  • Connect to external APIs and services
  • Implement real-time data synchronization
  • Add custom business logic and validation
  • Create multi-tenant configurations

Ready to take your PCF development skills to the next level? Start with these enhancements and explore the vast possibilities of custom Power Apps controls!

Happy coding! 🚀

Share the Post:
LinkedIn
Print
Email
WhatsApp

Related Posts