React detailslist始终为空

时间:2017-10-23 20:17:41

标签: javascript reactjs typescript promise

我正在尝试使用以下反应办公室ui组件: https://developer.microsoft.com/en-us/fabric#/components/detailslist

所以我有一个包含该组件的webpart,但我有两个问题:

  1. 第一次加载webpart时,列表尚未被选中,因此它应该打开属性页但不能打开,这就是为什么我必须做一个小技巧:我根本不喜欢
  2.  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 个答案:

答案 0 :(得分:1)

1&gt; 正在FactoryMethod组件的构造函数中调用用于打开属性窗格的回调。这不是一个好习惯,因为构造函数不应该有任何副作用(参考docs)。相反,在componentDidMount中调用此回调,这是一个生命周期方法,只能被调用一次,并且对于在组件初始加载后只需运行一次的任何代码都是理想的。 (有关此方法的详细信息,请参阅docs。)

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

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

componentWillReceiveProps中,您正在调用setInitialState,这会在您的组件每次收到任何新道具时重置您的状态。 (有关docscomponentWillReceiveProps的更多信息)

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

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

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

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

这样,您就可以从this.props compoenentDidMountnextProps componentWillReceiveProps传递shouldComponentUpdate并在函数中相应地使用它们。

希望这能解决你的问题。

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

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

  1. 在构造函数中设置初始状态。这就是它的用途。此外,任何功能绑定都应该放在这里。
  2. 不要连接您不会使用的生命周期方法。像componentDidMount这样的方法用于优化渲染,只有在你有正当理由的情况下才能使用。否则可能会降低性能。与往常一样,在使用之前检查方法的docs
  3. componentWillMount而不是import datetime import logging import os from google.appengine.ext import ndb import webapp2 from googleapiclient.discovery import build from oauth2client.client import GoogleCredentials class LaunchJob(webapp2.RequestHandler): credentials = GoogleCredentials.get_application_default() service = build('dataflow', 'v1b3', credentials=credentials) # Set the following variables to your values. JOBNAME = 'kiss-fn-dataflow-job' PROJECT = 'testing1-18001111' BUCKET = 'kiss-bucket' TEMPLATE = 'Test1' GCSPATH="gs://{bucket}/templates/{template}".format(bucket=BUCKET, template=TEMPLATE), BODY = { "jobName": "{jobname}".format(jobname=JOBNAME), "parameters": { "inputFile" : "gs://{bucket}/input/my_input.txt", "outputFile": "gs://{bucket}/output/my_output".format(bucket=BUCKET) }, "environment": { "tempLocation": "gs://{bucket}/temp".format(bucket=BUCKET), "zone": "us-central1-f" } } request = service.projects().templates().launch(projectId=PROJECT, gcsPath=GCSPATH, body=BODY) response = request.execute() app = webapp2.WSGIApplication([ ('/', LaunchJob), ], debug=True) 中执行任何资源提取或回溯操作,以便您的组件在完全更改之前已完全加载到DOM中。