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:
app linksapp links
la lista da cui vengono presi i links è questa:
lista linkslista links
Per realizzare questa app serve:
  • una lista SharePoint chaimata 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
La app, contenuta nel file htmljs/sgart-it-links.html, avrà tre blocchi principali:
  • 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
La view sarà questa:
<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>
Notare gli attibuti data-ng-* che servono a far funzionare AngularJS.
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
oltre a questi attibuti abbiamo le doppie parentesi graffe che sono l'equivalente di ng-bing.

La parte di codice JavaScript è la seguente:
  //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;
      }
    });

  })();
Possiamo individuare 3 blocchi principali con cui è costruita questa app AngularJS:
  • 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.

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.