次の方法で共有


AngularJS アプリケーションを SharePoint Framework に移行する

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

SharePoint リストに格納されている To Do 項目を管理するための AngularJS アプリケーション

AngularJS アプリケーションのソースは、GitHub で入手可能です (angular-migration/angular-todo)。

SharePoint Framework への移行後の AngularJS アプリケーションのソースは GitHub で入手可能です (samples/angular-todo)。

注:

この記事では、AngularJS v1.x & ngOfficeUIFabric など、古い古い&プロジェクトを参照しています。

プロジェクトをセットアップする

AngularJS アプリケーションの移行を始める前に、AngularJS アプリケーションをホストする SharePoint Framework の新しいプロジェクトを作成し、セットアップしてください。

新しいプロジェクトを作成する

  1. プロジェクト用の新しいフォルダーを作成します。

    md angular-todo
    
  2. プロジェクト フォルダーに移動します。

    cd angular-todo
    
  3. プロジェクト フォルダーで SharePoint Framework Yeoman ジェネレーターを実行して、新しい SharePoint Framework プロジェクトをスキャホールディングします。

    yo @microsoft/sharepoint
    
  4. プロンプトが表示されたら、以下の値を入力します (以下で省略されたすべてのプロンプトに対して既定のオプションを選択します)。

    • ソリューション名は何ですか?: angular-todo
    • どの種類のクライアント側コンポーネントを作成しますか?: Web パーツ
    • Web パーツ名は何ですか?: To do
    • どのテンプレートを使用しますか?: JavaScript なしのフレームワーク
  5. コード エディターでプロジェクト フォルダーを開きます。 このチュートリアルでは、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

Todo SharePoint リスト

AngularJS アプリケーションのファイルを Web パーツ プロジェクトにコピーする

  1. Web パーツ プロジェクトの src/webparts/toDo フォルダーに app という名前の新しいフォルダーを作成します。

    Visual Studio Code Explorer ウィンドウで強調表示された app フォルダー

  2. ソース アプリケーションから app フォルダーの内容を、新しく作成した Web パーツ プロジェクトの app フォルダーにコピーします。

    Visual Studio Code Explorer ウィンドウで強調表示された app のファイル

クライアント側の Web パーツで AngularJS アプリケーションを読み込む

  1. コード エディターで、./src/webparts/toDo/ToDoWebPart.ts ファイルを開きます。 最後の import ステートメントの後に、次のコードを追加します。

    import * as angular from 'angular';
    import 'ng-office-ui-fabric';
    
  2. 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 パーツをプレビューする

  1. コマンド ラインで、次を実行します。

    gulp serve --nobrowser
    
  2. SharePoint サイトの URL に /_layouts/workbench.aspx(例) を追加し、https://contoso.sharepoint.com/_layouts/workbench.aspx、Web ブラウザーでそこに移動します。

    すべての手順を正しく実行すると、ブラウザーに Web パーツが表示され、ToDo 項目を追加するフォームが表示されます。

    SharePoint にアップロードした SharePoint ワークベンチに表示されている移行対象 AngularJS アプリケーション

  3. Web パーツが正常に動作していることを確認するために、いくつかの ToDo 項目を追加します。

    スタイルが正しく設定されていない移行対象 AngularJS アプリケーション

Web パーツのスタイル設定を修正する

Web パーツは正しく動作していますが、最初の AngularJS アプリケーションとは表示が異なっています。 この現象は、ngOfficeUIFabric が使用している Office UI Fabric が、SharePoint ワークベンチで使用可能なものより前のバージョンであることが原因です。 ngOfficeUIFabric で使用される CSS スタイルを読み込めば簡単に修正できるように思えるかもしれません。 ここで問題になるのは、これらのスタイルは SharePoint ワークベンチで使用されている Office UI Fabric のスタイルと競合することがあり、その場合にユーザー インターフェイスが壊れることです。 より望ましい解決方法は、特定のコンポーネントに必要なスタイルを Web パーツのスタイルに追加することです。

  1. コード エディターで、./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}
      }
    }
    
  2. ./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 パーツを更新すると、正しくスタイル設定された状態で表示されます。

Web パーツで、移行対象 AngularJS アプリケーションの完了済み項目が正しくマークされている

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 アプリケーションを構成するその他の要素をすべて読み込むことができます。

  1. コード エディターで、./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']);
        }
      }
      // ...
    }
    
  2. TypeScript へのアップグレードが成功したかどうかを検証するためにコマンド ラインで以下を実行します。

    gulp serve --nobrowser
    
  3. Web ブラウザーで SharePoint ワークベンチを更新すると、以前と同様に Web パーツが表示されるはずです。

    Web パーツで、移行対象 AngularJS アプリケーションの完了済み項目が正しくマークされている

Web パーツの動作方法は変わっていませんが、コードは強化されています。 将来、更新がある場合、発の段階からコードの正確性と整合性を容易に確認できます。

AngularJS アプリケーションと SharePoint Framework の統合を強化する

この時点では、AngularJS アプリケーションは正常に動作し、SharePoint Framework のクライアント側の Web パーツにラップされています。 ユーザーはページに Web パーツを追加できますが、Web パーツの動作方法は構成できません。 すべての構成は、AngularJS アプリケーションのコードの中に埋め込まれています。 このセクションでは、Web パーツを拡張して、ToDo 項目の保存先リストの名前を構成したり、完了したタスクを Web パーツに表示するかどうかを構成したりできるようにします。

Web パーツのプロパティを定義する

  1. コード エディターで、./src/webparts/toDo/ToDoWebPart.manifest.json ファイルを開きます。 properties プロパティを 次のように変更します:

    "properties": {
      "todoListName": "Todo",
      "hideFinishedTasks": false
    }
    
  2. ./src/webparts/toDo/ToDoWebPart.ts ファイルで IToDoWebPartProps インターフェイスの定義を次のように変更します。

    export interface IToDoWebPartProps {
      todoListName: string;
      hideFinishedTasks: boolean;
    }
    
  3. ./src/webparts/toDo/ToDoWebPart.ts ファイルで 1 つ目の import ステートメントを次のように変更します。

    import {
      BaseClientSideWebPart,
      IPropertyPaneSettings,
      PropertyPaneTextField,
      PropertyPaneToggle
    } from '@microsoft/sp-webpart-base';
    
  4. 同じファイルで、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
                    })
                  ]
                }
              ]
            }
          ]
        };
      }
      // ...
    }
    
  5. ./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;
    }
    
  6. ./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;
  }
}

プロパティ変更イベントのブロードキャスト

  1. ./src/webparts/toDo/ToDoWebPart.ts ファイルで ToDoWebPartクラスに $injectorという新しいプロパティを追加します:

    export default class ToDoWebPart extends BaseClientSideWebPart<IToDoWebPartProps> {
      private $injector: angular.auto.IInjectorService;
      // ...
    }
    
  2. 同じファイルで、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
        });
      }
      // ...
    }
    
  3. ./src/webparts/toDo/ToDoWebPart.module.scss ファイルに .configurationNeededクラスに不足しているスタイルを追加します:

    .toDo {
      /* ... */
      .configurationNeeded {
        margin: 0 auto;
        width: 100%;
        text-align: center;
      }
      /* ... */
    }
    

プロパティ変更イベントに登録する

  1. コード エディターで、./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;
      // ...
    }
    
  2. 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);
        });
    }
    
      // ...
    }
    
  3. 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;
        }
      }
      // ...
    }
    
  4. 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;
              });
          });
      }
    }
    
  5. コマンド ラインで以下を実行して、Web パーツが正常に動作していることを確認します。

    gulp serve --nobrowser
    
  6. Web ブラウザーで SharePoint ワークベンチに移動し、Web パーツをキャンバスに追加します。 [完了したタスクを非表示にする] オプションを切り替えて、完了したタスクを表示または非表示にできます。

    Web パーツのプロパティの構成に従い、完了したタスクが非表示の AngularJS アプリケーション