SharePoint ソリューションをビルドするときに、SharePoint 開発者は多くの場合、FullCalendar jQuery プラグインを使用してデータをカレンダー ビューで表示します。 FullCalendar は、標準の SharePoint カレンダー ビューの優れた代替機能で、複数の予定表リストからの予定表のデータ、予定表リスト以外からのデータ、さらには SharePoint 以外のデータもレンダリングできます。 この記事では、スクリプト エディター Web パーツでビルドした FullCalendar を使用して SharePoint のカスタマイズを SharePoint Framework に移行する方法を示します。
スクリプト エディター Web パーツを使用してビルドした、予定表として表示されるタスクのリスト
FullCalendar を使用して SharePoint のカスタマイズを SharePoint Framework に移行するプロセスを例示するため、SharePoint リストから取得されるタスクのカレンダー ビューを表示する次のソリューションを使用します。
このソリューションは、標準の SharePoint スクリプト エディター Web パーツを使用して構築されています。 カスタマイズで使用されるコードを次に示します。
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.js"></script>
<link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
<div id="calendar"></div>
<script>
var PATH_TO_DISPFORM = _spPageContextInfo.webAbsoluteUrl + "/Lists/Tasks/DispForm.aspx";
var TASK_LIST = "Tasks";
var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34'];
displayTasks();
function displayTasks() {
$('#calendar').fullCalendar('destroy');
$('#calendar').fullCalendar({
weekends: false,
header: {
left: 'prev,next today',
center: 'title',
right: 'month,basicWeek,basicDay'
},
displayEventTime: false,
// open up the display form when a user clicks on an event
eventClick: function (calEvent, jsEvent, view) {
window.location = PATH_TO_DISPFORM + "?ID=" + calEvent.id;
},
editable: true,
timezone: "UTC",
droppable: true, // this allows things to be dropped onto the calendar
// update the end date when a user drags and drops an event
eventDrop: function (event, delta, revertFunc) {
updateTask(event.id, event.start, event.end);
},
// put the events on the calendar
events: function (start, end, timezone, callback) {
var startDate = start.format('YYYY-MM-DD');
var endDate = end.format('YYYY-MM-DD');
var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\
Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
$filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
$.ajax({
url: _spPageContextInfo.webAbsoluteUrl + restQuery,
type: "GET",
dataType: "json",
headers: {
Accept: "application/json;odata=nometadata"
}
})
.done(function (data, textStatus, jqXHR) {
var personColors = {};
var colorNo = 0;
var events = data.value.map(function (task) {
var assignedTo = task.AssignedTo.map(function (person) {
return person.Title;
}).join(', ');
var color = personColors[assignedTo];
if (!color) {
color = COLORS[colorNo++];
personColors[assignedTo] = color;
}
if (colorNo >= COLORS.length) {
colorNo = 0;
}
return {
title: task.Title + " - " + assignedTo,
id: task.ID,
color: color, // specify the background color and border color can also create a class and use className parameter.
start: moment.utc(task.StartDate).add("1", "days"),
end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day
};
});
callback(events);
});
}
});
}
function updateTask(id, startDate, dueDate) {
// subtract the previously added day to the date to store correct date
var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
startDate.format("hh:mm") + ":00Z";
if (!dueDate) {
dueDate = startDate;
}
var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" +
dueDate.format("hh:mm") + ":00Z";
$.ajax({
url: _spPageContextInfo.webAbsoluteUrl + '/_api/contextinfo',
type: 'POST',
headers: {
'Accept': 'application/json;odata=nometadata'
}
})
.then(function (data, textStatus, jqXHR) {
return $.ajax({
url: _spPageContextInfo.webAbsoluteUrl +
"/_api/Web/Lists/getByTitle('" + TASK_LIST + "')/Items(" + id + ")",
type: 'POST',
data: JSON.stringify({
StartDate: sDate,
DueDate: dDate,
}),
headers: {
Accept: "application/json;odata=nometadata",
"Content-Type": "application/json;odata=nometadata",
"X-RequestDigest": data.FormDigestValue,
"IF-MATCH": "*",
"X-Http-Method": "PATCH"
}
});
})
.done(function (data, textStatus, jqXHR) {
alert("Update Successful");
})
.fail(function (jqXHR, textStatus, errorThrown) {
alert("Update Failed");
})
.always(function () {
displayTasks();
});
}
</script>
注:
このソリューションは、Office Servers and Services の MVP でもある、PAIT グループの最高戦略責任者の Mark Rackley 氏の作業に基づいています。 オリジナル ソリューションの詳細については、「FullCalendar.io を使用して SharePoint で独自の予定表を作成する」を参照してください。
まず、最初の幾つかの <script>
と <link>
の要素で、カスタマイズで使用するライブラリとして jQuery、Moment.js、FullCalendar を読み込みます。
次に、生成されたカレンダー ビューが挿入される <div>
を定義します。
その後、カレンダー ビューにタスクを表示するために使用する displayTasks()
と、別の日付にタスクをドラッグ アンド ドロップするとトリガーされ、基になるリスト アイテムの日付を更新する updateTask()
の 2 つの関数を定義します。 各関数で独自の REST クエリを定義し、それを使用して SharePoint リストの REST API と通信し、リスト アイテムを取得あるいは更新します。
FullCalendar jQuery プラグインを使用してひと手間かけるだけで、イベントによって色を変えたり、ドラッグ アンド ドロップでイベントを再編成したりするなど、さまざまなことが可能となる便利なソリューションを利用できるようになります。
スクリプト エディター Web パーツから SharePoint Framework にタスク カレンダー ソリューションを移行する
スクリプト エディター Web パーツに基づくカスタマイズを SharePoint Framework に移行することで、よりユーザー フレンドリな構成や、ソリューションの一元管理など、数多くのメリットが得られます。 次に、ソリューションを SharePoint Framework に移行する方法を、順を追って示します。
まず、オリジナルのコードの変更を最小限に抑える形で、ソリューションを SharePoint Framework に移行します。 次に、ソリューションのコードを TypeScript に変換します。開発時のタイプ セーフな機能の利点を生かし、コードの一部を SharePoint Framework API に置換してその機能を最大限に活用することによって、ソリューションをさらに簡素化できます。
注:
移行のさまざまな段階におけるプロジェクトのソース コードは、「チュートリアル: スクリプト エディター Web パーツを使用してビルドした jQuery および FullCalendar ソリューションを SharePoint Framework に移行する」から入手できます。
新しい SharePoint Framework プロジェクトを作成する
まず、プロジェクト用の新しいフォルダーを作成します。
md fullcalendar-taskscalendar
プロジェクト フォルダーに移動します。
cd fullcalendar-taskscalendar
プロジェクト フォルダーで SharePoint Framework Yeoman ジェネレーターを実行して、新しい SharePoint Framework プロジェクトをスキャホールディングします。
yo @microsoft/sharepoint
プロンプトが表示されたら、以下の値を入力します (以下で省略されたすべてのプロンプトに対して既定のオプションを選択します)。
- ソリューション名は何ですか?: fullcalendar-taskscalendar
- どのベースライン パッケージをコンポーネントのターゲットにしたいですか?: SharePoint Online のみ (最新)
- どの種類のクライアント側コンポーネントを作成しますか?: WebPart
- Web パーツ名は何ですか?: Tasks calendar
- Web パーツで何を行いますか?: カレンダー ビューでタスクを表示する
- どのフレームワークを使用しますか?: JavaScript フレームワークなし
コード エディターでプロジェクト フォルダーを開きます。 このチュートリアルでは、Visual Studio Code を使用します。
JavaScript ライブラリを読み込む
スクリプト エディター Web パーツを使用してビルドしたオリジナルのソリューションと同様に、まずソリューションに必要な JavaScript ライブラリを読み込む必要があります。 通常、SharePoint Framework では 2 つの手順があり、読み込むライブラリの URL を指定し、コード内でライブラリを参照します。
読み込むライブラリの URL を指定します。
コード エディターで、./config/config.json ファイルを開き、
externals
セクションを次のように変更します。{ // .. "externals": { "jquery": { "path": "https://code.jquery.com/jquery-1.11.1.min.js", "globalName": "jQuery" }, "fullcalendar": { "path": "https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.js", "globalName": "jQuery", "globalDependencies": [ "jquery" ] }, "moment": "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js" }, // .. }
./src/webparts/tasksCalendar/TasksCalendarWebPart.ts ファイルを開いて、最後の
import
ステートメントの後に次のコードを追加します。import 'jquery'; import 'moment'; import 'fullcalendar';
コンテナー div を定義する
次に、オリジナルのソリューションと同様、予定表を表示する場所を定義します。
コード エディターで、./src/webparts/tasksCalendar/TasksCalendarWebPart.ts ファイルを開き、render()
メソッドを次のように変更します。
export default class ItRequestsWebPart extends BaseClientSideWebPart<IItRequestsWebPartProps> {
public render(): void {
this.domElement.innerHTML = `
<div class="${styles.tasksCalendar}">
<link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" />
<div id="calendar"></div>
</div>`;
}
// ...
}
FullCalendar の初期化とデータの読み込み
最後の手順では、FullCalendar jQuery プラグインを初期化して SharePoint からデータを読み込むコードを含めます。
./src/webparts/tasksCalendar フォルダーに、script.js という名前の新しいファイルを作成して、次のコードを貼り付けます。
var moment = require('moment'); var PATH_TO_DISPFORM = window.webAbsoluteUrl + "/Lists/Tasks/DispForm.aspx"; var TASK_LIST = "Tasks"; var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34']; displayTasks(); function displayTasks() { $('#calendar').fullCalendar('destroy'); $('#calendar').fullCalendar({ weekends: false, header: { left: 'prev,next today', center: 'title', right: 'month,basicWeek,basicDay' }, displayEventTime: false, // open up the display form when a user clicks on an event eventClick: function (calEvent, jsEvent, view) { window.location = PATH_TO_DISPFORM + "?ID=" + calEvent.id; }, editable: true, timezone: "UTC", droppable: true, // this allows things to be dropped onto the calendar // update the end date when a user drags and drops an event eventDrop: function (event, delta, revertFunc) { updateTask(event.id, event.start, event.end); }, // put the events on the calendar events: function (start, end, timezone, callback) { var startDate = start.format('YYYY-MM-DD'); var endDate = end.format('YYYY-MM-DD'); var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\ Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))"; $.ajax({ url: window.webAbsoluteUrl + restQuery, type: "GET", dataType: "json", headers: { Accept: "application/json;odata=nometadata" } }) .done(function (data, textStatus, jqXHR) { var personColors = {}; var colorNo = 0; var events = data.value.map(function (task) { var assignedTo = task.AssignedTo.map(function (person) { return person.Title; }).join(', '); var color = personColors[assignedTo]; if (!color) { color = COLORS[colorNo++]; personColors[assignedTo] = color; } if (colorNo >= COLORS.length) { colorNo = 0; } return { title: task.Title + " - " + assignedTo, id: task.ID, color: color, // specify the background color and border color can also create a class and use className parameter. start: moment.utc(task.StartDate).add("1", "days"), end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day }; }); callback(events); }); } }); } function updateTask(id, startDate, dueDate) { // subtract the previously added day to the date to store correct date var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" + startDate.format("hh:mm") + ":00Z"; if (!dueDate) { dueDate = startDate; } var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" + dueDate.format("hh:mm") + ":00Z"; $.ajax({ url: window.webAbsoluteUrl + '/_api/contextinfo', type: 'POST', headers: { 'Accept': 'application/json;odata=nometadata' } }) .then(function (data, textStatus, jqXHR) { return $.ajax({ url: window.webAbsoluteUrl + "/_api/Web/Lists/getByTitle('" + TASK_LIST + "')/Items(" + id + ")", type: 'POST', data: JSON.stringify({ StartDate: sDate, DueDate: dDate, }), headers: { Accept: "application/json;odata=nometadata", "Content-Type": "application/json;odata=nometadata", "X-RequestDigest": data.FormDigestValue, "IF-MATCH": "*", "X-Http-Method": "PATCH" } }); }) .done(function (data, textStatus, jqXHR) { alert("Update Successful"); }) .fail(function (jqXHR, textStatus, errorThrown) { alert("Update Failed"); }) .always(function () { displayTasks(); }); }
このコードは、スクリプト エディター Web パーツのカスタマイズのオリジナル コードとほぼ同じです。 唯一の違いは、オリジナル コードでは SharePoint が設定したグローバルな _spPageContextInfo
変数から現行 Web の URL を取得するのに対して、SharePoint Framework のコードでは、Web パーツに設定するカスタム変数を使用する点にあります。
SharePoint Framework のクライアント側 Web パーツは、従来のページと最新のページの両方で使用できます。
_spPageContextInfo
変数は、従来のページにありますが、最新のページでは使用できず、これに依存することはできないため、自分で制御するためのカスタム プロパティを設定する必要があります。
Web パーツでこのファイルを参照するために、コード エディターで ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts ファイルを開き、
render()
メソッドを次のように変更します。export default class ItRequestsWebPart extends BaseClientSideWebPart<IItRequestsWebPartProps> { public render(): void { this.domElement.innerHTML = ` <div class="${styles.tasksCalendar}"> <link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" /> <div id="calendar"></div> </div>`; (window as any).webAbsoluteUrl = this.context.pageContext.web.absoluteUrl; require('./script'); } // ... }
コマンド ラインで以下を実行して、Web パーツが想定どおりに動作していることを確認します。
gulp serve --nobrowser
Web パーツはそのデータを SharePoint から読み込むので、ホストされている SharePoint Framework ワークベンチを使用して Web パーツをテストする必要があります。
https://{your-tenant-name}.sharepoint.com/_layouts/workbench.aspx に移動し、Web パーツをキャンバスに追加します。 これで、FullCalendar jQuery プラグインを使用して、カレンダー ビューにタスクを表示できます。
Web パーツのプロパティを使用して Web パーツを構成するためのサポートを追加する
前の手順では、スクリプト エディター Web パーツから SharePoint Framework にタスク カレンダーのソリューションを移行しました。 ソリューションは既に想定どおりに機能していますが、SharePoint Framework の利点は何も活用されていません。 タスクの読み込み元であるリストの名前はコードに含まれており、そのコード自体はプレーンな JavaScript であり、そのリファクタリングは TypeScript よりも困難です。
次の手順では、データの読み込み元であるリストの名前をユーザーが指定できるように、既存のソリューションを拡張する方法について説明します。 その後、コードを TypeScript に変換し、そのタイプ セーフな機能を利用できるようにします。
リストの名前を格納するために Web パーツ プロパティを定義する
タスクの読み込み元のリスト名を格納するための Web パーツ プロパティを定義します。 コード エディターで ./src/webparts/tasksCalendar/TasksCalendarWebPart.manifest.json ファイルを開き、既定の
description
プロパティの名前をlistName
に変更し、その値をクリアします。マニフェスト内の変更を反映するよう Web パーツ プロパティ インターフェイスを更新します。 コード エディターで、./src/webparts/tasksCalendar/ITasksCalendarWebPartProps.ts ファイルを開き、その内容を次のように変更します。
export interface ITasksCalendarWebPartProps { listName: string; }
listName
プロパティの表示ラベルを更新します。./src/webparts/tasksCalendar/loc/mystrings.d.ts ファイルを開いて、その内容を次のように変更します。
declare interface ITasksCalendarStrings { PropertyPaneDescription: string; BasicGroupName: string; ListNameFieldLabel: string; } declare module 'tasksCalendarStrings' { const strings: ITasksCalendarStrings; export = strings; }
./src/webparts/tasksCalendar/loc/en-us.js ファイルを開いて、その内容を次のように変更します。
define([], function() { return { "PropertyPaneDescription": "Tasks calendar settings", "BasicGroupName": "Data", "ListNameFieldLabel": "List name" } });
新しく定義したプロパティを使用するよう Web パーツを更新します。 コード エディターで ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts ファイルを開き、getPropertyPaneConfiguration メソッドを次のように変更します。
export default class TasksCalendarWebPart extends BaseClientSideWebPart<ITasksCalendarWebPartProps> { // ... protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: strings.PropertyPaneDescription }, groups: [ { groupName: strings.BasicGroupName, groupFields: [ PropertyPaneTextField('listName', { label: strings.ListNameFieldLabel }) ] } ] } ] }; } protected get disableReactivePropertyChanges(): boolean { return true; } }
ユーザーがリストの名前を入力しているときに Web パーツが再度読み込まないようにするため、disableReactivePropertyChanges()
メソッドを追加して戻り値を true
に設定することにより、非反応性のプロパティ ウィンドウを使用するように Web パーツを設定しました。
データの読み込み元として設定したリスト名を使用する
最初に、データの読み込み元となるリストの名前が REST クエリに埋め込まれました。 これでユーザーがこの名前を構成できるようになり、構成された値は実行の前に REST クエリに挿入される必要があります。 そうするための最も簡単な方法は、script.js ファイルの内容をメインの Web パーツ ファイルに移動することです。
コード エディターで、./src/webparts/tasksCalendar/TasksCalendarWebPart.ts ファイルを開きます。
以前にファイルに追加した次のコードを見つけます。
import 'jquery'; import 'moment'; import 'fullcalendar';
コードを次のコードに更新します。
var $: any = require('jquery'); var moment: any = require('moment'); import 'fullcalendar'; var COLORS = ['#466365', '#B49A67', '#93B7BE', '#E07A5F', '#849483', '#084C61', '#DB3A34'];
Moment.js は後で使用するコードで参照されるため、その名前が TypeScript で識別されるようにしておかないと、プロジェクトのビルドが失敗します。 jQuery についても同じです。
FullCalendar は jQuery オブジェクトに付加される jQuery プラグインなので、インポートする方法を変更する必要はありません。
最後に、さまざまなイベントをマーキングするために使用する色のリストをコピーします。
displayTasks()
とupdateTask()
機能を script.js ファイルからコピーし、TasksCalendarWebPart
クラスに次のように貼り付けます。export default class TasksCalendarWebPart extends BaseClientSideWebPart<ITasksCalendarWebPartProps> { // ... private displayTasks() { $('#calendar').fullCalendar('destroy'); $('#calendar').fullCalendar({ weekends: false, header: { left: 'prev,next today', center: 'title', right: 'month,basicWeek,basicDay' }, displayEventTime: false, // open up the display form when a user clicks on an event eventClick: (calEvent, jsEvent, view) => { (window as any).location = this.context.pageContext.web.absoluteUrl + "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id; }, editable: true, timezone: "UTC", droppable: true, // this allows things to be dropped onto the calendar // update the end date when a user drags and drops an event eventDrop: (event, delta, revertFunc) => { this.updateTask(event.id, event.start, event.end); }, // put the events on the calendar events: (start, end, timezone, callback) => { var startDate = start.format('YYYY-MM-DD'); var endDate = end.format('YYYY-MM-DD'); var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\ Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))"; $.ajax({ url: this.context.pageContext.web.absoluteUrl + restQuery, type: "GET", dataType: "json", headers: { Accept: "application/json;odata=nometadata" } }) .done((data, textStatus, jqXHR) => { var personColors = {}; var colorNo = 0; var events = data.value.map((task) => { var assignedTo = task.AssignedTo.map((person) => { return person.Title; }).join(', '); var color = personColors[assignedTo]; if (!color) { color = COLORS[colorNo++]; personColors[assignedTo] = color; } if (colorNo >= COLORS.length) { colorNo = 0; } return { title: task.Title + " - " + assignedTo, id: task.ID, color: color, // specify the background color and border color can also create a class and use className parameter. start: moment.utc(task.StartDate).add("1", "days"), end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day }; }); callback(events); }); } }); } private updateTask(id, startDate, dueDate) { // subtract the previously added day to the date to store correct date var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" + startDate.format("hh:mm") + ":00Z"; if (!dueDate) { dueDate = startDate; } var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" + dueDate.format("hh:mm") + ":00Z"; $.ajax({ url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo', type: 'POST', headers: { 'Accept': 'application/json;odata=nometadata' } }) .then((data, textStatus, jqXHR) => { return $.ajax({ url: this.context.pageContext.web.absoluteUrl + "/_api/Web/Lists/getByTitle('" + escape(this.properties.listName) + "')/Items(" + id + ")", type: 'POST', data: JSON.stringify({ StartDate: sDate, DueDate: dDate, }), headers: { Accept: "application/json;odata=nometadata", "Content-Type": "application/json;odata=nometadata", "X-RequestDigest": data.FormDigestValue, "IF-MATCH": "*", "X-Http-Method": "PATCH" } }); }) .done((data, textStatus, jqXHR) => { alert("Update Successful"); }) .fail((jqXHR, textStatus, errorThrown) => { alert("Update Failed"); }) .always(() => { this.displayTasks(); }); // ... }
以前の状況と比較すると、いくらかの変更がコードに加えられています。
- プレーンな JavaScript の関数は、
function
キーワードをprivate
修飾子に置き換えることにより、TypeScript メソッドに変更されています。 それらをTaskCalendarWebPart
クラスに追加できるようにするために、これが必要です。 - これで両方のメソッドが Web パーツと同じファイルに入るため、現在のサイトの URL を保持するグローバル変数を定義するのではなく、
this.context.pageContext.web.absoluteUrl
プロパティを使用して Web パーツのコンテキストから直接、現在のサイトにアクセスできるようになります。 - すべての REST クエリで、固定のリスト名は、ユーザーが構成したリスト名を保有する
listName
プロパティの値に置き換わります。 値を使用する前に、lodash のescape()
関数を使用してエスケープし、スクリプトの挿入を禁止します。
- プレーンな JavaScript の関数は、
最後に、
render()
メソッドを変更して、新たに追加したdisplayTasks()
メソッドを呼び出します。export default class TasksCalendarWebPart extends BaseClientSideWebPart<ITasksCalendarWebPartProps> { public render(): void { this.domElement.innerHTML = ` <div class="${styles.tasksCalendar}"> <link type="text/css" rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.4.0/fullcalendar.min.css" /> <div id="calendar"></div> </div>`; this.displayTasks(); } // ... }
script.js ファイルのコンテンツをメインの Web パーツ ファイルに移動したため、script.js は不要となり、プロジェクトから削除できます。
Web パーツが予期したとおりに機能することを確認するため、コマンド ラインで次のように実行します。
gulp serve --nobrowser
ホストされているワークベンチに移動し、キャンバスに Web パーツを追加します。 Web パーツのプロパティ ウィンドウを開き、タスクが含まれているリスト名を指定します。その後、[適用] ボタンを選択して変更を確認します。 これで、Web パーツ内のカレンダー ビューにタスクが表示されるようになります。
プレーンな JavaScript コードを TypeScript に変換する
プレーンな JavaScript ではなく TypeScript を使用すると、さまざまなメリットがあります。 TypeScript のほうが管理およびリファクタリングが簡単なだけでなく、エラーを早期に把握することもできます。 次の手順では、元の JavaScript コードを TypeScript に変換する方法について説明します。
使用しているライブラリに型宣言を追加する
正しく機能するには、TypeScript にプロジェクトに使用されているさまざまなライブラリの型宣言が必要です。 型宣言は、多くの場合、名前空間の @types npm パッケージとして配布されます。
コマンド ラインで次のように実行して、JQuery の型宣言をインストールします。
npm install @types/jquery@1 --save-dev
Moment.js の型宣言は Moment.js パッケージとともに配布されます。 URL から Moment.js を読み込んでいる場合でも、その型を使用するためにはプロジェクトに Moment.js パッケージをインストールする必要があります。
コマンド ラインで次のように実行し、Moment.js パッケージをインストールします。
npm install moment --save
注:
FullCalendar ライブラリの型宣言をインストールしてはいないことが分かります。 この記事に掲載されているソリューションは、当時は有効な型宣言がなかった古いバージョンの FullCalendar を使用しているため、このソリューションでは TypeScript の型を選択的に使用します。
プロジェクトを FullCalendar ライブラリの新しいバージョンにアップグレードすることを検討するようお勧めします。 スターター ソリューションの基になっているバージョンから数多くの API 変更が実施されため、このことはこの記事の範囲外です。
パッケージ参照を更新する
インストールした型宣言の型を使用するには、ライブラリの参照方法を変更する必要があります。
コード エディターで、./src/webparts/tasksCalendar/TasksCalendarWebPart.ts ファイルを開きます
以前にファイルに追加した次のコードを見つけます。
var $: any = require('jquery'); var moment: any = require('moment'); import 'fullcalendar';
コードを次のコードに更新します。
import * as $ from 'jquery'; import 'fullcalendar'; import * as moment from 'moment';
メインの Web パーツ ファイルを TypeScript に更新する
プロジェクトにインストールされているすべてのライブラリの型宣言を取得したので、プレーンな JavaScript コードから TypeScript への変換を開始できます。
SharePoint リストから取得するタスクのインターフェイスを定義します。 コード エディターで ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts ファイルを開き、Web パーツ クラスのすぐ上に次のコード スニペットを追加します。
interface ITask { ID: number; Title: string; StartDate: string; DueDate: string; AssignedTo: [{ Title: string }]; }
Web パーツ クラスで
displayTasks()
とupdateTask()
メソッドを次のように変更します。private displayTasks() { ($('#calendar') as any).fullCalendar('destroy'); ($('#calendar') as any).fullCalendar({ weekends: false, header: { left: 'prev,next today', center: 'title', right: 'month,basicWeek,basicDay' }, displayEventTime: false, // open up the display form when a user clicks on an event eventClick: (calEvent: any, jsEvent: MouseEvent, view: any): void => { (window as any).location = this.context.pageContext.web.absoluteUrl + "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id; }, editable: true, timezone: "UTC", droppable: true, // this allows things to be dropped onto the calendar // update the end date when a user drags and drops an event eventDrop: (event: any, delta: moment.Duration, revertFunc: Function): void => { this.updateTask(<number>event.id, <moment.Moment>event.start, <moment.Moment>event.end); }, // put the events on the calendar events: (start: moment.Moment, end: moment.Moment, timezone: string, callback: Function): void => { var startDate = start.format('YYYY-MM-DD'); var endDate = end.format('YYYY-MM-DD'); var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\ Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))"; $.ajax({ url: this.context.pageContext.web.absoluteUrl + restQuery, type: "GET", dataType: "json", headers: { Accept: "application/json;odata=nometadata" } }) .done((data: { value: ITask[] }, textStatus: string, jqXHR: JQueryXHR) => { let personColors: { [person: string]: string; } = {}; var colorNo = 0; var events = data.value.map((task: ITask): any => { var assignedTo = task.AssignedTo.map((person: { Title: string }): string => { return person.Title; }).join(', '); var color = personColors[assignedTo]; if (!color) { color = COLORS[colorNo++]; personColors[assignedTo] = color; } if (colorNo >= COLORS.length) { colorNo = 0; } return { title: task.Title + " - " + assignedTo, id: task.ID, color: color, // specify the background color and border color can also create a class and use className parameter. start: moment.utc(task.StartDate).add("1", "days"), end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day }; }); callback(events); }); } }); } private updateTask(id: number, startDate: moment.Moment, dueDate: moment.Moment) { // subtract the previously added day to the date to store correct date var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" + startDate.format("hh:mm") + ":00Z"; if (!dueDate) { dueDate = startDate; } var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" + dueDate.format("hh:mm") + ":00Z"; $.ajax({ url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo', type: 'POST', headers: { 'Accept': 'application/json;odata=nometadata' } }) .then((data: { FormDigestValue: string }, textStatus: string, jqXHR: JQueryXHR): JQueryXHR => { return $.ajax({ url: this.context.pageContext.web.absoluteUrl + "/_api/Web/Lists/getByTitle('" + escape(this.properties.listName) + "')/Items(" + id + ")", type: 'POST', data: JSON.stringify({ StartDate: sDate, DueDate: dDate, }), headers: { Accept: "application/json;odata=nometadata", "Content-Type": "application/json;odata=nometadata", "X-RequestDigest": data.FormDigestValue, "IF-MATCH": "*", "X-Http-Method": "PATCH" } }); }) .done((data: {}, textStatus: string, jqXHR: JQueryXHR): void => { alert("Update Successful"); }) .fail((jqXHR: JQueryXHR, textStatus: string, errorThrown: string) => { alert("Update Failed"); }) .always((): void => { this.displayTasks(); }); // ... }
まず、プレーンな JavaScript を TypeScript に変換する際の明示的な型の変更が挙げられます。 これは必須ではないものの、どの型のデータがあるべきかがわかりやすくなります。 指定されたコントラクトからの何らかの逸脱があると、TypeScript によって即時に捕捉されるため、発生する可能性のある問題を開発プロセス中に見つけられます。 これは AJAX 応答やそのデータを扱う場合に有用です。
すでにお気づきかもしれませんが、もう 1 つの変化として、TypeScript の文字列補間が挙げられます。 文字列補間の使用により、動的な文字列の構成が簡素化され、コードの可読性が向上します。
プレーンな JavaScript と比較してみます。
var restQuery = "/_api/Web/Lists/GetByTitle('" + TASK_LIST + "')/items?$select=ID,Title,\
Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
$filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))";
変更後:
const restQuery: string = `/_api/Web/Lists/GetByTitle('${escape(this.properties.listName)}')/items?$select=ID,Title,\
Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\
$filter=((DueDate ge '${startDate}' and DueDate le '${endDate}')or(StartDate ge '${startDate}' and StartDate le '${endDate}'))`;
TypeScript の文字列挿入を使うことのその他のメリットとしては、引用符の使用を避ける必要がないため、REST クエリを簡単に作成できます。
すべてが想定どおりに機能することを確認するため、コマンド ラインで次のコマンドを実行します。
gulp serve --nobrowser
ホストされているワークベンチに移動し、キャンバスに Web パーツを追加します。 何も変更されていないように見えますが、新しいコード ベースは TypeScript とその型宣言を使用しており、ソリューションを維持できるようになっています。
jQuery の AJAX コールを SharePoint Framework の API に置き換える
現在、ソリューションは jQuery の AJAX コールを使用して SharePoint REST API と通信しています。 通常の GET 要求の場合、jQuery の AJAX API は SharePoint Framework SPHttpClient API を使用するのと同じくらい便利です。 実際の違いは、イベントを更新するときのような POST 要求を行う場合です。
$.ajax({
url: this.context.pageContext.web.absoluteUrl + '/_api/contextinfo',
type: 'POST',
headers: { 'Accept': 'application/json;odata=nometadata'}
})
.then((data: { FormDigestValue: string }, textStatus: string, jqXHR: JQueryXHR): JQueryXHR => {
return $.ajax({
url: `${this.context.pageContext.web.absoluteUrl}\
/_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`,
type: 'POST',
data: JSON.stringify({
StartDate: sDate,
DueDate: dDate,
}),
headers: {
Accept: "application/json;odata=nometadata",
"Content-Type": "application/json;odata=nometadata",
"X-RequestDigest": data.FormDigestValue,
"IF-MATCH": "*",
"X-Http-Method": "PATCH"
}
});
})
.done((data: {}, textStatus: string, jqXHR: JQueryXHR): void => {
alert("Update Successful");
});
// ...
リスト アイテムを更新するため、SharePoint に有効な要求ダイジェスト トークンを提供する必要があります。 ダイジェストはクラシック ページで利用できますが、3 分間のみ有効です。 そのため、更新作業を行う前に有効なトークンを自分で取得することが一番安全です。 要求ダイジェストを取得したら、更新要求の要求ヘッダーに追加する必要があります。 追加しないと、要求は失敗します。
SharePoint Framework の SPHttpClient API は、要求が POST 要求で有効な要求ダイジェストが必要かどうかを自動的に検出するので、SharePoint との通信を簡素化できます。 条件に該当する場合、SPHttpClient API は SharePoint からその POST 要求を自動的に取得し、要求に追加します。 比較すると、SPHttpClient API を使用して発行された同じ要求は次のようになります。
this.context.spHttpClient.post(`${this.context.pageContext.web.absoluteUrl}\
/_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, SPHttpClient.configurations.v1, {
body: JSON.stringify({
StartDate: sDate,
DueDate: dDate,
}),
headers: {
Accept: "application/json;odata=nometadata",
"Content-Type": "application/json;odata=nometadata",
"IF-MATCH": "*",
"X-Http-Method": "PATCH"
}
})
.then((response: SPHttpClientResponse): void => {
// ...
});
オリジナルの jQuery AJAX コールを SharePoint Framework の SPHttpClient API に置き換える場合は、コード エディターで ./src/webparts/tasksCalendar/TasksCalendarWebPart.ts ファイルを開きます。 次を既存の
import
ステートメントのリストに追加します。import { SPHttpClient, SPHttpClientResponse } from '@microsoft/sp-http';
TasksCalendarWebPart
クラスで、displayTasks()
とupdateTask()
メソッドを次のコードに置き換えます。private displayTasks() { ($('#calendar') as any).fullCalendar('destroy'); ($('#calendar') as any).fullCalendar({ weekends: false, header: { left: 'prev,next today', center: 'title', right: 'month,basicWeek,basicDay' }, displayEventTime: false, // open up the display form when a user clicks on an event eventClick: (calEvent: any, jsEvent: MouseEvent, view: any): void => { (window as any).location = this.context.pageContext.web.absoluteUrl + "/Lists/" + escape(this.properties.listName) + "/DispForm.aspx?ID=" + calEvent.id; }, editable: true, timezone: "UTC", droppable: true, // this allows things to be dropped onto the calendar // update the end date when a user drags and drops an event eventDrop: (event: any, delta: moment.Duration, revertFunc: Function): void => { this.updateTask(<number>event.id, <moment.Moment>event.start, <moment.Moment>event.end); }, // put the events on the calendar events: (start: moment.Moment, end: moment.Moment, timezone: string, callback: Function): void => { var startDate = start.format('YYYY-MM-DD'); var endDate = end.format('YYYY-MM-DD'); var restQuery = "/_api/Web/Lists/GetByTitle('" + escape(this.properties.listName) + "')/items?$select=ID,Title,\ Status,StartDate,DueDate,AssignedTo/Title&$expand=AssignedTo&\ $filter=((DueDate ge '" + startDate + "' and DueDate le '" + endDate + "')or(StartDate ge '" + startDate + "' and StartDate le '" + endDate + "'))"; this.context.spHttpClient.get(this.context.pageContext.web.absoluteUrl + restQuery, SPHttpClient.configurations.v1, { headers: { 'Accept': "application/json;odata.metadata=none" } }) .then((response: SPHttpClientResponse): Promise<{ value: ITask[] }> => { return response.json(); }) .then((data: { value: ITask[] }): void => { let personColors: { [person: string]: string; } = {}; var colorNo = 0; var events = data.value.map((task: ITask): any => { var assignedTo = task.AssignedTo.map((person: { Title: string }): string => { return person.Title; }).join(', '); var color = personColors[assignedTo]; if (!color) { color = COLORS[colorNo++]; personColors[assignedTo] = color; } if (colorNo >= COLORS.length) { colorNo = 0; } return { title: task.Title + " - " + assignedTo, id: task.ID, color: color, // specify the background color and border color can also create a class and use className parameter. start: moment.utc(task.StartDate).add("1", "days"), end: moment.utc(task.DueDate).add("1", "days") // add one day to end date so that calendar properly shows event ending on that day }; }); callback(events); }); } }); } private updateTask(id: number, startDate: moment.Moment, dueDate: moment.Moment) { // subtract the previously added day to the date to store correct date var sDate = moment.utc(startDate).add("-1", "days").format('YYYY-MM-DD') + "T" + startDate.format("hh:mm") + ":00Z"; if (!dueDate) { dueDate = startDate; } var dDate = moment.utc(dueDate).add("-1", "days").format('YYYY-MM-DD') + "T" + dueDate.format("hh:mm") + ":00Z"; this.context.spHttpClient.post(`${this.context.pageContext.web.absoluteUrl}\ /_api/Web/Lists/getByTitle('${escape(this.properties.listName)}')/Items(${id})`, SPHttpClient.configurations.v1, { body: JSON.stringify({ StartDate: sDate, DueDate: dDate, }), headers: { Accept: "application/json;odata=nometadata", "Content-Type": "application/json;odata=nometadata", "IF-MATCH": "*", "X-Http-Method": "PATCH" } }) .then((response: SPHttpClientResponse): void => { if (response.ok) { alert("Update Successful"); } else { alert("Update Failed"); } this.displayTasks(); }); }
重要
SharePoint Framework の SPHttpClient を使用しており、SharePoint REST API の応答でメタデータを抑制する場合は、Accept ヘッダーの値として、
application/json;odata.metadata=none
をapplication/json;odata=nometadata
の代わりに使用する必要があります。 SPHttpClient は OData 4.0 を使用しており、前者の値が必要になります。 後者を使用した場合、406 Not Acceptable の応答が返され、要求は失敗します。すべてが想定どおりに機能することを確認するため、コマンド ラインで次のコマンドを実行します。
gulp serve --nobrowser
ホストされているワークベンチに移動し、キャンバスに Web パーツを追加します。 依然として見た目には変化がありませんが、新しいコードは SharePoint Framework SPHttpClient を使用して、コードの簡素化とソリューションの維持を行います。