Skip to content

Commit a8db5d0

Browse files
waldekmastykarzVesaJuvonen
authored andcommitted
Updates to web parts guidance (SharePoint#828)
* Updated guidance on using JSOM to the latest version of SPFx * Updated code sample in the guidance on validating web part property values * Updated code samples in the guidance on using cascading dropdowns to the latest version of SPFx * Updated guidance on building custom web part properties
1 parent ca9d887 commit a8db5d0

File tree

4 files changed

+131
-97
lines changed

4 files changed

+131
-97
lines changed

docs/spfx/web-parts/guidance/build-custom-property-pane-controls.md

Lines changed: 84 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# Build custom controls for the property pane
22

3-
> Note. This article has not yet been verified with SPFx GA version, so you might have challenges on making this work as such with the latest release.
4-
53
The SharePoint Framework contains a set of standard controls for the property pane. But sometimes you need additional functionality beyond the basic controls. You might need asynchronous updates to the data on a control, or a specific user interface. Build a custom control for the property pane to get the functionality you need.
64

75
In this article you will learn how to build a custom control for the property pane. You will build a custom dropdown control that loads its data asynchronously from an external service without blocking the user interface of the web part.
@@ -78,12 +76,12 @@ export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWe
7876
}
7977
```
8078

81-
Update the **propertyPaneSettings** getter to:
79+
Update the **getPropertyPaneConfiguration** method to:
8280

8381
```ts
8482
export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> {
8583
// ...
86-
protected get propertyPaneSettings(): IPropertyPaneSettings {
84+
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
8785
return {
8886
pages: [
8987
{
@@ -108,10 +106,10 @@ export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWe
108106
}
109107
```
110108

111-
In the **src/webparts/listItems/loc/mystrings.d.ts** file change the **IListItemsStrings** interface to:
109+
In the **src/webparts/listItems/loc/mystrings.d.ts** file change the **IListItemsWebPartStrings** interface to:
112110

113111
```ts
114-
declare interface IListItemsStrings {
112+
declare interface IListItemsWebPartStrings {
115113
PropertyPaneDescription: string;
116114
BasicGroupName: string;
117115
ListFieldLabel: string;
@@ -134,26 +132,17 @@ In the **src/webparts/listItems/components/ListItems.tsx** file change the conte
134132

135133
```tsx
136134
export default class ListItems extends React.Component<IListItemsProps, {}> {
137-
public render(): JSX.Element {
135+
public render(): React.ReactElement<IListItemsProps> {
138136
return (
139137
<div className={styles.listItems}>
140138
<div className={styles.container}>
141-
<div className={css('ms-Grid-row ms-bgColor-themeDark ms-fontColor-white', styles.row)}>
142-
<div className='ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1'>
143-
<span className='ms-font-xl ms-fontColor-white'>
144-
Welcome to SharePoint!
145-
</span>
146-
<p className='ms-font-l ms-fontColor-white'>
147-
Customize SharePoint experiences using web parts.
148-
</p>
149-
<p className='ms-font-l ms-fontColor-white'>
150-
{this.props.listName}
151-
</p>
152-
<a
153-
className={css('ms-Button', styles.button)}
154-
href='https://github.com/SharePoint/sp-dev-docs/wiki'
155-
>
156-
<span className='ms-Button-label'>Learn more</span>
139+
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
140+
<div className="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1">
141+
<span className="ms-font-xl ms-fontColor-white">Welcome to SharePoint!</span>
142+
<p className="ms-font-l ms-fontColor-white">Customize SharePoint experiences using Web Parts.</p>
143+
<p className="ms-font-l ms-fontColor-white">{escape(this.props.listName)}</p>
144+
<a href="https://aka.ms/spfx" className={styles.button}>
145+
<span className={styles.label}>Learn more</span>
157146
</a>
158147
</div>
159148
</div>
@@ -164,6 +153,14 @@ export default class ListItems extends React.Component<IListItemsProps, {}> {
164153
}
165154
```
166155

156+
Next, open the **src/webparts/listItems/components/IListItemsProps.ts** file and replace its contents with:
157+
158+
```ts
159+
export interface IListItemsProps {
160+
listName: string;
161+
}
162+
```
163+
167164
Run the following command to verify that the project is running:
168165

169166
```sh
@@ -205,7 +202,7 @@ In the project **src** folder, create a hierarchy of three new folders so that y
205202
In the **src/controls/PropertyPaneAsyncDropdown/components** folder create a new file named **IAsyncDropdownProps.ts** and enter the following code:
206203
207204
```ts
208-
import { IDropdownOption } from 'office-ui-fabric-react';
205+
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
209206
210207
export interface IAsyncDropdownProps {
211208
label: string;
@@ -219,12 +216,12 @@ export interface IAsyncDropdownProps {
219216
220217
The **IAsyncDropdownProps** class defines properties that can be set on the React component used by the custom property pane control. The **label** property specifies the label for the dropdown control. The function associated with the **loadOptions** delegate is called by the control to load the available options. The function associated with the **onChanged** delegate is called after the user selected an option in the dropdown. The **selectedKey** property specifies the selected value which can be a string or a number. The **disabled** property specifies if the dropdown control is disabled or not. The **stateKey** property is used to force the React component to re-render.
221218

222-
#### Define asynchronous dropdown React component stateKey
219+
#### Define asynchronous dropdown React component interface
223220

224221
In the **src/controls/PropertyPaneAsyncDropdown/components** folder create new file named **IAsyncDropdownState.ts** and enter the following code:
225222

226223
```ts
227-
import { IDropdownOption } from 'office-ui-fabric-react';
224+
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
228225

229226
export interface IAsyncDropdownState {
230227
loading: boolean;
@@ -241,14 +238,17 @@ In the **src/controls/PropertyPaneAsyncDropdown/components** folder create a new
241238

242239
```tsx
243240
import * as React from 'react';
244-
import { Dropdown, Spinner } from 'office-ui-fabric-react';
241+
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
242+
import { Spinner } from 'office-ui-fabric-react/lib/components/Spinner';
245243
import { IAsyncDropdownProps } from './IAsyncDropdownProps';
246244
import { IAsyncDropdownState } from './IAsyncDropdownState';
247-
import { IDropdownOption } from 'office-ui-fabric-react';
248245

249246
export default class AsyncDropdown extends React.Component<IAsyncDropdownProps, IAsyncDropdownState> {
247+
private selectedKey: React.ReactText;
248+
250249
constructor(props: IAsyncDropdownProps, state: IAsyncDropdownState) {
251250
super(props);
251+
this.selectedKey = props.selectedKey;
252252

253253
this.state = {
254254
loading: false,
@@ -298,15 +298,33 @@ export default class AsyncDropdown extends React.Component<IAsyncDropdownProps,
298298
return (
299299
<div>
300300
<Dropdown label={this.props.label}
301-
isDisabled={this.props.disabled || this.state.loading || this.state.error !== undefined}
302-
onChanged={this.props.onChanged}
303-
selectedKey={this.props.selectedKey}
301+
disabled={this.props.disabled || this.state.loading || this.state.error !== undefined}
302+
onChanged={this.onChanged.bind(this)}
303+
selectedKey={this.selectedKey}
304304
options={this.state.options} />
305305
{loading}
306306
{error}
307307
</div>
308308
);
309309
}
310+
311+
private onChanged(option: IDropdownOption, index?: number): void {
312+
this.selectedKey = option.key;
313+
// reset previously selected options
314+
const options: IDropdownOption[] = this.state.options;
315+
options.forEach((o: IDropdownOption): void => {
316+
if (o.key !== option.key) {
317+
o.selected = false;
318+
}
319+
});
320+
this.setState((prevState: IAsyncDropdownState, props: IAsyncDropdownProps): IAsyncDropdownState => {
321+
prevState.options = options;
322+
return prevState;
323+
});
324+
if (this.props.onChanged) {
325+
this.props.onChanged(option, index);
326+
}
327+
}
310328
}
311329
```
312330

@@ -327,7 +345,7 @@ The second set of properties are private properties used internally inside the c
327345
In the **src/controls/PropertyPaneAsyncDropdown** folder create a new file named **IPropertyPaneAsyncDropdownProps.ts** and enter the following code:
328346

329347
```ts
330-
import { IDropdownOption } from 'office-ui-fabric-react';
348+
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
331349

332350
export interface IPropertyPaneAsyncDropdownProps {
333351
label: string;
@@ -365,7 +383,7 @@ import {
365383
IPropertyPaneField,
366384
PropertyPaneFieldType
367385
} from '@microsoft/sp-webpart-base';
368-
import { IDropdownOption } from 'office-ui-fabric-react';
386+
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
369387
import { IPropertyPaneAsyncDropdownProps } from './IPropertyPaneAsyncDropdownProps';
370388
import { IPropertyPaneAsyncDropdownInternalProps } from './IPropertyPaneAsyncDropdownInternalProps';
371389
import AsyncDropdown from './components/AsyncDropdown';
@@ -380,6 +398,7 @@ export class PropertyPaneAsyncDropdown implements IPropertyPaneField<IPropertyPa
380398
constructor(targetProperty: string, properties: IPropertyPaneAsyncDropdownProps) {
381399
this.targetProperty = targetProperty;
382400
this.properties = {
401+
key: properties.label,
383402
label: properties.label,
384403
loadOptions: properties.loadOptions,
385404
onPropertyChange: properties.onPropertyChange,
@@ -422,9 +441,9 @@ export class PropertyPaneAsyncDropdown implements IPropertyPaneField<IPropertyPa
422441

423442
The **PropertyPaneAsyncDropdown** class implements the standard SharePoint Framework **IPropertyPaneField** interface using the **IPropertyPaneAsyncDropdownProps** interface as a contract for its public properties that can be set from inside the web part. The class contains the following three public properties defined by the **IPropertyPaneField** interface:
424443

425-
* **type**: Must be set to **PropertyPaneFieldType.Custom** for a custom property pane control.
426-
* **targetProperty**: Used to specify the name of the web part property to be used with the control.
427-
* **properties**: Used to define control-specific properties.
444+
- **type**: Must be set to **PropertyPaneFieldType.Custom** for a custom property pane control.
445+
- **targetProperty**: Used to specify the name of the web part property to be used with the control.
446+
- **properties**: Used to define control-specific properties.
428447

429448
Notice how the **properties** property is of the internal **IPropertyPaneAsyncDropdownInternalProps** type rather than the public **IPropertyPaneAsyncDropdownProps** interface implemented by the class. This is on purpose so that the **properties** property can define the **onRender** method required by the SharePoint Framework. If the **onRender** method was a part of the public **IPropertyPaneAsyncDropdownProps** interface then, when using the asynchronous dropdown control in the web part, you would be required to assign a value to it inside the web part, which isn't desirable.
430449

@@ -458,7 +477,7 @@ import { PropertyPaneAsyncDropdown } from '../../controls/PropertyPaneAsyncDropd
458477
Then after that code, add a reference to the **IDropdownOption** interface and two helpers functions required to work with web part properties.
459478

460479
```ts
461-
import { IDropdownOption } from 'office-ui-fabric-react';
480+
import { IDropdownOption } from 'office-ui-fabric-react/lib/components/Dropdown';
462481
import { update, get } from '@microsoft/sp-lodash-subset';
463482
```
464483

@@ -500,21 +519,21 @@ export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWe
500519
// store new value in web part properties
501520
update(this.properties, propertyPath, (): any => { return newValue; });
502521
// refresh web part
503-
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
522+
this.render();
504523
}
505524
}
506525
```
507526

508527
After selecting a list in the list dropdown, the selected value should be persisted in web part properties and the web part should be re-rendered to reflect the selected property.
509528

510-
#### Render the list web part property using the asynchronous dropdown property pane control
529+
#### Render the list web part property using the asynchronous dropdown property pane control
511530

512-
In the **ListItemsWebPart** class change the **propertyPaneSettings** getter to use the asynchronous dropdown property pane control to render the **listName** web part property.
531+
In the **ListItemsWebPart** class change the **getPropertyPaneConfiguration** method to use the asynchronous dropdown property pane control to render the **listName** web part property.
513532

514533
```ts
515534
export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> {
516535
// ...
517-
protected get propertyPaneSettings(): IPropertyPaneSettings {
536+
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
518537
return {
519538
pages: [
520539
{
@@ -580,6 +599,15 @@ export interface IListItemsWebPartProps {
580599
}
581600
```
582601

602+
Change the contents of the **src/webparts/listItems/components/IListItemsProps.ts** file to:
603+
604+
```ts
605+
export interface IListItemsProps {
606+
listName: string;
607+
item: string;
608+
}
609+
```
610+
583611
In the **src/webparts/listItems/ListItemsWebPart.ts** file, change the code of the **render** method to:
584612

585613
```ts
@@ -597,10 +625,10 @@ export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWe
597625
}
598626
```
599627

600-
In the **src/webparts/listItems/loc/mystrings.d.ts** file change the **IListItemsStrings** interface to
628+
In the **src/webparts/listItems/loc/mystrings.d.ts** file change the **IListItemsWebPartStrings** interface to
601629

602630
```ts
603-
declare interface IListItemsStrings {
631+
declare interface IListItemsWebPartStrings {
604632
PropertyPaneDescription: string;
605633
BasicGroupName: string;
606634
ListFieldLabel: string;
@@ -627,29 +655,18 @@ In the **src/webparts/listItems/components/ListItems.tsx** file change the **ren
627655

628656
```tsx
629657
export default class ListItems extends React.Component<IListItemsProps, {}> {
630-
public render(): JSX.Element {
658+
public render(): React.ReactElement<IListItemsProps> {
631659
return (
632660
<div className={styles.listItems}>
633661
<div className={styles.container}>
634-
<div className={css('ms-Grid-row ms-bgColor-themeDark ms-fontColor-white', styles.row)}>
635-
<div className='ms-Grid-col ms-u-lg10 ms-u-xl8 ms-u-xlPush2 ms-u-lgPush1'>
636-
<span className='ms-font-xl ms-fontColor-white'>
637-
Welcome to SharePoint!
638-
</span>
639-
<p className='ms-font-l ms-fontColor-white'>
640-
Customize SharePoint experiences using web parts.
641-
</p>
642-
<p className='ms-font-l ms-fontColor-white'>
643-
{this.props.listName}
644-
</p>
645-
<p className='ms-font-l ms-fontColor-white'>
646-
{this.props.item}
647-
</p>
648-
<a
649-
className={css('ms-Button', styles.button)}
650-
href='https://github.com/SharePoint/sp-dev-docs/wiki'
651-
>
652-
<span className='ms-Button-label'>Learn more</span>
662+
<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
663+
<div className="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1">
664+
<span className="ms-font-xl ms-fontColor-white">Welcome to SharePoint!</span>
665+
<p className="ms-font-l ms-fontColor-white">Customize SharePoint experiences using Web Parts.</p>
666+
<p className="ms-font-l ms-fontColor-white">{escape(this.props.listName)}</p>
667+
<p className="ms-font-l ms-fontColor-white">{escape(this.props.item)}</p>
668+
<a href="https://aka.ms/spfx" className={styles.button}>
669+
<span className={styles.label}>Learn more</span>
653670
</a>
654671
</div>
655672
</div>
@@ -720,7 +737,7 @@ export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWe
720737
// store new value in web part properties
721738
update(this.properties, propertyPath, (): any => { return newValue; });
722739
// refresh web part
723-
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
740+
this.render();
724741
}
725742
}
726743
```
@@ -738,12 +755,12 @@ export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWe
738755
}
739756
```
740757

741-
Next change the code of the **propertyPaneSettings** getter to:
758+
Next change the code of the **getPropertyPaneConfiguration** method to:
742759

743760
```ts
744761
export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> {
745762
// ...
746-
protected get propertyPaneSettings(): IPropertyPaneSettings {
763+
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
747764
// reference to item dropdown needed later after selecting a list
748765
this.itemsDropDown = new PropertyPaneAsyncDropdown('item', {
749766
label: strings.ItemFieldLabel,
@@ -800,7 +817,7 @@ export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWe
800817
// store new value in web part properties
801818
update(this.properties, 'item', (): any => { return this.properties.item; });
802819
// refresh web part
803-
this.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
820+
this.render();
804821
// reset selected values in item dropdown
805822
this.itemsDropDown.properties.selectedKey = this.properties.item;
806823
// allow to load items

0 commit comments

Comments
 (0)