javascript - React 详细信息列表始终为空

标签 javascript reactjs typescript promise

我正在尝试使用以下 React Office ui 组件: https://developer.microsoft.com/en-us/fabric#/components/detailslist

所以我有一个带有该组件的 Web 部件,但有两个问题:

  1. 第一次加载 Web 部件时,尚未选择列表,因此它应该打开属性页,但它没有打开,这就是为什么我必须做一个小技巧:我根本不喜欢
 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);

  }

第二个问题是,当用户选择另一个列表来渲染项目时,不会调用 readItemsAndSetStatus,因此状态不会更新。

web部分代码如下:

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()
            }
          ]
        }
      ]
    };
  }
}

组件 Web 部件代码,为了简洁省略了代码

//#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> 用于打开属性 Pane 的回调函数正在 FactoryMethod 组件的构造函数中调用。这不是一个好的做法,因为构造函数不应该有任何副作用(引用 docs )。相反,请在 componentDidMount 中调用此回调,这是一个生命周期方法,只会调用一次,对于在组件初始加载后只需要运行一次的任何代码来说是理想的选择。 (有关此方法的更多信息,请参阅 docs)。

2> 函数 readitemandsetstatus 仅执行一次,因为您在 componentDidMount 中调用它,这是一个仅运行的生命周期方法一次,当组件第一次加载到页面上时。

public componentDidMount(): void {
    this.readItemsAndSetStatus();
  }

componentWillReceiveProps 中,您正在调用 setInitialState,每当您的组件收到任何新的 props 时,它都会重置您的状态。 (有关 docs 中的 componentWillReceiveProps 的更多信息)

public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
    this.listItemEntityTypeName = undefined;
    this.setInitialState();
  }

这将清除通过调用 componentDidMount 方法中的 readitemandsetchanges 所做的所有更改。这是你想要的吗?如果没有,那么您应该在此处调用 readitemandsetstatus 函数,以便根据通过 nextProps 传入的新 props 更新状态。

由于您要从 componentDidMount 以及 componentWillReceiveProps 调用相同的函数 readitemandsetstatus,因此您应该传递 props 您想在函数中用作参数。

private readItemsAndSetStatus(props): void {
...
}

这将允许您从 compoenentDidMount 传递 this.props 和从 componentWillReceiveProps 传递 nextProps 并相应地使用它们,在函数内。

希望这能解决您的问题。

更新 1: 首先,您作为引用共享的链接引用了一个非常旧的 React 版本。我建议浏览official tutorial和其他较新的资源(例如 Egghead 中的视频)来澄清您的概念。然后您可以重新编写代码并修复您看到的任何问题。

可以在您的代码中进行以下更改:

  1. 在构造函数中设置初始状态。这就是它的用途。另外,任何函数绑定(bind)都应该放在这里。
  2. 不要连接您不会使用的生命周期方法。像 shouldComponentUpdate 这样的方法用于优化渲染,只有在有正当理由的情况下才应该使用。否则它们可能会降低性能。与往常一样,请检查 docs在使用某个方法之前先对其进行了解。
  3. componentDidMount 中而不是在 componentWillMount 中执行任何资源获取或回调操作,以便您的组件在您进行任何更改之前已完全加载到 DOM 中。

关于javascript - React 详细信息列表始终为空,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46897792/

相关文章:

javascript - 使用 firebase 函数删除 Firestore 集合

javascript - 使用 ASP.NET 和 HTML5 Canvas 创建浏览器游戏

javascript - jQuery "Does not have attribute"选择器?

angular - Electron Angular2(angular-cli)要求内部模块失败

javascript - 从父级访问在子域上设置的 cookie

reactjs - React Router v5.1.2 公共(public)和 protected 经过身份验证和基于角色的路由

javascript - 在单独的文件中访问 React 组件的 props

reactjs - 如何在 Android 上设置 React Native 的日志记录级别?

javascript - 在 typescript 中循环遍历文本文件

javascript - react : How can I set my components' render content from outside?