Créez des directives AngularJS 1.x avancées : restrict, scope isolé, transclude, link, controller et cas d'usage enterprise réels.
Le Directive Definition Object (DDO)
Une directive AngularJS est définie via un objet de configuration appelé Directive Definition Object (DDO). Cet objet expose une vingtaine de propriétés qui contrôlent le comportement de la directive : son type (element, attribut, classe), son template, son scope, ses hooks de cycle de vie, etc.
// Squelette complet d'un DDO
angular.module('monApp')
.directive('maDirective', function() {
return {
// Comment la directive peut être utilisée dans le HTML
restrict: 'EA', // 'E'lement, 'A'ttribut, 'C'lasse, 'M'commentaire
// Template inline ou URL vers un fichier HTML
template: '<div>Mon template</div>',
// templateUrl: 'directives/ma-directive.html',
// Crée un scope isolé (non hérité du parent)
scope: {
title: '@', // string one-way depuis l'attribut
model: '=', // two-way binding
onAction: '&', // référence à une expression/fonction
},
// Permet d'insérer le contenu enfant dans le template
transclude: false,
// Logique DOM et listeners d'événements
link: function(scope, element, attrs) { },
// Logique de composant réutilisable (accessible par require)
controller: function($scope) { },
controllerAs: 'vm',
// Priorité d'exécution (utile quand plusieurs directives sur le même élément)
priority: 0,
// Si true, toutes les autres directives sur cet élément sont ignorées
terminal: false,
};
});
restrict : E, A, C, M
La propriété restrict détermine comment la directive peut être invoquée dans le HTML. On peut combiner les lettres pour autoriser plusieurs formes.
Les quatre modes
// 'E' — Element : <ma-carte></ma-carte>
// Recommandé pour les composants UI réutilisables
.directive('maCarte', function() {
return { restrict: 'E', template: '<div class="card">...</div>' };
});
// 'A' — Attribut : <div ma-tooltip="Texte">
// Recommandé pour les comportements ajoutés à un élément existant
.directive('maTooltip', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// attrs.maTooltip contient la valeur de l'attribut
element.attr('title', attrs.maTooltip);
},
};
});
// 'C' — Classe CSS : <div class="ma-mise-en-evidence">
// Peu utilisé, déconseillé en pratique (confusion HTML/comportement)
.directive('maMiseEnEvidence', function() {
return {
restrict: 'C',
link: function(scope, element) {
element.css('background-color', 'yellow');
},
};
});
'E' pour les composants (remplacent un élément) et 'A' pour les comportements (augmentent un élément existant). Évitez 'C' et 'M' dans les nouveaux projets.
Scope isolé : @, =, &
Par défaut, une directive hérite du scope de son parent (scope prototypal). Le scope isolé (scope: {}) crée un scope propre, ce qui rend la directive réutilisable et prévisible.
Les trois types de bindings
// Directive avec les trois types de bindings
angular.module('monApp')
.directive('userCard', function() {
return {
restrict: 'E',
scope: {
// '@' — one-way, reçoit une chaîne interpolée
// <user-card name="{{ user.name }}">
name: '@',
// '=' — two-way binding, synchronise les deux scopes
// <user-card user-data="currentUser">
userData: '=',
// '&' — expression/callback à appeler dans le scope parent
// <user-card on-delete="removeUser(id)">
onDelete: '&',
},
template: `
<div class="user-card">
<h3>{{ name }}</h3>
<p>Email : {{ userData.email }}</p>
<button ng-click="handleDelete()">Supprimer</button>
</div>
`,
link: function(scope) {
// Appel du callback parent avec les paramètres nommés
scope.handleDelete = function() {
scope.onDelete({ id: scope.userData.id });
};
},
};
});
<!-- Utilisation dans un template parent -->
<user-card
name="{{ currentUser.name }}"
user-data="currentUser"
on-delete="deleteUser(id)">
</user-card>
scope.onDelete() sans objet, les paramètres ne sont pas transmis au parent. Il faut toujours passer un objet avec les noms exacts des paramètres attendus : scope.onDelete({ id: value }).
transclude : projection de contenu
La transclusion permet d'injecter le contenu HTML placé entre les balises de la directive dans son template. C'est l'équivalent AngularJS de ng-content en Angular moderne.
// Directive panneau avec transclude = true
angular.module('monApp')
.directive('panneau', function() {
return {
restrict: 'E',
transclude: true, // active la transclusion
scope: { titre: '@' },
template: `
<div class="panneau">
<div class="panneau-header">
<h3>{{ titre }}</h3>
</div>
<div class="panneau-body">
<!-- ng-transclude insère le contenu de la directive ici -->
<ng-transclude></ng-transclude>
</div>
</div>
`,
};
});
<!-- Utilisation : le contenu entre les balises est transcludé -->
<panneau titre="Statistiques mensuelles">
<p>Visiteurs : <strong>12 450</strong></p>
<p>Conversions : <strong>3.2%</strong></p>
<p>Temps moyen : <strong>2m 34s</strong></p>
</panneau>
La fonction link
La fonction link s'exécute après que le DOM est compilé et lié. C'est l'endroit où on manipule directement le DOM, attache des event listeners natifs, et interagit avec des bibliothèques tierces (jQuery, D3, Chart.js...).
// Directive ripple effect — manipulation DOM avec link
angular.module('monApp')
.directive('rippleEffect', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
// 'element' est un objet jqLite (sous-ensemble de jQuery)
var color = attrs.rippleColor || 'rgba(255,255,255,0.4)';
// Ajoute un style de base à l'élément hôte
element.css('position', 'relative');
element.css('overflow', 'hidden');
// Attache un listener natif au clic
element.on('click', function(event) {
var rect = element[0].getBoundingClientRect();
var x = event.clientX - rect.left;
var y = event.clientY - rect.top;
// Crée l'élément visuel du ripple
var ripple = angular.element('<span class="ripple"></span>');
ripple.css({
left: x + 'px',
top: y + 'px',
background: color,
});
element.append(ripple);
// Supprime l'élément après l'animation
setTimeout(function() { ripple.remove(); }, 600);
});
// Nettoyage : retire les listeners quand la directive est détruite
scope.$on('$destroy', function() {
element.off('click');
});
},
};
});
scope.$on('$destroy', ...) pour supprimer les event listeners natifs et annuler les timers. Sans ce nettoyage, vous créez des fuites mémoire dans les Single Page Applications.
controller dans les directives
La propriété controller d'une directive expose une API publique que d'autres directives peuvent consommer via require. C'est le pattern de communication inter-directives.
// Directive accordéon avec controller exposé
angular.module('monApp')
.directive('accordion', function() {
return {
restrict: 'E',
transclude: true,
template: '<div class="accordion"><ng-transclude></ng-transclude></div>',
controller: function() {
var panels = [];
var openPanel = null;
// API publique accessible par les directives enfants
this.addPanel = function(panel) {
panels.push(panel);
};
this.toggle = function(panel) {
if (openPanel === panel) {
openPanel = null; // ferme si déjà ouvert
} else {
openPanel = panel; // ouvre le nouveau panneau
}
// Notifie tous les panneaux de l'état actuel
panels.forEach(function(p) {
p.isOpen = (p === openPanel);
});
};
this.isOpen = function(panel) {
return openPanel === panel;
};
},
};
});
require : communication inter-directives
require permet à une directive d'accéder au controller d'une autre directive parente ou voisine. C'est la base de la composition de composants en AngularJS.
// Directive panneau-item qui requiert le controller de 'accordion'
angular.module('monApp')
.directive('panneauItem', function() {
return {
restrict: 'E',
transclude: true,
scope: { titre: '@' },
require: '^accordion', // '^' signifie "chercher dans les ancêtres"
template: `
<div class="panneau-item">
<div class="panneau-header" ng-click="toggle()">
<span>{{ titre }}</span>
<span ng-if="isOpen">▲</span>
<span ng-if="!isOpen">▼</span>
</div>
<div class="panneau-body" ng-if="isOpen">
<ng-transclude></ng-transclude>
</div>
</div>
`,
link: function(scope, element, attrs, accordionCtrl) {
// accordionCtrl = instance du controller de l'accordion parent
scope.isOpen = false;
// S'enregistre auprès de l'accordion
accordionCtrl.addPanel(scope);
scope.toggle = function() {
accordionCtrl.toggle(scope);
scope.isOpen = accordionCtrl.isOpen(scope);
};
},
};
});
<!-- Utilisation composée -->
<accordion>
<panneau-item titre="Section 1">
<p>Contenu de la section 1...</p>
</panneau-item>
<panneau-item titre="Section 2">
<p>Contenu de la section 2...</p>
</panneau-item>
</accordion>
Équivalents en Angular moderne
En Angular (2+), les directives AngularJS sont remplacées par les composants (pour les directives avec template) et les directives structurelles/attributs (pour les comportements). La syntaxe est radicalement différente mais les concepts restent identiques.
| AngularJS | Angular 21 |
|---|---|
| directive restrict:'E' avec template | @Component standalone |
| directive restrict:'A' avec link | @Directive avec HostListener |
| scope: { x: '@' } | @Input() x: string |
| scope: { x: '=' } | @Input() + @Output() ou model() |
| scope: { x: '&' } | @Output() EventEmitter |
| transclude: true | <ng-content> |
| require: '^parent' | inject(ParentComponent) |
| link: function(scope, el, attrs) | ngAfterViewInit() + ElementRef |
// Équivalent Angular 21 de la directive accordion
@Component({
selector: 'app-accordion',
standalone: true,
template: '<ng-content />',
})
export class AccordionComponent {
private openPanel = signal<any>(null);
toggle(panel: any) {
this.openPanel.set(this.openPanel() === panel ? null : panel);
}
isOpen(panel: any) {
return this.openPanel() === panel;
}
}
host directives qui permettent de composer des comportements sur un composant sans héritage — une fonctionnalité encore plus puissante que require.