Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Nota:
Esta página se ha retirado ya que AngularJS v1 es muy antiguo y ya no es compatible con los mantenedores de Angular.
Si está familiarizado con AngularJS, también puede usar este popular marco para compilar elementos web del lado cliente. Gracias a su diseño modular, se puede usar para todo, desde aplicaciones complejas de una página de vista múltiple hasta componentes más pequeños, como elementos web. Muchas organizaciones han usado AngularJS para crear soluciones de SharePoint en el pasado. En este artículo, se muestra cómo usar AngularJS para compilar un elemento web del lado cliente de SharePoint Framework y aplicarle un estilo mediante Office UI Fabric.
Durante este tutorial, compilará un elemento web simple que administra tareas pendientes.
El origen del elemento web de trabajo está disponible en GitHub en samples/angular-todo/.
Nota:
Antes de seguir los pasos de este artículo, asegúrese de configurar el entorno de desarrollo para compilar soluciones de SharePoint Framework.
Crear un proyecto
Cree una carpeta para el proyecto
md angular-todo
Vaya a la carpeta del proyecto:
cd angular-todo
En la carpeta del proyecto, ejecute el generador de Yeoman de SharePoint Framework para aplicar scaffolding a un nuevo proyecto de SharePoint Framework:
yo @microsoft/sharepoint
Cuando se le pida, defina los valores de la manera siguiente:
- angular-todo como el nombre de la solución
- Usar la carpeta actual como la ubicación para colocar los archivos.
- Ningún marco de JavaScript como punto de partida para compilar el elemento web
- Tarea pendiente como el nombre del elemento web
- Administración simple de tareas pendientes como la descripción del elemento web
- Una vez finalizada la técnica de scaffolding, ejecute el comando siguiente para bloquear la versión de las dependencias del proyecto:
npm shrinkwrap
Abra la carpeta del proyecto en el editor de código. En este tutorial, usará Visual Studio Code.
Agregar AngularJS
En este tutorial, cargará AngularJS desde una red CDN.
En el editor de código, abra el archivo config/config.json y, en la propiedad externals
, agregue las líneas siguientes:
"angular": {
"path": "https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.6/angular.min.js",
"globalName": "angular"
}
Agregar declaraciones de tipo de AngularJS para TypeScript
Ya que hace referencia a AngularJS en el código del elemento web, también necesita declaraciones de tipos de AngularJS para TypeScript.
Para instalarlos, ejecute lo siguiente en la línea de comandos:
npm install @types/angular --save-dev
Implementar la aplicación de AngularJS
Una vez que todos los requisitos previos están preparados, puede comenzar a implementar la aplicación de ejemplo de AngularJS. Dado que consta de varios archivos, cree una carpeta independiente para ellos denominada app.
Implementar el servicio de datos de tareas pendientes
En la carpeta app que acaba de crear, cree un archivo denominado DataService.ts. Pegue el código siguiente en el archivo:
export interface ITodo {
id: number;
title: string;
done: boolean;
}
export interface IDataService {
getTodos(hideFinishedTasks: boolean): 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'];
private items: ITodo[] = [
{
id: 1,
title: 'Prepare demo Web Part',
done: true
},
{
id: 2,
title: 'Show demo',
done: false
},
{
id: 3,
title: 'Share code',
done: false
}
];
private nextId: number = 4;
constructor(private $q: angular.IQService) { }
public getTodos(hideFinishedTasks: boolean): angular.IPromise<ITodo[]> {
const deferred: angular.IDeferred<ITodo[]> = this.$q.defer();
const todos: ITodo[] = [];
for (let i: number = 0; i < this.items.length; i++) {
if (hideFinishedTasks && this.items[i].done) {
continue;
}
todos.push(this.items[i]);
}
deferred.resolve(todos);
return deferred.promise;
}
public addTodo(todo: string): angular.IPromise<{}> {
const deferred: angular.IDeferred<{}> = this.$q.defer();
this.items.push({
id: this.nextId++,
title: todo,
done: false
});
deferred.resolve();
return deferred.promise;
}
public deleteTodo(todo: ITodo): angular.IPromise<{}> {
const deferred: angular.IDeferred<{}> = this.$q.defer();
let pos: number = -1;
for (let i: number = 0; i < this.items.length; i++) {
if (this.items[i].id === todo.id) {
pos = i;
break;
}
}
if (pos > -1) {
this.items.splice(pos, 1);
deferred.resolve();
}
else {
deferred.reject();
}
return deferred.promise;
}
public setTodoStatus(todo: ITodo, done: boolean): angular.IPromise<{}> {
const deferred: angular.IDeferred<{}> = this.$q.defer();
for (let i: number = 0; i < this.items.length; i++) {
if (this.items[i].id === todo.id) {
this.items[i].done = done;
}
}
deferred.resolve();
return deferred.promise;
}
}
En el fragmento de código anterior, se implementan tres tipos:
- La interfaz ITodo, que representa una tarea pendiente en la aplicación.
- La interfaz IDataService, que define la firma del servicio de datos.
- La clase DataService, que es la responsable de recuperar y manipular las tareas pendientes.
El servicio de datos implementa métodos sencillos para agregar y modificar las tareas pendientes. Aunque las operaciones son instantáneas, por coherencia, cada función CRUD devuelve una promesa.
En este tutorial, las tareas pendientes se almacenan en la memoria. En cambio, podría ampliar fácilmente la solución para almacenar tareas en una lista de SharePoint y usar el servicio de datos para comunicarse con SharePoint mediante la API de REST.
Implementar el controlador
Después, implemente el controlador que realiza la comunicación entre la vista y el servicio de datos.
En la carpeta app, cree un archivo denominado HomeController.ts y pegue el código siguiente:
import { IDataService, ITodo } from './DataService';
export default class HomeController {
public isLoading: boolean = false;
public newItem: string = null;
public newToDoActive: boolean = false;
public todoCollection: any[] = [];
private hideFinishedTasks: boolean = false;
public static $inject: string[] = ['DataService', '$window', '$rootScope'];
constructor(private dataService: IDataService, private $window: angular.IWindowService, private $rootScope: angular.IRootScopeService) {
const vm: HomeController = this;
this.init();
}
private init(hideFinishedTasks?: boolean): void {
this.hideFinishedTasks = hideFinishedTasks;
this.loadTodos();
}
private loadTodos(): void {
const vm: HomeController = this;
this.isLoading = true;
this.dataService.getTodos(vm.hideFinishedTasks)
.then((todos: ITodo[]): void => {
vm.todoCollection = todos;
})
.finally((): void => {
vm.isLoading = false;
});
}
public todoKeyDown($event: any): void {
if ($event.keyCode === 13 && this.newItem.length > 0) {
$event.preventDefault();
this.todoCollection.unshift({ id: -1, title: this.newItem, done: false });
const vm: HomeController = this;
this.dataService.addTodo(this.newItem)
.then((): void => {
this.newItem = null;
this.dataService.getTodos(vm.hideFinishedTasks)
.then((todos: any[]): 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);
}
const vm: HomeController = this;
this.dataService.deleteTodo(todo)
.then((): void => {
this.dataService.getTodos(vm.hideFinishedTasks)
.then((todos: any[]): void => {
this.todoCollection = todos;
});
});
}
}
public completeTodo(todo: ITodo): void {
todo.done = true;
const vm: HomeController = this;
this.dataService.setTodoStatus(todo, true)
.then((): void => {
this.dataService.getTodos(vm.hideFinishedTasks)
.then((todos: any[]): void => {
this.todoCollection = todos;
});
});
}
public undoTodo(todo: ITodo): void {
todo.done = false;
const vm: HomeController = this;
this.dataService.setTodoStatus(todo, false)
.then((): void => {
this.dataService.getTodos(vm.hideFinishedTasks)
.then((todos: any[]): void => {
this.todoCollection = todos;
});
});
}
}
Primero debe cargar el servicio de datos que ha implementado antes. El controlador lo necesita para obtener la lista de tareas y modificarlas tal y como ha pedido el usuario. El servicio se inserta en el controlador mediante la inserción de dependencias de AngularJS. El controlador implementa una serie de funciones que se exponen en el modelo de vista y a las que se llama desde la plantilla. Con estas funciones, los usuarios pueden agregar nuevas tareas, marcarlas como finalizadas o como pendientes, o eliminarlas.
Implementar el módulo principal
Con el servicio de datos y el controlador listos, defina el módulo principal de la aplicación y registre el servicio de datos y el controlador con él.
En la carpeta app, cree un archivo denominado app-module.ts y pegue el contenido siguiente:
import * as angular from 'angular';
import HomeController from './HomeController';
import DataService from './DataService';
const todoapp: angular.IModule = angular.module('todoapp', []);
todoapp
.controller('HomeController', HomeController)
.service('DataService', DataService);
Empiece por hacer referencia a AngularJS y cargar el controlador y el servicio de datos previamente implementados. Ahora, defina el módulo de la aplicación. Por último, registre el controlador y el servicio de datos con la aplicación.
Acaba de compilar una aplicación de AngularJS para el elemento web. En los pasos siguientes, registrará la aplicación de AngularJS con el elemento web y la podrá configurar mediante propiedades de elementos web.
Registrar una aplicación de AngularJS con un elemento web
El siguiente paso consiste en agregar la aplicación de AngularJS al elemento web.
En el editor de código, abra el archivo ToDoWebPart.ts.
Justo antes de la declaración de clase, agregue las líneas siguientes:
import * as angular from 'angular'; import './app/app-module';
Esto nos permite cargar una referencia a AngularJS y a la aplicación, las necesita para arrancar la aplicación de AngularJS.
Cambie la función render() del elemento web por:
public render(): void { if (this.renderedOnce === false) { this.domElement.innerHTML = ` <div class="${styles.toDo}"> <div data-ng-controller="HomeController as vm"> <div class="${styles.loading}" ng-show="vm.isLoading"> <div class="${styles.spinner}"> <div class="${styles.spinnerCircle} ${styles.spinnerLarge}"></div> <div class="${styles.spinnerLabel}">Loading...</div> </div> </div> <div ng-show="vm.isLoading === false"> <div class="${styles.textField} ${styles.underlined}" ng-class="{'${styles.isActive}': vm.newToDoActive}"> <label for="newToDo" class="${styles.label}">New to do:</label> <input type="text" label="New to do:" id="newToDo" value="" class="${styles.field}" aria-invalid="false" ng-model="vm.newItem" ng-keydown="vm.todoKeyDown($event)" ng-focus="vm.newToDoActive = true" ng-blur="vm.newToDoActive = false"> </div> </div> <div class="list" ng-show="vm.isLoading === false"> <div class="listSurface"> <div class="listPage"> <div class="listCell" ng-repeat="todo in vm.todoCollection" uif-item="todo" ng-class="{'${styles.done}': todo.done}"> <div class="${styles.listItem}"> <span class="${styles.listItemPrimaryText}">{{todo.title}}</span> <div class="${styles.listItemActions}"> <div class="${styles.listItemAction}" ng-click="vm.completeTodo(todo)" ng-show="todo.done === false"> <i class="${styles.icon} ${styles.iconCheckMark}"></i> </div> <div class="${styles.listItemAction}" ng-click="vm.undoTodo(todo)" ng-show="todo.done"> <i class="${styles.icon} ${styles.iconUndo}"></i> </div> <div class="${styles.listItemAction}" ng-click="vm.deleteTodo(todo)"> <i class="${styles.icon} ${styles.iconRecycleBin}"></i> </div> </div> </div> </div> </div> </div> </div> </div> </div>`; angular.bootstrap(this.domElement, ['todoapp']); } }
En primer lugar, el código asigna la plantilla de la aplicación directamente al elemento DOM del elemento web. En el elemento raíz, especifique el nombre del controlador que controla los eventos y el enlace de datos en la plantilla.
Arranque la aplicación con el nombre todoapp que ha usado anteriormente al declarar el módulo principal.
Con la propiedad
renderedOnce
del elemento web, asegúrese de que la aplicación de AngularJS arranque solo una vez. Sin ella, si ha cambiado alguna de las propiedades del elemento web, la funciónrender()
se vuelve a invocar, lo que provoca un nuevo arranque de la aplicación de AngularJS, que da lugar a un error.Implemente los estilos CSS que use en la plantilla. En el editor de código, abra el archivo ToDo.module.scss y reemplace su contenido por lo siguiente:
.toDo { .loading { margin: 0 auto; width: 6em; .spinner { display: inline-block; margin: 10px 0; @-webkit-keyframes spinnerSpin { 0% { -webkit-transform:rotate(0); transform:rotate(0); } 100% { -webkit-transform:rotate(360deg); transform:rotate(360deg); } } @keyframes spinnerSpin { 0% { -webkit-transform:rotate(0); transform:rotate(0); } 100% { -webkit-transform:rotate(360deg); transform:rotate(360deg); } } .spinnerCircle { margin: auto; box-sizing: border-box; border-radius: 50%; width: 100%; height: 100%; border: 1.5px solid #c7e0f4; border-top-color: #0078d7; -webkit-animation: spinnerSpin 1.3s infinite cubic-bezier(.53, .21, .29, .67); animation: spinnerSpin 1.3s infinite cubic-bezier(.53, .21, .29, .67); &.spinnerLarge { width: 28px; height: 28px; } } .spinnerLabel { color: #0078d7; margin-top: 10px; text-align: center; } } } .label { 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; color: #333333; box-sizing: border-box; display: block; padding: 5px 0 } .textField { font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; box-sizing: border-box; margin: 0; padding: 0; box-shadow: none; color: #333333; font-size: 14px; font-weight: 400; margin-bottom: 8px; &.underlined { border-bottom: 1px solid #c8c8c8; display: table; width: 100%; &:hover { border-color: #767676; } &.isActive, &:active { border-color: #0078d7; } .field { border: 0; display: table-cell; padding-top: 8px; padding-bottom: 3px } } .label { padding-right: 0; padding-left: 12px; margin-right: 8px; font-size: 14px; display: table-cell; vertical-align: top; padding-top: 9px; height: 32px; width: 1%; white-space: nowrap; font-weight: 400; } .field { text-align: left; float: left; box-sizing: border-box; margin: 0; padding: 0; box-shadow: none; font-family: "Segoe UI WestEuropean", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; border: 1px solid #c8c8c8; border-radius: 0; font-weight: 400; font-size: 14px; color: #333333; height: 32px; padding: 0 12px 0 12px; width: 100%; outline: 0; text-overflow: ellipsis; } .field:hover { border-color: #767676; } } .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; &::before { display: table; content: ""; line-height: 0; } &::after { display: table; content: ""; line-height: 0; clear: both; } .listItemPrimaryText { 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; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; } .listItemActions { max-width: 80px; position: absolute; right: 30px; text-align: right; top: 10px; .listItemAction { color: #a6a6a6; display: inline-block; font-size: 15px; position: relative; text-align: center; top: 3px; cursor: pointer; height: 16px; width: 16px; &:hover { color: #666666; outline: 1px solid transparent; } .icon { vertical-align: top; } } } } .done { text-decoration: line-through; } .icon { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; display: inline-block; font-family: FabricMDL2Icons; font-style: normal; font-weight: 400; speak: none; &.iconCheckMark::before { content: "\E73E"; } &.iconUndo::before { content: "\E7A7"; } &.iconRecycleBin::before { content: "\EF87"; } } }
Ejecute el comando siguiente para confirmar que todo funciona como esperaba:
gulp serve
En el explorador, debería ver el elemento web Tareas pendientes con tareas pendientes.
Permitir la configuración del elemento web
En este momento, el elemento web Tareas pendientes muestra una lista fija de tareas pendientes. Después, la extiende con una opción de configuración para permitir que los usuarios elijan si quieren ver las tareas que están marcadas como realizadas o no.
Agregar una propiedad en el manifiesto del elemento web
Comience por agregar una propiedad de configuración en el manifiesto del elemento web.
En el editor de código, abra el archivo ToDoWebPart.manifest.json. En la sección preconfiguredEntries
, vaya a la matriz properties
y reemplace la propiedad description
existente por la siguiente línea:
"hideFinishedTasks": false
Actualizar la firma de la interfaz de propiedades del elemento web
Después, actualice la firma de la interfaz de propiedades del elemento web.
En el editor de código, abra el archivo ToDoWebPart.ts y actualice la interfaz IToDoWebPartProps
a lo siguiente:
export interface IToDoWebPartProps {
hideFinishedTasks: boolean;
}
Agregar la propiedad al panel de propiedades del elemento web
Para permitir que los usuarios configuren el valor de la propiedad que acaba de agregar, tiene que exponerla mediante el panel de propiedades del elemento web.
En el editor de código, abra el archivo ToDoWebPart.ts.
En la primera
import
instrucción, reemplace porPropertyPaneToggle
PropertyPaneTextField
:import { BaseClientSideWebPart, IPropertyPaneConfiguration, PropertyPaneToggle } from '@microsoft/sp-webpart-base';
Cambie la implementación de la función
propertyPaneSettings
por:protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration { return { pages: [ { header: { description: strings.PropertyPaneDescription }, groups: [ { groupName: strings.ViewGroupName, groupFields: [ PropertyPaneToggle('hideFinishedTasks', { label: strings.HideFinishedTasksFieldLabel }) ] } ] } ] }; }
Para corregir las referencias de cadena que faltan, primero tiene que cambiar la firma de las cadenas. En el editor de código, abra el archivo loc/mystrings.d.ts y cambie su contenido por:
declare interface IToDoWebPartStrings { PropertyPaneDescription: string; ViewGroupName: string; HideFinishedTasksFieldLabel: string; } declare module 'ToDoWebPartStrings' { const strings: IToDoWebPartStrings; export = strings; }
Proporcione los valores reales de las cadenas que acaba de definir. En el editor de código, abra el archivo loc/en-us.js y cambie su contenido por:
define([], function() { return { "PropertyPaneDescription": "Manage configuration of the To do web part", "ViewGroupName": "View", "HideFinishedTasksFieldLabel": "Hide finished tasks" } });
Ejecute el comando siguiente para confirmar que todo funciona como esperaba:
gulp serve
En el panel de propiedades del elemento web debería ver un botón de alternancia para la propiedad que acaba de definir.
Si en este momento selecciona el botón de alternancia, no pasará nada en el elemento web, ya que no está conectado con AngularJS. Lo podrá hacer en el paso siguiente.
Permitir la configuración de la aplicación de AngularJS mediante las propiedades del elemento web
En el paso anterior, ha definido una propiedad del elemento web que permite a los usuarios elegir si quieren mostrar las tareas completadas o no. Ahora, pasará el valor seleccionado por el usuario a la aplicación de AngularJS, para que pueda cargar elementos.
Difundir un evento de AngularJS en el cambio de propiedad del elemento web
En el editor de código, abra el archivo ToDoWebPart.ts. Justo encima del método de representación
render()
, agregue la línea siguiente:private $injector: angular.auto.IInjectorService;
Cambie la función
render()
del elemento web por lo siguiente:public render(): void { if (this.renderedOnce === false) { this.domElement.innerHTML = ` <div class="${styles.toDo}"> <div data-ng-controller="HomeController as vm"> <div class="${styles.loading}" ng-show="vm.isLoading"> <div class="${styles.spinner}"> <div class="${styles.spinnerCircle} ${styles.spinnerLarge}"></div> <div class="${styles.spinnerLabel}">Loading...</div> </div> </div> <div ng-show="vm.isLoading === false"> <div class="${styles.textField} ${styles.underlined}" ng-class="{'${styles.isActive}': vm.newToDoActive}"> <label for="newToDo" class="${styles.label}">New to do:</label> <input type="text" label="New to do:" id="newToDo" value="" class="${styles.field}" aria-invalid="false" ng-model="vm.newItem" ng-keydown="vm.todoKeyDown($event)" ng-focus="vm.newToDoActive = true" ng-blur="vm.newToDoActive = false"> </div> </div> <div class="list" ng-show="vm.isLoading === false"> <div class="listSurface"> <div class="listPage"> <div class="listCell" ng-repeat="todo in vm.todoCollection" uif-item="todo" ng-class="{'${styles.done}': todo.done}"> <div class="${styles.listItem}"> <span class="${styles.listItemPrimaryText}">{{todo.title}}</span> <div class="${styles.listItemActions}"> <div class="${styles.listItemAction}" ng-click="vm.completeTodo(todo)" ng-show="todo.done === false"> <i class="${styles.icon} ${styles.iconCheckMark}"></i> </div> <div class="${styles.listItemAction}" ng-click="vm.undoTodo(todo)" ng-show="todo.done"> <i class="${styles.icon} ${styles.iconUndo}"></i> </div> <div class="${styles.listItemAction}" ng-click="vm.deleteTodo(todo)"> <i class="${styles.icon} ${styles.iconRecycleBin}"></i> </div> </div> </div> </div> </div> </div> </div> </div> </div>`; this.$injector = angular.bootstrap(this.domElement, ['todoapp']); } this.$injector.get('$rootScope').$broadcast('configurationChanged', { hideFinishedTasks: this.properties.hideFinishedTasks }); }
En el ejemplo de código anterior, cada vez que se cambia la propiedad del elemento web, difunde un evento de AngularJS al que se suscribe la aplicación de AngularJS. Una vez que la aplicación de AngularJS recibe el evento, lo controla correctamente.
Suscribirse al evento de cambio de propiedad del elemento web en AngularJS
En el editor de código, abra el archivo app/HomeController.ts. Extienda el constructor de la siguiente forma:
constructor(private dataService: IDataService, private $window: angular.IWindowService, private $rootScope: angular.IRootScopeService) { const vm: HomeController = this; this.init(); $rootScope.$on('configurationChanged', (event: angular.IAngularEvent, args: { hideFinishedTasks: boolean }): void => { vm.init(args.hideFinishedTasks); }); }
Compruebe que la aplicación de AngularJS funciona como se esperaba y responde correctamente a la propiedad cambiada; para ello, ejecute lo siguiente en la línea de comandos:
gulp serve
Si alterna el valor de la propiedad Ocultar las tareas finalizadas, el elemento web debería mostrar u ocultar las tareas finalizadas correctamente.