鍋綿ブログ

C#・SharePoint・SharePoint Framework・Office365を中心に扱うブログです。

SharePoint Framework フィールドカスタマイザーでリストビューに完了ボタンを付けてみた

SharePointのリストビューをカスタマイズする方法はいくつかありますが、
SharePoint Framework(以下SPFx)のフィールドカスタマイザーでは
JavaScriptコードを動かせるので柔軟なカスタマイズが可能です。

今回はフィールドカスタマイザーのサンプルとして、
ビューに完了ボタンを表示させて列の値を更新するようにしてみました。
これを応用すれば、「ビューから直接承認」「一括承認」などが作れそうです。

 

 

開発したサンプルの挙動

f:id:micknabewata:20180929140826j:plain

「完了」列を更新するトグルボタンを実装した

SharePointリストに「完了」列を用意して、トグルボタンで値を変更できるようにカスタマイズしました。

「完了」列の種類は「はい/いいえ」にしています。
カスタマイズしていない標準ビューでは以下ように表示されます。

f:id:micknabewata:20180929141129j:plain

標準の見た目

これだとアイテムの完了状態を変更するのに、クイック編集とかプロパティ編集とかが必要になって面倒ですよね。
モダンビューの書式設定機能では、見た目を変えられますが値の編集まではできません。フィールドカスタマイザーの出番ですね。

中身の概説

SPFxの開発環境が整っていることを前提として進めます。
SPFx初体験の方は先に以下を参照してください。

初めてのSharePoint Framework (略称:SPFx) : Hello World ! - 鍋綿ブログ

 

SharePointリストの構造

今回のサンプルで利用する「完了」列を追加しました。
また、ビューに「完了」列と「ID」列を表示しました。
その他の設定はサンプルに影響しません。とりあえず自分に見やすくしてあります。
列の設定は以下です。

 表示名:完了

 内部名:completed

 種類 :はい/いいえ

 

注)フィールドカスタマイザーでは、JSLinkと同様に
 「ビューに表示されている列しか扱えない」という制約があります。
 そのためカスタマイズ対象である「完了」列と
 更新のキーである「ID」列の表示が必須です。

SPFxソリューションの構成

ソリューションを作成するコマンドは以下のとおりです。

f:id:micknabewata:20180929142634j:plain

yoコマンドのパラメータ

ソリューションが出来たら、Configフォルダ内のserve.jsonを編集します。

 pageUrl : 値をカスタマイズ対象ビューの絶対URLに置き換え

 InternalFieldName : プロパティ名をカスタマイズ対象の列内部名に置き換え

f:id:micknabewata:20180929142809j:plain

環境に合わせてserve.jsonを編集

とりあえずこれだけでも初期コードを動かせます。
gulp serveコマンドを実行してみてください。

 

注)私はよく見ずにブラウザのURLをコピって一瞬ハマりましたが、
  皆さんは大丈夫だと信じています。
  SharePointがブラウザに出してくるURLパラメータ付きのものでは動きません。

 

で、コードを編集していきます。
まずはReactコンポーネント(.tsファイル)を以下のようにしました。

import { override } from '@microsoft/decorators';
import * as React from 'react';

import styles from './SampleCustomizer.module.scss';

// Office UI FabricのToggleコントロールをインポート
import { Toggle } from 'office-ui-fabric-react/lib/Toggle';

/** このReactコンポーネントが受け取るの形式定義 */
export interface ISampleCustomizerProps {

  /** チェックの初期状態 */
  defaultChecked : boolean;

  /** チェックが変更された時のコールバック */
  checkedCallBack : (checked : boolean) => {};
}

/** このReactコンポーネントのステートの形式定義 */
export interface ISampleCustomizerStates {
}

/** 描画用Reactコンポーネント */
export default class SampleCustomizer extends React.Component<ISampleCustomizerProps, ISampleCustomizerStates> {

  /**
   * コンストラクタ
   */
  public constructor()
  {
    // 継承元コンストラクタの呼び出し
    super();

    // ステートの初期化
    this.state = {};
  }

  /**
   * Reactコンポーネントがマウントされた後のイベント
   */
  @override
  public componentDidMount(): void {
  }

  /**
   * Reactコンポーネントがマウントされる直前のイベント
   */
  @override
  public componentWillUnmount(): void {
  }

  /**
   * レンダリング
   */
  @override
  public render(): React.ReactElement<{}> {
    return (
      <div className={ styles.cell }>
        <Toggle
          defaultChecked={ this.props.defaultChecked }
          onChanged={ this.props.checkedCallBack  }
        />
      </div>
    );
  }
}

言われるがままにトグルを表示してコールバックするだけの非常にシンプルなコンポーネントです。

続いてフィールドカスタマイザー(.tsファイル)のコードです。

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import {
  SPHttpClient
 } from '@microsoft/sp-http';

import { override } from '@microsoft/decorators';
import {
  BaseFieldCustomizer,
  IFieldCustomizerCellEventParameters
} from '@microsoft/sp-listview-extensibility';

import * as strings from 'SampleCustomizerFieldCustomizerStrings';
import SampleCustomizer, { ISampleCustomizerProps } from './components/SampleCustomizer';

/**
* フィールドカスタマイザーのプロパティ定義
* serve.jsonで記述したプロパティが連携される
*/
export interface ISampleCustomizerFieldCustomizerProperties {
}

/** フィールドカスタマイザークラス */
export default class SampleCustomizerFieldCustomizer
 
extends BaseFieldCustomizer<ISampleCustomizerFieldCustomizerProperties> {

 
private fieldValueMap = { 'はい' : true, 'いいえ' : false };

 
/**
   * 初期化イベント
   * Promise.resolve()が呼び出されるまでonRenderCellの呼び出しを待機する
   */
  @override
 
public onInit(): Promise<void> {
   
return Promise.resolve();
 
}

 
/**
   * セルのレンダリングイベント
   * ReactDOM.renderされた内容がフィールドに記述される
  */
  @override
 
public onRenderCell(event: IFieldCustomizerCellEventParameters): void {

   
// フィールドの現在の値をbooleanにマッピング
   
const fieldValue : boolean = this.fieldValueMap[event.fieldValue];

   
// アイテムIDを取得
   
let itemId : number | null = null;
   
let idField = event.listItem.fields.filter((val, idx) => { return val.internalName == 'ID'; });
   
if(idField.length == 1)
   
{
      itemId =
event.listItem.getValue(idField[0]);
   
}

  // React要素を取得
   
const sampleCustomizer: React.ReactElement<{}> =
      React.createElement(
        SampleCustomizer,
       
{
          defaultChecked : fieldValue,
          checkedCallBack : (checked) => 
{
           
this.onToggleChanged(
             
this.context.spHttpClient,
             
this.context.pageContext.web.absoluteUrl,
             
this.context.pageContext.list.title,
              itemId,
              checked
              );
         
}
       
} as ISampleCustomizerProps);

   
// 描画
    ReactDOM.render(sampleCustomizer,
event.domElement);
 
}

 
/**
   * 終了イベント
   * オブジェクトの破棄を行う
   * このサンプルでは前段でReactDom.renderをしたので、
   * ReactDOM.unmountComponentAtNodeを呼び出してDOM要素を破棄している
   */
  @override
 
public onDisposeCell(event: IFieldCustomizerCellEventParameters): void {
    ReactDOM.unmountComponentAtNode(
event.domElement);
   
super.onDisposeCell(event);
 
}

 
/**
   * トグルの切り替えイベント
   * 該当アイテムの完了列を更新します。
   */
 
private onToggleChanged(client : SPHttpClient, webUrl : string, listTitle : string, itemId : number, checked : boolean) : void {
   
try
   
{
     
if(!itemId)
     
{
       
alert('アイテムIDが取得できませんでした。ビューにID列を含めていることを確認してください。');
     
}
     
else
     
{
       
const apiUrl = `${ webUrl }/_api/web/lists/GetByTitle('${ listTitle }')/items(${ itemId })`;
       
const body :string = JSON.stringify({
         
'__metadata' : { 'type' : 'SP.Data.SampleListListItem' },
         
'completed' : checked
       
});

        client.post(
          apiUrl,
          SPHttpClient.configurations.v1,
         
{
            headers:
[
             
['accept', 'application/json;odata=nometadata'],
             
['Content-type', 'application/json;odata=verbose'],
             
['odata-version', ''],
             
['X-HTTP-Method', 'MERGE'],
             
['IF-MATCH', '*' ]
           
],
            body : body
         
}
        ).then(
          (res) =>
{
           
if(res.ok)
           
{
           
}
           
else
           
{
              res.text().then(
                (val) =>
{ alert(`status : ${ res.status }, error : ${ val }`); },
                (err) =>
{ alert(`text retrival error : ${ err }`); }
              );
           
}
         
},
          (err) =>
{ alert(err); }
        );
     
}
   
}
   
catch(err)
   
{
     
alert(err);
   
}
 
}
}

サンプルなのでSharePointへの更新命令まですべて1ファイルで行っていますが、
実際のプロジェクトではヘルパークラス的なものにSharePoint関連をまとめるなどしたほうが良いでしょうね。

ポイントは以下です。

  • Post時のリクエストヘッダ
今回はアイテム更新なので上記5つが必要です。
SharePoint REST APIのリファレンスなどが参考になります。
  • Post時のリクエスト本文内 __metadataの値
ListItemEntityTypeFullNameと呼ばれるものです。
命名規則が決まっているので固定文字列として生成するの良いですが、
SharePoint側で命名規則を変えた時に対応できないので、
本来は動的に取得すべきものです。
以下のURLにGETリクエストすれば得られます。
GETなのでとりあえず知りたいだけならブラウザで叩けます。
 (サイトURL)_api/web/lists/GetByTitle('リスト名')

ソースコード全体

GitHubに公開予しています。ReadMeは英語です。

github.com

 

以上、参考になれば幸いです。