【SharePoint Online】SharePoint に接続してリスト情報を取得するようにカスタムする ーSPFx開発 その2ー

2019年9月16日

こんにちは。
今回はこのページを色々調べてやっていこうと思います。
詳細を知りたい方は元ページを確認してください。
私の方では自分がわかりやすいように噛み砕いてざっくりやることに重点をおいて書いていこうと思います。

前回の記事の続きですのでそちらを見てもOKです。
一応最後にスクリプト全部乗っけておきます。

SharePoint Frameworkの記事一覧

ページ コンテキストへのアクセスを取得する

ワークベンチをSPOでホストしている場合、以下の情報を簡単に取得することができるようです。

・Web タイトル
・Web の絶対 URL
・Web サーバー関連 URL
・ユーザー サインイン名

前回作成したプロジェクトの「C:\SPFx\HelloWorld\src\webparts\HelloWorld\ HelloWorldWebPart.ts」ファイルにタイトルを取得するように追加してみます。

this.domElement.innerHTML = `
  <div class="${ styles.helloWorld }">
    <div class="${ styles.container }">
      <div class="${ styles.row }">
        <div class="${ styles.column }">
          <span class="${ styles.title }">Welcome to SharePoint!</span>
          <p class="${ styles.subTitle }">Customize SharePoint experiences using web parts.</p>
          <p class="${ styles.description }">${escape(this.properties.description)}</p>
          <p class="${ styles.description }">${escape(this.properties.test)}</p>
          <p class="${ styles.description }">Loading from ${escape(this.context.pageContext.web.title)}</p>
          <a href="https://aka.ms/spfx" class="${ styles.button }">
            <span class="${ styles.label }">Learn more</span>
          </a>
        </div>
      </div>
    </div>
  </div>`;

因みに、ファイルへの変更はgulpが自動的に検知してコンパイルしてくれるそうです。
ブラウザのワークベンチのページを更新するだけで読み込み直すことができます。

ローカルのワークベンチから読み込んだ場合
テストサイトから読み込んだ場合

リスト モデルの定義

リストを取得するには、2 つのモデルが必要なようです。
イマイチ説明不足感はありますが、恐らく[ISPList]でリスト一覧を格納するための入れ物、
[Title, Id]で列情報の入れ物を作成しておくという感じでしょうか。
以下のコードをクラスの直前に追加します。

export interface ISPLists {
  value: ISPList[];
}

export interface ISPList {
  Title: string;
  Id: string;
}

ローカルのワークベンチでテストするためにはモックストアというものが必要なようです。
擬似的なMSのストアのようなものと思われます。
MockHttpClient.tsというファイルを[src\webparts\HelloWorld]直下に新規作成します。

import { ISPList } from './HelloWorldWebPart';

export default class MockHttpClient  {

    private static _items: ISPList[] = [{ Title: 'Mock List', Id: '1' },
                                        { Title: 'Mock List 2', Id: '2' },
                                        { Title: 'Mock List 3', Id: '3' }];

    public static get(): Promise<ISPList[]> {
    return new Promise<ISPList[]>((resolve) => {
            resolve(MockHttpClient._items);
        });
    }
}

そしてHelloWorldWebPart.tsに戻り、作成したMockHttpClientを読み込むためのコードと、HelloWorldWebPartクラス内でリスト検索をモックするプライベート メソッドを追加します。

import MockHttpClient from ‘./MockHttpClient’;

→import * as strings from ‘HelloWorldWebPartStrings’;直下に入力

private _getMockListData(): Promise<ISPLists> {
  return MockHttpClient.get()
    .then((data: ISPList[]) => {
      var listData: ISPLists = { value: data };
      return listData;
    }) as Promise<ISPLists>;
}

→クラス内に記入

SharePoint サイトからリストを検索

次にSharePoint REST API を使用して、サイトからリストを取得します。
これを実現するクラスが@microsoft/sp-http モジュール内にある、
[spHttpClient]というクラスです。
これを呼び出すコードを、
先程記載した[import MockHttpClient from ‘./MockHttpClient’;]直下に記載します。

import {
  SPHttpClient,
  SPHttpClientResponse   
} from '@microsoft/sp-http';

次に以下のプライベートメソッドを[HelloWorldWebPart]クラス内に記載します。

private _getListData(): Promise<ISPLists> {
  return this.context.spHttpClient.get(this.context.pageContext.web.absoluteUrl + `/_api/web/lists?$filter=Hidden eq false`, SPHttpClient.configurations.v1)
    .then((response: SPHttpClientResponse) => {
      return response.json();
    });
}

新しいスタイルの追加

SPFxではSassをCSSのプリプロセッサとして使用しているそうです。
因みに、SharePoint フレームワークには、Sass ファイルを通常の CSS ファイルに変換する SCSS コンパイラが付いており、開発中に使用する型指定されたバージョンも提供されます。

[HelloWorldWebPart.module.scss]を展開してください。
.buttonスタイルの直後に、以下のコードを入力します。

.list {
  color: #333333;
  font-family: 'Segoe UI Regular WestEuropean', 'Segoe UI', Tahoma, Arial, sans-serif;
  font-size: 14px;
  font-weight: normal;
  box-sizing: border-box;
  margin: 10;
  padding: 10;
  line-height: 50px;
  list-style-type: none;
  box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

.listItem {
  color: #333333;
  vertical-align: center;
  font-family: 'Segoe UI Regular WestEuropean', 'Segoe UI', Tahoma, Arial, sans-serif;
  font-size: 14px;
  font-weight: normal;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  box-shadow: none;
  *zoom: 1;
  padding: 9px 28px 3px;
  position: relative;
}

読み込むリストに対してのスタイルをここで定義しているようです。

リスト情報を表示する

SharePoint のワークベンチは便利ですが、これはEnvironmentType モジュールを使用して Web パーツを実行している環境を理解しやすくすることで、この機能を補助しているそうです。
このEnvironmentTypeを使用できるように書き込んでいきます。
以下のコードをインポートセクションに書き込みます。
先程書き込んだSPHttpClientの配下でいいでしょう。

import {
   Environment,
   EnvironmentType
 } from '@microsoft/sp-core-library';

つづいて、[HelloWorldWebPart]クラス内に以下のコードを記載します。

private _renderList(items: ISPList[]): void {
  let html: string = '';
  items.forEach((item: ISPList) => {
    html += `
  <ul class="${styles.list}">
    <li class="${styles.listItem}">
      <span class="ms-font-l">${item.Title}</span>
    </li>
  </ul>`;
  });

  const listContainer: Element = this.domElement.querySelector('#spListContainer');
  listContainer.innerHTML = html;
}

このメソッドは、styles 変数を使用して以前に追加された新しい CSS スタイルを参照し、REST API から受信されるリスト情報をレンダリングするために使用されるそうです。

引き続き[HelloWorldWebPart]クラス内に、
以下メソッドを記載し、それぞれのメソッドを呼び出してリスト データを取得します。

private _renderListAsync(): void {
  // Local environment
  if (Environment.type === EnvironmentType.Local) {
    this._getMockListData().then((response) => {
      this._renderList(response.value);
    });
  }
  else if (Environment.type == EnvironmentType.SharePoint || 
            Environment.type == EnvironmentType.ClassicSharePoint) {
    this._getListData()
      .then((response) => {
        this._renderList(response.value);
      });
  }
}

実行環境によって、実行するメソッドが変わっていますね。
ローカル環境用とSP用とがあります。

リストデータの取得

最後に、レンダリング メソッドに移動し、メソッドの内部のコードを次のコードに置き換えます。

this.domElement.innerHTML = `
  <div class="${ styles.helloWorld }">
    <div class="${ styles.container }">
      <div class="${ styles.row }">
        <div class="${ styles.column }">
          <span class="${ styles.title }">Welcome to SharePoint!</span>
          <p class="${ styles.subTitle }">Customize SharePoint experiences using web parts.</p>
          <p class="${ styles.description }">${escape(this.properties.description)}</p>
          <p class="${ styles.description }">${escape(this.properties.test)}</p>
          <p class="${ styles.description }">Loading from ${escape(this.context.pageContext.web.title)}</p>
          <a href="https://aka.ms/spfx" class="${ styles.button }">
            <span class="${ styles.label }">Learn more</span>
          </a>
        </div>
      </div>
      <div id="spListContainer" />
    </div>
  </div>`;

  this._renderListAsync();

[_renderListAsync]を読み込むようになり、spListContainerのidが付与されました。
spListContainerのidは[_renderList]メソッド内でDOM読み込み→HTML追加
という処理をしているために追加しているようです。

ファイルを保存して、gulp serve コンソール ウィンドウ内でエラーが出てないことを確認しましょう。

ローカルワークベンチをリロードしてみると・・・

読み込んでる!
じゃあSPサイトのワークベンチはどうでしょう・・・

サイトのリスト・ライブラリが読み込まれました!
思ったより簡単ですね!

引っかかった点・注意事項

作業している途中、最後のリスト表示の際にちょっと問題が発生しました。
調べてみると、前回の記事の [Web パーツのプロパティ ウィンドウの構成]セクション以降をちゃんと実施しておかないと正常に稼働しないようでした。
コピペするだけなので数分で解決したのですが、皆さんご注意してください。

最後に

今回はここまでです。
今回作成したソースを以下に置いておきますので参考にしてください。
かなり説明不足だったりするのでここで賄っておいて頂ければと・・・(笑)

それではまた。

HelloWorldWebPart.ts

import { Version } from '@microsoft/sp-core-library';
import {
  IPropertyPaneConfiguration,
  PropertyPaneTextField,
  PropertyPaneCheckbox,
  PropertyPaneDropdown,
  PropertyPaneToggle
} from '@microsoft/sp-property-pane';
import {
  BaseClientSideWebPart,
} from '@microsoft/sp-webpart-base';
import { escape } from '@microsoft/sp-lodash-subset';

import {
  Environment,
  EnvironmentType
} from '@microsoft/sp-core-library';

import styles from './HelloWorldWebPart.module.scss';
import * as strings from 'HelloWorldWebPartStrings';
import MockHttpClient from './MockHttpClient';
import {
  SPHttpClient,
  SPHttpClientResponse   
} from '@microsoft/sp-http';

export interface IHelloWorldWebPartProps {
  description: string;
  test: string;
  test1: boolean;
  test2: string;
  test3: boolean;
}

export interface ISPLists {
  value: ISPList[];
}

export interface ISPList {
  Title: string;
  Id: string;
}

export default class HelloWorldWebPart extends BaseClientSideWebPart<IHelloWorldWebPartProps> {

  public render(): void {
  this.domElement.innerHTML = `
    <div class="${ styles.helloWorld }">
      <div class="${ styles.container }">
        <div class="${ styles.row }">
          <div class="${ styles.column }">
            <span class="${ styles.title }">Welcome to SharePoint!</span>
            <p class="${ styles.subTitle }">Customize SharePoint experiences using web parts.</p>
            <p class="${ styles.description }">${escape(this.properties.description)}</p>
            <p class="${ styles.description }">${escape(this.properties.test)}</p>
            <p class="${ styles.description }">Loading from ${escape(this.context.pageContext.web.title)}</p>
            <a href="https://aka.ms/spfx" class="${ styles.button }">
              <span class="${ styles.label }">Learn more</span>
            </a>
          </div>
        </div>
        <div id="spListContainer" />
      </div>
    </div>`;

    this._renderListAsync();

  }

  protected get dataVersion(): Version {
    return Version.parse('1.0');
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
    return {
      pages: [
        {
          header: {
            description: strings.PropertyPaneDescription
          },
          groups: [
            {
              groupName: strings.BasicGroupName,
              groupFields: [
              PropertyPaneTextField('description', {
                label: 'Description'
              }),
              PropertyPaneTextField('test', {
                label: 'Multi-line Text Field',
                multiline: true
              }),
              PropertyPaneCheckbox('test1', {
                text: 'Checkbox'
              }),
              PropertyPaneDropdown('test2', {
                label: 'Dropdown',
                options: [
                  { key: '1', text: 'One' },
                  { key: '2', text: 'Two' },
                  { key: '3', text: 'Three' },
                  { key: '4', text: 'Four' }
                ]}),
              PropertyPaneToggle('test3', {
                label: 'Toggle',
                onText: 'On',
                offText: 'Off'
              })
            ]
            }
          ]
        }
      ]
    };
  }

  private _getMockListData(): Promise<ISPLists> {
    return MockHttpClient.get()
      .then((data: ISPList[]) => {
        var listData: ISPLists = { value: data };
        return listData;
      }) as Promise<ISPLists>;
  }
  private _getListData(): Promise<ISPLists> {
    return this.context.spHttpClient.get(this.context.pageContext.web.absoluteUrl + `/_api/web/lists?$filter=Hidden eq false`, SPHttpClient.configurations.v1)
      .then((response: SPHttpClientResponse) => {
        return response.json();
      });
  }
  private _renderList(items: ISPList[]): void {
    let html: string = '';
    items.forEach((item: ISPList) => {
      html += `
    <ul class="${styles.list}">
      <li class="${styles.listItem}">
        <span class="ms-font-l">${item.Title}</span>
      </li>
    </ul>`;
    });
  
    const listContainer: Element = this.domElement.querySelector('#spListContainer');
    listContainer.innerHTML = html;
  }
  
  private _renderListAsync(): void {
    // Local environment
    if (Environment.type === EnvironmentType.Local) {
      this._getMockListData().then((response) => {
        this._renderList(response.value);
      });
    }
    else if (Environment.type == EnvironmentType.SharePoint || 
              Environment.type == EnvironmentType.ClassicSharePoint) {
      this._getListData()
        .then((response) => {
          this._renderList(response.value);
        });
    }
  }
    
  
}

MockHttpClient.ts

import { ISPList } from './HelloWorldWebPart';

export default class MockHttpClient  {

    private static _items: ISPList[] = [{ Title: 'Mock List', Id: '1' },
                                        { Title: 'Mock List 2', Id: '2' },
                                        { Title: 'Mock List 3', Id: '3' }];

    public static get(): Promise<ISPList[]> {
    return new Promise<ISPList[]>((resolve) => {
            resolve(MockHttpClient._items);
        });
    }
}

HelloWorldWebpartWebPart.module.scss

@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';

.helloWorld {
  .container {
    max-width: 700px;
    margin: 0px auto;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
  }

  .row {
    @include ms-Grid-row;
    @include ms-fontColor-white;
    background-color: $ms-color-themeDark;
    padding: 20px;
  }

  .column {
    @include ms-Grid-col;
    @include ms-lg10;
    @include ms-xl8;
    @include ms-xlPush2;
    @include ms-lgPush1;
  }

  .title {
    @include ms-font-xl;
    @include ms-fontColor-white;
  }

  .subTitle {
    @include ms-font-l;
    @include ms-fontColor-white;
  }

  .description {
    @include ms-font-l;
    @include ms-fontColor-white;
  }

  .button {
    // Our button
    text-decoration: none;
    height: 32px;

    // Primary Button
    min-width: 80px;
    background-color: $ms-color-themePrimary;
    border-color: $ms-color-themePrimary;
    color: $ms-color-white;

    // Basic Button
    outline: transparent;
    position: relative;
    font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
    -webkit-font-smoothing: antialiased;
    font-size: $ms-font-size-m;
    font-weight: $ms-font-weight-regular;
    border-width: 0;
    text-align: center;
    cursor: pointer;
    display: inline-block;
    padding: 0 16px;

    .label {
      font-weight: $ms-font-weight-semibold;
      font-size: $ms-font-size-m;
      height: 32px;
      line-height: 32px;
      margin: 0 4px;
      vertical-align: top;
      display: inline-block;
    }
  }
  
  .list {
    color: #333333;
    font-family: 'Segoe UI Regular WestEuropean', 'Segoe UI', Tahoma, Arial, sans-serif;
    font-size: 14px;
    font-weight: normal;
    box-sizing: border-box;
    margin: 10;
    padding: 10;
    line-height: 50px;
    list-style-type: none;
    box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
  }
  
  .listItem {
    color: #333333;
    vertical-align: center;
    font-family: 'Segoe UI Regular WestEuropean', 'Segoe UI', Tahoma, Arial, sans-serif;
    font-size: 14px;
    font-weight: normal;
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    box-shadow: none;
    *zoom: 1;
    padding: 9px 28px 3px;
    position: relative;
  }
}

HelloWorldWebpartWebPart.manifest.json

{
  "$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
  "id": "483a3455-9721-4742-a446-c2a0c78ff0d2",
  "alias": "HelloWorldWebPart",
  "componentType": "WebPart",

// The "*" signifies that the version should be taken from the package.json
  "version": "*",
  "manifestVersion": 2,

  // If true, the component can only be installed on sites where Custom Script is allowed.
  // Components that allow authors to embed arbitrary script code should set this to true.
  // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
  "requiresCustomScript": false,
  "supportedHosts": ["SharePointWebPart"],

  "preconfiguredEntries": [{
    "groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
    "group": { "default": "Other" },
    "title": { "default": "HelloWorld" },
    "description": { "default": "HelloWorld description" },
    "officeFabricIconFontName": "Page",
    "properties": {
      "description": "HelloWorld",
      "test": "Multi-line text field",
      "test1": true,
      "test2": "2",
      "test3": true
    }
  }]
}

次回:
【SharePoint Online】クライアント側の Web パーツを SharePoint ページに展開する ーSPFx開発 その3ー

SharePoint Frameworkの記事一覧