Tra i framework JavaScript MV*, AngularJS forse quello che ha suscitato più interesse, probabilmente per fattori quali la sponsorizzazione di Google, la promessa di un setup rapido ed un approccio dichiarativo che permette di essere produttivi in breve tempo.
In realtà tutto dipende dalla complessità dell'applicazione che si deve realizzare. Se da una parte molti tutorial presentano scenari semplici ma di effetto (come riportare quanto digitato in un input all'interno di un altro elemento), arriva il momento in cui ci si scontra con alcune caratteristiche architetturali di AngularJS che, se non comprese a fondo, possono creare problemi. Una di queste caratteristiche è lo scope.
L'oggetto scope
Nella terminologia di AngularJS uno scope (spesso indicato nel codice con $scope
) indica il contesto in cui vengono salvati i dati di un'applicazione (il model) ed in cui vengono valutate le espressioni utilizzate nella view.
Per la natura di AngularJS, in cui esiste una stretta correlazione fra view e model, ogni elemento di una view è associato ad uno scope direttamente o in modo ereditario; inoltre, nonostante non sia una best practice, l'interazione con lo scope può avvenire direttamente dalla view.
Infine, seguendo la natura di JavaScript, gli scope possono essere innestati al fine di ereditare proprietà e metodi degli scope padre.
>> Leggi di più sullo Scope in JavaScript
AngularJS e JavaScript
Prima di approfondire gli scope è importante capire che AngularJS non introduce feature nuove o paradigmi diversi da quelli già presenti in JavaScript.
AngularJS non gestisce il model come Backbone o Ember, ma lo considera come un qualsiasi oggetto JavaScript, utilizzando il dirty checking per verificarne lo stato.
Questo significa che tutti i metodi e le proprietà utilizzate nelle view (che in AngularJS sono l'HTML stesso) sottostanno alle regole di ereditarietà e reference di un qualsiasi oggetto JavaScript.
Metodi e proprietà di uno scope
Come detto, la caratteristica peculiare dello scope è quella di essere un semplice oggetto JavaScript a cui vengono aggiunti alcuni metodi e proprietà custom.
Questo vuol dire che la gestione dei dati associati allo scope (quello che in altri framework definiremmo model) avviene con semplici proprietà:
$scope.name = 'Mario';
$scope.surname = 'Rossi';
Per lo stesso motivo è possibile definire dei metodi direttamente sullo scope:
$scope.getFullName = function () {
return $scope.name + ' ' + $scope.surname;
};
Utilizzando questa notazione saremo in grado di esporre proprietà e metodi dello scope nella view ad esso associata:
<div>
Ciao {{getFullName()}}
<!-- Ciao Mario Rossi -->
</div>
Proprietà e metodi privati
Per ogni scope che viene creato, AngularJS aggiunge all'oggetto una serie di proprietà e metodi privati distinguibili dal fatto che sono tutti prefissati da $
(dollaro) o $$
(doppio dollaro). Quelli più rilevanti implementano un sistema di "PubSub" a livello di applicazione:
Membro privato | Descrizione |
---|---|
$$listeners |
eventi associati allo scope; |
$on(evento, funzione) |
accoda una funzione in ascolto su un evento; |
$emit(evento, argomenti) $broadcast(evento, argomenti) |
lanciano un evento con degli argomenti opzionali |
La differenza fra $emit
e $broadcast
è che il primo lancia l'evento sullo scope corrente e su tutti gli scope padre, mentre il secondo si propaga verso gli scope figli; ciò permette una gestione più granulare degli eventi e facilita la gestione degli stati di ogni singola parte di un'applicazione.
Il metodo $watch
Un altro metodo che potrebbe tornare utile è $watch, che permette di rimanere in ascolto su una proprietà dello scope per lanciare una funzione ad ogni cambiamento del suo valore:
$scope.name = 'Mario';
$scope.$watch('name', function (newValue, oldValue) {
console.log('Nuovo valore: ' + newValue);
});
Da notare che la funzione di watch sarà lanciata anche alla prima esecuzione del codice, poiché il valore della proprietà nome
viene valutato come nuovo.
Scope radice o $rootScope
Il primo livello di scope identificabile in un'applicazione AngularJS è quello del $rootScope ed è identificato dalla porzione di view compresa nell'elemento con l'attributo ng-app
:
<div class="container">
<a href ng-click="messaggio = 'AngularJS rocks'">clicca qui</a>
<section ng-app class="message">
<h1>{{messaggio}}</h1>
</section>
</div>
Nell'esempio precedente il $rootScope è circoscritto dall'elemento section.message
. Se infatti proveremo a cliccare sul link "clicca qui" non noteremo alcun cambiamento.
Spostando l'attributo sull'elemento div.container
, l'espressione definita in ng-click
sarà invece inserita nello stesso scope dell'espressione {{messaggio}}
e verranno quindi messe in relazione:
<div class="container" ng-app>
<a href ng-click="messaggio = 'AngularJS rocks'">clicca qui</a>
<section class="message">
<h1>{{messaggio}}</h1>
</section>
</div>
Esempio live.
Funzione del $rootScope
Ad una prima analisi si potrebbe pensare che il $rootScope
possa essere utilizzato come collettore di proprietà comuni dell'applicazione oppure per salvare e condividere lo stato dell'applicazione. In realtà per tale scopo è molto meglio utilizzare dei servizi.
>> Leggi di più sugli Angular services
La funzione principale del $rootScope dovrebbe essere quella di gestire eventuali eventi a livello di applicazione , ad esempio quelli lanciati con il metodo $emit
da uno scope figlio.
In generale e per scenari comuni, dove le interazioni vanno oltre quella di questo esempio è comunque buona norma utilizzare il meno possibile il $rootScope.
Scope del Controller
Uno dei modi per suddividere un'applicazione in più parti è attraverso l'uso di controller.
>> Leggi di più sui controller in AngularJS
Ogni controller crea un nuovo scope che eredita tutte le proprietà del $rootScope. Inoltre, poiché è possibile innestare più controller l'uno dentro l'altro, ogni controller figlio eredita le caratteristiche dei padri. Vediamone un esempio:
<div class="container" ng-app="myApp">
<a href ng-click="messaggio = 'AngularJS rocks'">clicca qui</a>
<p>Messaggio nel rootScope: <code>{{messaggio}}</code></p>
<div id="toolbar" ng-controller="MainController">
<a href ng-click="messaggio = 'AngularJS really rocks'" ng-show="messaggio">clicca anche qui</a>
<p>Messaggio nel MainController: <code>{{messaggio}}</code></p>
<section class="message" ng-controller="MessageController">
<p>Messaggio nel MessageController: <code>{{messaggio}}</code></p>
</section>
</div>
</div>
<script>
angular.module('myApp', [])
.controller('MainController', function ($scope) {})
.controller('MessageController', function ($scope) {});
</script>
L'utilità fondamentale di questa caratteristica sta nel fatto che è possibile condividere dei metodi, ad esempio per poter gestire lo stato generale dell'applicazione (esempio live):
<div class="container" ng-app="myApp" ng-controller="MainController">
<p>MainController</p>
<p><a href ng-click="loading('MainController')">carica</a></p>
<p ng-show="loadingSource">Richiesta caricamento proveniente da <code>{{loadingSource}}</code></p>
<div id="toolbar" ng-controller="ToolbarController">
<p>ToolbarController</p>
<a href ng-click="loading('ToolbarController')">carica</a>
</div>
</div>
<script>
angular.module('myApp', [])
.controller('MainController', function ($scope) {
$scope.loadingSource = null;
$scope.loading = function (source) {
$scope.loadingSource = source;
};
})
.controller('ToolbarController', function ($scope) {});
</script>
Da notare, comunque, che in questo modo aumenta l'interdipendenza dei controller, perciò se state sviluppando l'applicazione in un'ottica di riusabilità sarà sempre meglio rifarsi ad un servizio.
Scope delle direttive
Un ulteriore caso in cui può essere generato uno scope è attraverso l'uso di una direttiva. Fra quelle native di AngularJS vi sono ad esempio ng-repeat
, ng-switch
, ng-view
e ng-include
. Ognuna di queste direttive crea uno scope figlio (o isolated scope) in modo da poter gestire logiche complesse senza interferire con altre parti dell'applicazione.
ng-repeat
Ad esempio, la direttiva ng-repeat crea uno scope figlio per ogni iterazione (quindi per ogni elemento della collezione), il quale eredita metodi e proprietà dallo scope padre. Oltre a ciò, per ognuno degli scope creati, AngularJS aggiunge una proprietà specifica per il singolo elemento dell'iterazione in modo da garantirne un accesso più rapido:
<script>
angular.module('myApp', [])
.controller('MainController', function ($scope) {
$scope.labels = {
'F': 'donna',
'M': 'uomo'
};
$scope.people = [
{
name: 'Mario',
surname: 'Rossi',
sex: 'M'
},
{
name: 'Sara',
surname: 'Bianchi',
sex: 'F'
}
];
});
</script>
<div class="container" ng-app="myApp" ng-controller="MainController">
<p>Lista persone</p>
<ul>
<li ng-repeat="person in people">
{{person.name}} {{person.surname}} ({{labels[person.sex]}})
</li>
</ul>
</div>
Nell'esempio allo scope di ogni iterazione è assegnata una proprietà person
con il valore dell'indice corrente, mentre labels
viene ereditata dal controller.
Questa caratteristica spiega anche perché, nell'esempio seguente, il valore della proprietà checked
venga impostato all'interno di ng-repeat
ma non sia raggiungibile dall'esterno (esempio live):
<div class="container" ng-app="myApp" ng-controller="MainController">
<p>Lista persone</p>
<ul>
<li ng-repeat="person in people" ng-class="{'is-checked' : checked}">
{{person.name}} {{person.surname}} ({{labels[person.sex]}})
<input type="checkbox" ng-model="checked" />
</li>
</ul>
<p>
Selezionato? {{checked}}
</p>
</div>
Riferimenti
Comprendere a fondo cosa siano e come funzionino gli scope in AngularJS è il primo passo per poter affrontare la progettazione di applicazioni complesse con questo framework.
Chi volesse approfondire l'argomento può fare riferimento alla documentazione ufficiale e all'esauriente pagina della wiki.
Riportiamo anche un servizio che elenca i framework JavaScript MV* con lo scopo di aiutare nella scelta.