我正在尝试使用以下 React Office ui 组件:https : //developer.microsoft.com/en-us/fabric#/components/detailslist
所以我有一个带有组件的 webpart,但我有两个问题:
public render(): void { const element: React.ReactElement<IFactoryMethodProps > = React.createElement( FactoryMethod, { spHttpClient: this.context.spHttpClient, siteUrl: this.context.pageContext.web.absoluteUrl, listName: this._dataProvider.selectedList === undefined ? "GenericList" : this._dataProvider.selectedList.Title, dataProvider: this._dataProvider, configureStartCallback: this.openPropertyPane } ); //ReactDom.render(element, this.domElement); this._factorymethodContainerComponent = <FactoryMethod>ReactDom.render(element, this.domElement); }
第二个问题是当用户选择另一个 List 来呈现项目时, readItemsAndSetStatus 没有被调用,因此状态没有得到更新。
webpart代码如下:
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "@microsoft/sp-core-library";
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
IPropertyPaneDropdownOption,
IPropertyPaneField,
PropertyPaneLabel
} from "@microsoft/sp-webpart-base";
import * as strings from "FactoryMethodWebPartStrings";
import FactoryMethod from "./components/FactoryMethod";
import { IFactoryMethodProps } from "./components/IFactoryMethodProps";
import { IFactoryMethodWebPartProps } from "./IFactoryMethodWebPartProps";
import * as lodash from "@microsoft/sp-lodash-subset";
import List from "./components/models/List";
import { Environment, EnvironmentType } from "@microsoft/sp-core-library";
import IDataProvider from "./components/dataproviders/IDataProvider";
import MockDataProvider from "./test/MockDataProvider";
import SharePointDataProvider from "./components/dataproviders/SharepointDataProvider";
export default class FactoryMethodWebPart extends BaseClientSideWebPart<IFactoryMethodWebPartProps> {
private _dropdownOptions: IPropertyPaneDropdownOption[];
private _selectedList: List;
private _disableDropdown: boolean;
private _dataProvider: IDataProvider;
private _factorymethodContainerComponent: FactoryMethod;
protected onInit(): Promise<void> {
this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");
/*
Create the appropriate data provider depending on where the web part is running.
The DEBUG flag will ensure the mock data provider is not bundled with the web part when you package the
solution for distribution, that is, using the --ship flag with the package-solution gulp command.
*/
if (DEBUG && Environment.type === EnvironmentType.Local) {
this._dataProvider = new MockDataProvider();
} else {
this._dataProvider = new SharePointDataProvider();
this._dataProvider.webPartContext = this.context;
}
this.openPropertyPane = this.openPropertyPane.bind(this);
/*
Get the list of tasks lists from the current site and populate the property pane dropdown field with the values.
*/
this.loadLists()
.then(() => {
/*
If a list is already selected, then we would have stored the list Id in the associated web part property.
So, check to see if we do have a selected list for the web part. If we do, then we set that as the selected list
in the property pane dropdown field.
*/
if (this.properties.spListIndex) {
this.setSelectedList(this.properties.spListIndex.toString());
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
}
});
return super.onInit();
}
// render method of the webpart, actually calls Component
public render(): void {
const element: React.ReactElement<IFactoryMethodProps > = React.createElement(
FactoryMethod,
{
spHttpClient: this.context.spHttpClient,
siteUrl: this.context.pageContext.web.absoluteUrl,
listName: this._dataProvider.selectedList === undefined ? "GenericList" : this._dataProvider.selectedList.Title,
dataProvider: this._dataProvider,
configureStartCallback: this.openPropertyPane
}
);
//ReactDom.render(element, this.domElement);
this._factorymethodContainerComponent = <FactoryMethod>ReactDom.render(element, this.domElement);
}
// loads lists from the site and filld the dropdown.
private loadLists(): Promise<any> {
return this._dataProvider.getLists()
.then((lists: List[]) => {
// disable dropdown field if there are no results from the server.
this._disableDropdown = lists.length === 0;
if (lists.length !== 0) {
this._dropdownOptions = lists.map((list: List) => {
return {
key: list.Id,
text: list.Title
};
});
}
});
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
/*
Check the property path to see which property pane feld changed. If the property path matches the dropdown, then we set that list
as the selected list for the web part.
*/
if (propertyPath === "spListIndex") {
this.setSelectedList(newValue);
}
/*
Finally, tell property pane to re-render the web part.
This is valid for reactive property pane.
*/
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
// sets the selected list based on the selection from the dropdownlist
private setSelectedList(value: string): void {
const selectedIndex: number = lodash.findIndex(this._dropdownOptions,
(item: IPropertyPaneDropdownOption) => item.key === value
);
const selectedDropDownOption: IPropertyPaneDropdownOption = this._dropdownOptions[selectedIndex];
if (selectedDropDownOption) {
this._selectedList = {
Title: selectedDropDownOption.text,
Id: selectedDropDownOption.key.toString()
};
this._dataProvider.selectedList = this._selectedList;
}
}
// we add fields dynamically to the property pane, in this case its only the list field which we will render
private getGroupFields(): IPropertyPaneField<any>[] {
const fields: IPropertyPaneField<any>[] = [];
// we add the options from the dropdownoptions variable that was populated during init to the dropdown here.
fields.push(PropertyPaneDropdown("spListIndex", {
label: "Select a list",
disabled: this._disableDropdown,
options: this._dropdownOptions
}));
/*
When we do not have any lists returned from the server, we disable the dropdown. If that is the case,
we also add a label field displaying the appropriate message.
*/
if (this._disableDropdown) {
fields.push(PropertyPaneLabel(null, {
text: "Could not find tasks lists in your site. Create one or more tasks list and then try using the web part."
}));
}
return fields;
}
private openPropertyPane(): void {
this.context.propertyPane.open();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
/*
Instead of creating the fields here, we call a method that will return the set of property fields to render.
*/
groupFields: this.getGroupFields()
}
]
}
]
};
}
}
组件webpart代码,为简洁起见省略代码
//#region Imports
import * as React from "react";
import styles from "./FactoryMethod.module.scss";
import { IFactoryMethodProps } from "./IFactoryMethodProps";
import {
IDetailsListItemState,
IDetailsNewsListItemState,
IDetailsDirectoryListItemState,
IDetailsAnnouncementListItemState,
IFactoryMethodState
} from "./IFactoryMethodState";
import { IListItem } from "./models/IListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { escape } from "@microsoft/sp-lodash-subset";
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
import { ListItemFactory} from "./ListItemFactory";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import {
DetailsList,
DetailsListLayoutMode,
Selection,
IColumn
} from "office-ui-fabric-react/lib/DetailsList";
import { MarqueeSelection } from "office-ui-fabric-react/lib/MarqueeSelection";
import { autobind } from "office-ui-fabric-react/lib/Utilities";
//#endregion
export default class FactoryMethod extends React.Component<IFactoryMethodProps, IFactoryMethodState> {
private listItemEntityTypeName: string = undefined;
private _selection: Selection;
constructor(props: IFactoryMethodProps, state: any) {
super(props);
this.setInitialState();
this._configureWebPart = this._configureWebPart.bind(this);
}
public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
this.listItemEntityTypeName = undefined;
this.setInitialState();
}
public componentDidMount(): void {
this.readItemsAndSetStatus();
}
public setInitialState(): void {
this.state = {
type: "ListItem",
status: this.listNotConfigured(this.props)
? "Please configure list in Web Part properties"
: "Ready",
DetailsListItemState:{
columns:[],
items:[]
},
DetailsNewsListItemState:{
columns:[],
items:[]
},
DetailsDirectoryListItemState:{
columns:[],
items:[]
},
DetailsAnnouncementListItemState:{
columns:[],
items:[]
},
};
}
private _configureWebPart(): void {
this.props.configureStartCallback();
}
// reusable inline component
public ListMarqueeSelection = (itemState: {columns: IColumn[], items: IListItem[] }) => (
<div>
<MarqueeSelection selection={ this._selection }>
<DetailsList
items={ itemState.items }
columns={ itemState.columns }
setKey="set"
layoutMode={ DetailsListLayoutMode.fixedColumns }
selection={ this._selection }
selectionPreservedOnEmptyClick={ true }
compact={ true }>
</DetailsList>
</MarqueeSelection>
</div>
)
public render(): React.ReactElement<IFactoryMethodProps> {
switch(this.props.listName) {
case "GenericList":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsListItemState.items} columns={this.state.DetailsListItemState.columns} />;
case "News":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsNewsListItemState.items} columns={this.state.DetailsNewsListItemState.columns}/>;
case "Announcements":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsAnnouncementListItemState.items} columns={this.state.DetailsAnnouncementListItemState.columns}/>;
case "Directory":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsDirectoryListItemState.items} columns={this.state.DetailsDirectoryListItemState.columns}/>;
default:
return null;
}
}
// read items using factory method pattern and sets state accordingly
private readItemsAndSetStatus(): void {
this.setState({
status: "Loading all items..."
});
const factory: ListItemFactory = new ListItemFactory();
const items: IListItem[] = factory.getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName);
const keyPart: string = this.props.listName === "GenericList" ? "" : this.props.listName;
if(items != null )
{
// the explicit specification of the type argument `keyof {}` is bad and
// it should not be required.
this.setState<keyof {}>({
status: `Successfully loaded ${items.length} items`,
["Details" + keyPart + "ListItemState"] : {
items,
columns: [
]
}
});
}
}
private listNotConfigured(props: IFactoryMethodProps): boolean {
return props.listName === undefined ||
props.listName === null ||
props.listName.length === 0;
}
}
readitemsandsetstatus 显然只在开始时执行一次,而不是在源更改时执行
更新 1:
感谢首先回答的人,根据他的回答,我去研究了生命周期事件,发现了这篇不错的帖子:
https://staminaloops.github.io/undefinedisnotafunction/understanding-react/
基于这一点和你的回答,我更新了我的代码:
//#region Imports
import * as React from "react";
import styles from "./FactoryMethod.module.scss";
import { IFactoryMethodProps } from "./IFactoryMethodProps";
import {
IDetailsListItemState,
IDetailsNewsListItemState,
IDetailsDirectoryListItemState,
IDetailsAnnouncementListItemState,
IFactoryMethodState
} from "./IFactoryMethodState";
import { IListItem } from "./models/IListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { escape } from "@microsoft/sp-lodash-subset";
import { SPHttpClient, SPHttpClientResponse } from "@microsoft/sp-http";
import { ListItemFactory} from "./ListItemFactory";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import {
DetailsList,
DetailsListLayoutMode,
Selection,
buildColumns,
IColumn
} from "office-ui-fabric-react/lib/DetailsList";
import { MarqueeSelection } from "office-ui-fabric-react/lib/MarqueeSelection";
import { autobind } from "office-ui-fabric-react/lib/Utilities";
import PropTypes from "prop-types";
//#endregion
export default class FactoryMethod extends React.Component<IFactoryMethodProps, IFactoryMethodState> {
private _selection: Selection;
constructor(props: IFactoryMethodProps, state: any) {
super(props);
}
// lifecycle help here: https://staminaloops.github.io/undefinedisnotafunction/understanding-react/
//#region Mouting events lifecycle
// the object returned by this method sets the initial value of this.state
getInitialState(): {} {
return {
type: "GenericList",
status: this.listNotConfigured(this.props)
? "Please configure list in Web Part properties"
: "Ready",
columns: [],
DetailsListItemState:{
items:[]
},
DetailsNewsListItemState:{
items:[]
},
DetailsDirectoryListItemState:{
items:[]
},
DetailsAnnouncementListItemState:{
items:[]
},
};
}
// the object returned by this method sets the initial value of this.props
// if a complex object is returned, it is shared among all component instances
getDefaultProps(): {} {
return {
};
}
// invoked once BEFORE first render
componentWillMount(nextProps: IFactoryMethodProps): void {
// calling setState here does not cause a re-render
this.readItemsAndSetStatus(nextProps);
}
// the data returned from render is neither a string nor a DOM node.
// it's a lightweight description of what the DOM should look like.
// inspects this.state and this.props and create the markup.
// when your data changes, the render method is called again.
// react diff the return value from the previous call to render with
// the new one, and generate a minimal set of changes to be applied to the DOM.
public render(nextProps: IFactoryMethodProps): React.ReactElement<IFactoryMethodProps> {
this.readItemsAndSetStatus(nextProps);
switch(this.props.listName) {
case "GenericList":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsListItemState.items} columns={this.state.columns} />;
case "News":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsNewsListItemState.items} columns={this.state.columns}/>;
case "Announcements":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsAnnouncementListItemState.items} columns={this.state.columns}/>;
case "Directory":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsDirectoryListItemState.items} columns={this.state.columns}/>;
default:
return null;
}
}
// invoked once, only on the client (not on the server), immediately AFTER the initial rendering occurs.
public componentDidMount(nextProps: IFactoryMethodProps): void {
// you can access any refs to your children
// (e.g., to access the underlying DOM representation - ReactDOM.findDOMNode).
// the componentDidMount() method of child components is invoked before that of parent components.
// if you want to integrate with other JavaScript frameworks,
// set timers using setTimeout or setInterval,
// or send AJAX requests, perform those operations in this method.
this._configureWebPart = this._configureWebPart.bind(this);
// calling read items does not make any sense here, so I called in the will Mount, is that correct?
// this.readItemsAndSetStatus(nextProps);
}
//#endregion
//#region Props changes lifecycle events (after a property changes from parent component)
public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
this.readItemsAndSetStatus(nextProps);
}
// determines if the render method should run in the subsequent step
// dalled BEFORE a second render
// not called for the initial render
shouldComponentUpdate(nextProps: IFactoryMethodProps, nextState: IFactoryMethodProps): boolean {
// if you want the render method to execute in the next step
// return true, else return false
return true;
}
// called IMMEDIATELY BEFORE a second render
componentWillUpdate(nextProps: IFactoryMethodProps, nextState: IFactoryMethodProps): void {
// you cannot use this.setState() in this method
}
// called IMMEDIATELY AFTER a second render
componentDidUpdate(prevProps: IFactoryMethodProps, prevState: IFactoryMethodProps): void {
// nothing here yet
}
//#endregion
// called IMMEDIATELY before a component is unmounted from the DOM, No region here, its only one method for that lifecycle
componentWillUnmount(): void {
// nothing here yet
}
//#region private methods
private _configureWebPart(): void {
this.props.configureStartCallback();
}
// reusable inline component
private ListMarqueeSelection = (itemState: {columns: IColumn[], items: IListItem[] }) => (
<div>
<MarqueeSelection selection={ this._selection }>
<DetailsList
items={ itemState.items }
columns={ itemState.columns }
setKey="set"
layoutMode={ DetailsListLayoutMode.fixedColumns }
selection={ this._selection }
selectionPreservedOnEmptyClick={ true }
compact={ true }>
</DetailsList>
</MarqueeSelection>
</div>
)
// read items using factory method pattern and sets state accordingly
private readItemsAndSetStatus(props: IFactoryMethodProps): void {
this.setState({
status: "Loading all items..."
});
const factory: ListItemFactory = new ListItemFactory();
factory.getItems(props.spHttpClient, props.siteUrl, props.listName)
.then((items: IListItem[]) => {
const keyPart: string = props.listName === "GenericList" ? "" : props.listName;
// the explicit specification of the type argument `keyof {}` is bad and
// it should not be required.
this.setState<keyof {}>({
status: `Successfully loaded ${items.length} items`,
["Details" + keyPart + "ListItemState"] : {
items
},
columns: buildColumns(items)
});
});
}
private listNotConfigured(props: IFactoryMethodProps): boolean {
return props.listName === undefined ||
props.listName === null ||
props.listName.length === 0;
}
//#endregion
}
那么,现在,它更有意义吗?
1>用于打开属性窗格的回调正在FactoryMethod
组件的构造函数中调用。这不是一个好的做法,因为构造函数不应该有任何副作用(参考文档)。相反,调用这个回调componentDidMount
是一个生命周期方法,它只会被调用一次,对于任何需要在组件初始加载后只运行一次的代码来说都是理想的。(有关此方法的更多信息,请参阅文档)。
2>该函数readitemandsetstatus
只执行一次,因为你在 中调用它componentDidMount
,这是一个生命周期方法,当组件第一次加载到页面上时只运行一次。
public componentDidMount(): void {
this.readItemsAndSetStatus();
}
在componentWillReceiveProps
您调用setInitialState
它时,每次您的组件收到任何新道具时都会重置您的状态。(更多信息componentWillReceiveProps
来自文档)
public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
this.listItemEntityTypeName = undefined;
this.setInitialState();
}
这将清除通过调用中完成所有的改变readitemandsetchanges
的componentDidMount
方法。这是你想要的吗?如果没有,那么您可能应该readitemandsetstatus
在此处调用您的函数,以便根据通过 nextProps 传入的新道具更新状态。
由于您将调用相同的函数readitemandsetstatus
fromcomponentDidMount
以及 fromcomponentWillReceiveProps
您应该将props
要在函数中使用的作为参数传递。
private readItemsAndSetStatus(props): void {
...
}
这将允许您通过this.props
从compoenentDidMount
和nextProps
从componentWillReceiveProps
并相应地使用它们,函数内。
希望这能解决您的问题。
更新 1:首先,您作为参考共享的链接是指一个非常旧的 React 版本。我建议您阅读官方教程和其他较新的资源(例如Egghead 上的这个视频)来清除您的概念。然后,您可以重新编写代码并修复您看到的任何问题。
可以在您的代码中进行以下更改:
shouldComponentUpdate
的方法用于优化渲染,只有在有正当理由时才应该使用。否则它们可能会降低性能。与往常一样,在使用方法之前先检查文档。componentDidMount
而不是在 中执行任何资源获取或回调操作componentWillMount
,以便您的组件在您以任何方式对其进行更改之前已完全加载到 DOM 中。本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句