Migrer des applications AngularJS vers SharePoint Framework
De nombreuses organisations utilisent AngularJS v1.x pour créer des solutions SharePoint depuis des années. Cet article explique comment migrer une application AngularJS existante à laquelle des styles sont appliqués avec ngOfficeUIFabric qui regroupe les directives AngularJS pour la structure d’IU d’Office, vers un composant WebPart côté client de SharePoint Framework. L’exemple d’application utilisé pour ce didacticiel gère des tâches à faire, stockées dans une liste SharePoint.
La source de l’application AngularJS est disponible sur GitHub à l’adresse angular-migration/angular-todo.
La source de l’application AngularJS migrée vers SharePoint Framework est disponible sur GitHub à l’adresse samples/angular-todo.
Notes
Cet article fait référence à des projets anciens, obsolètes et non maintenus tels que AngularJS v1.x et ngOfficeUIFabric.
Configuration d’un projet
Avant de commencer la migration de votre application AngularJS, créez et configurez un nouveau projet SharePoint Framework pour héberger l’application AngularJS.
Création d’un projet
Créez un dossier pour votre projet :
md angular-todoAccédez au dossier du projet :
cd angular-todoDans le dossier du projet, exécutez le générateur Yeoman pour SharePoint Framework afin de structurer un projet SharePoint Framework :
yo @microsoft/sharepointLorsque vous y êtes invité, entrez les valeurs suivantes (sélectionnez l’option par défaut pour toutes les invites omises ci-dessous) :
- Quel est le nom de votre solution ? : angular-todo
- Quels packages de base voulez-vous cibler pour votre ou vos composants ? : SharePoint Online uniquement (dernière version)
- Quel type de composant côté client voulez-vous créer ? : composant WebPart
- Quel est le nom de votre composant WebPart ? : To do
- Quelle est la description de votre composant WebPart ? : gestion simple des tâches à faire
- Quelle infrastructure voulez-vous utiliser ? : aucune infrastructure JavaScript
Ouvrez le dossier de votre projet dans votre éditeur de code. Dans ce didacticiel, vous allez utiliser Visual Studio Code.
Ajout d’AngularJS et de ngOfficeUIFabric
Dans ce didacticiel, vous chargez AngularJS et ngOfficeUIFabric à partir du CDN.
Dans l’éditeur de code, ouvrez le fichier config/config.json et, dans la propriété externals, ajoutez les lignes suivantes :
"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"
Ajout de typages AngularJS pour TypeScript
Étant donné que vous référencez AngularJS dans le code de votre composant WebPart, vous avez également besoin des typages AngularJS pour TypeScript. Pour les installer, exécutez le code suivant dans la ligne de commande :
npm install @types/angular --save-dev
Migration de l’application AngularJS telle quelle
Commencez par migrer l’application AngularJS uniquement avec les modifications de code minimales. Plus tard, vous mettrez à niveau le code JavaScript simple de l’application vers TypeScript et améliorerez son intégration avec le composant WebPart côté client.
Création d’une liste SharePoint
Sur votre site Microsoft Office SharePoint Online, créez une liste appelée Liste de tâches. Ajoutez-y une nouvelle colonne de choix appelée Statut. Entrez les deux choix suivants :
Not started
In progress
Completed

Copie des fichiers d’application AngularJS dans le projet de composant WebPart
Dans le projet de composant WebPart, dans le dossier src/webparts/toDo, créez un dossier nommé app.

Dans l’application source, copiez le contenu du dossier app vers le nouveau dossier app créé dans le projet de composant WebPart.

Chargement de l’application AngularJS dans le composant WebPart côté client
Dans l’éditeur de code, ouvrez le fichier ./src/webparts/toDo/ToDoWebPart.ts. Après la dernière instruction
import, ajoutez le code suivant :import * as angular from 'angular'; import 'ng-office-ui-fabric';Remplacez le contenu de la méthode
render()par :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']); } } // ... }
Mise à jour des chemins d’accès aux sites
Dans l’éditeur de code, ouvrez le fichier ./src/webparts/toDo/app/app.config.js. Remplacez la valeur de la constante sharepointApi par l’URL relative au serveur du site SharePoint où vous avez créé la liste de tâches, suivi de /_api/.
Ajout de styles CSS
Vous devez également implémenter les styles CSS que vous utilisez dans le modèle. Dans l’éditeur de code, ouvrez le fichier ToDoWebPart.module.scss et remplacez son contenu par :
.toDo {
.loading {
margin: 0 auto;
width: 6em;
}
}
Aperçu du composant WebPart dans le Workbench hébergé
Dans la ligne de commande, exécutez :
gulp serve --nobrowserÀ l’URL de votre site SharePoint, ajoutez /_layouts/Workbench.aspx, par exemple, https://contoso.sharepoint.com/_layouts/workbench.aspx et accédez-y dans le navigateur web.
Si vous avez suivi les étapes correctement, le composant WebPart doit normalement afficher le formulaire permettant d’ajouter des tâches.

Ajoutez quelques tâches pour vérifier que le composant WebPart fonctionne comme prévu.

Correction du style du composant WebPart
Bien que le composant WebPart fonctionne correctement, il n’a pas le même aspect que l’application AngularJS avec laquelle vous avez commencé. Cela est dû au fait que la version de la structure d’IU Office utilisée par ngOfficeUIFabric est plus ancienne que celle qui est disponible dans SharePoint Workbench. Le mode de résolution le plus simple serait de charger les styles CSS utilisés par ngOfficeUIFabric. Le problème est que ces styles entreraient en conflit avec ceux de la structure d’IU Office utilisés par SharePoint Workbench, ce qui en diviserait l’interface utilisateur. Une meilleure solution consiste à ajouter aux styles de composant WebPart les styles requis par chaque composant spécifique.
Dans l’éditeur de code, ouvrez le fichier ./src/webparts/toDo/ToDoWebPart.module.scss. Remplacez son contenu par :
.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} } }Dans le fichier ./src/webparts/toDo/ToDoWebPart.ts, dans la méthode
render(), modifiez le modèle de rendu de l’application de manière à pouvoir utiliser de nouvelles icônes de la structure d’IU Office.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']); } } // ... }
Si vous actualisez le composant WebPart dans le navigateur web, vous voyez que les bons styles sont maintenant appliqués.

Mise à niveau de l’application AngularJS vers TypeScript
L’application AngularJS initiale est écrite en JavaScript simple, ce qui peut occasionner des erreurs lors des tâches de maintenance. Lorsque vous créez des composants WebPart côté client pour SharePoint Framework, vous pouvez utiliser TypeScript et bénéficier de ses fonctionnalités de sécurité de type design-time (« au moment de la conception »). Dans la section suivante, vous allez migrer le code AngularJS du format JavaScript simple vers TypeScript.
Mise à niveau de la configuration de l’application
Dans votre projet, remplacez le nom du fichier ./src/webparts/toDo/app/app.config.js par app.config.ts. Remplacez son contenu par :
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);
}
Mise à niveau du service de données
Dans votre projet, remplacez le nom du fichier ./src/webparts/toDo/app/data.service.js par DataService.ts. Remplacez son contenu par :
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;
}
}
Mise à niveau du contrôleur de la page d’accueil
Dans votre projet, remplacez le nom du fichier ./src/webparts/toDo/app/home.controller.js par HomeController.ts. Remplacez son contenu par :
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;
});
});
}
}
Mise à niveau du module d’application
Dans votre projet, remplacez le nom du fichier ./src/webparts/toDo/app/app.module.js par app.module.ts. Remplacez son contenu par :
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);
Mise à jour de la référence à l’application AngularJS dans le composant WebPart
Maintenant que l’application AngularJS a été générée à l’aide de TypeScript et que ses différentes parties font référence les unes aux autres, il n’est plus nécessaire que le composant WebPart fasse référence à tous les éléments de l’application. Il doit uniquement charger le module principal, ce qui charge ensuite tous les autres éléments qui composent l’application AngularJS.
Dans l’éditeur de code, ouvrez le fichier ./src/webparts/toDo/ToDoWebPart.ts. Mettre à jour la méthode
render()par :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']); } } // ... }Pour vérifier que la mise à niveau vers TypeScript a réussi, exécutez la commande suivante dans la ligne de commande :
gulp serve --nobrowserDans le navigateur web, actualisez l’instance SharePoint Workbench qui doit afficher votre composant WebPart, comme précédemment.

Bien que le mode de fonctionnement du composant WebPart n’ait pas changé, le code est amélioré. dans le cadre d’une nouvelle mise à jour, vous pouvez plus facilement vérifier l’exactitude et l’intégrité de votre code dès la phase de développement.
Amélioration de l’intégration de l’application AngularJS avec SharePoint Framework
À ce stade, l’application AngularJS fonctionne correctement et est intégrée dans un composant WebPart côté client de SharePoint Framework. Bien que les utilisateurs puissent ajouter le composant WebPart à la page, ils ne peuvent pas configurer son fonctionnement. Toute la configuration est incorporée dans le code de l’application AngularJS. Dans cette section, vous allez étendre le composant WebPart afin de permettre aux utilisateurs de configurer le nom de la liste de tâches et d’indiquer si le composant WebPart doit afficher ou non les tâches terminées.
Définition des propriétés du composant WebPart
Dans l’éditeur de code, ouvrez le fichier ./src/webparts/toDo/ToDoWebPart.manifest.json. Remplacez la propriété
propertiespar :"properties": { "todoListName": "Todo", "hideFinishedTasks": false }Dans le fichier ./src/webparts/toDo/ToDoWebPart.ts, remplacez la définition de l’interface
IToDoWebPartPropspar :export interface IToDoWebPartProps { todoListName: string; hideFinishedTasks: boolean; }Dans le fichier ./src/webparts/toDo/ToDoWebPart.ts, remplacez la première instruction d’importation de la façon suivante :
import { BaseClientSideWebPart, IPropertyPaneSettings, PropertyPaneTextField, PropertyPaneToggle } from '@microsoft/sp-webpart-base';Mettre à jour la méthode
getPropertyPaneConfiguration()par :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 }) ] } ] } ] }; } // ... }Ajoutez les chaînes de ressources manquantes en remplaçant le contenu du fichier ./src/webparts/toDo/loc/mystrings.d.ts par :
declare interface IToDoWebPartStrings { PropertyPaneDescription: string; BasicGroupName: string; ListNameFieldLabel: string; HideFinishedTasksFieldLabel: string; } declare module 'ToDoWebPartStrings' { const strings: IToDoWebPartStrings; export = strings; }Dans le fichier ./src/webparts/toDo/loc/en-us.js, ajoutez des traductions pour les chaînes que vous venez d’ajouter :
define([], function() { return { "PropertyPaneDescription": "Description", "BasicGroupName": "Group Name", "ListNameFieldLabel": "List name", "HideFinishedTasksFieldLabel": "Hide finished tasks" } });
Transmission des valeurs de propriétés du composant WebPart à l’application AngularJS
À ce stade, les utilisateurs peuvent configurer le mode de fonctionnement du composant WebPart, mais l’application AngularJS n’utilise pas ces valeurs. Dans les sections suivantes, vous allez étendre l’application AngularJS de manière à utiliser les valeurs de configuration fournies par les utilisateurs dans le volet de propriétés du composant WebPart. Pour cela, vous pouvez diffuser un événement AngularJS dans la méthode render() et créer un abonnement à cet événement dans le contrôleur utilisé par le composant WebPart.
Suppression du fichier de configuration AngularJS
Dans votre projet, supprimez le fichier ./src/webparts/toDo/app/app.config.ts. Dans les étapes suivantes vous allez mettre à jour l’application de manière à obtenir les valeurs de configuration à partir des propriétés du composant WebPart.
Suppression de la référence à la configuration
Dans le fichier ./src/webparts/toDo/app/app.module.ts, supprimez la référence à la configuration AngularJS en remplaçant son contenu par :
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);
Mise à jour du service de données afin d’accepter la valeur de configuration dans les paramètres de méthode
Le service de données récupérait initialement sa configuration à partir de l’une des constantes définies dans le fichier app.config.ts. Pour utiliser les valeurs configurées dans les propriétés du composant WebPart à la place, les méthodes concernées doivent accepter ces paramètres.
Dans l’éditeur de code, ouvrez le fichier ./src/webparts/toDo/app/DataService.ts et remplacez son contenu par :
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;
}
}
Diffusion de l’événement de modification des propriétés
Dans le fichier ./src/webparts/toDo/ToDoWebPart.ts, ajoutez une nouvelle propriété nommée
$injectorà la classeToDoWebPart:export default class ToDoWebPart extends BaseClientSideWebPart<IToDoWebPartProps> { private $injector: angular.auto.IInjectorService; // ... }Dans le même fichier, mettez à jour la méthode
render()de la façon suivante :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 }); } // ... }Dans le fichier ./src/webparts/toDo/ToDoWebPart.module.scss, ajoutez les styles manquants pour la classe
.configurationNeeded:.toDo { /* ... */ .configurationNeeded { margin: 0 auto; width: 100%; text-align: center; } /* ... */ }
Abonnement à l’événement de modification des propriétés
Dans l’éditeur de code, ouvrez le fichier ./src/webparts/toDo/app/HomeController.ts. Dans la classe
HomeController, ajoutez les propriétés suivantes :export default class HomeController { // ... private sharePointApi: string = undefined; private todoListName: string = undefined; private hideFinishedTasks: boolean = false; private configurationNeeded: boolean = true; // ... }Étendez le constructeur de la classe HomeController en y injectant le service d’étendue racine, et remplacez son contenu par :
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); }); } // ... }Dans la classe HomeController, ajoutez la méthode
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; } } // ... }Mettez à jour toutes les autres méthodes de la classe
HomeControllerafin d’utiliser les valeurs de configuration des propriétés de la classe :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; }); }); } }Vérifiez que le composant WebPart fonctionne correctement en exécutant la commande suivante dans la ligne de commande :
gulp serve --nobrowserDans votre navigateur web, accédez à SharePoint Workbench et ajoutez le composant WebPart à la zone de dessin. Si vous modifiez l’option Masquer les tâches terminées, les tâches doivent normalement être affichées ou masquées comme demandé.
