SharePoint e AngularJs
Questo è un esempio di come può essere usato AngularJS 1.0 per costruire una app all'interno di una pagina SharePoint.
L'esempio visualizza all'interno di alcuni box, una serie di link presi da una lista SharePoint:la lista da cui vengono presi i links è questa:Per realizzare questa app serve:
Notare gli attibuti data-ng-* che servono a far funzionare AngularJS.
La parte di codice JavaScript è la seguente:
Possiamo individuare 3 blocchi principali con cui è costruita questa app AngularJS:
Il file completo può essere scaricato da qui sgart-it-links.zip
L'esempio visualizza all'interno di alcuni box, una serie di link presi da una lista SharePoint:la lista da cui vengono presi i links è questa:Per realizzare questa app serve:
- una lista SharePoint chiamata SgartItLinks con i seguenti campi: Title (testo), ImageSrc (testo), OpenNewWindow (yes/no), Link (testo) e Category (choice)
- una document library chiamata HtmlJs un cui mettere il codice JavaScript e html
- la libreria AngularJS 1.0 scaricabile da qui angular.min.js da salvare in HtmlJs
- un file sgart-it-links.html con la app (vedi dopo)
- una content editor web part per che punta al file htmljs/sgart-it-links.html con il codice dalla app AngularJS
- una view con l'html che rappresenta la app
- il codice JavaScript che farà funzionare la app (con la libreria angular.min.js)
- gli stili CSS per renderla più carina
HTML
<div data-ng-app="sgartItLinks" class="sgart-it-links ng-cloak">
<!-- view -->
<div data-ng-controller="LinksCtrl as ctrl">
<div class="link-form">
Categorie:
<select data-ng-model="ctrl.category" data-ng-options="v.i as v.d for v in ctrl.categoryAll" data-ng-change="ctrl.doSearch()">
</select>
<div class="buttons">
<button type="button" data-ng-repeat="item in ctrl.categoryAll" data-ng-click="ctrl.doSearch(item.i)" data-ng-bind="item.d"
data-ng-class="{'selected': ctrl.isCurrentCategory(item)}"></button>
</div>
</div>
<div class="items-empty" data-ng-show="ctrl.isEmpty()">No links</div>
<div class="sgart-it-updating" data-ng-show="isUpdating()">Wait...</div>
<ul class="results" data-ng-hide="ctrl.isEmpty()">
<li data-ng-repeat="item in ctrl.items">
<a data-ng-href="{{item.link}}" target="ctrl.getTarget(item)">
<div class="img-wrapper">
<img src="" data-ng-src="{{ctrl.getimageSrc(item)}}" data-ng-show="ctrl.isVisibleImage(item)">
</div>
<strong class="title-wrapper"><span data-ng-bind="item.title"></span></strong>
</a>
</li>
</ul>
</div>
</div>
<!-- angular js ver. 1.5.x -->
<script src="../htmljs/angular.min.js"></script>
<script type="text/javascript">
... vedi dopo ...
</script>
Il prefisso data- è opzionale serve solo per rendere compatibile il codice con i validatori html, potevano essere scritti anche come ng-*
In particolare abbiamo:- ng-app dichiara la app, tutto il contenuto all'interno sarà sotto il controllo di AngularJS
- ng-controller definisce il controller JavaScript che gestirà questo pezzo della view
- ng-repeat l'equivalente di un foreach C# serve per ciclare su un array JavaScript
- ng-model per realizzare il binding bidirezionale tra la view e il controller
- ng-bind per realizzare il binding unidirezionale tra la view e il controller
- ng-show per realizzare visualizzare o meno una parte della view in base a una proprietà booleana presente nel controller
- ng-options per realizzare il binding di un array con l'elemento html select
- ng-change per gestire l'evento onchange della select
- ng-click per gestire l'evento onclick
La parte di codice JavaScript è la seguente:
JavaScript
//vedi anche http://www.sgart.it/IT/informatica/angularjs-come-funziona/post
/*
Usa la lista SharePoint .../Lists/SgartItLinks
con i seguenti campi:
- Title (string) required
- Link (string) required
- Category (chioce) required
- ImageSrc (string)
- OpenNewWindow (bool)
*/
(function () { // richiudo tutto in una enclosure per evitare conflitti di nomi
"use strict";
//costanti globali
var PARAMETERS = {
siteUrl: '', // es. '/sites/s1'
listName: 'SgartItLinks', //nome dalla lista
fields: {
title: 'Title',
link: 'Link',
category: 'CategoryValue', // suffisso Value aggiunto da SharePoint
imageSrc: 'ImageSrc',
openNewWindow: 'OpenNewWindow'
}
};
//dichiarazione app / inizializzo angular
var app = angular.module('sgartItLinks', []);
//*********************************************
// controller usato dalla view
// creo un CONTROLLER
// la variabile $scope viene "iniettata"" (inject) da angular
// la variabile linkFactory viene "iniettata"" (inject) da angular
app.controller('LinksCtrl', function($scope, $rootScope, linkFactory) {
var self = this;
// dichiaro le variabili e le aggancio allo scope da usare nella view
// conterrà tutti i link letti
self.items = [{ title: '', link: '', imageSrc: '', openNew: true, category: '' }];
// conterrà l'elenco delle categorie letto dal campo choice
self.categoryAll = [];
// la categoria selezionata
self.category = "";
//dichiaro le funzioni usate nella view
self.doSearch = function (id) { // esegue il seach sui link in base alla categoria
self.items = [];
if (typeof id === "undefined")
id = self.category;
else
self.category = id;
linkFactory.getLinks(id).then(function (result) {
self.items = result;
});
};
self.isEmpty = function () {
if($rootScope.isUpdating()) return false;
return self.items === null || self.items.length === 0;
};
self.getimageSrc = function (item) {
return item.imageSrc;
if (self.isVisibleImage())
return item.imageSrc;
else
return null;
};
self.isVisibleImage = function (item) {
if (typeof item === "undefined") return false;
if (typeof item.imageSrc === "undefined") return false;
return !(item.imageSrc === null || item.imageSrc === "");
};
self.isCurrentCategory = function (item) {
return item.i === self.category;
};
self.getTarget = function (item) {
if (item.openNew === true)
return "_blank";
else
return "";
};
// inizializzo il controller
function init() {
self.categoryAll = [];
self.category = "";
self.items = [];
linkFactory.getCategories().then(function (result) {
self.categoryAll = result;
self.doSearch();
});
};
init();
});
//*********************************************
// factory per l'accesso ai dati tramite il client object model di SharePoint
// inietto il factory $http per le chiamate ajax
// inietto stateFactory per gestire lo stato di update/loading
app.factory("linkFactory", function($http, stateFactory) {
//variabili globali
var dbApiBase = PARAMETERS.siteUrl;
// le funzioni esposte dal factory
return {
'getLinks': _getLinks,
'getCategories': _getCategories
}
function _getCategories() {
stateFactory.updating();
return $http.get(dbApiBase + "/_vti_bin/listdata.svc/" + PARAMETERS.listName + "()?$orderby=" + PARAMETERS.fields.category, {
cache: false,
params: {
t: new Date().getTime() //per evitare il caching su IE
}
}).then(successCallbackCategory, errorCallback);
};
function successCallbackCategory(response) {
var result = [{ i: '', d: 'All' }];
var items = response.data.d.results;
var prev = {};
//leggo le category associate ai link facendo un distinct
for (var i = 0; i < items.length; i++) {
var cat = items[i][PARAMETERS.fields.category];
if (typeof prev[cat] === 'undefined') {
result.push({ i: cat, d: cat });
prev[cat] = 1;
}
}
stateFactory.updated();
return result;
};
function _getLinks(category) {
stateFactory.updating();
var filter = "";
if (!(typeof category === 'undefined' || category === null || category === ''))
filter = "&$filter=" + PARAMETERS.fields.category + " eq '" + category + "'";
return $http.get(dbApiBase + "/_vti_bin/listdata.svc/" + PARAMETERS.listName + "()?$orderby=" + PARAMETERS.fields.title + filter, {
cache: false,
params: {
t: new Date().getTime() //per evitare il caching su IE
}
}).then(successCallbackLinks, errorCallback);
};
//{title:'', link:'', imageSrc:'', openNew:true, category:''}
function successCallbackLinks(response) {
var result = [];
var items = response.data.d.results;
for (var i = 0; i < items.length; i++) {
var item = items[i];
result.push({
title: item[PARAMETERS.fields.title],
link: item[PARAMETERS.fields.link],
imageSrc: item[PARAMETERS.fields.imageSrc],
openNew: item[PARAMETERS.fields.openNewWindow],
category: item[PARAMETERS.fields.category]
});
}
stateFactory.updated();
return result;
};
//gestione errori comune
function errorCallback(response) {
stateFactory.updated();
alert(response);
};
});
//*********************
// loading service per la gestione del feedback visuale all'utente durante le chiamate ajax
app.factory("stateFactory", function($rootScope) {
var updatingCount = 0;
//aggancio la funzione al root scope
//per averla sempre disponibile
$rootScope.isUpdating=function(){
return updatingCount !=0;
};
return {
'updating': _updating,
'updated': _updated,
'reset': _reset,
'isUpdating': $rootScope.isUpdating
};
function _updating(){
updatingCount++;
}
function _updated(){
if(updatingCount>0)
updatingCount--;
else if(updatingCount<0)
_reset();
}
function _reset(){
updatingCount=0;
}
});
})();
- dichiarazione della app e inizializzazione di AngularJS ( angular.module()
- definizione del controller (app.controller - LinksCtrl)
- definizione del factory per l'accesso ai dati ( app.factory - linkFactory)
Attenzione se deve essere usato all'interno di SharePoint 2010 è necessario modificare il meta tag X-UA-Compatible da IE=8 a IE=9 o meglio ancora IE=edge
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
altrimenti AngularJS non funziona correttamente.
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
altrimenti AngularJS non funziona correttamente.
Il file completo può essere scaricato da qui sgart-it-links.zip
L'app può facilmente essere riadattata per girare in un abiente diverso da SharePoint, ad esempio un sito ASP.NET MVC che usa un DB per persistere l'elenco dei links. In questo caso bisognerà solo modificare il factory per gestire le nuove API Json esposte da MVC in modo che i metodi del factory ritornino sempre gli stessi oggetti.