This is the second post of three that explain how to add dependent properties to Client Web Part Property Pane.
As an example of such properties I decided to use Lists dropdown and Views dropdown. Views dropdown is populated based on selected List.
Here is a content of 3 posts: As a result of these three posts we'll have a fully working web part with ability to select List View in Property Pane:

- SPFx. Client Web Part Property Pane Dependent Properties. Part I: Preparation.
- SPFx. Client Web Part Property Pane Dependent Properties. Part II: Dependent Properties Control. (current post)
- SPFx. Client Web Part Property Pane Dependent Properties. Part III: Styling Dependent Properties Control as Office UI Fabric dropdowns. Knockout version.
I'm using Visual Studio Code IDE for the tutorial.
This post describes how to create custom Property Pane Field with 2 simple unstyled dropdowns (<select> elements).
- Data request is an asynchronous operation. But I didn't find any method to override that will wait for this data before rerender the property pane.
- Property Pane rerender and propertyPaneSettings property getter are called dynamically only if web part property pane behavior is set to Reactive. It means that you won't be able to do anything in non-Reactive property pane (when there is an Apply button at the bottom of the pane).
- You can't initiate Property Pane refresh from your custom field or from web part.
/**
* Complex object to define property in a web part
*/
export interface IPropertyPaneViewSelectorProps {
/**
* Selected list id
*/
listId: string;
/**
* Selected view id
*/
viewId: string;
}
import { IPropertyPaneViewSelectorProps } from './controls/Common';
export interface IDepPropsWebPartProps {
depProps: IPropertyPaneViewSelectorProps;
}
Let's set defaults for these properties. For that we need to go to DepPropsWebPart.manifest.json and in preconfiguredEntries object change properties property like that:
"preconfiguredEntries": [{
//
// ... some other properties
//
"properties": {
"depProps": {
"listId": "-1",
"viewId": "-1"
}
}
}]
public render(): void {
this.domElement.innerHTML = `
<div class="${styles.depProps}">
<div class="${styles.container}">
<div class="ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}">
<div class="ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1">
<p class="ms-font-l ms-fontColor-white">Selected List Id: ${this.properties.depProps.listId}</p>
<p class="ms-font-l ms-fontColor-white">Selected View Id: ${this.properties.depProps.viewId}</p>
</div>
</div>
</div>
</div>`;
}
Now let's start with custom Property Pane Field. To create such field we need to create a class that implements IPropertyPaneField interface:.
/**
* PropertyPane field.
*/
export interface IPropertyPaneField<TProperties> {
/**
* Type of the PropertyPane field.
*/
type: IPropertyPaneFieldType;
/**
* Target property from the web part's property bag.
*/
targetProperty: string;
/**
* Strongly typed properties object. Specific to each field type.
* Example: Checkbox has ICheckboxProps, TextField has ITextField props.
*
* @internalremarks - These props are from the office-ui-fabric-react.
* These props may not be extensive as the fabric-react ones. This is intentional.
* - We are not including any callbacks as part of the props, as this might end up breaking
* the internal flow and cause unwanted problems.
* - Currently discussions are going on whether to include styling elements or not. Based on
* the output of the discussions, changes to the styling related props will take place.
*
* We are including only those which are supported by the web part framework.
*/
properties: TProperties;
}
As you can see it's a so called Generic interface and when implemented TProperties should be changed to some specific interface. And one more thing about this interface - it should extend IPropertyPaneCustomFieldProps if we want to be able to customize rendering. And this interface will be used to pass parameters to the constructor of our field. It means it should contain all the properties we need to get from web part to initialize the field correctly.
So we need to think what properties we need in our field.
- onRender and onDispose? from base interface
- listId and viewId - our main data
- also we need some labels to display near dropdowns. Of course we can hardcode them or get from some source inside the field. But we can set them from web part. It will make our field more isolated and flexible. Let's name these properties as listLabel and viewLabel
- we will use data helpers created in previous post. And these helpers use web part context. So we need this context in our properties as well.
- we need to notify web part that something has changed in our field. For this purpose we'll add onPropertyChange property in the interface to get the handler from a web part.
/**
* PropertyPaneViewSelector component internal props
*/
export interface IPropertyPaneViewSelectorFieldPropsInternal extends IPropertyPaneCustomFieldProps {
/**
* Path to target property in web part properties
*/
targetProperty: string;
/**
* web part context
*/
context: IWebPartContext;
/**
* selected list id
*/
listId: string;
/**
* selected view id
*/
viewId: string;
/**
* onPropertyChange event handler
*/
onPropertyChange(propertyPath: string, newValue: any): void;
/**
* Label to show in list dropdown
*/
listLabel: string;
/**
* Label to show in view dropdown
*/
viewLabel: string;
}
For now our custom class will look like that (/webparts/depProps/controls/PropertyPaneViewSelector.ts):
/**
* PropertyPaneViewSelector component
*/
class PropertyPaneViewSelector implements IPropertyPaneField<IPropertyPaneViewSelectorFieldPropsInternal> {
/**
* This is a Custom field
*/
public type: IPropertyPaneFieldType = IPropertyPaneFieldType.Custom;
/**
* Path to target property in web part properties
*/
public targetProperty: string;
/**
* component properties
*/
public properties: IPropertyPaneViewSelectorFieldPropsInternal;
/**
* selected list id
*/
public listId: string;
/**
* selected view id
*/
public viewId: string;
/**
* ctor
*/
public constructor(_targetProperty: string, _properties: IPropertyPaneViewSelectorFieldPropsInternal) {
this.targetProperty = _targetProperty;
this.properties = _properties;
this.properties.onRender = this._render.bind(this); // applying custom rendering method
this.properties.onDispose = this._dispose.bind(this); // applying custom disposing method
this.listId = _properties.listId;
this.viewId = _properties.viewId;
}
/**
* Rendering the component
*/
private _render(elem: HTMLElement): void {
// custom rendering
}
private _dispose(elem: HTMLElement): void {
// custom dispose
}
}
Let's create helper function for our class. Also this function will allow us to hide onRender and onDispose as we want to implement them internally. That's why we created Internal properties interface. Let's create 'external' one (/webparts/depProps/controls/Common.ts):
/**
* PropertyPaneViewSelector component public props
*/
export interface IPropertyPaneViewSelectorFieldProps {
/**
* web part context
*/
context: IWebPartContext;
/**
* selected list id
*/
listId: string;
/**
* selected view id
*/
viewId: string;
/**
* onPropertyChange event handler
*/
onPropertyChange(propertyPath: string, newValue: any): void;
/**
* Label to show in list dropdown
*/
listLabel: string;
/**
* Label to show in view dropdown
*/
viewLabel: string;
}
/**
* Helper method to create a ViewSelectorField on the PropertyPane.
* @param targetProperty - Target property the viewselector is associated to.
* @param properties - Strongly typed viewselector properties.
*/
export function PropertyPaneViewSelectorField(targetProperty: string, properties: IPropertyPaneViewSelectorFieldProps): IPropertyPaneField<IPropertyPaneViewSelectorFieldProps> {
var internalProps: IPropertyPaneViewSelectorFieldPropsInternal = {
context: properties.context,
listId: properties.listId,
viewId: properties.viewId,
listLabel: properties.listLabel,
viewLabel: properties.viewLabel,
onPropertyChange: properties.onPropertyChange,
targetProperty: targetProperty,
onRender: null,
onDispose: null
};
return new PropertyPaneViewSelector(targetProperty, internalProps);
}
protected get propertyPaneSettings(): IPropertyPaneSettings {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneViewSelectorField('depProps', {
context: this.context,
listId: this.properties.depProps && this.properties.depProps.listId,
viewId: this.properties.depProps && this.properties.depProps.viewId,
listLabel: strings.SelectList,
viewLabel: strings.SelectView,
onPropertyChange: this.onPropertyChange
})
]
}
]
}
]
};
}
I prefer to use Knockout.js as a framework (because I know it better than React and because looks like it is used for Lists and Libraries new experience).
To use Knockout we need to install npm module:
npm i knockout --save-dev
{
"entries": [
//...
],
"externals": {
//...
"knockout": "node_modules/knockout/build/output/knockout-latest.js"
},
//...
}
So we'll create Model, View and ViewModel for our field.
Let's start with model. It should retrieve lists and views from data helpers and... that's it(/webparts/depProps/controls/PropertyPaneViewSelectorModel.ts):
import {
IWebPartContext
} from '@microsoft/sp-client-preview';
import { IDataHelper } from '../data-helpers/DataHelperBase';
import { DataHelpersFactory } from '../data-helpers/DataHelpersFactory';
import { ISPList, ISPView } from '../common/SPEntities';
/**
* ViewSelector component model
*/
export class PropertyPaneViewSelectorModel {
/**
* data helper
*/
private _dataHelper: IDataHelper;
/**
* ctor
*/
constructor(_context: IWebPartContext) {
this._dataHelper = DataHelpersFactory.createDataHelper(_context);
}
/**
* Gets lists collection
*/
public getLists(): Promise<ISPList[]> {
return this._dataHelper.getLists();
}
/**
* Gets views collection
*/
public getViews(listId: string): Promise<ISPView[]> {
return this._dataHelper.getViews(listId);
}
}
/**
* ViewSelector component view
*/
export class PropertyPaneViewSelectorView {
private _template: string = `
<div>
<select data-bind="options: lists, optionsText: 'Title', optionsValue: 'Id', value: currentList, optionsCaption: listLabel">
</select>
</div>
<div>
<select data-bind="options: views, optionsText: 'Title', optionsValue: 'Id', value: currentView, optionsCaption: viewLabel, enable: isListSelected"></select>
</div>
`;
/**
* Renders the HTML markup
*/
public render(element: HTMLElement): Promise<void> {
return new Promise<void>((resolve) => {
element.innerHTML += this._template;
resolve();
});
}
}
import {
IWebPartContext
} from '@microsoft/sp-client-preview';
import { ISPList, ISPView } from '../common/SPEntities';
import { PropertyPaneViewSelectorModel } from './PropertyPaneViewSelectorModel';
import * as ko from 'knockout';
import {
IPropertyPaneViewSelectorFieldPropsInternal
} from './Common';
import { IDropdownOption } from '../components/dropdown/viewmodel';
/**
* ViewSelector component view model
*/
export class PropertyPaneViewSelectorViewModel {
/**
* Web part context
*/
private _context: IWebPartContext;
/**
* Current List Id
*/
private _listId: string;
/**
* Current View Id
*/
private _viewId: string;
/**
* ViewSelector component properties
*/
private _properties: IPropertyPaneViewSelectorFieldPropsInternal;
/**
* MVVM model
*/
private _model: PropertyPaneViewSelectorModel;
/**
* Observable collection of lists
*/
protected lists: KnockoutObservableArray<ISPList>;
/**
* Observable collection of views
*/
protected views: KnockoutObservableArray<ISPView>;
/**
* Current selected list
*/
protected currentList: KnockoutObservable<string>;
/**
* Current selected view
*/
protected currentView: KnockoutObservable<string>;
/**
* Flag if there is a list selection
*/
protected isListSelected: KnockoutObservable<boolean>;
/**
* List dropdown label
*/
protected listLabel: string;
/**
* View dropdown label
*/
protected viewLabel: string;
/**
* ctor
* */
public constructor(_properties: IPropertyPaneViewSelectorFieldPropsInternal) {
this._properties = _properties;
this._context = _properties.context;
this._listId = _properties.listId;
this._viewId = _properties.viewId;
this.listLabel = _properties.listLabel;
this.viewLabel = _properties.viewLabel;
this.currentList = ko.observable<string>(this._listId);
this.currentView = ko.observable<string>(this._viewId);
this.isListSelected = ko.observable<boolean>(this._listId !== '-1');
// subscribing on changes in lists dropdown
this.currentList.subscribe((value) => {
this._listId = value;
this._initViews().then(() => {
this.isListSelected(this._listId !== '-1');
});
this._firePropertyChange();
});
// subscribing on changes in view dropdown
this.currentView.subscribe((value) => {
this._viewId = value;
this._firePropertyChange();
});
this.lists = ko.observableArray<ISPList>();
this.views = ko.observableArray<ISPView>();
this._model = new PropertyPaneViewSelectorModel(this._context);
}
/**
* Fires property changed event handler
* This method should be called in 'Reactive' mode of web part properties pane
*/
private _firePropertyChange(): void {
if (this._properties.onPropertyChange) {
this._properties.onPropertyChange(this._properties.targetProperty, { listId: this._listId || '-1', viewId: this._viewId || '-1' });
}
}
/**
* Initializes the view model
*/
public init(): Promise>void< {
return new Promise>void<((resolve) => {
this._model.getLists().then((lists) => { // getting lists
this.lists(lists);
this._initViews().then(() => {
resolve();
});
});
});
}
/**
* Initializes views collection based on selected list
*/
private _initViews(): Promise<void> {
return new Promise<void>((resolve) => {
if (this._listId) {
this._model.getViews(this._listId).then((views) => { // getting views
this.views(views);
resolve();
});
}
else
resolve();
});
}
}
Final step is to call View's rendering in Field render method, initialize ViewModel and apply Knockout bindings:
/**
* Rendering the component
*/
private _render(elem: HTMLElement): void {
this._view.render(elem).then(() => { // rendering of HTML markup
this._viewModel.init().then(() => { // then getting data
ko.applyBindings(this._viewModel, elem); // then applying bindings
});
});
}
Now you can see the result:

See you in next post.
Have fun!
Comments