ไม่ได้ใช้วงจรชีวิตปฏิกิริยาอย่างเหมาะสม

ฉันมี webpart ของ Sharepoint Framework ซึ่งโดยทั่วไปมีแถบด้านข้างคุณสมบัติที่ฉันสามารถเลือกรายการ Sharepoint และจากการเลือกนั้นจะแสดงรายการจากรายการนั้นลงในคอมโพเนนต์ Office UI DetailList

เมื่อฉันดีบักการเรียก REST ก็โอเค แต่ปัญหาคือฉันไม่เคยได้รับข้อมูลใด ๆ ที่แสดงบนหน้าจอเลย

ดังนั้นหากฉันเลือก GenericList ควรสืบค้น Generic LIst หากฉันเลือก Directory ก็ควรสืบค้นรายการ Directory แต่เมื่อฉันเลือก Directory มันยังคงแจ้งว่าส่วนที่เลือกนั้นเป็น GenericList ไม่ใช่ไดเรกทอรี

นี่คือรหัส 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 fill 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()
            }
          ]
        }
      ]
    };
  }
}

นี่คือรหัสส่วนประกอบของฉัน

//#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> {
  constructor(props: IFactoryMethodProps, state: any) {
    super(props);
    this.setInitialState();
  }


  // lifecycle help here: https://staminaloops.github.io/undefinedisnotafunction/understanding-react/
  //#region Mouting events lifecycle
  // 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(): React.ReactElement<IFactoryMethodProps> {
    if (this.state.hasError) {
      // you can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    } else {
      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;
      }
    }
  }

  public componentDidCatch(error: any, info: any): void {
    // display fallback UI
    this.setState({ hasError: true });
    // you can also log the error to an error reporting service
    console.log(error);
    console.log(info);
  }



  // componentDidMount() is invoked immediately after a component is mounted. Initialization that requires DOM nodes should go here.
  // if you need to load data from a remote endpoint, this is a good place to instantiate the network request.
  // this method is a good place to set up any subscriptions. If you do that, don’t forget to unsubscribe in componentWillUnmount().
  // calling setState() in this method will trigger an extra rendering, but it is guaranteed to flush during the same tick.
  // this guarantees that even though the render() will be called twice in this case, the user won’t see the intermediate state.
  // use this pattern with caution because it often causes performance issues. It can, however, be necessary for cases like modals and
  // tooltips when you need to measure a DOM node before rendering something that depends on its size or position.
   public componentDidMount(): void {
    this._configureWebPart = this._configureWebPart.bind(this);
    this.readItemsAndSetStatus();
  }

  //#endregion
  //#region Props changes lifecycle events (after a property changes from parent component)
  // componentWillReceiveProps() is invoked before a mounted component receives new props.
  // if you need to update the state in response to prop
  // changes (for example, to reset it), you may compare this.props and nextProps and perform state transitions
  // using this.setState() in this method.
  // note that React may call this method even if the props have not changed, so make sure to compare the current
  // and next values if you only want to handle changes.
  // this may occur when the parent component causes your component to re-render.
  // react doesn’t call componentWillReceiveProps() with initial props during mounting. It only calls this
  // method if some of component’s props may update
  // calling this.setState() generally doesn’t trigger componentWillReceiveProps()
  public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
    if(nextProps.listName !== this.props.listName) {
      this.readItemsAndSetStatus();
    }
  }

  //#endregion
  //#region private methods
  private _configureWebPart(): void {
    this.props.configureStartCallback();
  }

  public setInitialState(): void {
    this.state = {
      hasError: false,
      status: this.listNotConfigured(this.props)
        ? "Please configure list in Web Part properties"
        : "Ready",
      columns:[],
      DetailsListItemState:{
        items:[]
      },
      DetailsNewsListItemState:{
        items:[]
      },
      DetailsDirectoryListItemState:{
        items:[]
      },
      DetailsAnnouncementListItemState:{
        items:[]
      },
    };
  }

  // reusable inline component
  private ListMarqueeSelection = (itemState: {columns: IColumn[], items: IListItem[] }) => (
      <div>
          <DetailsList
            items={ itemState.items }
            columns={ itemState.columns }
            setKey="set"
            layoutMode={ DetailsListLayoutMode.fixedColumns }
            selectionPreservedOnEmptyClick={ true }
            compact={ true }>
          </DetailsList>
      </div>
  )

  // read items using factory method pattern and sets state accordingly
  private readItemsAndSetStatus(): void {
    this.setState({
      status: "Loading all items..."
    });

    const factory: ListItemFactory = new ListItemFactory();
    factory.getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName)
    .then((items: any[]) => {

      var myItems: any = null;
      switch(this.props.listName) {
          case "GenericList":
              myItems = items as IListItem[];
              break;
          case "News":
              myItems = items as INewsListItem[];
              break;
          case "Announcements":
              myItems = items as IAnnouncementListItem[];
              break;
          case "Directory":
              myItems = items as IDirectoryListItem[];
              break;
      }

      const keyPart: string = this.props.listName === "GenericList" ? "" : this.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"] : {
            myItems
          },
          columns: buildColumns(myItems)
        });
    });
  }

  private listNotConfigured(props: IFactoryMethodProps): boolean {
    return props.listName === undefined ||
      props.listName === null ||
      props.listName.length === 0;
  }

  //#endregion
}

ฉันคิดว่ารหัสที่เหลือนั้นไม่จำเป็น

อัปเดต SharepointDataProvider.ts

import {
    SPHttpClient,
    SPHttpClientBatch,
    SPHttpClientResponse
  } from "@microsoft/sp-http";
  import { IWebPartContext } from "@microsoft/sp-webpart-base";
  import List from "../models/List";
  import IDataProvider from "./IDataProvider";

  export default class SharePointDataProvider implements IDataProvider {
      private _selectedList: List;
      private _lists: List[];
      private _listsUrl: string;
      private _listItemsUrl: string;
      private _webPartContext: IWebPartContext;

      public set selectedList(value: List) {
        this._selectedList = value;
        this._listItemsUrl = `${this._listsUrl}(guid'${value.Id}')/items`;
      }

      public get selectedList(): List {
        return this._selectedList;
      }

      public set webPartContext(value: IWebPartContext) {
        this._webPartContext = value;
        this._listsUrl = `${this._webPartContext.pageContext.web.absoluteUrl}/_api/web/lists`;
      }

      public get webPartContext(): IWebPartContext {
        return this._webPartContext;
      }

      // get all lists, not only tasks lists
      public getLists(): Promise<List[]> {
        // const listTemplateId: string = '171';
        // const queryString: string = `?$filter=BaseTemplate eq ${listTemplateId}`;
        // const queryUrl: string = this._listsUrl + queryString;
        return this._webPartContext.spHttpClient.get(this._listsUrl, SPHttpClient.configurations.v1)
          .then((response: SPHttpClientResponse) => {
            return response.json();
          })
          .then((json: { value: List[] }) => {
            return this._lists = json.value;
          });
      }
    }

Idataprovider.ts

import { IWebPartContext } from "@microsoft/sp-webpart-base";
import List from "../models/List";
import {IListItem} from "../models/IListItem";

interface IDataProvider {
  selectedList: List;
  webPartContext: IWebPartContext;
  getLists(): Promise<List[]>;
}

export default IDataProvider;

person Luis Valencia    schedule 15.05.2018    source แหล่งที่มา
comment
วิธีการ render ของคุณถูกเรียกอย่างถูกต้องหรือไม่? คุณสามารถบันทึก this.props.listName และรับค่าที่ถูกต้องได้หรือไม่   -  person azium    schedule 15.05.2018
comment
ครั้งแรกที่มันไม่ได้กำหนดไว้จริงๆ screencast.com/t/kBChpQtLIs   -  person Luis Valencia    schedule 15.05.2018
comment
ดูเหมือนว่า this._dataProvider.selectedList.Title ไม่ได้รับการตั้งค่าอย่างถูกต้อง คุณใช้ new MockDataProvider(); หรือ new SharePointDataProvider(); หรือไม่? นั่นน่าจะช่วยให้คุณแคบลงได้   -  person Chase DeAnda    schedule 15.05.2018
comment
ฉันอยู่บนโต๊ะทำงานออนไลน์ของ sharepoint ดังนั้นฉันจึงสามารถรับข้อมูลได้โดยใช้ SharepointDataProvider ฉันจะวางโค้ดเพิ่มเติมด้านบน   -  person Luis Valencia    schedule 15.05.2018
comment
ให้ฉันแชร์รหัสของฉันบน GitHub อาจจะง่ายกว่านี้   -  person Luis Valencia    schedule 15.05.2018
comment
นี่คือวิธีการปัจจุบันของฉัน ซึ่งคอมไพล์แต่ทำงานไม่ถูกต้อง github.com/levalencia/sp-dev-fx-webparts/tree/   -  person Luis Valencia    schedule 15.05.2018
comment
มันสมเหตุสมผลแล้วที่ครั้งแรกจะเป็น undefined เพราะ render จะถูกเรียกก่อนวิธีวงจรชีวิตอื่นๆ ทั้งหมด   -  person azium    schedule 16.05.2018


คำตอบ (2)


เมื่อชื่อรายการเปลี่ยนไป คุณกำลังเรียกใช้ readItemsAndSetStatus:

  public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
    if(nextProps.listName !== this.props.listName) {
      this.readItemsAndSetStatus();
    }
  }

อย่างไรก็ตาม readItemsAndSetStatus ไม่ได้รับพารามิเตอร์ และยังคงใช้ this.props.listName ซึ่งยังไม่มีการเปลี่ยนแปลง

private readItemsAndSetStatus(): void {       
    ...
    const factory: ListItemFactory = new ListItemFactory();
    factory.getItems(this.props.spHttpClient, this.props.siteUrl, this.props.listName)
    ...
}

ลองส่ง nextProps.listName ไปที่ readItemsAndSetStatus:

  public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
    if(nextProps.listName !== this.props.listName) {
      this.readItemsAndSetStatus(nextProps.listName);
    }
  }

จากนั้นใช้พารามิเตอร์ขาเข้าหรือตั้งค่าเริ่มต้นเป็น this.props.listName:

private readItemsAndSetStatus(listName): void {       
    ...
    const factory: ListItemFactory = new ListItemFactory();
    factory.getItems(this.props.spHttpClient, this.props.siteUrl, listName || this.props.listName)
    ...
}
person lux    schedule 24.05.2018
comment
แล้ว ComponentDIdMount ฉันยังใช้ readItemAndSetStatus ที่นั่นโดยไม่มีพารามิเตอร์ - person Luis Valencia; 24.05.2018
comment
@LuisValencia คุณสามารถส่ง this.readItemAndSetStatus(this.props.listName) ใน CDM ได้อย่างแน่นอน ซึ่ง จะ มีความชัดเจนมากกว่าการตั้งค่าเริ่มต้นเป็น this.props.listName ในการเรียก factory.getItems หากไม่มีการส่งผ่านพารามิเตอร์ ที่กล่าวว่า ถ้าคุณ ทำ ตั้งค่าเริ่มต้นเป็น this.props.listName ในการโทรนั้น ก็ปลอดภัยที่จะไม่ส่งผ่านสิ่งใดๆ ดังที่คุณกำลังทำอยู่ตอนนี้ใน CDM เพราะยังไงซะ มันก็จะมีค่าเริ่มต้นเป็น this.props.listName อยู่ดี - person lux; 24.05.2018

ใน "โค้ด webpart" แรกของคุณ เมธอด onInit() จะส่งกลับก่อนที่ loadLists() จะเสร็จสิ้น:

onInit() {
  this.loadLists()  // <-- Sets this._dropdownOptions
    .then(() => {
      this.setSelectedList();
    });

  return super.onInit();  // <-- Doesn't wait for the promise to resolve
}

ซึ่งหมายความว่า getGroupFields() อาจไม่มีข้อมูลสำหรับ _dropdownOptions นั่นหมายความว่า getPropertyPaneConfiguration() อาจไม่มีข้อมูลที่ถูกต้อง

ฉันไม่แน่ใจว่านั่นคือปัญหา หรือปัญหาเดียว ฉันไม่มีประสบการณ์กับ SharePoint เลย ดังนั้นลองพิจารณาทั้งหมดนี้ดู


ฉันเห็นว่าใน react-todo-basic พวกเขากำลังทำสิ่งเดียวกันกับคุณ

อย่างไรก็ตาม ที่อื่นฉันเห็นผู้คนดำเนินการเพิ่มเติมภายในตาม super.onInit สัญญา:

person Reed Dunkle    schedule 23.05.2018