10/11/2016

SPFx. Client Web Part Property Pane Dependent Properties. Part I: Preparation.

This is the first post of three that explain how to add dependent properties to Client Web Part Property Pane.
As an example of such properties I decided to use Lists dropdown and Views dropdown. Views dropdown is populated based on selected List.
As a result of these three posts we'll have a fully working web part with ability to select List View in Property Pane:

Here is a content of 3 posts:
  1. SPFx. Client Web Part Property Pane Dependent Properties. Part I: Preparation. (current post)
  2. SPFx. Client Web Part Property Pane Dependent Properties. Part II: Dependent Properties Control.
  3. SPFx. Client Web Part Property Pane Dependent Properties. Part III: Styling Dependent Properties Control as Office UI Fabric dropdowns. Knockout version.
All the code is available on GitHub (https://github.com/AJIXuMuK/SPFx/tree/master/dep-props-custom-class)
I'm using Visual Studio Code IDE for the tutorial.

So let's start with preparation.

This post describes how to set up your environment and implement code to request data from SharePoint or Mock Store based on your environment (local workbench or SharePoint Online).

Setting Up Environment

If it's your first touch of SharePoint Framework (SPFx) then you'll need to set up your environment (O365 and local) as it is described in Office Dev Center:

Scaffolding Web Part Project

After setting up is done you can start with your web part creation. The process is described in Build your first web part.
In brief, you need to execute multiple command line operations:

Create a new directory in location you want:
mkdir dep-props-webpart
Go to created directory:
cd dep-props-webpart
"Scaffold" your project with Yeoman generator:
yo @microsoft/sharepoint
Select name, description and base framework for the web part (I'm not using any default framework in this example. I tried to select knockout but it was not compiling).
Wait for a while until all the dependencies will be downloaded.
Play with your Hello World web part using Gulp:
gulp serve
If you're developing on Mac you'll probably need to run
sudo gulp serve
It will run local server and SharePoint workbench for testing your web parts locally.

Creating Data Helpers

Basically, all the code of getting data is described in Connect to SharePoint.
In brief, we need to implement retrieving some mock data for local environment (when we test our web part on local server created by gulp serve) and retrieving SharePoint data.
We can use IWebPartContext.environment.type property to understand what data to retrieve.
I don't like the idea of putting all the logic into the web part class itself. That's why I propose to create mock data helper, SharePoint data helper and data helpers factory to created needed helper based on current environment.
First, let's create all base entities we'll use in our app. (I created a subfolder named common in web part folder and SPEntities.ts file in it.)

ISPList - represents SharePoint List object with Title and Id
/**
 * Represents SharePoint List object
 */
export interface ISPList {
  Title: string;
  Id: string;
}

ISPLists - represents SharePoint REST service response for /_api/web/lists service call
/**
 * Represents SharePoint REST service response for /_api/web/lists service call
 */
export interface ISPLists {
  value: ISPList[];
}

ISPView - represents SharePoint View object with Title, Id and ListId
/**
 * Represents SharePoint View object
 */
export interface ISPView {
  Title: string;
  Id: string;
  ListId: string;
}

ISPViews - represents SharePoint REST service response for /_api/web/lists('id')/views service call
/**
 * Represents SharePoint REST service response for /_api/web/lists('id')/views service call
 */
export interface ISPViews {
  value: ISPView[];
}

Now we can start with data helpers. Let's create a separate subfolder for them and name it data-helpers.
First of all, we need some base interface that can be used to abstract of current data helper. What methods do we need? Well, not so much: a method to get lists and a method to get views of the list. Let's create such interface in a file named DataHelperBase.ts
// importing base entities
import { ISPList, ISPView } from '../common/SPEntities';

/**
 * Data Helpers interface
 */
export interface IDataHelper {
  /**
   * API to get lists from the source
   */
  getLists(): Promise<ISPList[]>;
  /**
   * API to get views from the source
   */
  getViews(listId: string): Promise<ISPView[]>;
}

Now let's create a mock helper. We'll use some static data for test. The mock helper will be created in DataHelperMock.ts file.
import {
  IWebPartContext
} from '@microsoft/sp-client-preview';
import { ISPList, ISPView } from '../common/SPEntities';
import { IDataHelper } from './DataHelperBase';

/**
 * MOCK data helper. Gets data from hardcoded values
 */
export class DataHelperMock implements IDataHelper {
  /**
   * hardcoded collection of lists
   */
  private static _lists: ISPList[] = [{ Title: 'Test 1', Id: '1' }, { Title: 'Test 2', Id: '2' }, { Title: 'Test 3', Id: '3' }];
  /**
   * hardcoded collection of views
   */
  private static _views: ISPView[] = [{ Title: 'All Items', Id: '1', ListId: '1' }, { Title: 'Demo', Id: '2', ListId: '1' }, { Title: 'All Items', Id: '1', ListId: '2' }, { Title: 'All Items', Id: '1', ListId: '3' }];

  /**
   * API to get lists from the source
   */
  public getLists(): Promise<ISPList[]> {
    return new Promise<ISPList[]>((resolve) => {
      resolve(DataHelperMock._lists);
    });
  }

  /**
   * API to get views from the source
   */
  public getViews(listId: string): Promise<ISPView[]> {
    return new Promise<ISPView[]>((resolve) => {
      const result: ISPView[] = DataHelperMock._views.filter((value, index, array) => {
        return value.ListId === listId;
      });
      resolve(result);
    });
  }
}

Time to create SharePoint data helper. For this data helper we'll need a web part context and especially pageContext.web.absoluteUrl property that will be used to create a request url. Let's create a DataHelperSP.ts file and implement the helper there:
import {
  IWebPartContext
} from '@microsoft/sp-client-preview';
import { ISPList, ISPView, ISPLists, ISPViews } from '../common/SPEntities';
import { IDataHelper } from './DataHelperBase';

/**
 * List with views interface
 */
interface ISPListWithViews extends ISPList {
  /**
   * List Views
   */
  Views: ISPView[];
}

/**
 * SharePoint Data Helper class.
 * Gets information from current web
 */
export class DataHelperSP implements IDataHelper {
  /**
   * Web part context
   */
  public context: IWebPartContext;

  /**
   * Loaded lists
   */
  private _lists: ISPListWithViews[];

  /**
   * ctor
   */
  public constructor(_context: IWebPartContext) {
    this.context = _context;
  }

  /**
   * API to get lists from the source
   */
  public getLists(): Promise<ISPList[]> {
    return this.context.httpClient.get(this.context.pageContext.web.absoluteUrl + `/_api/web/lists?$filter=Hidden eq false`) // sending the request to SharePoint REST API
      .then((response: Response) => { // httpClient.get method returns a response object where json method creates a Promise of getting result
        return response.json(); 
      }).then((response: ISPLists) => { // response is an ISPLists object
        return response.value;
      });
  }

  /**
   * API to get views from the source
   */
  public getViews(listId: string): Promise<ISPView[]> {
    if (listId && listId == '-1' || listId == '0')
      return new Promise<ISPView[]>((resolve) => {
      resolve(new Array<ISPView>());
    });

    //
    // trying to get views from cache
    //
    const lists: ISPListWithViews[] = this._lists && this._lists.length && this._lists.filter((value, index, array) => { return value.Id === listId; });

    if (lists && lists.length) {
      return new Promise<ISPView[]>((resolve) => {
        resolve(lists[0].Views);
      });
    }
    else {
      return this.context.httpClient.get(this.context.pageContext.web.absoluteUrl + '/_api/web/lists(\'' + listId + '\')/views') // requesting views from SharePoint REST API
        .then((response: Response) => { // httpClient.get method returns a response object where json method creates a Promise of getting result
          return response.json();
        }).then((response: ISPViews) => { // response is an ISPView object
          var views = response.value;
          if (!this._lists || !this._lists.length)
            this._lists = new Array<ISPListWithViews>();
          this._lists.push({ Id: listId, Title: '', Views: views }); // saving views to cache
          return views;
        });
    }
  }

The final part is to create a data helper factory that will create a data helper based on current environment. Let's do it in a DataHelpersFactory.ts file.
import {
  IWebPartContext // importing web part context class
} from '@microsoft/sp-client-preview';
import { EnvironmentType } from '@microsoft/sp-client-base'; // importing EnvironmentType enum to check current environment
//
// importing data helpers
//
import { IDataHelper } from './DataHelperBase';
import { DataHelperMock } from './DataHelperMock';
import { DataHelperSP } from './DataHelperSP';

/**
 * Factory object to create data helper based on current EnvironmentType
 */
export class DataHelpersFactory {
  /**
   * API to create data helper
   * @context: web part context
   */
  public static createDataHelper(context: IWebPartContext): IDataHelper {
    if (context.environment.type === EnvironmentType.Local) {
      return new DataHelperMock();
    }
    else {
      return new DataHelperSP(context);
    }
  }
}

That's it. We have everything we need to get information from mock store as well as from SharePoint. It means that it can be used on your local server or on workbench.aspx from your dev site collection.
Let's test the results. In web part file add imports for factory, data helpers interface and base SP Entities:
import { IDataHelper } from './data-helpers/DataHelperBase';
import { DataHelpersFactory } from './data-helpers/DataHelpersFactory';
import { ISPList, ISPView } from './common/SPEntities';

Then, in render method add code to retrieve lists and views and log information to console:
const dataHelper: IDataHelper = DataHelpersFactory.createDataHelper(this.context);
dataHelper.getLists().then((lists: ISPList[]) => {
  console.log(lists); // logging lists
  if (lists.length) {
    const list: ISPList = lists[0];
    dataHelper.getViews(list.Id).then((views: ISPView[]) => {
      console.log(views); // logging views
    });
  }
});
And somewhere in console you'll find the results:




















This is it for today. In the next post I'll describe how to create a custom field in a Property Pane that will use information from created data helpers and will dynamically update options in Views select when user selects a list.

Have fun!

No comments:

Post a Comment