Maîtrisez $scope, $rootScope et le two-way data binding AngularJS 1.x : ng-model, $watch, $apply et patterns avancés.
Introduction à $scope dans AngularJS
Dans AngularJS 1.x, $scope est l'objet fondamental qui fait le lien entre le contrôleur et la vue. C'est le modèle de données partagé qui permet à Angular de synchroniser automatiquement l'interface utilisateur avec la logique applicative.
Comprendre $scope est essentiel pour tout développeur AngularJS. C'est l'ancêtre direct des Signals Angular modernes et la base du data binding bidirectionnel qui a fait la réputation d'AngularJS.
$scope est un objet JavaScript ordinaire enrichi par AngularJS. Il forme un arbre hiérarchique calqué sur la hiérarchie des contrôleurs dans le DOM.
// Déclaration d'un contrôleur AngularJS basique avec $scope
angular.module('monApp', [])
.controller('MonController', function($scope) {
// Propriété exposée à la vue via $scope
$scope.message = 'Bonjour depuis AngularJS !';
// Méthode accessible depuis le HTML
$scope.saluer = function() {
alert('Salut, ' + $scope.nom + ' !');
};
// Tableau de données lié à ng-repeat
$scope.fruits = ['Pomme', 'Banane', 'Orange'];
});
La vue HTML correspondante accède directement aux propriétés de $scope :
<!-- HTML lié au contrôleur MonController -->
<div ng-controller="MonController">
<!-- Double accolades = interpolation de $scope.message -->
<p>{{ message }}</p>
<!-- ng-model crée un binding bidirectionnel avec $scope.nom -->
<input ng-model="nom" placeholder="Votre nom">
<!-- ng-click appelle la méthode $scope.saluer() -->
<button ng-click="saluer()">Saluer</button>
<!-- ng-repeat itère sur $scope.fruits -->
<ul>
<li ng-repeat="fruit in fruits">{{ fruit }}</li>
</ul>
</div>
$scope vs $rootScope
AngularJS crée un arbre de scopes. $rootScope est le scope racine, ancêtre de tous les autres. Chaque contrôleur obtient son propre $scope enfant, créé par prototypage JavaScript.
L'héritage prototypal des scopes
Un scope enfant hérite des propriétés de son scope parent via la chaîne prototypale JavaScript. C'est puissant mais source de bugs subtils, notamment avec les types primitifs.
// Exemple illustrant l'héritage prototypal des scopes
angular.module('appHierarchie', [])
.controller('ParentCtrl', function($scope) {
// Propriété primitive : source de problèmes d'héritage
$scope.valeurPrimitive = 'parent';
// Propriété objet : héritage correct et recommandé
$scope.modele = {
valeur: 'parent'
};
})
.controller('EnfantCtrl', function($scope) {
// ✅ Bonne pratique : modifier via l'objet intermédiaire
$scope.modele.valeur = 'enfant modifié';
// ❌ Mauvaise pratique : crée une nouvelle propriété locale
// $scope.valeurPrimitive = 'enfant'; // ne modifie PAS le parent !
});
<!-- Démonstration de la hiérarchie de scopes -->
<div ng-controller="ParentCtrl">
<p>Parent : {{ modele.valeur }}</p>
<div ng-controller="EnfantCtrl">
<!-- Hérite et modifie le même objet parent -->
<p>Enfant : {{ modele.valeur }}</p>
<input ng-model="modele.valeur">
</div>
</div>
Quand utiliser $rootScope
$rootScope est accessible partout dans l'application. Il est tentant de l'utiliser comme store global, mais cette pratique est déconseillée car elle crée des couplages forts. Préférez les services pour partager des données globales.
// Utilisation raisonnée de $rootScope
angular.module('monApp')
.controller('AppCtrl', function($rootScope, AuthService) {
// Stocker l'état d'authentification global (acceptable)
$rootScope.utilisateurConnecte = AuthService.getUser();
// Écouter les changements de route pour le titre de page
$rootScope.$on('$routeChangeSuccess', function(event, current) {
// Met à jour le titre depuis les données de route
$rootScope.titrePage = current.$$route.title || 'Mon App';
});
});
$rootScope aux données vraiment globales (utilisateur connecté, thème). Pour tout le reste, utilisez des services AngularJS.
Scope isolé dans les directives
Les directives peuvent créer un scope totalement isolé, sans héritage prototypal. C'est le comportement recommandé pour les composants réutilisables.
// Directive avec scope isolé
angular.module('monApp')
.directive('monComposant', function() {
return {
restrict: 'E',
// scope: true → scope enfant avec héritage
// scope: false → partage le scope parent
scope: {
// Binding par valeur (one-way, depuis l'attribut HTML)
titre: '@',
// Binding bidirectionnel (two-way)
donnees: '=',
// Binding de fonction (expression)
onSave: '&'
},
template: '<div><h3>{{titre}}</h3></div>'
};
});
Le two-way data binding
Le data binding bidirectionnel (two-way binding) est la fonctionnalité phare d'AngularJS. Il synchronise automatiquement la vue et le modèle dans les deux sens : toute modification du modèle met à jour la vue, et toute interaction utilisateur met à jour le modèle.
Comment fonctionne le binding unidirectionnel
// Binding unidirectionnel : du modèle vers la vue uniquement
// Utilisation de {{ }} ou ng-bind
angular.module('monApp')
.controller('BindingCtrl', function($scope) {
// Propriété qui s'affiche dans la vue (lecture seule depuis la vue)
$scope.compteur = 0;
// Méthode pour incrémenter (modifie le modèle, la vue se met à jour)
$scope.incrementer = function() {
$scope.compteur++;
};
});
<!-- Binding unidirectionnel vers la vue -->
<div ng-controller="BindingCtrl">
<!-- {{ compteur }} lit $scope.compteur mais ne le modifie pas -->
<p>Compteur : {{ compteur }}</p>
<button ng-click="incrementer()">+1</button>
</div>
Le two-way binding avec ng-model
// Démonstration complète du two-way binding
angular.module('monApp')
.controller('TwoWayCtrl', function($scope) {
// Modèle initialisé avec des valeurs par défaut
$scope.profil = {
nom: 'Alice',
age: 30,
newsletter: true
};
// Méthode affichant les données du modèle en temps réel
$scope.afficherProfil = function() {
// Ces valeurs reflètent exactement ce que l'utilisateur a saisi
console.log('Nom :', $scope.profil.nom);
console.log('Age :', $scope.profil.age);
console.log('Newsletter :', $scope.profil.newsletter);
};
});
<!-- Formulaire avec two-way binding complet -->
<form ng-controller="TwoWayCtrl" ng-submit="afficherProfil()">
<!-- ng-model lie l'input à $scope.profil.nom dans les deux sens -->
<input type="text" ng-model="profil.nom" placeholder="Nom">
<!-- Affichage en temps réel sans bouton Submit -->
<p>Bonjour {{ profil.nom }} !</p>
<input type="number" ng-model="profil.age">
<input type="checkbox" ng-model="profil.newsletter"> Newsletter
<button type="submit">Enregistrer</button>
</form>
bindOnce (::valeur) pour les données statiques.
<!-- Binding one-time avec :: (AngularJS 1.3+) -->
<!-- Les données sont liées une seule fois et plus surveillées -->
<p>{{ ::utilisateur.nom }}</p>
<!-- Très utile pour les listes longues de données statiques -->
<li ng-repeat="item in ::listeStatique">{{ ::item.titre }}</li>
ng-model en profondeur
ng-model est la directive centrale du data binding bidirectionnel. Elle ne fait pas que lier une valeur : elle gère la validation, les états CSS, les parsers/formatters et l'intégration avec ngForm.
Les états de validation ng-model
<!-- ng-model expose des états de validation dans le scope -->
<form name="monFormulaire">
<input
type="email"
name="email"
ng-model="utilisateur.email"
required
ng-minlength="5"
>
<!-- Accès aux états de validation via monFormulaire.email -->
<p ng-show="monFormulaire.email.$invalid && monFormulaire.email.$touched">
Email invalide
</p>
<!-- Classes CSS automatiques ajoutées par ng-model -->
<!-- .ng-valid, .ng-invalid, .ng-pristine, .ng-dirty, .ng-touched -->
</form>
Parsers et formatters personnalisés
La directive ng-model expose un contrôleur ngModel qui permet d'ajouter des transformations via $parsers (vue → modèle) et $formatters (modèle → vue).
// Directive personnalisant ng-model avec parsers/formatters
angular.module('monApp')
.directive('majuscule', function() {
return {
// Nécessite le contrôleur ngModel du champ hôte
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
// Parser : transforme la valeur saisie avant de la stocker
// Côté vue → modèle : convertit en MAJUSCULES dans le modèle
ngModel.$parsers.push(function(valeurVue) {
return valeurVue ? valeurVue.toUpperCase() : valeurVue;
});
// Formatter : transforme la valeur du modèle pour l'affichage
// Côté modèle → vue : affiche en minuscules dans l'input
ngModel.$formatters.push(function(valeurModele) {
return valeurModele ? valeurModele.toLowerCase() : valeurModele;
});
}
};
});
<!-- Utilisation de la directive majuscule -->
<!-- L'utilisateur tape en minuscules, le modèle stocke en MAJUSCULES -->
<input type="text" ng-model="texte" majuscule>
<p>Modèle : {{ texte }}</p>
$parsers et $formatters sont exécutés dans l'ordre d'ajout. Vous pouvez en chaîner plusieurs pour des transformations complexes (validation + formatage + masquage).
$watch, $apply et le cycle $digest
Le mécanisme central du data binding AngularJS est le cycle $digest. C'est lui qui détecte les changements et met à jour la vue. Comprendre ce cycle est indispensable pour éviter les bugs et les problèmes de performance.
Le cycle $digest expliqué
À chaque interaction (clic, saisie, requête HTTP), AngularJS lance un cycle $digest qui parcourt tous les watchers enregistrés et vérifie si leurs valeurs ont changé. Si oui, il met à jour la vue et relance un nouveau cycle jusqu'à stabilisation (max 10 cycles).
// $watch : surveille une expression et réagit aux changements
angular.module('monApp')
.controller('WatchCtrl', function($scope) {
$scope.prix = 100;
$scope.quantite = 2;
$scope.total = 0;
// $watch simple : surveille une propriété primitive
// Paramètres : expression, callback(nouvelleValeur, ancienneValeur)
$scope.$watch('prix', function(nouveauPrix, ancienPrix) {
console.log('Prix changé de', ancienPrix, 'à', nouveauPrix);
$scope.total = $scope.prix * $scope.quantite;
});
// $watch sur une expression calculée
$scope.$watch(
function() {
// Fonction d'expression : retourne la valeur à surveiller
return $scope.prix * $scope.quantite;
},
function(nouveauTotal) {
// Callback exécuté quand le produit prix*quantité change
$scope.total = nouveauTotal;
}
);
// $watchCollection : surveille les éléments d'un tableau/objet
// Plus léger que $watch avec deep:true
$scope.panier = [];
$scope.$watchCollection('panier', function(nouveauPanier) {
// Déclenché quand un élément est ajouté ou supprimé
$scope.nbArticles = nouveauPanier.length;
});
});
$watch profond vs $watchCollection
// $watch avec deep watching (coûteux en performances)
$scope.$watch(
'objetComplexe',
function(nouvelObjet, ancienObjet) {
// Déclenché si N'IMPORTE QUELLE propriété imbriquée change
console.log('Objet modifié en profondeur');
},
true // true = deep watch (compare récursivement)
);
// $watchCollection (compromis performance/réactivité)
// Surveille les ajouts/suppressions mais pas les mutations internes
$scope.$watchCollection('listeObjets', function(nouvelleListe) {
$scope.compter = nouvelleListe.length;
});
$apply : intégrer du code externe
Quand vous utilisez du code externe à AngularJS (setTimeout, jQuery, WebSocket…), les changements de $scope ne déclenchent pas automatiquement le cycle $digest. Vous devez appeler $apply manuellement.
// Problème : setTimeout est hors du contexte AngularJS
angular.module('monApp')
.controller('ApplyCtrl', function($scope, $timeout) {
$scope.message = 'En attente...';
// ❌ MAUVAIS : Angular ne détecte pas ce changement
setTimeout(function() {
$scope.message = 'Terminé !'; // Vue non mise à jour
}, 2000);
// ✅ CORRECT option 1 : $apply force le cycle $digest
setTimeout(function() {
$scope.$apply(function() {
$scope.message = 'Terminé !'; // Vue mise à jour
});
}, 2000);
// ✅ CORRECT option 2 : $timeout (wrappeur AngularJS de setTimeout)
// Gère automatiquement $apply, préférable dans tous les cas
$timeout(function() {
$scope.message = 'Terminé avec $timeout !';
}, 2000);
});
$evalAsync vs $apply
// $evalAsync : planifie l'exécution dans le prochain cycle $digest
// Évite les erreurs "$apply already in progress"
$scope.$evalAsync(function() {
// Exécuté dans le prochain cycle $digest, pas immédiatement
$scope.resultat = calculerQuelqueChose();
});
// Nettoyer un watcher quand il n'est plus nécessaire
// $watch retourne une fonction de désinscription
var stopWatch = $scope.$watch('donnees', function(val) {
if (val === 'valeurCible') {
console.log('Cible atteinte, arrêt de la surveillance');
stopWatch(); // Appel de la fonction retournée pour stopper
}
});
$watch est exécuté à chaque cycle $digest. Une application avec 2000+ watchers peut devenir lente. Désabonnez-vous toujours avec la fonction retournée par $watch dans le callback $destroy.
$broadcast, $emit et $on
AngularJS propose un système d'événements pour la communication entre scopes non directement liés. Ces méthodes permettent une communication découplée entre contrôleurs.
// Système d'événements inter-scopes AngularJS
angular.module('monApp')
.controller('ParentCtrl', function($scope) {
// $broadcast : diffuse l'événement VERS LES ENFANTS (descendant)
$scope.notifierEnfants = function(message) {
$scope.$broadcast('evenement:message', {
texte: message,
timestamp: new Date()
});
};
// $on : écoute un événement sur ce scope
$scope.$on('enfant:reponse', function(event, data) {
// event.targetScope : scope qui a émis l'événement
console.log('Réponse reçue :', data.texte);
});
})
.controller('EnfantCtrl', function($scope) {
// Écoute l'événement diffusé par le parent
$scope.$on('evenement:message', function(event, data) {
console.log('Message reçu :', data.texte);
// event.stopPropagation() stoppe la propagation vers d'autres enfants
// $emit : remonte l'événement VERS LES PARENTS (ascendant)
$scope.$emit('enfant:reponse', { texte: 'Message bien reçu !' });
});
});
Pattern service pour éviter $broadcast/$emit excessif
// Meilleure alternative : service avec callbacks
angular.module('monApp')
.service('EventBus', function() {
var listeners = {};
// Abonnement à un événement nommé
this.on = function(event, callback) {
if (!listeners[event]) listeners[event] = [];
listeners[event].push(callback);
};
// Déclenchement de l'événement avec données
this.emit = function(event, data) {
if (listeners[event]) {
listeners[event].forEach(function(cb) { cb(data); });
}
};
})
.controller('EmetteurCtrl', function($scope, EventBus) {
$scope.envoyer = function() {
// Émet via le service, sans traverser l'arbre de scopes
EventBus.emit('donnees:mises-a-jour', { valeur: 42 });
};
})
.controller('ReceveurCtrl', function($scope, EventBus) {
EventBus.on('donnees:mises-a-jour', function(data) {
// Reçoit les données sans couplage scope
$scope.$apply(function() {
$scope.valeur = data.valeur;
});
});
});
$broadcast descend dans l'arbre de scopes (coûteux), $emit remonte vers la racine. Préférez les services ou les patterns d'observateurs pour les applications complexes.
Patterns avancés avec $scope
Au-delà des usages basiques, $scope supporte des patterns avancés pour gérer des applications complexes de façon maintenable.
Controller As : alternative à $scope explicite
Depuis AngularJS 1.2, la syntaxe Controller As permet d'utiliser this dans le contrôleur au lieu de $scope, rendant le code plus lisible et plus proche des classes ES6.
// Pattern "Controller As" : utilise this au lieu de $scope
angular.module('monApp')
.controller('ProfilCtrl', function() {
// 'this' fait référence à l'instance du contrôleur
// Exposé dans la vue sous l'alias 'vm' (ViewModel)
var vm = this;
vm.titre = 'Mon Profil';
vm.utilisateur = { nom: 'Bob', age: 25 };
vm.sauvegarder = function() {
console.log('Sauvegarde de', vm.utilisateur.nom);
};
});
<!-- Utilisation avec ng-controller et l'alias "as vm" -->
<div ng-controller="ProfilCtrl as vm">
<h1>{{ vm.titre }}</h1>
<input ng-model="vm.utilisateur.nom">
<button ng-click="vm.sauvegarder()">Sauvegarder</button>
</div>
Nettoyage des ressources avec $destroy
// Nettoyage propre des ressources lors de la destruction du scope
angular.module('monApp')
.controller('TimerCtrl', function($scope, $interval) {
var intervalle;
// Démarre un timer toutes les secondes
$scope.demarrer = function() {
intervalle = $interval(function() {
$scope.compteur = ($scope.compteur || 0) + 1;
}, 1000);
};
// Nettoyage OBLIGATOIRE : $destroy est déclenché quand
// le contrôleur est détruit (navigation hors de la vue)
$scope.$on('$destroy', function() {
// Stoppe l'intervalle pour éviter les memory leaks
if (intervalle) {
$interval.cancel(intervalle);
}
});
});
Comparaison avec Angular moderne
Angular 2+ a radicalement changé l'approche du data binding. Comprendre ces différences est essentiel pour envisager une migration.
| Concept | AngularJS 1.x | Angular 17+ |
|---|---|---|
| Data binding | $scope + dirty checking |
Zone.js + Signals |
| Two-way binding | ng-model |
[(ngModel)] |
| Détection changements | $digest cycle (polling) | Change Detection + Signals |
| Communication | $broadcast / $emit | @Input/@Output / EventEmitter |
| État global | $rootScope / Services | Services / NgRx / Signals |
| Watchers | $watch / $watchCollection | effect() avec Signals |
// Équivalent Angular moderne du two-way binding AngularJS
// AngularJS : ng-model sur $scope
// Angular moderne : [(ngModel)] avec FormsModule ou signal()
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-profil',
template: `
<!-- Two-way binding avec signal via modèle de formulaire -->
<input [(ngModel)]="nom">
<!-- Affichage réactif : mis à jour automatiquement -->
<p>Bonjour {{ nom() }} !</p>
<!-- computed() = équivalent de $watch avec expression calculée -->
<p>Longueur : {{ longueurNom() }}</p>
`
})
export class ProfilComponent {
// signal() remplace $scope.nom
nom = signal('Alice');
// computed() remplace $watch avec expression
longueurNom = computed(() => this.nom().length);
}
Conclusion
$scope et le data binding bidirectionnel sont le coeur d'AngularJS 1.x. Le cycle $digest, les $watch, $apply, et le système d'événements $broadcast/$emit forment un écosystème cohérent qui a révolutionné le développement front-end à son époque.
Si vous maintenez une application AngularJS, maîtriser ces mécanismes vous permettra de corriger des bugs subtils et d'optimiser les performances. Si vous envisagez une migration vers Angular moderne, comprendre $scope vous aidera à saisir pourquoi les Signals et la détection de changements unidirectionnelle sont une amélioration majeure.
$destroy pour éviter les fuites mémoire. Préférez la syntaxe Controller As à $scope explicite pour un code plus maintenable.