Data Flow in between Multiple SPFx Webparts using Dynamic Data - Netwoven
Blog

Data Flow in between Multiple SPFx Webparts using Dynamic Data

By Priyanka Sen  |  Published on December 18, 2019

Data Flow in between Multiple SPFx Webparts using Dynamic Data

Sometimes two or more SharePoint Framework webparts of same page need same data or need to exchange data between each other. This can be done in different ways like using: Dynamic Data, PnPClientStorage, rx-lite etc. Dynamic Data is the recommended way to share data between SharePoint Framework client-side web parts. It is available from SharePoint Framework 1.7 or higher.

Here we will use Dynamic Data to exchange data between two SPFX webparts. For simplicity and ease of understanding we will send data from a sender SPFX webpart and receive that data in a receiving SPFX webpart.

Sender SPFX webpart displays a list of list products and sends the product name when the product is selected. Receiving SPFX webpart receives the data and display the selected product name. Here the sender acts as Dynamic data source.

Data Flow in between Multiple SPFx Webparts using Dynamic Data

To receive data from the sender, the receiving webpart needs to connect to the sender webpart as the dynamic data source.

Data Flow in between Multiple SPFx Webparts using Dynamic Data
Data Flow in between Multiple SPFx Webparts using Dynamic Data
Data Flow in between Multiple SPFx Webparts using Dynamic Data

Following is the Sender webpart code:

First of all import the following –

import { IDynamicDataPropertyDefinition, IDynamicDataCallables } from '@microsoft/sp-dynamic-data';

Create product interface-

export interface IProduct {
  product: string;
}

Implements IDynamicDataCallables in ProductListWebPart class-

default class ProductListWebPart extends BaseClientSideWebPart<IProductListWebPartProps> implements IDynamicDataCallables

To register ProductListWebPart as dynamic data source write the following code in onInit method-

protected onInit(): Promise<void> {
    // register this web part as dynamic data source
    this.context.dynamicDataSourceManager.initializeSource(this);
    return Promise.resolve();
  }

Create a class variable to store selected product-

private _selectedProduct: IProduct;

Write an event handler method for selecting a Product from the list. Store the selected product in the class variable so that connected component can retrieve its value. Then notify connected components about the value change.

private _productSelected = (product: IProduct): void => {
    this._selectedProduct = product;
    this.context.dynamicDataSourceManager.notifyPropertyChanged('product');
}

Define “getPropertyDefinitions” method to return list of dynamic data properties that this dynamic data source returns-

public getPropertyDefinitions(): ReadonlyArray<IDynamicDataPropertyDefinition> {
    return [
      {
        id: 'product',
        title: 'Product'
      }
    ];
  }

Define “getPropertyValue “ method. This method returns the value of the dynamic data to the connected component depending on the notification for value change.

public getPropertyValue(propertyId: string): IProduct {
    switch (propertyId) {
      case 'product':
        return this._selectedProduct;
    }
    throw new Error('Bad property id');
  }

In render method pass the product select event handler method to the product list component from where the product will be selected.

public render(): void {
    const element: React.ReactElement<IProductListProps> = React.createElement(
      ProductList,
      {
        description: this.properties.description,
        _productSelected: this._productSelected,
      }
    );
    ReactDom.render(element, this.domElement);
  }

From Product list component call the product select event handler and pass the selected product

this.props._productSelected({ product: selectedProduct });

Following is the complete code of sender webpart-

ProductListWebPart.ts

import { IDynamicDataPropertyDefinition, IDynamicDataCallables } from '@microsoft/sp-dynamic-data';
export interface IProduct {
  product: string;
}
default class ProductListWebPart extends BaseClientSideWebPart<IProductListWebPartProps> implements IDynamicDataCallables {
  private _selectedProduct: IProduct;
  private _productSelected = (product: IProduct): void => {
    this._selectedProduct = product;
    this.context.dynamicDataSourceManager.notifyPropertyChanged('product');
  }
  protected onInit(): Promise<void> {
    this.context.dynamicDataSourceManager.initializeSource(this);
    return Promise.resolve();
  }
  public getPropertyDefinitions(): ReadonlyArray<IDynamicDataPropertyDefinition> {
    return [
      {
        id: 'product',
        title: 'Product'
      }
    ];
}
  public getPropertyValue(propertyId: string): IProduct {
    switch (propertyId) {
      case 'product':
        return this._selectedProduct;
    }
    throw new Error('Bad property id');
  }
  public render(): void {
    const element: React.ReactElement<IProductListProps> = React.createElement(
      ProductList,
      {
        description: this.properties.description,
        _productSelected: this._productSelected,
      }
    );
    ReactDom.render(element, this.domElement);
  }
}

ProductList.tsx

export default class ProductList extends React.Component<IProductListProps, any> {
  constructor(props: any) {
    super(props);
    this.state = { products: ["Laptop", "Monitor", "Keyboard", "Mouse", "Headphone"],
             selectedProduct: ""};
  }
  public componentWillMount() {
    this.props._productSelected({ product: this.state.selectedProduct });
  }
  public render(): React.ReactElement<IProductListProps> {
    return (
      <div className={styles.productList}>
        <div className={styles.container}>
          <div className={styles.title}>{this.props.description} Webpart</div>
          {this.state.products.map(item => {
            return (
              <div>
                <div className={item == this.state.selectedProduct ? styles.submenuItemActive : styles.submenuItem} onClick={() => {
                  this.setState({ selectedProduct: item });
                  this.props._productSelected({ product: item });
                }}>{item}</div>
              </div>
            );
          })}
        </div>
      </div>
    );
  }
}

Following is the recieving webpart code:

Import the following in webpart.ts file-

import { BaseClientSideWebPart, IWebPartPropertiesMetadata } from '@microsoft/sp-webpart-base';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField,
  PropertyPaneDynamicFieldSet,
  PropertyPaneDynamicField,
  DynamicDataSharedDepth,
  IPropertyPaneConditionalGroup
} from '@microsoft/sp-property-pane';
import { DynamicProperty } from '@microsoft/sp-component-base';

Modify WebPartProps interface to add DynamicProperty-

export interface IDetailsWebPartProps {
  description: string;
  product: DynamicProperty<string>;
}

Define onConfigure method to open the propertyPane.

private _onConfigure = (): void => {
    this.context.propertyPane.open();
}

In render method retrieve the dynamic property value i.e. the selected product and a Boolean to determine the configuration status. Pass the selected product value, configuration status and “onConfigure” method to the child component.

public render(): void {
    const product: string | undefined = this.properties.product.tryGetValue();
    const needsConfiguration: boolean = (!product && !this.properties.product.tryGetSource());
    const element: React.ReactElement<IDetailsProps> = React.createElement(
      Details,
      {
        description: this.properties.description,
        dynamicProduct: !!this.properties.product.tryGetSource(),
        product: `${product}`,
        needsConfiguration: needsConfiguration,
        onConfigure: this._onConfigure,
      }
    );
    ReactDom.render(element, this.domElement);
  }

Define propertiesMetadata method to specify the web part properties data type to allow the selected product information to be serialized by the SharePoint Framework

protected get propertiesMetadata(): IWebPartPropertiesMetadata {
    return {
      'product': {
        dynamicPropertyType: 'string'
      }
    };
  }

Define getPropertyPaneConfiguration method as follows to retrieve the selected product information from the connected dynamic data source webpart-

protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          groups: [
            {
              primaryGroup: {
                groupName: strings.DataGroupName,
                groupFields: [
                  PropertyPaneTextField('description', {
                    label: strings.DescriptionFieldLabel
                  })
                ]
              },
              secondaryGroup: {
                groupName: strings.DataGroupName,
                groupFields: [
                  PropertyPaneDynamicFieldSet({
                    label: 'product',
                    fields: [
                      PropertyPaneDynamicField('product', {
                        label: strings.ProductFieldLabel
                      })
                    ],
                    sharedConfiguration: {
                      depth: DynamicDataSharedDepth.Property
                    }
                  })
                ]
              },
              showSecondaryGroup: !!this.properties.product.tryGetSource()
            } as IPropertyPaneConditionalGroup
          ]
        }
      ]
    };
  }

In the render method of the child component write the following code to show selected product name retrieved from connected dynamic data source depending on configuration status-

public render(): React.ReactElement<IDetailsProps> {
    return (
      <div className={styles.details}>
<div className={styles.container}>
          <div className={styles.title}>{this.props.description} Webpart</div>
          <div className={styles.submenuItemActive}>
            {
              this.props.needsConfiguration &&
              <button onClick={() => this.props.onConfigure()}>Configure</button>
              || (this.props.product == '' && "Select Product from menu" || "You have selected: " + this.props.product)
            }
          </div>
        </div>
      </div>
    );

Following is the complete code of receiving webpart-

DetailsWebPart.ts
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '@microsoft/sp-core-library';
import { BaseClientSideWebPart, IWebPartPropertiesMetadata } from '@microsoft/sp-webpart-base';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField,
  PropertyPaneDynamicFieldSet,
  PropertyPaneDynamicField,
  DynamicDataSharedDepth,
  IPropertyPaneConditionalGroup
} from '@microsoft/sp-property-pane';
import { DynamicProperty } from '@microsoft/sp-component-base';
import * as strings from 'DetailsWebPartStrings';
import Details from './components/Details';
import { IDetailsProps } from './components/IDetailsProps';

export interface IDetailsWebPartProps {
  description: string;
  product: DynamicProperty<string>;
}

export default class DetailsWebPart extends BaseClientSideWebPart<IDetailsWebPartProps> {
  private _onConfigure = (): void => {
    this.context.propertyPane.open();
  }
  public render(): void {
    const product: string | undefined = this.properties.product.tryGetValue();
    const needsConfiguration: boolean = (!product && !this.properties.product.tryGetSource());
    const element: React.ReactElement<IDetailsProps> = React.createElement(
      Details,
      {
        description: this.properties.description,
        dynamicProduct: !!this.properties.product.tryGetSource(),
        product: `${product}`,
        needsConfiguration: needsConfiguration,
        onConfigure: this._onConfigure,
      }
    );
    ReactDom.render(element, this.domElement);
  }
  protected onDispose(): void {
    ReactDom.unmountComponentAtNode(this.domElement);
  }
  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }
  protected get propertiesMetadata(): IWebPartPropertiesMetadata {
    return {
      'product': {
        dynamicPropertyType: 'string'
      }
    };
  }
  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          groups: [
            {
              primaryGroup: {
                groupName: strings.DataGroupName,
                groupFields: [
                  PropertyPaneTextField('description', {
                    label: strings.DescriptionFieldLabel
                  })
                ]
              },
              secondaryGroup: {
                groupName: strings.DataGroupName,
                groupFields: [
                  PropertyPaneDynamicFieldSet({
                    label: 'product',
                    fields: [
                      PropertyPaneDynamicField('product', {
                        label: strings.ProductFieldLabel
                      })
                    ],
                    sharedConfiguration: {
                      depth: DynamicDataSharedDepth.Property
                    }
                  })
                ]
              },
              showSecondaryGroup: !!this.properties.product.tryGetSource()
            } as IPropertyPaneConditionalGroup
          ]
        }
      ]
    };
  }
  protected get disableReactivePropertyChanges(): boolean {
    return true;
  }
}
Details.tsx
import * as React from 'react';
import styles from './Details.module.scss';
import { IDetailsProps } from './IDetailsProps';

export default class Details extends React.Component<IDetailsProps, {}> {
  public render(): React.ReactElement<IDetailsProps> {
    return (
      <div className={styles.details}>
        <div className={styles.container}>
          <div className={styles.title}>{this.props.description} Webpart</div>
          <div className={styles.submenuItemActive}>
            {
              this.props.needsConfiguration &&
              <button onClick={() => this.props.onConfigure()}>Configure</button>
              || (this.props.product == '' && "Select Product from menu" || "You have selected: " + this.props.product)
            }
          </div>
        </div>
      </div>
    );
  }
}

Conclusion: Similar way other web parts can also connect with the same sender webpart. Even any webpart can act as both sender and receiver at the same time. Sharing data can potentially reduce number of API calls in a page and make it more dynamic and interactive. It also helps to design the page well.

Leave a comment

Your email address will not be published. Required fields are marked *

Unravel The Complex
Stay Connected

Subscribe and receive the latest insights

Netwoven Inc. - Microsoft Solutions Partner

Get involved by tagging Netwoven experiences using our official hashtag #UnravelTheComplex