Recently Wictor Wilén penned a great how to on dynamically loading data into a SharePoint Framework Webpart property pane. Yet what if you want to dynamically, with asynchronous data, populate a property’s options based on the value of another property?
Within the Framework, we configure the property pane within the method: propertyPaneSettings(), which returns IPropertyPaneSettings, basically an array of “pages” for the pane. That catch is that this does not return a Promise, rather a static object, thus we have to have all data available that we want to present within the property pane at the time of loading the property pane. Wictor’s solution gets around this by loading asynchronous data for one field in the webpart’s OnInit method, which works well one level of dynamic data, but not dynamic data based on other property pane values.
A solution was found to this issue by using a method call, this.configureStart(); within a the propertyPaneSettings() method, thus allowing us to reload the property pane once our Async promise is complete. This has been documented in part on the GitHub Framework wiki, yet I want to offer a complete demonstration of this in action.
Caveat: As the SharePoint Framework is still in Developer Preview, currently up to Drop 5, this solution may change.
The Code:
All code is available from github at https://github.com/eoverfield/SPFx-Demos/tree/master/WebPart-Properties-Dynamic-Load. If you clone the repo from https://github.com/eoverfield/SPFx-Demos, look in the WebPart-Properties-Dynamic-Load folder. This is based on the basic SPFx Yeoman template, Code Drop 5.
Walkthrough:
I suggest you download al code form GitHub although I will highlight the major changes made to enable asynchronous property pane options.
All significant modifications are within the Webpart src folder, in this case /src/webparts/ demoWpDynamicProperties.
Define the additional webpart properties
Within the webpart property configuration file, i.e. IDemoWpDynamicPropertiesWebPartProps.ts, add the additional properties we want to store.
1 2 3 4 5 6 | export interface IDemoWpDynamicPropertiesWebPartProps { title: string; description: string; listName: string; listColumn: string; } |
Set default values for the additional webpart properties
This is not a requirement, but within the webpart manifest, it is best practices to set default properties values so that nothing is left undefined. In this case, update DemoWpDynamicPropertiesWebPart.manifest.json. Be aware that if a change is made to this file, to see the changes in the browser you will want to stop and start gulp.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | { "$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json", "id": "f149d188-ff1c-4149-92fa-8fbbc316e784", "componentType": "WebPart", "version": "0.0.1", "manifestVersion": 2, "preconfiguredEntries": [{ "groupId": "f149d188-ff1c-4149-92fa-8fbbc316e784", "group": { "default": "Under Development" }, "title": { "default": "Demo Dynamic Properties" }, "description": { "default": "Demo of Dynamic Webpart Properties" }, "officeFabricIconFontName": "Page", "properties": { "title": "Default title", "description": "Default Description", "listName": "", "listColumn": "" } }] } |
Create a new Interface to handle SharePoint and Mock data
I added a new file, ListService.ts, that provides a new interface, IListsService. There are quite a few ways to handle mock / SharePoint data, but I have been leaning more towards an interface to address access to specific types of data, in this case SharePoint List data.
The basics of this interface is that there are two methods, one to get all lists within a web and a second method to get all of the columns (fields) within a list. I am only providing the basic code for demonstration purposes. There is room for improvement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | 'use strict'; import { IWebPartContext, IPropertyPaneDropdownOption } from '@microsoft/sp-client-preview'; //import the HttpClient to make calls to SharePoint import { HttpClient } from '@microsoft/sp-client-base'; //create the exportable interface to different classes export interface IListsService { getListNames(): Promise<IPropertyPaneDropdownOption[]>; getListColumnNames(listId: string): Promise<IPropertyPaneDropdownOption[]>; } //create an interface for array helpers. Should normally be in its own file, but for ease of use, added here export interface IArrayHelper { sortByKey(array: Object[], key: string): Object[]; } export class ArrayHelper implements IArrayHelper { constructor() { } //return a new array that is sorted by the object key of an array of objects public sortByKey(array: Object[], key: string) { return array.sort((a, b) => { var x = a[key]; var y = b[key]; return ((x < y) ? -1 : ((x > y) ? 1 : 0)); }); } } //Class for mock data - when not conntected to SharePoint - for testing export class MockListsService implements IListsService { constructor() { } //get a list of list names public getListNames(): Promise<IPropertyPaneDropdownOption[]> { return new Promise<IPropertyPaneDropdownOption[]>(resolve => { //add a delay to simulate slow connection setTimeout(() => { var options: Array<IPropertyPaneDropdownOption> = new Array<IPropertyPaneDropdownOption>(); options.push( { key: '1', text: 'List 1' }); options.push( { key: '2', text: 'List 2' }); resolve(options); }, 1000); }); } //get a list of columns within a list public getListColumnNames(listId: string): Promise<IPropertyPaneDropdownOption[]> { return new Promise<IPropertyPaneDropdownOption[]>(resolve => { //add a delay to simulate slow connection setTimeout(() => { var options: Array<IPropertyPaneDropdownOption> = new Array<IPropertyPaneDropdownOption>(); options.push( { key: 'column1', text: 'Column 1' }); options.push( { key: 'column2', text: 'Column 2' }); options.push( { key: 'column3', text: 'Column 3' }); options.push( { key: 'column4', text: 'Column 4' }); resolve(options); }, 1000); }); } } //Class for getting live SharePoint data export class ListsService implements IListsService { //add class vars for use in methods private _httpClient: HttpClient; private _webAbsoluteUrl: string; private _arrayHelper: ArrayHelper = new ArrayHelper(); //constructor of class, set up values for our HttpClient and more public constructor(webPartContext: IWebPartContext) { this._httpClient = webPartContext.httpClient as any; // tslint:disable-line:no-any this._webAbsoluteUrl = webPartContext.pageContext.web.absoluteUrl; } //get the lists found in the current web public getListNames(): Promise<IPropertyPaneDropdownOption[]> { //using SharePoint API to get list of lists, ordered by Title. Select ID and Title of each list return this._httpClient.get(this._webAbsoluteUrl + `/_api/Lists/?$select=Id,Title&$filter=Hidden ne true&$orderby Title`) .then((response: Response) => { var options: Array<IPropertyPaneDropdownOption> = new Array<IPropertyPaneDropdownOption>(); return response.json().then((data) => { data.value.forEach(list => { options.push( { key: list.Id, text: list.Title }); }); //since the returned values are pre-ordered, just return the list of lists return options; }); }); } //get a list of columns within a list public getListColumnNames(listId: string): Promise<IPropertyPaneDropdownOption[]> { //based on the list id, get the fields for a given list. Unable to sort as orderBy does not work with field selection return this._httpClient.get(this._webAbsoluteUrl + `/_api/web/lists(guid'` + listId + `')/fields?$select=Id,Title&$filter=Hidden ne true`) .then((response: Response) => { var options: Array<IPropertyPaneDropdownOption> = new Array<IPropertyPaneDropdownOption>(); return response.json().then((data) => { //sort the fields data.value = this._arrayHelper.sortByKey(data.value, 'Title'); data.value.forEach(list => { options.push( { key: list.Id, text: list.Title }); }); return options; }); }); } } |
Update the Webpart code
Most of the magic now happens in our Webpart source file, i.e. DemoWpDynamicPropertiesWebPart.ts.
First we need to import a few more references from @microsoft/sp-client-preview, PropertyPaneDroopDown to access dropdowns and IPropertyPaneDropdownOption to work with dropdown options.
1 2 3 4 5 6 7 8 | import { BaseClientSideWebPart, IPropertyPaneSettings, IWebPartContext, PropertyPaneTextField, PropertyPaneDropdown, //add reference to drop down IPropertyPaneDropdownOption //add reference to drop down options } from '@microsoft/sp-client-preview'; |
We want to also import interfaces from our ListService.ts file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | //import in the ListService class we create import * as ListService from './ListService'; [\cc] And we will be using <strong>DisplayMode</strong> from <strong>@microsoft/sp-client-base</strong> to know if we are in edit mode so we only load our lists for the property pane when in edit mode. [cc lang="csharp" lines="2" tab_size="3" line_numbers="true" width="600" escaped="true"] //import Env type from sp-client-base to be able to determine env currently running in import { EnvironmentType, DisplayMode, } from '@microsoft/sp-client-base'; [\cc] Within our Webpart class, i.e. <strong>DemoWpDynamicPropertiesWebPart</strong> that extends <strong>BaseClientSideWebPart</strong>, we need to add / change a few aspects around. Add new class level private variables to hold a list of lists and a list of columns for a specific list. [cc lang="csharp" lines="3" tab_size="3" line_numbers="true" width="600" escaped="true"] //vars to hold a list of lists and a list of columns for a given list private _listOptions: IPropertyPaneDropdownOption[]; private _columnOptions: IPropertyPaneDropdownOption[]; To load the initial list of lists, I prefer Wictor’s solution of loading the list in the <strong>OnInit</strong> method, although in this case we are also resetting our class arrays of lists and columns, and then only in edit mode, we go and grab a list of lists. [cc lang="csharp" lines="20" tab_size="3" line_numbers="true" width="600" escaped="true"] //on the webpart initialization, process public onInit<T>(): Promise<T> { this._listOptions = []; this._columnOptions = []; //only execute when in edit mode if (DisplayMode.Edit === 2) { //go and load the dropdown list of SharePoint lists upon init so as to speed up process //inspiration from http://www.wictorwilen.se/sharepoint-framework-how-to-properly-dynamically-populate-dropdown-property-pane-fields var dataService = (this.context.environment.type === EnvironmentType.Test || this.context.environment.type === EnvironmentType.Local) ? new ListService.MockListsService() : new ListService.ListsService(this.context); return new Promise<T>((resolve: (args: T) => void, reject: (error: Error) => void) => { dataService.getListNames().then( (lists : IPropertyPaneDropdownOption[]) => { this._listOptions = lists; } ); resolve(undefined); }); } } |
When the selected list property value changes, as in when a property value of interst changes, we want to trap that and possibly do something different. In this case, when a new list is selected from the list dropdown, we want to trap this and reset the columns within that list dropdown, as well as reset the column previously selected. This may or may not be necessary when you want to dynamically load async property pane properties.
What I do find interesting is that after we handle our special case, we should call super.onPropertyChange(propertyPath, newValue); to have the default method called for all other properties, and to kick off other default onchange events.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //override property change method so that when new list selected, we can update other columns public onPropertyChange(propertyPath: string, newValue: any): void { this.properties[propertyPath] = newValue; //if the list was changed, we need to go and get columns for that list if (propertyPath === 'listName') { //reset header and body columns this._columnOptions = []; this.properties.listColumn = ''; } //and now complete normaly property change super.onPropertyChange(propertyPath, newValue); } |
Finally we need to update the method propertyPaneSettings(). This method simply returns a iPropertyPageSettings object, which is an array of “pages”, or property pane pages. First we load up our options based on the data we have available to us, i.e. the list of lists. If we do not have a list of columns, yet a list has been selected, we kick off a method in our ListService interface that returns a promise. When the promise completes, we can call a magic function, this.configureStart();, which makes this whole process work. this.configureStart(); will cause the property pane to reset itself with the newly retrieved data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | protected get propertyPaneSettings(): IPropertyPaneSettings { //set up additional dropdown properties for column selection based on list selection let templatePropertyColumn: any; //set up the column dropdown property with default settings templatePropertyColumn = PropertyPaneDropdown('listColumn', { label: 'Select Column', isDisabled: true, options: [{key: "", text: "Select List First"}] }); //check to see if the list has been selected if (this.properties.listName && this.properties.listName.length > 0) { //if we have a list, check to see if columns are loaded. If they are not, then go and get them and after promise, kick off configureStart to reload if ((this._columnOptions.length < 1)) { templatePropertyColumn.properties.label = 'Loading Column List'; var dataService = (this.context.environment.type === EnvironmentType.Test || this.context.environment.type === EnvironmentType.Local) ? new ListService.MockListsService() : new ListService.ListsService(this.context); dataService.getListColumnNames(this.properties.listName).then( (columns : IPropertyPaneDropdownOption[]) => { this._columnOptions = columns; //we now have the columns, so kick off reload //based on suggestion: https://github.com/SharePoint/sp-dev-docs/wiki/Async-data-fetch-in-the-property-pane //note, bugs may exist as documented on GitHub this.configureStart(); }); } else { //since we have the columns, go and and load them up now. templatePropertyColumn.properties.options = this._columnOptions; templatePropertyColumn.properties.selectedKey = this.properties.listColumn; templatePropertyColumn.properties.isDisabled = false; } } return { pages: [ { header: { description: strings.BasicGroupName }, groups: [ { groupName: 'Web Part Options', groupFields: [ PropertyPaneTextField('title', { label: 'Title' }), PropertyPaneTextField('description', { label: 'Description' }), PropertyPaneDropdown('listName', { label: 'Select List', options: this._listOptions }), templatePropertyColumn ] } ] } ] }; } |
Wrapup
I did skip a few minor changes, such as updates to the webpart render function, so download the code from github if you want to test the entire solution. Follow the Readme in the project folder as well as you will need to execute a few additional commands, i.e. npm install, to have NodeJS go and get all of the toolchain’s dependencies after you clone the repo.
how to fetch list item based on dropdown selection and display it
The strategy I use to work with dynamic properties based on dropdowns, selections, etc, came from a sample solution: https://github.com/SharePoint/sp-dev-solutions/tree/master/solutions/ModernSearch/react-search-refiners/spfx/src/webparts/searchResults. I suggest you review how this webpart builds its property pane.
This is very close to what I am looking for since I need to ability to dynamically use a selection for a column as a drop down in one of my web part properties. Thanks for publishing this little snippet, very very helpful.
Hi there,
I am new to the framework and I am trying to do something on change of one the the property pane dropdowns. I have implemented the onPropertyChange function as you have above. However, when I call super.onPropertyChange i am getting an error ‘Method does not exist’. Another question is where does the onPropertChange function get called?
Tim, yes, frustrating, but also expected as SPFx continues to change. Check out the latest documentation on creating more complex property pane experiences: https://docs.microsoft.com/en-us/sharepoint/dev/spfx/web-parts/guidance/use-cascading-dropdowns-in-web-part-properties