SharePoint Framework - Build Custom Controls For Web Part Property Pane

In this article, we will build custom controls for property pane and will develop a list dropdown selection property pane.

Overview

 
SharePoint Framework web parts support various predefined typed objects (e.g. textbox, checkbox, link, slider, etc.) to define properties for our SPFx web part. These predefined typed objects are sufficient in most cases; however, to meet certain business scenarios, we have to go beyond that and create our own custom controls. One classic example of this is list selection for the web part. We can have a simple textbox in web part property pane to allow the user to enter the SharePoint list name. However, listing all available SharePoint lists in a dropdown will give a more seamless experience to the end users.
 

Create SPFx Solution

 
Open the command prompt. Create a directory for SPFx solution.
  1. md spfx-customcontrol-propertypane  
Navigate to the above created directory.
  1. cd spfx-customcontrol-propertypane  
Run the Yeoman SharePoint Generator to create the solution. 
  1. yo @microsoft/sharepoint  
Yeoman generator will present you with the wizard by asking questions about the solution to be created. 
 
SharePoint Framework - Build Custom Controls For Web Part Property Pane 
  
Solution Name: Hit Enter to have default name (spfx-customcontrol-propertypane in this case) or type in any other name for your solution.
Selected choice: Hit Enter
 
Target for component: Here, we can select the target environment where we are planning to deploy the client web part, i.e., SharePoint Online or SharePoint OnPremise (SharePoint 2016 or 2019 onwards).
Selected choice: SharePoint Online only (latest)
 
Place of files: We may choose to use the same folder or create a subfolder for our solution.
Selected choice: Use the current folder
 
Deployment option: Selecting Y will allow the app to be deployed instantly to all sites and will be accessible everywhere.
Selected choice: Y
 
Permissions to access web APIs: Choose if the components in the solution require permissions to access web APIs that are unique and not shared with other components in the tenant.
Selected choice: N (solution contains unique permissions)
 
Type of client-side component to create: We can choose to create a client-side web part or an extension.
Selected choice: WebPart
 
Web Part Name: Hit Enter to select the default name or type in any other name.
Selected choice: CustomPropertyPaneDemo
 
Web part description: Hit Enter to select the default description or type in any other value.
Selected choice: Hit Enter
 
Framework to use: Select any JavaScript framework to develop the component. Available choices are No JavaScript Framework, React, and Knockout.
Selected choice: React
 
Once the scaffolding is completed, lock down the version of project dependencies by running the below command.
  1. npm shrinkwrap  
On the command prompt, type this command to open the solution in a code editor of your choice.
  1. code .  

 

Define Web Part Property

 
We are building a custom control to display the lists from the current SharePoint site. We will define a web part property for storing the user selected list from the web part property pane.
  1. Open CustomPropertyPaneDemoWebPart.manifest.json under “\src\webparts\customPropertyPaneDemo” folder.
  2. Rename the default description property to listName.

    SharePoint Framework - Build Custom Controls For Web Part Property Pane

  3. Open “src\webparts\customPropertyPaneDemo\components\ICustomPropertyPaneDemoProps.ts” and update it to use listName property. 
    1. export interface ICustomPropertyPaneDemoProps {    
    2.   listName: string;    
    3. }   
  4. In the “\src\webparts\customPropertyPaneDemo\CustomPropertyPaneDemoWebPart.ts”, update the render method to use listName property.
    1. public render(): void {    
    2.     const element: React.ReactElement<ICustomPropertyPaneDemoProps> = React.createElement(    
    3.       CustomPropertyPaneDemo,    
    4.       {    
    5.         listName: this.properties.listName    
    6.       }    
    7.     );    
    8.     
    9.     ReactDom.render(element, this.domElement);    
    10.   }  
  5. Update getPropertyPaneConfiguration method. 
    1. protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {    
    2.     return {    
    3.       pages: [    
    4.         {    
    5.           header: {    
    6.             description: strings.PropertyPaneDescription    
    7.           },    
    8.           groups: [    
    9.             {    
    10.               groupName: strings.BasicGroupName,    
    11.               groupFields: [    
    12.                 PropertyPaneTextField('listName', {    
    13.                   label: strings.ListFieldLabel    
    14.                 })    
    15.               ]    
    16.             }    
    17.           ]    
    18.         }    
    19.       ]    
    20.     };    
    21.   }  
  6. Update the interface at “src\webparts\customPropertyPaneDemo\loc\mystrings.d.ts”.
    1. declare interface ICustomPropertyPaneDemoWebPartStrings {    
    2.   PropertyPaneDescription: string;    
    3.   BasicGroupName: string;    
    4.   ListFieldLabel: string;    
    5. }   
  7. In the “src\webparts\customPropertyPaneDemo\loc\en-us.js”, update definition as below. 
    1. define([], function() {    
    2.   return {    
    3.     "PropertyPaneDescription""Description",    
    4.     "BasicGroupName""Group Name",    
    5.     "ListFieldLabel""List"    
    6.   }    
    7. });   
  8. In the “src\webparts\customPropertyPaneDemo\components\CustomPropertyPaneDemo.tsx”, update the render method to use listName property field. 
    1. public render(): React.ReactElement<ICustomPropertyPaneDemoProps> {    
    2.     return (    
    3.       <div className={ styles.customPropertyPaneDemo }>    
    4.         <div className={ styles.container }>    
    5.           <div className={ styles.row }>    
    6.             <div className={ styles.column }>    
    7.               <span className={ styles.title }>Welcome to SharePoint!</span>    
    8.               <p className={ styles.subTitle }>Customize SharePoint experiences using Web Parts.</p>    
    9.               <p className={ styles.description }>{escape(this.props.listName)}</p>    
    10.               <a href="https://aka.ms/spfx" className={ styles.button }>    
    11.                 <span className={ styles.label }>Learn more</span>    
    12.               </a>    
    13.             </div>    
    14.           </div>    
    15.         </div>    
    16.       </div>    
    17.     );    
    18.   }   
  9. Update props at “src\webparts\customPropertyPaneDemo\components\ICustomPropertyPaneDemoProps.ts”.
    1. export interface ICustomPropertyPaneDemoProps {    
    2.   listName: string;    
    3. }    
  10. On the command prompt, type “gulp server” to see the new web part property in action.
SharePoint Framework - Build Custom Controls For Web Part Property Pane
 

Add Dropdown Property Pane Control

 
Let us implement a drop-down for showing the lists from SharePoint site.
 
Define React Props for dropdown
 
Under “src\webparts\customPropertyPaneDemo\components”, add a file IListDropdownProps.ts.
  1. import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';    
  2.     
  3. export interface IListDropdownProps {    
  4.   label: string;    
  5.   loadOptions: () => Promise<IDropdownOption[]>;    
  6.   onChanged: (option: IDropdownOption, index?: number) => void;    
  7.   selectedKey: string | number;    
  8.   disabled: boolean;    
  9.   stateKey: string;    
  10. }    
The above class defines properties for React component being used on web part property pane.
  • label: specifies label for dropdown control.
  • loadOptions: function associated with this delegate loads available options.
  • onChanged: function associated with this delegate is called upon user selection.
  • selectKey: specified selected value.
  • disabled: specifies if dropdown is disabled or not.
  • stateKey: forces React component to re-render.
Define React State for dropdown
 
Under “src\webparts\customPropertyPaneDemo\components”, add a file IListDropdownState.ts.
  1. import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';    
  2.     
  3. export interface IListDropdownState {    
  4.   loading: boolean;    
  5.   options: IDropdownOption[];    
  6.   error: string;    
  7. }    
The above interface defines state for React component.
  • loading: defines if component is loading its options
  • options: holds all available dropdown option.
  • error: defines any error occurred.
Define DropDown React Component
 
Under “src\webparts\customPropertyPaneDemo\components”, add a file ListDropdown.tsx.
  1. import * as React from 'react';  
  2. import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';  
  3. import { Spinner } from 'office-ui-fabric-react/lib/components/Spinner';  
  4. import { IListDropdownProps } from './IListDropdownProps';  
  5. import { IListDropdownState } from './IListDropdownState';  
  6.   
  7. export default class ListDropdown extends React.Component<IListDropdownProps, IListDropdownState> {  
  8.   private selectedKey: React.ReactText;  
  9.   
  10.   constructor(props: IListDropdownProps, state: IListDropdownState) {  
  11.     super(props);  
  12.     this.selectedKey = props.selectedKey;  
  13.   
  14.     this.state = {  
  15.       loading: false,  
  16.       options: undefined,  
  17.       error: undefined  
  18.     };  
  19.   }  
  20.   
  21.   public componentDidMount(): void {  
  22.     this.loadOptions();  
  23.   }  
  24.   
  25.   public componentDidUpdate(prevProps: IListDropdownProps, prevState: IListDropdownState): void {  
  26.     if (this.props.disabled !== prevProps.disabled ||  
  27.       this.props.stateKey !== prevProps.stateKey) {  
  28.       this.loadOptions();  
  29.     }  
  30.   }  
  31.   
  32.   private loadOptions(): void {  
  33.     this.setState({  
  34.       loading: true,  
  35.       error: undefined,  
  36.       options: undefined  
  37.     });  
  38.   
  39.     this.props.loadOptions()  
  40.       .then((options: IDropdownOption[]): void => {  
  41.         this.setState({  
  42.           loading: false,  
  43.           error: undefined,  
  44.           options: options  
  45.         });  
  46.       }, (error: any): void => {  
  47.         this.setState((prevState: IListDropdownState, props: IListDropdownProps): IListDropdownState => {  
  48.           prevState.loading = false;  
  49.           prevState.error = error;  
  50.           return prevState;  
  51.         });  
  52.       });  
  53.   }  
  54.   
  55.   public render(): JSX.Element {  
  56.     const loading: JSX.Element = this.state.loading ? <div><Spinner label={'Loading options...'} /></div> : <div />;  
  57.     const error: JSX.Element = this.state.error !== undefined ? <div className={'ms-TextField-errorMessage ms-u-slideDownIn20'}>Error while loading items: {this.state.error}</div> : <div />;  
  58.   
  59.     return (  
  60.       <div>  
  61.         <Dropdown label={this.props.label}  
  62.           disabled={this.props.disabled || this.state.loading || this.state.error !== undefined}  
  63.           onChanged={this.onChanged.bind(this)}  
  64.           selectedKey={this.selectedKey}  
  65.           options={this.state.options} />  
  66.         {loading}  
  67.         {error}  
  68.       </div>  
  69.     );  
  70.   }  
  71.   
  72.   private onChanged(option: IDropdownOption, index?: number): void {  
  73.     this.selectedKey = option.key;  
  74.     // reset previously selected options  
  75.     const options: IDropdownOption[] = this.state.options;  
  76.     options.forEach((o: IDropdownOption): void => {  
  77.       if (o.key !== option.key) {  
  78.         o.selected = false;  
  79.       }  
  80.     });  
  81.     this.setState((prevState: IListDropdownState, props: IListDropdownProps): IListDropdownState => {  
  82.       prevState.options = options;  
  83.       return prevState;  
  84.     });  
  85.     if (this.props.onChanged) {  
  86.       this.props.onChanged(option, index);  
  87.     }  
  88.   }  
  89. }  
The ListDropdown class represents React component to render the dropdown property pane control.
 
The component loads the available options by calling loadOptions method. Once options are loaded, the component state is updated to show the options. The dropdown is rendered using Office UI fabric React dropdown component.
 

Add List Dropdown to Property Pane Control

 
To define public properties for dropdown property pane control, add IPropertyPaneDropdownProps.ts file under “src\webparts\customPropertyPaneDemo\components” folder.
  1. import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';  
  2.   
  3. export interface IPropertyPaneDropdownProps {  
  4.   label: string;  
  5.   loadOptions: () => Promise<IDropdownOption[]>;  
  6.   onPropertyChange: (propertyPath: string, newValue: any) => void;  
  7.   selectedKey: string | number;  
  8.   disabled?: boolean;  
  9. }  
In the above interface,
  • label: specifies label for dropdown control.
  • loadOptions: function associated with this delegate loads available options.
  • onPropertyChang: function associated with this delegate is called upon user selection.
  • selectKey: specified selected value.
  • disabled: specifies if dropdown is disabled or not.

Define internal properties for dropdown property pane control

 
Create a new file IPropertyPaneDropdownInternalProps.ts under “src\webparts\customPropertyPaneDemo\components\” folder.
  1. import { IPropertyPaneCustomFieldProps } from '@microsoft/sp-webpart-base';  
  2. import { IPropertyPaneDropdownProps } from './IPropertyPaneDropdownProps';  
  3.   
  4. export interface IPropertyPaneDropdownInternalProps extends IPropertyPaneDropdownProps, IPropertyPaneCustomFieldProps {  
  5. }  
This interface does not define any new properties; however, it combines the properties from previously defined IPropertyPaneDropdownProps interface and standard SharePoint Framework IPropertyPaneCustomFieldProps interface for custom control to run correctly.
 

Define dropdown property pane control

 
Create a new file “PropertyPaneDropdown.ts” under “src\webparts\customPropertyPaneDemo\components\” folder.
  1. import * as React from 'react';  
  2. import * as ReactDom from 'react-dom';  
  3. import {  
  4.   IPropertyPaneField,  
  5.   PropertyPaneFieldType  
  6. } from '@microsoft/sp-webpart-base';  
  7. import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';  
  8. import { IPropertyPaneDropdownProps } from './IPropertyPaneDropdownProps';  
  9. import { IPropertyPaneDropdownInternalProps } from './IPropertyPaneDropdownInternalProps';  
  10. import ListDropdown from './ListDropdown';  
  11. import { IListDropdownProps } from './IListDropdownProps';  
  12.   
  13. export class PropertyPaneDropdown implements IPropertyPaneField<IPropertyPaneDropdownProps> {  
  14.   public type: PropertyPaneFieldType = PropertyPaneFieldType.Custom;  
  15.   public targetProperty: string;  
  16.   public properties: IPropertyPaneDropdownInternalProps;  
  17.   private elem: HTMLElement;  
  18.   
  19.   constructor(targetProperty: string, properties: IPropertyPaneDropdownProps) {  
  20.     this.targetProperty = targetProperty;  
  21.     this.properties = {  
  22.       key: properties.label,  
  23.       label: properties.label,  
  24.       loadOptions: properties.loadOptions,  
  25.       onPropertyChange: properties.onPropertyChange,  
  26.       selectedKey: properties.selectedKey,  
  27.       disabled: properties.disabled,  
  28.       onRender: this.onRender.bind(this)  
  29.     };  
  30.   }  
  31.   
  32.   public render(): void {  
  33.     if (!this.elem) {  
  34.       return;  
  35.     }  
  36.   
  37.     this.onRender(this.elem);  
  38.   }  
  39.   
  40.   private onRender(elem: HTMLElement): void {  
  41.     if (!this.elem) {  
  42.       this.elem = elem;  
  43.     }  
  44.   
  45.     const element: React.ReactElement<IListDropdownProps> = React.createElement(ListDropdown, {  
  46.       label: this.properties.label,  
  47.       loadOptions: this.properties.loadOptions,  
  48.       onChanged: this.onChanged.bind(this),  
  49.       selectedKey: this.properties.selectedKey,  
  50.       disabled: this.properties.disabled,  
  51.       // required to allow the component to be re-rendered by calling this.render() externally  
  52.       stateKey: new Date().toString()  
  53.     });  
  54.     ReactDom.render(element, elem);  
  55.   }  
  56.   
  57.   private onChanged(option: IDropdownOption, index?: number): void {  
  58.     this.properties.onPropertyChange(this.targetProperty, option.key);  
  59.   }  
  60. }  

Use Dropdown Property Pane Control in Web Part

 
Add list information interface
 
Define an interface that represents the SharePoint list.
 
Create a new file IListInfo.ts under “src\webparts\customPropertyPaneDemo” folder.
  1. export interface IListInfo {  
  2.     Id: string;  
  3.     Title: string;  
  4. }  
Reference Dropdown Property Pane in WebPart
 
In the webpart file src\webparts\customPropertyPaneDemo\CustomPropertyPaneDemoWebPart.ts, import PropertyPaneDropdown class.
  1. import { PropertyPaneDropdown } from './components/PropertyPaneDropdown';   
Add reference to interface and helper functions to work with web part properties.
  1. import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';  
  2. import { update, get } from '@microsoft/sp-lodash-subset';  
Add method to load available lists. For the sake of simplicity, we are using mock data here. 
  1. private loadLists(): Promise<IDropdownOption[]> {  
  2.     return new Promise<IDropdownOption[]>((resolve: (options: IDropdownOption[]) => void, reject: (error: any) => void) => {  
  3.       setTimeout(() => {  
  4.         resolve([{  
  5.           key: 'sharedDocuments',  
  6.           text: 'Shared Documents'  
  7.         },  
  8.           {  
  9.             key: 'myDocuments',  
  10.             text: 'My Documents'  
  11.           }]);  
  12.       }, 2000);  
  13.     });  
  14.   }  
Add method to handle property dropdown value change. 
  1. private onListChange(propertyPath: string, newValue: any): void {  
  2.   const oldValue: any = get(this.properties, propertyPath);  
  3.   // store new value in web part properties  
  4.   update(this.properties, propertyPath, (): any => { return newValue; });  
  5.   // refresh web part  
  6.   this.render();  
  7. }  
Update getPropertyPaneConfiguration method to use dropdown property pane control to render the listName web part property. 
  1. protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {  
  2.   return {  
  3.     pages: [  
  4.       {  
  5.         header: {  
  6.           description: strings.PropertyPaneDescription  
  7.         },  
  8.         groups: [  
  9.           {  
  10.             groupName: strings.BasicGroupName,  
  11.             groupFields: [  
  12.               new PropertyPaneDropdown('listName', {  
  13.                 label: strings.ListFieldLabel,  
  14.                 loadOptions: this.loadLists.bind(this),  
  15.                 onPropertyChange: this.onListChange.bind(this),  
  16.                 selectedKey: this.properties.listName  
  17.               })  
  18.             ]  
  19.           }  
  20.         ]  
  21.       }  
  22.     ]  
  23.   };  
  24. }  

Test the Custom Property Pane

 
On the command prompt, type “gulp serve”. Add the web part and verify the property pane.
 
SharePoint Framework - Build Custom Controls For Web Part Property Pane
 

Summary

 
In this article, we explored how to build custom control for SPFx web part property panes. Predefined typed objects are sufficient in most cases, however, to meet certain business scenarios, we have to go beyond and create our own custom controls. The process to develop custom controls is a bit tedious to make it work.