在 Web 部件属性中使用级联下拉列表
为 SharePoint 客户端 Web 部件设计属性窗格时,可能有一个 Web 部件属性,该属性根据另一个属性中选择的值显示其选项。 实现级联下拉列表控件时通常会遇到这种情况。 本文将介绍如何在 Web 部件属性窗格中创建级联下拉列表控件,而无需开发自定义属性窗格控件。
可以从 GitHub (sp-dev-fx-webparts/samples/react-custompropertypanecontrols/) 获取有效 Web 部件的源。
注意
请务必先设置 SharePoint 客户端 Web 部件开发环境,再执行本文中的步骤。
新建项目
首先,为项目新建文件夹:
md react-cascadingdropdowns
转到项目文件夹:
cd react-cascadingdropdowns
在项目文件夹中,运行 SharePoint Framework Yeoman 生成器,以搭建新的 SharePoint Framework 项目:
yo @microsoft/sharepoint
出现提示时,请输入以下值(为下面省略的所有提示选择默认选项):
- 解决方案名称是什么?:react-cascadingdropdowns
- 要创建哪种类型的客户端组件?:WebPart
- Web 部件的名称是什么?:列表项
- 要使用哪个模板?:React
在代码编辑器中,打开项目文件夹。 本文的步骤和屏幕截图中使用 Visual Studio Code,但你可以使用你喜欢的任何编辑器。
定义用于存储选定列表的 Web 部件属性
需要生成显示选定 SharePoint 列表中列表项的 Web 部件。 用户可以在 Web 部件属性窗格中选择列表。 要存储选定列表,请新建 Web 部件属性,名为 listName
。
在代码编辑器中,打开 src/webparts/listItems/ListItemsWebPartManifest.json 文件。 使用名为
description
的新属性替换默认的listName
属性。{ ... "preconfiguredEntries": [{ ... "properties": { "listName": "" } }] }
打开 src/webparts/listItems/ListItemsWebPart.ts 文件,并将
IListItemsWebPartProps
接口替换为以下内容:export interface IListItemsWebPartProps { listName: string; }
在 src/webparts/listItems/ListItemsWebPart.ts 文件中,将
render()
方法更改为:export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... public render(): void { const element: React.ReactElement<IListItemsProps> = React.createElement(ListItems, { listName: this.properties.listName }); ReactDom.render(element, this.domElement); } // ... }
将
getPropertyPaneConfiguration()
更新为:export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: strings.PropertyPaneDescription }, groups: [ { groupName: strings.BasicGroupName, groupFields: [ PropertyPaneTextField('listName', { label: strings.ListNameFieldLabel }) ] } ] } ] }; } // ... }
在 src/webparts/listItems/loc/mystrings.d.ts 文件中,将
IListItemsStrings
接口更改为:declare interface IListItemsStrings { PropertyPaneDescription: string; BasicGroupName: string; ListNameFieldLabel: string; }
在 src/webparts/listItems/loc/en-us.js 文件中,添加
ListNameFieldLabel
字符串缺少的定义:define([], function() { return { "PropertyPaneDescription": "Description", "BasicGroupName": "Group Name", "ListNameFieldLabel": "List" } });
在 src/webparts/listItems/components/ListItems.tsx 文件中,将
render()
方法的内容更改为:public render(): JSX.Element { const { listName } = this.props; return ( <section className={`${styles.listItems} ${hasTeamsContext ? styles.teams : ''}`}> <div className={styles.welcome}> <img alt="" src={isDarkTheme ? require('../assets/welcome-dark.png') : require('../assets/welcome-light.png')} className={styles.welcomeImage} /> <h2>Well done, {escape(userDisplayName)}!</h2> <div>{environmentMessage}</div> <div>List name: <strong>{escape(listName)}</strong></div> </div> </section> ); }
在 src/webparts/listItems/components/IListItemsProps.ts 文件中,将
IListItemsProps
接口更改为:export interface IListItemsProps { listName: string; }
运行以下命令,确认项目是否正在运行:
gulp serve
在 Web 浏览器中,将列表项 Web 部件添加到画布,然后打开其属性。 验证为 List 属性设置的值是否显示在 Web 部件主体中。
在下拉列表中填充可供选择的 SharePoint 列表
此时,用户通过手动输入列表名称,指定 Web 部件应使用的列表。 此操作容易出错。理想情况下,不妨让用户选择当前 SharePoint 网站中的现有列表之一。
使用下拉列表控件呈现 listName 属性
在
ListItemsWebPart
类中,在 Web 部件顶部添加对PropertyPaneDropdown
类的引用。 将加载PropertyPaneTextField
类的导入子句替换为:import { IPropertyPaneConfiguration, PropertyPaneTextField, PropertyPaneDropdown, IPropertyPaneDropdownOption } from '@microsoft/sp-property-pane';
在
ListItemsWebPart
类中,添加名为lists
的新变量,用于存储当前网站中所有可用列表的相关信息:export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { private lists: IPropertyPaneDropdownOption[]; // ... }
添加一个名为
listsDropdownDisabled
的新类变量: 此变量决定了是否启用“列表”下拉列表。 在 Web 部件检索到当前网站中可用列表的相关信息前,此下拉列表应处于禁用状态。export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... private listsDropdownDisabled: boolean = true; // ... }
更改
getPropertyPaneConfiguration()
方法以使用下拉列表控件呈现listName
属性:export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: strings.PropertyPaneDescription }, groups: [ { groupName: strings.BasicGroupName, groupFields: [ PropertyPaneDropdown('listName', { label: strings.ListNameFieldLabel, options: this.lists, disabled: this.listsDropdownDisabled }) ] } ] } ] }; } }
运行以下命令,验证它能否正常运行:
gulp serve
在“列表”下拉列表中显示可用列表
之前已将 listName
属性的下拉列表控件与 lists
类属性相关联。 由于尚未向其中加载任何值,因此 Web 部件属性窗格中的列表下拉列表仍处于禁用状态。 此部分将介绍如何把 Web 部件扩展为加载可用列表的相关信息。
在
ListItemsWebPart
类中,添加用于加载可用列表的方法。 你将使用模拟数据,但也可以调用 SharePoint REST API 从当前网站中检索一系列可用列表。 为了模拟从外部服务加载选项,此方法使用两秒延迟。export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... private loadLists(): Promise<IPropertyPaneDropdownOption[]> { return new Promise<IPropertyPaneDropdownOption[]>((resolve: (options: IPropertyPaneDropdownOption[]) => void, reject: (error: any) => void) => { setTimeout((): void => { resolve([{ key: 'sharedDocuments', text: 'Shared Documents' }, { key: 'myDocuments', text: 'My Documents' }]); }, 2000); }); } }
在“列表”下拉列表中加载可用列表的相关信息。 在
ListItemsWebPart
类中,使用以下代码重写onPropertyPaneConfigurationStart()
方法:export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... protected onPropertyPaneConfigurationStart(): void { this.listsDropdownDisabled = !this.lists; if (this.lists) { return; } this.context.statusRenderer.displayLoadingIndicator(this.domElement, 'lists'); this.loadLists() .then((listOptions: IPropertyPaneDropdownOption[]): void => { this.lists = listOptions; this.listsDropdownDisabled = false; this.context.propertyPane.refresh(); this.context.statusRenderer.clearLoadingIndicator(this.domElement); this.render(); }); } // ... }
在 Web 部件的 Web 部件属性窗格打开后,SharePoint 框架会调用
onPropertyPaneConfigurationStart()
方法。- 首先,此方法会检查是否已加载当前网站中的可用列表的相关信息。
- 如果列表信息已加载,则会启用“列表”下拉列表。
- 如果列表信息尚未加载,则会显示“正在加载”指示器,告诉用户 Web 部件正在加载列表信息。
在可用列表的相关信息已加载后,此方法会将检索到的数据分配给
lists
类变量,“列表”下拉列表可通过它使用这些数据。接下来,下拉列表会启用,允许用户选择列表。 调用
this.context.propertyPane.refresh()
可刷新 Web 部件属性窗格,反映“列表”下拉列表的最新变化。在列表信息已加载后,立即调用
clearLoadingIndicator()
方法,删除“正在加载”指示器。 因为调用此方法会清除 Web 部件用户界面,所以需要调用render()
方法来强制 Web 部件重新呈现。运行以下命令,确认一切是否都按预期正常运行:
gulp serve
将 Web 部件添加到画布并打开其属性窗格后,你应该会看到“列表”下拉列表中填充了可供用户选择的可用列表。
允许用户选择选定列表中的列表项
生成 Web 部件时,经常需要允许用户从一组值中选择一个值,而这组值又是根据之前选择的值进行确定的。例如,根据选定的洲来选择国家/地区,或选择选定列表中的列表项。 这种用户体验通常称为“级联下拉列表”。 使用标准的 SharePoint 框架客户端 Web 部件功能,可以在 Web 部件属性窗格中生成级联下拉列表。 为此,需要将之前生成的 Web 部件扩展为,能够根据之前选择的列表选择列表项。
添加“列表项”Web 部件属性
在代码编辑器中,打开“src/webparts/listItems/ListItemsWebPart.manifest.json”文件。 向
properties
部分添加名为itemName
的新属性,如下所示:{ // ... "properties": { "listName": "", "itemName": "" } // ... }
将 src/webparts/listItems/IListItemsWebPartProps.ts 文件中的代码更改为:
export interface IListItemsWebPartProps { listName: string; itemName: string; }
将 src/webparts/listItems/components/IListItemsProps.ts 文件中的代码更改为:
export interface IListItemsProps { listName: string; itemName: string; }
在 src/webparts/listItems/ListItemsWebPart.ts 文件中,将
render()
方法的代码更改为:export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... public render(): void { const element: React.ReactElement<IListItemsProps> = React.createElement(ListItems, { listName: this.properties.listName, itemName: this.properties.itemName }); ReactDom.render(element, this.domElement); } // ... }
在 src/webparts/listItems/loc/mystrings.d.ts 文件中,将
IListItemsStrings
接口更改为:declare interface IListItemsStrings { PropertyPaneDescription: string; BasicGroupName: string; ListNameFieldLabel: string; ItemNameFieldLabel: string; }
在 src/webparts/listItems/loc/en-us.js 文件中,添加
ItemNameFieldLabel
字符串缺少的定义。define([], function() { return { "PropertyPaneDescription": "Description", "BasicGroupName": "Group Name", "ListNameFieldLabel": "List", "ItemNameFieldLabel": "Item" } });
呈现“列表项”Web 部件属性值
在 src/webparts/listItems/components/ListItems.tsx 文件中,将 render()
方法更改为:
export default class ListItems extends React.Component<IListItemsProps, {}> {
public render(): JSX.Element {
const {
listName,
itemName
} = this.props;
return (
<section className={`${styles.listItems} ${hasTeamsContext ? styles.teams : ''}`}>
<div className={styles.welcome}>
<img alt="" src={isDarkTheme ? require('../assets/welcome-dark.png') : require('../assets/welcome-light.png')} className={styles.welcomeImage} />
<h2>Well done, {escape(userDisplayName)}!</h2>
<div>{environmentMessage}</div>
<div>List name: <strong>{escape(listName)}</strong></div>
<div>Item name: <strong>{escape(itemName)}</strong></div>
</div>
</section>
);
}
}
允许用户选择列表中的列表项
与用户通过使用下拉列表选择列表的方式类似,用户可以从一组可用列表项中选择一项。
在
ListItemsWebPart
类中,添加名为items
的新变量,用于存储当前选定列表中所有可用列表项的相关信息。export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... private items: IPropertyPaneDropdownOption[]; // ... }
添加一个名为
itemsDropdownDisabled
的新类变量: 此变量决定了是否应启用“列表项”下拉列表。 用户只有在选择列表后才能选择列表项。export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... private itemsDropdownDisabled: boolean = true; // ... }
更改
getPropertyPaneConfiguration()
方法以使用下拉列表控件呈现itemName
属性。export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: strings.PropertyPaneDescription }, groups: [ { groupName: strings.BasicGroupName, groupFields: [ PropertyPaneDropdown('listName', { label: strings.ListNameFieldLabel, options: this.lists, disabled: this.listsDropdownDisabled }), PropertyPaneDropdown('itemName', { label: strings.ItemNameFieldLabel, options: this.items, disabled: this.itemsDropdownDisabled }) ] } ] } ] }; } }
运行以下命令,确认一切是否正常运行:
gulp serve
在“列表项”下拉列表中显示选定列表中的可用列表项
之前,定义了下拉列表控件,用于在 Web 部件属性窗格中呈现 itemName
属性。 接下来,将 Web 部件扩展为,能够加载选定列表中的可用列表项的相关信息,并在“列表项”下拉列表中显示列表项。
添加用于加载列表项的方法。 在 src/webparts/listItems/ListItemsWebPart.ts 文件中,向
ListItemsWebPart
类添加新方法,用于加载选定列表中的可用列表项。 (就像用于加载可用列表的方法一样,将使用模拟数据。)export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... private loadItems(): Promise<IPropertyPaneDropdownOption[]> { if (!this.properties.listName) { // resolve to empty options since no list has been selected return Promise.resolve(); } const wp: ListItemsWebPart = this; return new Promise<IPropertyPaneDropdownOption[]>((resolve: (options: IPropertyPaneDropdownOption[]) => void, reject: (error: any) => void) => { setTimeout(() => { const items = { sharedDocuments: [ { key: 'spfx_presentation.pptx', text: 'SPFx for the masses' }, { key: 'hello-world.spapp', text: 'hello-world.spapp' } ], myDocuments: [ { key: 'isaiah_cv.docx', text: 'Isaiah CV' }, { key: 'isaiah_expenses.xlsx', text: 'Isaiah Expenses' } ] }; resolve(items[wp.properties.listName]); }, 2000); }); } }
loadItems()
方法返回之前选定的列表的模拟列表项。 没有选择列表时,此方法可解决这一承诺,而不包含任何数据。在“列表项”下拉列表中加载可用列表项的相关信息。 在
ListItemsWebPart
类中,扩展onPropertyPaneConfigurationStart()
方法以加载所选列表的项:export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... protected onPropertyPaneConfigurationStart(): void { this.listsDropdownDisabled = !this.lists; this.itemsDropdownDisabled = !this.properties.listName || !this.items; if (this.lists) { return; } this.context.statusRenderer.displayLoadingIndicator(this.domElement, 'options'); this.loadLists() .then((listOptions: IPropertyPaneDropdownOption[]): Promise<IPropertyPaneDropdownOption[]> => { this.lists = listOptions; this.listsDropdownDisabled = false; this.context.propertyPane.refresh(); return this.loadItems(); }) .then((itemOptions: IPropertyPaneDropdownOption[]): void => { this.items = itemOptions; this.itemsDropdownDisabled = !this.properties.listName; this.context.propertyPane.refresh(); this.context.statusRenderer.clearLoadingIndicator(this.domElement); this.render(); }); } // ... }
初始化时,Web 部件会先确定是否应启用“列表项”下拉列表。 如果用户之前选择了列表,便可以选择相应列表中的列表项。 如果未选择任何列表,那么“列表项”下拉列表处于禁用状态。
你扩展了之前为了加载可用列表的相关信息而定义的代码,使其能够加载选定列表中的可用列表项的相关信息。 然后,此代码会将检索到的信息分配给
items
类变量,以供“列表项”下拉列表使用。 最后,此代码会清除“正在加载”指示器,允许用户开始使用 Web 部件。运行以下命令,确认一切是否正常运行:
gulp serve
根据要求,“列表项”下拉列表最初处于禁用状态,迫使用户先选择列表。 但此时,即使用户已选择列表,“列表项”下拉列表仍处于禁用状态。
在用户选择列表后更新 Web 部件属性窗格。 当用户在属性窗格中选择一个列表后,Web 部件应进行更新,以启用“列表项”下拉列表,并显示选定列表中的一组列表项。
在 ListItemsWebPart.ts 文件中,使用以下代码替代
ListItemsWebPart
类中的onPropertyPaneFieldChanged()
方法:export default class ListItemsWebPart extends BaseClientSideWebPart<IListItemsWebPartProps> { // ... protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void { if (propertyPath === 'listName' && newValue) { // push new list value super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue); // get previously selected item const previousItem: string = this.properties.itemName; // reset selected item this.properties.itemName = undefined; // push new item value this.onPropertyPaneFieldChanged('itemName', previousItem, this.properties.itemName); // disable item selector until new items are loaded this.itemsDropdownDisabled = true; // refresh the item selector control by repainting the property pane this.context.propertyPane.refresh(); // communicate loading items this.context.statusRenderer.displayLoadingIndicator(this.domElement, 'items'); this.loadItems() .then((itemOptions: IPropertyPaneDropdownOption[]): void => { // store items this.items = itemOptions; // enable item selector this.itemsDropdownDisabled = false; // clear status indicator this.context.statusRenderer.clearLoadingIndicator(this.domElement); // re-render the web part as clearing the loading indicator removes the web part body this.render(); // refresh the item selector control by repainting the property pane this.context.propertyPane.refresh(); }); } else { super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue); } } // ... }
在用户选择列表后,Web 部件会保留新选择的值。 由于选定列表已更改,因此 Web 部件会重置之前选择的列表项。 至此,用户选择了一个列表,Web 部件属性窗格会加载这一特定列表的列表项。 加载项时,用户无法选择项。
在选定列表的列表项加载后,它们会分配给 items 类变量,“列表项”下拉列表可通过它引用这些列表项。 至此,可用列表项的相关信息已加载,“列表项”下拉列表处于启用状态,允许用户选择列表项。 “正在加载”指示器已删除,这会清除 Web 部件主体,这也就是为什么应重新呈现 Web 部件的原因所在。 最后,刷新 Web 部件属性窗格,以反映最新变化。