3/23/2019

Connected SharePoint Framework Web Parts Using Dynamic Data

Using the dynamic data capability, you can connect SharePoint Framework client-side web parts and extensions to each other and exchange information between the components.
Official documentation contains great example on how to connect web parts using DynamicProperty web part properties that can consume values from other web part, or, generally, data source.
In my case I want to connect web parts in "old-school" way - with events firing and handling. Moreover, I want "receiver" web part to handle event from multiple "senders".
This blog post is based on Page Sections Navigation sample from SPFx web parts samples repository.
So, the initial idea is to create a solution that allows to render page sections navigation on SharePoint modern page.
If you think of such a solution one of possible implementations would be to have "master" web part that renders the navigation, and multiple "anchor" web parts that can be added by a user anywhere on the page.
"Master" web part should know about all the anchors, their positions and titles to render navigation and handle navigation events.
And this is actually doable using dynamic data capability.
Let's start with "Anchor" web part implementation. I'll be partially skipping details of code to focus attention on Dynamic Data features and APIs.
As I mentioned above, "anchor" web part should notify "master" web part about its title and position (or DOM element instead of position). To do that we need to do few things:
  • mark "anchor" web part as a "data source" - implement IDynamicDataCallables interface and two methods from this interface:
    • getPropertyDefinitions to return list of dynamic data properties
    • getPropertyValue to get the value of specified property
  • register the web part as a data source
  • notify the engine if any of dynamic properties has been changed
There are two ways to implement dynamic properties for the "anchor" web part: either implement two properties: title and domElement, or combine them in single object. I selected the second approach.
First, let's declare IAnchorItem interface:
/**
 * Anchor interface to be transferred to the "master" web part
 */
export interface IAnchorItem {
    /**
     * Title
     */
    title?: string;
    /**
     * DOM element
     */
    domElement?: HTMLElement;
}
Next, implement the IDynamicDataCallables interface:
import { IDynamicDataPropertyDefinition, IDynamicDataCallables, IDynamicDataSource } from '@microsoft/sp-dynamic-data';
export default class PageSectionsNavigationAnchorWebPart extends BaseClientSideWebPart<IPageSectionsNavigationAnchorWebPartProps> implements IDynamicDataCallables {
  // ...
  // anchor data object related to the current web part
  private _anchor: IAnchorItem;
  /**
   * implementation of getPropertyDefinitions from IDynamicDataCallables
   */
  public getPropertyDefinitions(): ReadonlyArray<IDynamicDataPropertyDefinition> {
    return [{
      id: 'anchor',
      title: 'Anchor'
    }];
  }

  /**
   * implementation of getPropertyValue from IDynamicDataCallables
   * @param propertyId property Id
   */
  public getPropertyValue(propertyId: string): IAnchorItem {
    switch (propertyId) {
      case 'anchor':
        return this._anchor;
    }

    throw new Error('Bad property id');
  }
}
Next step is to register the web part as a data source using this.context.dynamicDataSourceManager.initializeSource. Let's do that in onInit method:
  protected onInit(): Promise<void> {
    // registering current web part as a data source
    this.context.dynamicDataSourceManager.initializeSource(this);
  }
Now we can notify subscribers every time when title or DOM element has been changed using this.context.dynamicDataSourceManager.notifyPropertyChanged method.
In provided sample I'm doing that based on events fired from React component:
  public render(): void {
    //...
    const element: React.ReactElement<IPageSectionsNavigationAnchorProps> = React.createElement(
      PageSectionsNavigationAnchor,
      {
        //...
        updateProperty: (title => {
          this._anchor.title = this.properties.title = title;
          // notifying that title has been changed
          this.context.dynamicDataSourceManager.notifyPropertyChanged('anchor');
        }),
        anchorElRef: (el => {
          // notifying subscribers that the anchor component has been rendered
          this._anchor.domElement = el;
          this.context.dynamicDataSourceManager.notifyPropertyChanged('anchor');
        }),
        navPosition: position
      }
    );

    ReactDom.render(element, this.domElement);

  }

Now let's work on "master" web part.
In this web part we need to collect all "anchor" data sources (web parts registered as data sources) from the page and subscribe to the changes of anchor property. Two things to consider here:
  1. The page can contain data sources other than "anchors". These data sources should not be processed by the "master" web part.
  2. "anchors" data sources can be dynamically added/removed based on user's interaction.
Currently, there is no tutorial on how to correctly work with data sources. But there is an SharePoint Framework API Reference that will help us! And, in particular, DynamicDataProvider and DynamicDataSourceManager classes.
The first one is exposed as this.context.dynamicDataProvider in web parts and allows to get all data sources from the page as well as register for the change of available data sources.
/**
* Registers a callback to an event that raises when the list of available Dynamic Data Sources is updated.
*
* @param callback - Function to execute when the sources are updated.
*/
registerAvailableSourcesChanged(callback: () => void): void;
/**
* Returns a list with all available Dynamic Data Sources.
*
* @returns Read-only array with all available sources.
*/
getAvailableSources(): ReadonlyArray<IDynamicDataSource>;
The second one is exposed as this.context.dynamicDataSourceManager in web parts and allows to register on properties' changes of specific data sources.
Knowing the above API we can easily collect "anchor" data sources dynamically and register on "anchor" property change.
First, let's add a field that will store current "anchors" on the page:
// "Anchor" data sources
private _dataSources: IDynamicDataSource[] = [];
Next, let's create the method that will iterate through page's data sources, update _dataSources collection, and subscribe on "anchor" property change event:
/**
 * Initializes collection of "Anchor" data soures based on collection of existing page's data sources
 */
private _initDataSources() {
  // all data sources on the page
  const availableDataSources = this.context.dynamicDataProvider.getAvailableSources();

  if (availableDataSources && availableDataSources.length) {
    // "Anchor" data sources cached in the web part from prev call
    const dataSources = this._dataSources;
    //
    // removing deleted data sources if any
    //
    const availableDataSourcesIds = availableDataSources.map(ds => ds.id);
    for (let i = 0, len = dataSources.length; i < len; i++) {
      let dataSource = dataSources[i];
      if (availableDataSourcesIds.indexOf(dataSource.id) == -1) {
        dataSources.splice(i, 1);
        try {
          this.context.dynamicDataProvider.unregisterPropertyChanged(dataSource.id, 'anchor', this._onAnchorChanged);
        }
        catch (err) { }
        i--;
        len--;
      }
    }

    //
    // adding new data sources
    //
    for (let i = 0, len = availableDataSources.length; i < len; i++) {
      let dataSource = availableDataSources[i];
      if (!dataSource.getPropertyDefinitions().filter(pd => pd.id === 'anchor').length) {
        continue; // we don't need data sources other than anchors
      }
      if (!dataSources || !dataSources.filter(ds => ds.id === dataSource.id).length) {
        dataSources.push(dataSource);
        this.context.dynamicDataProvider.registerPropertyChanged(dataSource.id, 'anchor', this._onAnchorChanged);
      }
    }
  }
}
The last part is to call this method when the "master" web part is being initialize and every time when data sources are being changed.
protected onInit(): Promise<void> {    
  // getting data sources that have already been added on the page
  this._initDataSources();
  // registering for changes in available datasources
  this.context.dynamicDataProvider.registerAvailableSourcesChanged(this._initDataSources.bind(this, true));
}

Conclusion

Connecting web part using SPFx Dynamic Data is simple. Here are configuration/implementation steps:
  • In sender:
    • Implement IDynamicDataCallables interface in a web part with two methods: getPropertyDefinitions and getPropertyValue
    • Register a web part as a Data Source using this.context.dynamicDataSourceManager.initializeSource
    • Notify subscriber that some of properties have been changed using this.context.dynamicDataSourceManager.notifyPropertyChanged
  • In receiver:
    • Use this.context.dynamicDataProvider.registerAvailableSourcesChanged and this.context.dynamicDataProvider.getAvailableSources to iterate through available page's data source and select the ones to work with
    • Use this.context.dynamicDataProvider.registerPropertyChanged to register event handlers for specific data sources and properties
The example from this post processes single event from one type of data source. But you can use the same API to process different properties, data sources, and also implement circle connections between web parts.

That's all for today!
Have fun!

4 comments:

  1. Hi Alex, Awesome work! I've played around with the code and the web part, and it's perfect for learning how to use dynamic data in spfx.

    However, I notice some odd behaviour. On a page where I added the page-section-navigation web parts, when I click on other links to other modern site pages in the same site collection, the other site pages have issues in loading. In the browser console, I see errors 'TypeError: Cannot read property 'dynamicDataSourceManager' of undefined at anchorElRef'. I added the Quick Links web part, or just created hyperlinks in a Text web part, to other site pages, the content of the site pages cannot be loaded. Only after refreshing the page the contents of the other page appears.
    Could it be that clicking on these other hyperlinks triggering the React event and then trying to access the dynamicDataSourceManager properties.

    Thanks,
    ST Chiew

    ReplyDelete
    Replies
    1. Hi! Great catch!
      Thanks for that!
      I've created a PR to fix that issue. You can follow the PR here:
      https://github.com/SharePoint/sp-dev-fx-webparts/pull/837

      Delete
  2. Fantastic! I've tried out your code in the PR and in works. The trick was in disposing the data source manager...
    Thanks again for your wonderful work...

    ReplyDelete