SharePointのリストビューをカスタマイズする方法はいくつかありますが、 SharePoint Framework(以下SPFx)のフィールドカスタマイザーでは JavaScriptコードを動かせるので柔軟なカスタマイズが可能です。
今回はフィールドカスタマイザーのサンプルとして、 ビューに完了ボタンを表示させて列の値を更新するようにしてみました。 これを応用すれば、「ビューから直接承認」「一括承認」などが作れそうです。
開発したサンプルの挙動
「完了」列を更新するトグルボタンを実装した
SharePointリストに「完了」列を用意して、トグルボタンで値を変更できるようにカスタマイズしました。
「完了」列の種類は「はい/いいえ」にしています。 カスタマイズしていない標準ビューでは以下ように表示されます。
標準の見た目
これだとアイテムの完了状態を変更するのに、クイック編集とかプロパティ編集とかが必要になって面倒ですよね。 モダンビューの書式設定機能では、見た目を変えられますが値の編集まではできません。フィールドカスタマイザーの出番ですね。
中身の概説
SPFxの開発環境が整っていることを前提として進めます。 SPFx初体験の方は先に以下を参照してください。
初めてのSharePoint Framework (略称:SPFx) : Hello World ! - 鍋綿ブログ
SharePointリストの構造
今回のサンプルで利用する「完了」列を追加しました。 また、ビューに「完了」列と「ID」列を表示しました。 その他の設定はサンプルに影響しません。とりあえず自分に見やすくしてあります。 列の設定は以下です。
表示名:完了
内部名:completed
種類 :はい/いいえ
注)フィールドカスタマイザーでは、JSLinkと同様に 「ビューに表示されている列しか扱えない」という制約があります。 そのためカスタマイズ対象である「完了」列と 更新のキーである「ID」列の表示が必須です。
SPFxソリューションの構成
ソリューションを作成するコマンドは以下のとおりです。
yoコマンドのパラメータ
ソリューションが出来たら、Configフォルダ内のserve.jsonを編集します。
pageUrl : 値をカスタマイズ対象ビューの絶対URLに置き換え
InternalFieldName : プロパティ名をカスタマイズ対象の列内部名に置き換え
環境に合わせて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' ;
import { Toggle } from 'office-ui-fabric-react/lib/Toggle' ;
export interface ISampleCustomizerProps {
defaultChecked : boolean ;
checkedCallBack : (checked : boolean ) => {} ;
}
export interface ISampleCustomizerStates {
}
export default class SampleCustomizer extends React.Component<ISampleCustomizerProps, ISampleCustomizerStates> {
public constructor()
{
super ();
this .state = {} ;
}
@override
public componentDidMount(): void {
}
@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' ; export interface ISampleCustomizerFieldCustomizerProperties { } export default class SampleCustomizerFieldCustomizer extends BaseFieldCustomizer<ISampleCustomizerFieldCustomizerProperties> { private fieldValueMap = { 'はい' : true , 'いいえ' : false } ; @override public onInit(): Promise<void> { return Promise.resolve(); } @override public onRenderCell( event : IFieldCustomizerCellEventParameters): void { const fieldValue : boolean = this .fieldValueMap [ event .fieldValue ] ; 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 ] ); } 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); } @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時のリクエスト本文内 __metadataの値
ListItemEntityTypeFullNameと呼ばれるものです。 命名規則が決まっているので固定文字列として生成するの良いですが、 SharePoint側で命名規則を変えた時に対応できないので、 本来は動的に取得すべきものです。 以下のURLにGETリクエストすれば得られます。 GETなのでとりあえず知りたいだけならブラウザで叩けます。 (サイトURL)_api/web/lists/GetByTitle('リスト名')
ソースコード全体
GitHubに公開予しています。ReadMeは英語です。
github.com
以上、参考になれば幸いです。