AngularJS : directives custom avancées

Front-end 06/04/2026 14:00:00 Mezgani said
Angularjs Directives Custom Composants Frontend
AngularJS : directives custom avancées

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');
    },
  };
});
Bonne pratique : Utilisez '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>
Piège classique avec & : Quand vous appelez 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');
        });
      },
    };
  });
Toujours nettoyer ! Écoutez 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;
  }
}
Pour aller plus loin : Angular 21 introduit les host directives qui permettent de composer des comportements sur un composant sans héritage — une fonctionnalité encore plus puissante que require.

Partager