これまで多くの組織が SharePoint ソリューションのビルドにAngularJS v1.xを使用してきました。 この記事では、ngOfficeUIFabric (Office UI Fabric のための AngularJS ディレクティブ) を使用してスタイル設定された既存の AngularJS アプリケーションを、SharePoint Framework クライアント側 Web パーツに移行する方法を示します。 このチュートリアルで使用するサンプル アプリケーションは、SharePoint リストに格納されている ToDo 項目を管理します。

AngularJS アプリケーションのソースは、GitHub で入手可能です (angular-migration/angular-todo)。
SharePoint Framework への移行後の AngularJS アプリケーションのソースは GitHub で入手可能です (samples/angular-todo)。
注:
この記事では、AngularJS v1.x & ngOfficeUIFabric など、古い古い&プロジェクトを参照しています。
プロジェクトをセットアップする
AngularJS アプリケーションの移行を始める前に、AngularJS アプリケーションをホストする SharePoint Framework の新しいプロジェクトを作成し、セットアップしてください。
新しいプロジェクトを作成する
プロジェクト用の新しいフォルダーを作成します。
md angular-todo
プロジェクト フォルダーに移動します。
cd angular-todo
プロジェクト フォルダーで SharePoint Framework Yeoman ジェネレーターを実行して、新しい SharePoint Framework プロジェクトをスキャホールディングします。
yo @microsoft/sharepoint
プロンプトが表示されたら、以下の値を入力します (以下で省略されたすべてのプロンプトに対して既定のオプションを選択します)。
- ソリューション名は何ですか?: angular-todo
- どの種類のクライアント側コンポーネントを作成しますか?: Web パーツ
- Web パーツ名は何ですか?: To do
- どのテンプレートを使用しますか?: JavaScript なしのフレームワーク
コード エディターでプロジェクト フォルダーを開きます。 このチュートリアルでは、Visual Studio Code を使用します。
AngularJS と ngOfficeUIFabric を追加する
このチュートリアルでは、CDN から AngularJS と ngOfficeUIFabric の両方を読み込みます。
コード エディターで config/config.json ファイルを開き、externals
プロパティに次の行を追加します:
"angular": {
"path": "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.6/angular.min.js",
"globalName": "angular"
},
"ng-office-ui-fabric": "https://cdnjs.cloudflare.com/ajax/libs/ngOfficeUiFabric/0.12.3/ngOfficeUiFabric.js"
TypeScript の AngularJS typings を追加する
Web パーツのコードで AngularJS を参照するので、TypeScript の AngularJS typings も必要です。 それらをインストールするには、コマンド ラインで次のように実行します。
npm install @types/angular --save-dev
AngularJS アプリケーションをそのまま移行する
最初に、最小限のコード変更だけを加えて AngularJS アプリケーションを移行します。 後で、アプリケーションのプレーンな JavaScript コードを TypeScript にアップグレードし、クライアント側の Web パーツとの統合を強化します。
SharePoint リストを作成する
SharePoint サイトで Todo という新しいリストを作成します。 リストに Status という新しい選択肢列を追加します。 使用可能な選択肢として次を入力します。
Not started
In progress
Completed
AngularJS アプリケーションのファイルを Web パーツ プロジェクトにコピーする
Web パーツ プロジェクトの src/webparts/toDo フォルダーに app という名前の新しいフォルダーを作成します。
ソース アプリケーションから app フォルダーの内容を、新しく作成した Web パーツ プロジェクトの app フォルダーにコピーします。
クライアント側の Web パーツで AngularJS アプリケーションを読み込む
コード エディターで、./src/webparts/toDo/ToDoWebPart.ts ファイルを開きます。 最後の
import
ステートメントの後に、次のコードを追加します。import * as angular from 'angular'; import 'ng-office-ui-fabric';
render()
メソッドのコンテンツを次のように変更します:export default class ToDoWebPart extends BaseClientSideWebPart<IToDoWebPartProps> { // ... public render(): void { if (this.renderedOnce === false) { require('./app/app.module'); require('./app/app.config'); require('./app/data.service'); require('./app/home.controller'); this.domElement.innerHTML = ` <div class="${styles.toDo}"> <div data-ng-controller="homeController as vm"> <div class="${styles.loading}" ng-show="vm.isLoading"> <uif-spinner>Loading...</uif-spinner> </div> <div class="entryform" ng-show="vm.isLoading === false"> <uif-textfield uif-label="New to do:" uif-underlined ng-model="vm.newItem" ng-keydown="vm.todoKeyDown($event)"></uif-textfield> </div> <uif-list class="items" ng-show="vm.isLoading === false" > <uif-list-item ng-repeat="todo in vm.todoCollection" uif-item="todo" ng-class="{'done': todo.done}"> <uif-list-item-primary-text>{{todo.title}}</uif-list-item-primary-text> <uif-list-item-actions> <uif-list-item-action ng-click="vm.completeTodo(todo)" ng-show="todo.done === false"> <uif-icon uif-type="check"></uif-icon> </uif-list-item-action> <uif-list-item-action ng-click="vm.undoTodo(todo)" ng-show="todo.done"> <uif-icon uif-type="reactivate"></uif-icon> </uif-list-item-action> <uif-list-item-action ng-click="vm.deleteTodo(todo)"> <uif-icon uif-type="trash"></uif-icon> </uif-list-item-action> </uif-list-item-actions> </uif-list-item> </uif-list> </div> </div>`; angular.bootstrap(this.domElement, ['todoapp']); } } // ... }
サイト パスを更新する
コード エディターで、./src/webparts/toDo/app/app.config.js ファイルを開きます。 sharepointApi
定数の値を、Todo リストを作成した SharePoint サイトを表すサーバーからの相対 URL に変更し、その後ろに /_api/
を続けます。
CSS スタイルを追加する
テンプレートを使用している CSS スタイルを実装する必要もあります。 コード エディターで、ToDoWebPart.module.scss ファイルを開き、その内容を次のように置き換えます:
.toDo {
.loading {
margin: 0 auto;
width: 6em;
}
}
ホストされているワークベンチで Web パーツをプレビューする
コマンド ラインで、次を実行します。
gulp serve --nobrowser
SharePoint サイトの URL に /_layouts/workbench.aspx(例) を追加し、https://contoso.sharepoint.com/_layouts/workbench.aspx、Web ブラウザーでそこに移動します。
すべての手順を正しく実行すると、ブラウザーに Web パーツが表示され、ToDo 項目を追加するフォームが表示されます。
Web パーツが正常に動作していることを確認するために、いくつかの ToDo 項目を追加します。
Web パーツのスタイル設定を修正する
Web パーツは正しく動作していますが、最初の AngularJS アプリケーションとは表示が異なっています。 この現象は、ngOfficeUIFabric が使用している Office UI Fabric が、SharePoint ワークベンチで使用可能なものより前のバージョンであることが原因です。 ngOfficeUIFabric で使用される CSS スタイルを読み込めば簡単に修正できるように思えるかもしれません。 ここで問題になるのは、これらのスタイルは SharePoint ワークベンチで使用されている Office UI Fabric のスタイルと競合することがあり、その場合にユーザー インターフェイスが壊れることです。 より望ましい解決方法は、特定のコンポーネントに必要なスタイルを Web パーツのスタイルに追加することです。
コード エディターで、./src/webparts/toDo/ToDoWebPart.module.scss ファイルを開きます。 内容を次のように変更します。
.toDo { .loading { margin: 0 auto; width: 6em; } .done :global .ms-ListItem-primaryText { text-decoration: line-through; } ul, li { margin: 0; padding: 0; } :global { .ms-Spinner{position:relative;height:20px}.ms-Spinner.ms-Spinner--large{height:28px}.ms-Spinner.ms-Spinner--large .ms-Spinner-label{left:34px;top:6px}.ms-Spinner-circle{position:absolute;border-radius:100px;background-color:#0078d7;opacity:0}@media screen and (-ms-high-contrast:active){.ms-Spinner-circle{background-color:#fff}}@media screen and (-ms-high-contrast:black-on-white){.ms-Spinner-circle{background-color:#000}}.ms-Spinner-label{position:relative;color:#333;font-family:Segoe UI Regular WestEuropean,Segoe UI,Tahoma,Arial,sans-serif;font-size:12px;font-weight:400;color:#0078d7;left:28px;top:2px} .ms-TextField{color:#333;font-family:Segoe UI Regular WestEuropean,Segoe UI,Tahoma,Arial,sans-serif;font-size:14px;font-weight:400;box-sizing:border-box;margin:0;padding:0;box-shadow:none;margin-bottom:8px}.ms-TextField.is-disabled .ms-TextField-field{background-color:#f4f4f4;border-color:#f4f4f4;pointer-events:none;cursor:default}.ms-TextField.is-disabled:-moz-placeholder,.ms-TextField.is-disabled:-ms-input-placeholder,.ms-TextField.is-disabled::-moz-placeholder,.ms-TextField.is-disabled::-webkit-input-placeholder{color:#a6a6a6}.ms-TextField.is-required .ms-Label:after{content:' *';color:#a80000}.ms-TextField.is-required:-moz-placeholder:after,.ms-TextField.is-required:-ms-input-placeholder:after,.ms-TextField.is-required::-moz-placeholder:after,.ms-TextField.is-required::-webkit-input-placeholder:after{content:' *';color:#a80000}.ms-TextField.is-active{border-color:#0078d7}.ms-TextField-field{box-sizing:border-box;margin:0;padding:0;box-shadow:none;border:1px solid #c8c8c8;border-radius:0;font-family:Segoe UI Semilight WestEuropean,Segoe UI Semilight,Segoe UI,Tahoma,Arial,sans-serif;font-size:12px;color:#333;height:32px;padding:6px 10px 8px;width:100%;min-width:180px;outline:0}.ms-TextField-field:hover{border-color:#767676}.ms-TextField-field:focus{border-color:#0078d7}@media screen and (-ms-high-contrast:active){.ms-TextField-field:focus,.ms-TextField-field:hover{border-color:#1aebff}}@media screen and (-ms-high-contrast:black-on-white){.ms-TextField-field:focus,.ms-TextField-field:hover{border-color:#37006e}}.ms-TextField-field:-moz-placeholder,.ms-TextField-field:-ms-input-placeholder,.ms-TextField-field::-moz-placeholder,.ms-TextField-field::-webkit-input-placeholder{color:#666}.ms-TextField-description{color:#767676;font-size:11px}.ms-TextField.ms-TextField--placeholder{position:relative}.ms-TextField.ms-TextField--placeholder .ms-Label{position:absolute;font-family:Segoe UI Semilight WestEuropean,Segoe UI Semilight,Segoe UI,Tahoma,Arial,sans-serif;font-size:12px;color:#666;padding:7px 0 7px 10px}.ms-TextField.ms-TextField--placeholder.is-disabled,.ms-TextField.ms-TextField--placeholder.is-disabled .ms-Label{color:#a6a6a6}@media screen and (-ms-high-contrast:active){.ms-TextField.ms-TextField--placeholder.is-disabled .ms-Label{color:#0f0}}@media screen and (-ms-high-contrast:black-on-white){.ms-TextField.ms-TextField--placeholder.is-disabled .ms-Label{color:#600000}}.ms-TextField.ms-TextField--underlined{border-bottom:1px solid #c8c8c8;display:table;width:100%;min-width:180px}.ms-TextField.ms-TextField--underlined:hover{border-color:#767676}@media screen and (-ms-high-contrast:active){.ms-TextField.ms-TextField--underlined:hover{border-color:#1aebff}}@media screen and (-ms-high-contrast:black-on-white){.ms-TextField.ms-TextField--underlined:hover{border-color:#37006e}}.ms-TextField.ms-TextField--underlined:active,.ms-TextField.ms-TextField--underlined:focus{border-color:#0078d7}.ms-TextField.ms-TextField--underlined .ms-Label{font-size:12px;margin-right:8px;display:table-cell;vertical-align:bottom;padding-left:12px;padding-bottom:5px;height:32px;width:1%;white-space:nowrap}.ms-TextField.ms-TextField--underlined .ms-TextField-field{border:0;float:left;display:table-cell;text-align:left;padding-top:8px;padding-bottom:2px}.ms-TextField.ms-TextField--underlined .ms-TextField-field:active,.ms-TextField.ms-TextField--underlined .ms-TextField-field:focus,.ms-TextField.ms-TextField--underlined .ms-TextField-field:hover{outline:0}.ms-TextField.ms-TextField--underlined.is-disabled{border-bottom-color:#eaeaea}.ms-TextField.ms-TextField--underlined.is-disabled .ms-Label{color:#a6a6a6}@media screen and (-ms-high-contrast:active){.ms-TextField.ms-TextField--underlined.is-disabled .ms-Label{color:#0f0}}@media screen and (-ms-high-contrast:black-on-white){.ms-TextField.ms-TextField--underlined.is-disabled .ms-Label{color:#600000}}.ms-TextField.ms-TextField--underlined.is-disabled .ms-TextField-field{background-color:transparent;color:#a6a6a6}.ms-TextField.ms-TextField--underlined.is-active{border-color:#0078d7}@media screen and (-ms-high-contrast:active){.ms-TextField.ms-TextField--underlined.is-active{border-color:#1aebff}}@media screen and (-ms-high-contrast:black-on-white){.ms-TextField.ms-TextField--underlined.is-active{border-color:#37006e}}.ms-TextField.ms-TextField--multiline .ms-TextField-field{line-height:17px;min-height:60px;min-width:260px;padding-top:6px;overflow:auto}.ms-Label,.ms-TextField.ms-TextField--multiline .ms-TextField-field{color:#333;font-family:Segoe UI Regular WestEuropean,Segoe UI,Tahoma,Arial,sans-serif;font-size:12px;font-weight:400} .ms-Label{margin:0;padding:0;box-shadow:none;box-sizing:border-box;display:block;padding:5px 0}.ms-Label.is-required:after{content:' *';color:#a80000}.ms-Label.is-disabled{color:#a6a6a6}@media screen and (-ms-high-contrast:active){.ms-Label.is-disabled{color:#0f0}}@media screen and (-ms-high-contrast:black-on-white){.ms-Label.is-disabled{color:#600000}}.is-disabled .ms-Label{color:#a6a6a6}@media screen and (-ms-high-contrast:active){.is-disabled .ms-Label{color:#0f0}}@media screen and (-ms-high-contrast:black-on-white){.is-disabled .ms-Label{color:#600000}}.ms-Toggle{color:#333;font-family:Segoe UI Regular WestEuropean,Segoe UI,Tahoma,Arial,sans-serif;font-size:14px;font-weight:400;box-sizing:border-box;margin:0;padding:0;box-shadow:none;position:relative;display:block;margin-bottom:26px}.ms-Toggle .ms-Label{position:relative;padding:0 0 0 62px;font-size:12px}.ms-Toggle:hover .ms-Label{color:#000}.ms-Toggle:active .ms-Label{color:#333}.ms-Toggle.is-disabled .ms-Label{color:#a6a6a6}@media screen and (-ms-high-contrast:active){.ms-Toggle.is-disabled .ms-Label{color:#0f0}}@media screen and (-ms-high-contrast:black-on-white){.ms-Toggle.is-disabled .ms-Label{color:#600000}} .ms-ListItem{font-family:"Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;font-size:14px;font-weight:400;box-sizing:border-box;margin:0;padding:0;box-shadow:none;padding:9px 28px 3px;position:relative;display:block}.ms-ListItem::after,.ms-ListItem::before{display:table;content:"";line-height:0}.ms-ListItem::after{clear:both}.ms-ListItem-primaryText,.ms-ListItem-secondaryText,.ms-ListItem-tertiaryText{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block}.ms-ListItem-primaryText{font-family:"Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;font-size:21px;font-weight:100;padding-right:80px;position:relative;top:-4px}.ms-ListItem-secondaryText{font-family:"Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;font-size:14px;font-weight:400;line-height:25px;position:relative;top:-7px;padding-right:30px}.ms-ListItem-tertiaryText{font-family:"Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;font-size:14px;font-weight:400;position:relative;top:-9px;margin-bottom:-4px;padding-right:30px}.ms-ListItem-metaText{font-family:"Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;font-size:11px;font-weight:400;position:absolute;right:30px;top:39px}.ms-ListItem-image{float:left;height:70px;margin-left:-8px;margin-right:10px;width:70px}.ms-ListItem-selectionTarget{display:none}.ms-ListItem-actions{max-width:80px;position:absolute;right:30px;text-align:right;top:10px}.ms-ListItem-action{color:#a6a6a6;display:inline-block;font-size:15px;position:relative;text-align:center;top:3px;cursor:pointer;height:16px;width:16px}.ms-ListItem-action .ms-Icon{vertical-align:top}.ms-ListItem-action:hover{color:#666666;outline:1px solid transparent}.ms-ListItem.is-unread{border-left:3px solid #0078d7;padding-left:27px}.ms-ListItem.is-unread .ms-ListItem-metaText,.ms-ListItem.is-unread .ms-ListItem-secondaryText{color:#0078d7;font-weight:600}.ms-ListItem.is-unseen:after{border-right:10px solid transparent;border-top:10px solid #0078d7;left:0;position:absolute;top:0}.ms-ListItem.is-selectable .ms-ListItem-selectionTarget{display:block;height:20px;left:6px;position:absolute;top:13px;width:20px}.ms-ListItem.is-selectable .ms-ListItem-image{margin-left:0}.ms-ListItem.is-selectable:hover{background-color:#eaeaea;cursor:pointer;outline:1px solid transparent}.ms-ListItem.is-selectable:hover:before{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-family:FabricMDL2Icons;font-style:normal;font-weight:400;speak:none;position:absolute;top:12px;left:6px;height:15px;width:15px;border:1px solid #767676}.ms-ListItem.is-selected:before{border:1px solid transparent}.ms-ListItem.is-selected:before,.ms-ListItem.is-selected:hover:before{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-family:FabricMDL2Icons;font-style:normal;font-weight:400;speak:none;content:'\e041';font-size:15px;color:#767676;position:absolute;top:12px;left:6px}.ms-ListItem.is-selected:hover{background-color:#c7e0f4;outline:1px solid transparent}.ms-ListItem.ms-ListItem--document{padding:0}.ms-ListItem.ms-ListItem--document .ms-ListItem-itemIcon{width:70px;height:70px;float:left;text-align:center}.ms-ListItem.ms-ListItem--document .ms-ListItem-itemIcon .ms-Icon{font-size:38px;line-height:70px;color:#666666}.ms-ListItem.ms-ListItem--document .ms-ListItem-primaryText{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;padding-top:15px;padding-right:0;position:static}.ms-ListItem.ms-ListItem--document .ms-ListItem-secondaryText{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:"Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;-webkit-font-smoothing:antialiased;font-size:11px;font-weight:400;padding-top:6px}.MailList{overflow-y:auto;-webkit-overflow-scrolling:touch;max-height:500px}.MailTile{margin-bottom:5px;padding:10px;background:red} } }
./src/webparts/toDo/ToDoWebPart.ts ファイルの
render()
メソッドで、新しい Office UI Fabric のアイコンを使用するよう、アプリケーション レンダリング テンプレートを変更します。export default class ToDoWebPart extends BaseClientSideWebPart<IToDoWebPartProps> { // ... public render(): void { if (this.renderedOnce === false) { require('./app/app.module'); require('./app/app.config'); require('./app/data.service'); require('./app/home.controller'); this.domElement.innerHTML = ` <div class="${styles.toDo}"> <div data-ng-controller="homeController as vm"> <div class="${styles.loading}" ng-show="vm.isLoading"> <uif-spinner>Loading...</uif-spinner> </div> <div id="entryform" ng-show="vm.isLoading === false"> <uif-textfield uif-label="New to do:" uif-underlined ng-model="vm.newItem" ng-keydown="vm.todoKeyDown($event)"></uif-textfield> </div> <uif-list id="items" ng-show="vm.isLoading === false" > <uif-list-item ng-repeat="todo in vm.todoCollection" uif-item="todo" ng-class="{'${styles.done}': todo.done}"> <uif-list-item-primary-text>{{todo.title}}</uif-list-item-primary-text> <uif-list-item-actions> <uif-list-item-action ng-click="vm.completeTodo(todo)" ng-show="todo.done === false"> <i class="ms-Icon ms-Icon--CheckMark" aria-hidden="true"></i> </uif-list-item-action> <uif-list-item-action ng-click="vm.undoTodo(todo)" ng-show="todo.done"> <i class="ms-Icon ms-Icon--RevToggleKey" aria-hidden="true"></i> </uif-list-item-action> <uif-list-item-action ng-click="vm.deleteTodo(todo)"> <i class="ms-Icon ms-Icon--Delete" aria-hidden="true"></i> </uif-list-item-action> </uif-list-item-actions> </uif-list-item> </uif-list> </div> </div>`; angular.bootstrap(this.domElement, ['todoapp']); } } // ... }
Web ブラウザーで Web パーツを更新すると、正しくスタイル設定された状態で表示されます。
AngularJS アプリケーションを TypeScript にアップグレードする
元の AngularJS アプリケーションはプレーンな JavaScript で書かれており、メンテナンス時にエラーが発生しがちです。 SharePoint Framework のクライアント側の Web パーツを作成するときに TypeScript や、デザイン時のタイプ セーフ機能からの特典を使用できます。 次のセクションでは、プレーンな JavaScript AngularJS コードを TypeScript に移行します。
アプリケーション構成をアップグレードする
プロジェクトの ./src/webparts/toDo/app/app.config.js ファイルを app.config.ts に名前を変更します。 内容を次のように変更します:
import * as angular from 'angular';
export default function() {
const todoapp: ng.IModule = angular.module('todoapp');
todoapp.constant('sharepointApi', '/todo/_api/');
todoapp.constant('todoListName', 'Todo');
todoapp.constant('hideFinishedTasks', false);
}
データ サービスをアップグレードする
プロジェクトの ./src/webparts/toDo/app/data.service.js ファイルを DataService.ts に名前を変更します。 内容を次のように変更します:
import * as angular from 'angular';
export interface ITodo {
id: number;
title: string;
done: boolean;
}
interface ITodoItem {
Id: number;
Title: string;
Status: string;
}
export interface IDataService {
getTodos: () => angular.IPromise<ITodo[]>;
addTodo: (todo: string) => angular.IPromise<{}>;
deleteTodo: (todo: ITodo) => angular.IPromise<{}>;
setTodoStatus: (todo: ITodo, done: boolean) => angular.IPromise<{}>;
}
export default class DataService implements IDataService {
public static $inject: string[] = ['$q', '$http', 'sharepointApi', 'todoListName', 'hideFinishedTasks'];
constructor(private $q: angular.IQService,
private $http: angular.IHttpService,
private sharepointApi: string,
private todoListName: string,
private hideFinishedTasks: boolean) {
}
public getTodos(): angular.IPromise<ITodo[]> {
const deferred: angular.IDeferred<ITodo[]> = this.$q.defer();
let url: string = `${this.sharepointApi}web/lists/getbytitle('${this.todoListName}')/items?$select=Id,Title,Status&$orderby=ID desc`;
if (this.hideFinishedTasks === true) {
url += "&$filter=Status ne 'Completed'";
}
this.$http({
url: url,
method: 'GET',
headers: {
'Accept': 'application/json;odata=nometadata'
}
}).then((result: angular.IHttpPromiseCallbackArg<{ value: ITodoItem[] }>): void => {
const todos: ITodo[] = [];
for (let i: number = 0; i < result.data.value.length; i++) {
const todo: ITodoItem = result.data.value[i];
todos.push({
id: todo.Id,
title: todo.Title,
done: todo.Status === 'Completed'
});
}
deferred.resolve(todos);
});
return deferred.promise;
}
public addTodo(todo: string): angular.IPromise<{}> {
const deferred: angular.IDeferred<{}> = this.$q.defer();
let listItemEntityTypeFullName: string = undefined;
this.getListItemEntityTypeFullName()
.then((entityTypeName: string): angular.IPromise<string> => {
listItemEntityTypeFullName = entityTypeName;
return this.getRequestDigest();
})
.then((requestDigest: string): void => {
const body: string = JSON.stringify({
'__metadata': { 'type': listItemEntityTypeFullName },
'Title': todo
});
this.$http({
url: `${this.sharepointApi}web/lists/getbytitle('${this.todoListName}')/items`,
method: 'POST',
headers: {
'Accept': 'application/json;odata=nometadata',
'Content-type': 'application/json;odata=verbose',
'X-RequestDigest': requestDigest
},
data: body
}).then((result: angular.IHttpPromiseCallbackArg<{}>): void => {
deferred.resolve();
});
});
return deferred.promise;
}
public deleteTodo(todo: ITodo): angular.IPromise<{}> {
const deferred: angular.IDeferred<{}> = this.$q.defer();
this.getRequestDigest()
.then((requestDigest: string): void => {
this.$http({
url: `${this.sharepointApi}web/lists/getbytitle('${this.todoListName}')/items(${todo.id})`,
method: 'POST',
headers: {
'Accept': 'application/json;odata=nometadata',
'X-RequestDigest': requestDigest,
'IF-MATCH': '*',
'X-HTTP-Method': 'DELETE'
}
}).then((result: angular.IHttpPromiseCallbackArg<{}>): void => {
deferred.resolve();
});
});
return deferred.promise;
}
public setTodoStatus(todo: ITodo, done: boolean): angular.IPromise<{}> {
const deferred: angular.IDeferred<{}> = this.$q.defer();
let listItemEntityTypeFullName: string = undefined;
this.getListItemEntityTypeFullName()
.then((entityTypeName: string): angular.IPromise<string> => {
listItemEntityTypeFullName = entityTypeName;
return this.getRequestDigest();
})
.then((requestDigest: string): void => {
const body: string = JSON.stringify({
'__metadata': { 'type': listItemEntityTypeFullName },
'Status': done ? 'Completed' : 'Not started'
});
this.$http({
url: `${this.sharepointApi}web/lists/getbytitle('${this.todoListName}')/items(${todo.id})`,
method: 'POST',
headers: {
'Accept': 'application/json;odata=nometadata',
'Content-type': 'application/json;odata=verbose',
'X-RequestDigest': requestDigest,
'IF-MATCH': '*',
'X-HTTP-Method': 'MERGE'
},
data: body
}).then((result: angular.IHttpPromiseCallbackArg<{}>): void => {
deferred.resolve();
});
});
return deferred.promise;
}
private getRequestDigest(): angular.IPromise<string> {
const deferred: angular.IDeferred<string> = this.$q.defer();
this.$http({
url: this.sharepointApi + 'contextinfo',
method: 'POST',
headers: {
'Accept': 'application/json;odata=nometadata'
}
}).then((result: angular.IHttpPromiseCallbackArg<{ FormDigestValue: string }>): void => {
deferred.resolve(result.data.FormDigestValue);
}, (err: any): void => {
deferred.reject(err);
});
return deferred.promise;
}
private getListItemEntityTypeFullName(): angular.IPromise<string> {
const deferred: angular.IDeferred<string> = this.$q.defer();
this.$http({
url: `${this.sharepointApi}web/lists/getbytitle('${this.todoListName}')?$select=ListItemEntityTypeFullName`,
method: 'GET',
headers: {
'Accept': 'application/json;odata=nometadata'
}
}).then((result: angular.IHttpPromiseCallbackArg<{ ListItemEntityTypeFullName: string }>): void => {
deferred.resolve(result.data.ListItemEntityTypeFullName);
}, (err: any): void => {
deferred.reject(err);
});
return deferred.promise;
}
}
ホーム コントローラーをアップグレードする
プロジェクトの./src/webparts/toDo/app/home.controller.js ファイルを HomeController.ts に名前を変更します。 内容を次のように変更します。
import * as angular from 'angular';
import { IDataService, ITodo } from './DataService';
export default class HomeController {
public isLoading: boolean = false;
public newItem: string = null;
public todoCollection: ITodo[] = [];
public static $inject: string[] = ['DataService', '$window'];
constructor(private dataService: IDataService, private $window: angular.IWindowService) {
this.loadTodos();
}
private loadTodos(): void {
this.isLoading = true;
this.dataService.getTodos()
.then((todos: ITodo[]): void => {
this.todoCollection = todos;
})
.finally((): void => {
this.isLoading = false;
});
}
public todoKeyDown($event: KeyboardEvent): void {
if ($event.keyCode === 13 && this.newItem.length > 0) {
$event.preventDefault();
this.todoCollection.unshift({ id: -1, title: this.newItem, done: false });
this.dataService.addTodo(this.newItem)
.then((): void => {
this.newItem = null;
this.dataService.getTodos()
.then((todos: ITodo[]): void => {
this.todoCollection = todos;
});
});
}
}
public deleteTodo(todo: ITodo): void {
if (this.$window.confirm('Are you sure you want to delete this todo item?')) {
let index: number = -1;
for (let i: number = 0; i < this.todoCollection.length; i++) {
if (this.todoCollection[i].id === todo.id) {
index = i;
break;
}
}
if (index > -1) {
this.todoCollection.splice(index, 1);
}
this.dataService.deleteTodo(todo)
.then((): void => {
this.dataService.getTodos()
.then((todos: ITodo[]): void => {
this.todoCollection = todos;
});
});
}
}
public completeTodo(todo: ITodo): void {
todo.done = true;
this.dataService.setTodoStatus(todo, true)
.then((): void => {
this.dataService.getTodos()
.then((todos: ITodo[]): void => {
this.todoCollection = todos;
});
});
}
public undoTodo(todo: ITodo): void {
todo.done = false;
this.dataService.setTodoStatus(todo, false)
.then((): void => {
this.dataService.getTodos()
.then((todos: ITodo[]): void => {
this.todoCollection = todos;
});
});
}
}
アプリケーション モジュールをアップグレードする
プロジェクトの./src/webparts/toDo/app/app.module.js ファイルを app.module.ts に名前を変更します。 内容を次のように変更します。
import * as angular from 'angular';
import config from './app.config';
import HomeController from './HomeController';
import DataService from './DataService';
import 'ng-office-ui-fabric';
const todoapp: angular.IModule = angular.module('todoapp', [
'officeuifabric.core',
'officeuifabric.components'
]);
config();
todoapp
.controller('HomeController', HomeController)
.service('DataService', DataService);
Web パーツの AngularJS アプリケーションへの参照を更新する
この段階で AngularJS アプリケーションは TypeScript を使って作成されており、各部分が相互に参照し合っているため、Web パーツがアプリケーションのすべての部分を参照する必要はなくなりました。 代わりにメイン モジュールを読み込みさえすれば、AngularJS アプリケーションを構成するその他の要素をすべて読み込むことができます。
コード エディターで、./src/webparts/toDo/ToDoWebPart.ts ファイルを開きます。
render()
メソッドを次のように変更します:export default class ToDoWebPart extends BaseClientSideWebPart<IToDoWebPartProps> { // ... public render(): void { if (this.renderedOnce === false) { require('./app/app.module'); this.domElement.innerHTML = ` <div class="${styles.toDo}"> <div data-ng-controller="HomeController as vm"> <div class="${styles.loading}" ng-show="vm.isLoading"> <uif-spinner>Loading...</uif-spinner> </div> <div id="entryform" ng-show="vm.isLoading === false"> <uif-textfield uif-label="New to do:" uif-underlined ng-model="vm.newItem" ng-keydown="vm.todoKeyDown($event)"></uif-textfield> </div> <uif-list id="items" ng-show="vm.isLoading === false" > <uif-list-item ng-repeat="todo in vm.todoCollection" uif-item="todo" ng-class="{'${styles.done}': todo.done}"> <uif-list-item-primary-text>{{todo.title}}</uif-list-item-primary-text> <uif-list-item-actions> <uif-list-item-action ng-click="vm.completeTodo(todo)" ng-show="todo.done === false"> <i class="ms-Icon ms-Icon--CheckMark" aria-hidden="true"></i> </uif-list-item-action> <uif-list-item-action ng-click="vm.undoTodo(todo)" ng-show="todo.done"> <i class="ms-Icon ms-Icon--RevToggleKey" aria-hidden="true"></i> </uif-list-item-action> <uif-list-item-action ng-click="vm.deleteTodo(todo)"> <i class="ms-Icon ms-Icon--Delete" aria-hidden="true"></i> </uif-list-item-action> </uif-list-item-actions> </uif-list-item> </uif-list> </div> </div>`; angular.bootstrap(this.domElement, ['todoapp']); } } // ... }
TypeScript へのアップグレードが成功したかどうかを検証するためにコマンド ラインで以下を実行します。
gulp serve --nobrowser
Web ブラウザーで SharePoint ワークベンチを更新すると、以前と同様に Web パーツが表示されるはずです。
Web パーツの動作方法は変わっていませんが、コードは強化されています。 将来、更新がある場合、発の段階からコードの正確性と整合性を容易に確認できます。
AngularJS アプリケーションと SharePoint Framework の統合を強化する
この時点では、AngularJS アプリケーションは正常に動作し、SharePoint Framework のクライアント側の Web パーツにラップされています。 ユーザーはページに Web パーツを追加できますが、Web パーツの動作方法は構成できません。 すべての構成は、AngularJS アプリケーションのコードの中に埋め込まれています。 このセクションでは、Web パーツを拡張して、ToDo 項目の保存先リストの名前を構成したり、完了したタスクを Web パーツに表示するかどうかを構成したりできるようにします。
Web パーツのプロパティを定義する
コード エディターで、./src/webparts/toDo/ToDoWebPart.manifest.json ファイルを開きます。
properties
プロパティを 次のように変更します:"properties": { "todoListName": "Todo", "hideFinishedTasks": false }
./src/webparts/toDo/ToDoWebPart.ts ファイルで
IToDoWebPartProps
インターフェイスの定義を次のように変更します。export interface IToDoWebPartProps { todoListName: string; hideFinishedTasks: boolean; }
./src/webparts/toDo/ToDoWebPart.ts ファイルで 1 つ目の import ステートメントを次のように変更します。
import { BaseClientSideWebPart, IPropertyPaneSettings, PropertyPaneTextField, PropertyPaneToggle } from '@microsoft/sp-webpart-base';
同じファイルで、
getPropertyPaneConfiguration()
メソッドを次のように変更します:export default class ToDoWebPart extends BaseClientSideWebPart<IToDoWebPartProps> { // ... protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: strings.PropertyPaneDescription }, groups: [ { groupName: strings.BasicGroupName, groupFields: [ PropertyPaneTextField('todoListName', { label: strings.ListNameFieldLabel }), PropertyPaneToggle('hideFinishedTasks', { label: strings.HideFinishedTasksFieldLabel }) ] } ] } ] }; } // ... }
./src/webparts/toDo/loc/mystrings.d.ts ファイルの内容を次のように変更し、欠けているリソース文字列を追加します。
declare interface IToDoWebPartStrings { PropertyPaneDescription: string; BasicGroupName: string; ListNameFieldLabel: string; HideFinishedTasksFieldLabel: string; } declare module 'ToDoWebPartStrings' { const strings: IToDoWebPartStrings; export = strings; }
./src/webparts/toDo/loc/en-us.js ファイルで、新しく追加された文字列の翻訳を追加します。
define([], function() { return { "PropertyPaneDescription": "Description", "BasicGroupName": "Group Name", "ListNameFieldLabel": "List name", "HideFinishedTasksFieldLabel": "Hide finished tasks" } });
Web パーツのプロパティ値を AngularJS アプリケーションに渡す
現時点で、ユーザーは Web パーツがどのように動作するかを構成できますが、AngularJS アプリケーションはこれらの値を使用していません。 次のセクションでは、AngularJS アプリケーションを拡張して、Web パーツ プロパティ ウィンドウでユーザーが指定する構成値を使用できるようにします。 1 つの方法は、render()
メソッドで AngularJS イベントをブロードキャストしてから、Web パーツで使用されているコントローラーでこのイベントを登録することです。
AngularJS 構成ファイルを削除する
プロジェクトの ./src/webparts/toDo/app/app.config.ts ファイルを削除します。 次の手順では、Web パーツのプロパティの構成値を取得するようアプリケーションを更新します。
構成への参照を削除する
./src/webparts/toDo/app/app.module.ts ファイルで、ファイルの内容を次のように変更することで、AngularJS の構成への参照を削除します。
import * as angular from 'angular';
import HomeController from './HomeController';
import DataService from './DataService';
import 'ng-office-ui-fabric';
const todoapp: angular.IModule = angular.module('todoapp', [
'officeuifabric.core',
'officeuifabric.components'
]);
todoapp
.controller('HomeController', HomeController)
.service('DataService', DataService);
メソッド パラメーターの構成値を受け入れるようデータ サービスを更新する
データ サービスは、もともと app.config.ts ファイルで定義された定数から構成を取得していました。 その代わりに Web パーツのプロパティで構成された構成値を使用するには、特定のメソッドでパラメーターを受け入れる必要があります。
コード エディターで、./src/webparts/toDo/app/DataService.ts ファイルを開き、ファイルの内容を次のように変更します。
import * as angular from 'angular';
export interface ITodo {
id: number;
title: string;
done: boolean;
}
interface ITodoItem {
Id: number;
Title: string;
Status: string;
}
export interface IDataService {
getTodos: (sharePointApi: string, todoListName: string, hideFinishedTasks: boolean) => angular.IPromise<ITodo[]>;
addTodo: (todo: string, sharePointApi: string, todoListName: string) => angular.IPromise<{}>;
deleteTodo: (todo: ITodo, sharePointApi: string, todoListName: string) => angular.IPromise<{}>;
setTodoStatus: (todo: ITodo, done: boolean, sharePointApi: string, todoListName: string) => angular.IPromise<{}>;
}
export default class DataService implements IDataService {
public static $inject: string[] = ['$q', '$http'];
constructor(private $q: angular.IQService, private $http: angular.IHttpService) {
}
public getTodos(sharePointApi: string, todoListName: string, hideFinishedTasks: boolean): angular.IPromise<ITodo[]> {
const deferred: angular.IDeferred<ITodo[]> = this.$q.defer();
let url: string = `${sharePointApi}web/lists/getbytitle('${todoListName}')/items?$select=Id,Title,Status&$orderby=ID desc`;
if (hideFinishedTasks === true) {
url += "&$filter=Status ne 'Completed'";
}
this.$http({
url: url,
method: 'GET',
headers: {
'Accept': 'application/json;odata=nometadata'
}
}).then((result: angular.IHttpPromiseCallbackArg<{ value: ITodoItem[] }>): void => {
const todos: ITodo[] = [];
for (let i: number = 0; i < result.data.value.length; i++) {
const todo: ITodoItem = result.data.value[i];
todos.push({
id: todo.Id,
title: todo.Title,
done: todo.Status === 'Completed'
});
}
deferred.resolve(todos);
});
return deferred.promise;
}
public addTodo(todo: string, sharePointApi: string, todoListName: string): angular.IPromise<{}> {
const deferred: angular.IDeferred<{}> = this.$q.defer();
let listItemEntityTypeFullName: string = undefined;
this.getListItemEntityTypeFullName(sharePointApi, todoListName)
.then((entityTypeName: string): angular.IPromise<string> => {
listItemEntityTypeFullName = entityTypeName;
return this.getRequestDigest(sharePointApi);
})
.then((requestDigest: string): void => {
const body: string = JSON.stringify({
'__metadata': { 'type': listItemEntityTypeFullName },
'Title': todo
});
this.$http({
url: `${sharePointApi}web/lists/getbytitle('${todoListName}')/items`,
method: 'POST',
headers: {
'Accept': 'application/json;odata=nometadata',
'Content-type': 'application/json;odata=verbose',
'X-RequestDigest': requestDigest
},
data: body
}).then((result: angular.IHttpPromiseCallbackArg<{}>): void => {
deferred.resolve();
});
});
return deferred.promise;
}
public deleteTodo(todo: ITodo, sharePointApi: string, todoListName: string): angular.IPromise<{}> {
const deferred: angular.IDeferred<{}> = this.$q.defer();
this.getRequestDigest(sharePointApi)
.then((requestDigest: string): void => {
this.$http({
url: `${sharePointApi}web/lists/getbytitle('${todoListName}')/items(${todo.id})`,
method: 'POST',
headers: {
'Accept': 'application/json;odata=nometadata',
'X-RequestDigest': requestDigest,
'IF-MATCH': '*',
'X-HTTP-Method': 'DELETE'
}
}).then((result: angular.IHttpPromiseCallbackArg<{}>): void => {
deferred.resolve();
});
});
return deferred.promise;
}
public setTodoStatus(todo: ITodo, done: boolean, sharePointApi: string, todoListName: string): angular.IPromise<{}> {
const deferred: angular.IDeferred<{}> = this.$q.defer();
let listItemEntityTypeFullName: string = undefined;
this.getListItemEntityTypeFullName(sharePointApi, todoListName)
.then((entityTypeName: string): angular.IPromise<string> => {
listItemEntityTypeFullName = entityTypeName;
return this.getRequestDigest(sharePointApi);
})
.then((requestDigest: string): void => {
const body: string = JSON.stringify({
'__metadata': { 'type': listItemEntityTypeFullName },
'Status': done ? 'Completed' : 'Not started'
});
this.$http({
url: `${sharePointApi}web/lists/getbytitle('${todoListName}')/items(${todo.id})`,
method: 'POST',
headers: {
'Accept': 'application/json;odata=nometadata',
'Content-type': 'application/json;odata=verbose',
'X-RequestDigest': requestDigest,
'IF-MATCH': '*',
'X-HTTP-Method': 'MERGE'
},
data: body
}).then((result: angular.IHttpPromiseCallbackArg<{}>): void => {
deferred.resolve();
});
});
return deferred.promise;
}
private getRequestDigest(sharePointApi: string): angular.IPromise<string> {
const deferred: angular.IDeferred<string> = this.$q.defer();
this.$http({
url: sharePointApi + 'contextinfo',
method: 'POST',
headers: {
'Accept': 'application/json;odata=nometadata'
}
}).then((result: angular.IHttpPromiseCallbackArg<{ FormDigestValue: string }>): void => {
deferred.resolve(result.data.FormDigestValue);
}, (err: any): void => {
deferred.reject(err);
});
return deferred.promise;
}
private getListItemEntityTypeFullName(sharePointApi: string, todoListName: string): angular.IPromise<string> {
const deferred: angular.IDeferred<string> = this.$q.defer();
this.$http({
url: `${sharePointApi}web/lists/getbytitle('${todoListName}')?$select=ListItemEntityTypeFullName`,
method: 'GET',
headers: {
'Accept': 'application/json;odata=nometadata'
}
}).then((result: angular.IHttpPromiseCallbackArg<{ ListItemEntityTypeFullName: string }>): void => {
deferred.resolve(result.data.ListItemEntityTypeFullName);
}, (err: any): void => {
deferred.reject(err);
});
return deferred.promise;
}
}
プロパティ変更イベントのブロードキャスト
./src/webparts/toDo/ToDoWebPart.ts ファイルで
ToDoWebPart
クラスに$injector
という新しいプロパティを追加します:export default class ToDoWebPart extends BaseClientSideWebPart<IToDoWebPartProps> { private $injector: angular.auto.IInjectorService; // ... }
同じファイルで、
render()
メソッドを次のように更新します。export default class ToDoWebPart extends BaseClientSideWebPart<IToDoWebPartProps> { // ... public render(): void { if (this.renderedOnce === false) { require('./app/app.module'); this.domElement.innerHTML = ` <div class="${styles.toDo}"> <div data-ng-controller="HomeController as vm"> <div class="${styles.configurationNeeded}" ng-show="vm.configurationNeeded"> Please configure the web part </div> <div ng-show="vm.configurationNeeded === false"> <div id="loading" ng-show="vm.isLoading"> <uif-spinner>Loading...</uif-spinner> </div> <div id="entryform" ng-show="vm.isLoading === false"> <uif-textfield uif-label="New to do:" uif-underlined ng-model="vm.newItem" ng-keydown="vm.todoKeyDown($event)"></uif-textfield> </div> <uif-list id="items" ng-show="vm.isLoading === false" > <uif-list-item ng-repeat="todo in vm.todoCollection" uif-item="todo" ng-class="{'${styles.done}': todo.done}"> <uif-list-item-primary-text>{{todo.title}}</uif-list-item-primary-text> <uif-list-item-actions> <uif-list-item-action ng-click="vm.completeTodo(todo)" ng-show="todo.done === false"> <i class="ms-Icon ms-Icon--CheckMark" aria-hidden="true"></i> </uif-list-item-action> <uif-list-item-action ng-click="vm.undoTodo(todo)" ng-show="todo.done"> <i class="ms-Icon ms-Icon--RevToggleKey" aria-hidden="true"></i> </uif-list-item-action> <uif-list-item-action ng-click="vm.deleteTodo(todo)"> <i class="ms-Icon ms-Icon--Delete" aria-hidden="true"></i> </uif-list-item-action> </uif-list-item-actions> </uif-list-item> </uif-list> </div> </div> </div>`; this.$injector = angular.bootstrap(this.domElement, ['todoapp']); } this.$injector.get('$rootScope').$broadcast('configurationChanged', { sharePointApi: this.context.pageContext.web.absoluteUrl + '/_api/', todoListName: this.properties.todoListName, hideFinishedTasks: this.properties.hideFinishedTasks }); } // ... }
./src/webparts/toDo/ToDoWebPart.module.scss ファイルに
.configurationNeeded
クラスに不足しているスタイルを追加します:.toDo { /* ... */ .configurationNeeded { margin: 0 auto; width: 100%; text-align: center; } /* ... */ }
プロパティ変更イベントに登録する
コード エディターで、./src/webparts/toDo/app/HomeController.ts ファイルを開きます。
HomeController
クラスに、次に示すプロパティを追加します:export default class HomeController { // ... private sharePointApi: string = undefined; private todoListName: string = undefined; private hideFinishedTasks: boolean = false; private configurationNeeded: boolean = true; // ... }
HomeController クラスのコンストラクターにルート スコープ サービスを挿入することで拡張し、内容を次のように変更します:
export default class HomeController { // ... public static $inject: string[] = ['DataService', '$window', '$rootScope']; constructor(private dataService: IDataService, private $window: angular.IWindowService, $rootScope: angular.IRootScopeService) { const vm: HomeController = this; this.init(undefined, undefined); $rootScope.$on('configurationChanged', (event: angular.IAngularEvent, args: { sharePointApi: string; todoListName: string; hideFinishedTasks: boolean; }): void => { vm.init(args.sharePointApi, args.todoListName, args.hideFinishedTasks); }); } // ... }
HomeController クラスに、
init()
メソッドを追加します。export default class HomeController { // ... private init(sharePointApi: string, todoListName: string, hideFinishedTasks?: boolean): void { if (sharePointApi !== undefined && sharePointApi.length > 0 && todoListName !== undefined && todoListName.length > 0) { this.sharePointApi = sharePointApi; this.todoListName = todoListName; this.hideFinishedTasks = hideFinishedTasks; this.loadTodos(); this.configurationNeeded = false; } else { this.configurationNeeded = true; } } // ... }
HomeController
クラス内の残りのすべてのメソッドがクラス プロパティからの構成値を使用するよう更新します:export default class HomeController { // ... private loadTodos(): void { this.isLoading = true; this.dataService.getTodos(this.sharePointApi, this.todoListName, this.hideFinishedTasks) .then((todos: ITodo[]): void => { this.todoCollection = todos; }) .finally((): void => { this.isLoading = false; }); } public todoKeyDown($event: KeyboardEvent): void { if ($event.keyCode === 13 && this.newItem.length > 0) { $event.preventDefault(); this.todoCollection.unshift({ id: -1, title: this.newItem, done: false }); this.dataService.addTodo(this.newItem, this.sharePointApi, this.todoListName) .then((): void => { this.newItem = null; this.dataService.getTodos(this.sharePointApi, this.todoListName, this.hideFinishedTasks) .then((todos: ITodo[]): void => { this.todoCollection = todos; }); }); } } public deleteTodo(todo: ITodo): void { if (this.$window.confirm('Are you sure you want to delete this todo item?')) { let index: number = -1; for (let i: number = 0; i < this.todoCollection.length; i++) { if (this.todoCollection[i].id === todo.id) { index = i; break; } } if (index > -1) { this.todoCollection.splice(index, 1); } this.dataService.deleteTodo(todo, this.sharePointApi, this.todoListName) .then((): void => { this.dataService.getTodos(this.sharePointApi, this.todoListName, this.hideFinishedTasks) .then((todos: ITodo[]): void => { this.todoCollection = todos; }); }); } } public completeTodo(todo: ITodo): void { todo.done = true; this.dataService.setTodoStatus(todo, true, this.sharePointApi, this.todoListName) .then((): void => { this.dataService.getTodos(this.sharePointApi, this.todoListName, this.hideFinishedTasks) .then((todos: ITodo[]): void => { this.todoCollection = todos; }); }); } public undoTodo(todo: ITodo): void { todo.done = false; this.dataService.setTodoStatus(todo, false, this.sharePointApi, this.todoListName) .then((): void => { this.dataService.getTodos(this.sharePointApi, this.todoListName, this.hideFinishedTasks) .then((todos: ITodo[]): void => { this.todoCollection = todos; }); }); } }
コマンド ラインで以下を実行して、Web パーツが正常に動作していることを確認します。
gulp serve --nobrowser
Web ブラウザーで SharePoint ワークベンチに移動し、Web パーツをキャンバスに追加します。 [完了したタスクを非表示にする] オプションを切り替えて、完了したタスクを表示または非表示にできます。