Smithery Logo
MCPsSkillsDocsPricing
Login
NewFlame, an assistant that learns and improves. Available onTelegramSlack
    neversight

    angular-directives

    neversight/angular-directives
    Coding
    2

    About

    SKILL.md

    Install

    • Telegram
      Telegram
    • Slack
      Slack
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    • Download skill
    ├─
    ├─
    └─
    Smithery Logo

    Give agents more agency

    Resources

    DocumentationPrivacy PolicySystem Status

    Company

    PricingAboutBlog

    Connect

    © 2026 Smithery. All rights reserved.

    About

    Create custom directives in Angular v20+ for DOM manipulation and behavior extension...

    SKILL.md

    Angular Directives

    Create custom directives for reusable DOM manipulation and behavior in Angular v20+.

    Attribute Directives

    Modify the appearance or behavior of an element:

    import { Directive, input, effect, inject, ElementRef } from '@angular/core';
    
    @Directive({
      selector: '[appHighlight]',
    })
    export class HighlightDirective {
      private el = inject(ElementRef<HTMLElement>);
      
      // Input with alias matching selector
      color = input('yellow', { alias: 'appHighlight' });
      
      constructor() {
        effect(() => {
          this.el.nativeElement.style.backgroundColor = this.color();
        });
      }
    }
    
    // Usage: <p appHighlight="lightblue">Highlighted text</p>
    // Usage: <p appHighlight>Default yellow highlight</p>
    

    Using host Property

    Prefer host over @HostBinding/@HostListener:

    @Directive({
      selector: '[appTooltip]',
      host: {
        '(mouseenter)': 'show()',
        '(mouseleave)': 'hide()',
        '[attr.aria-describedby]': 'tooltipId',
      },
    })
    export class TooltipDirective {
      text = input.required<string>({ alias: 'appTooltip' });
      position = input<'top' | 'bottom' | 'left' | 'right'>('top');
      
      tooltipId = `tooltip-${crypto.randomUUID()}`;
      private tooltipEl: HTMLElement | null = null;
      private el = inject(ElementRef<HTMLElement>);
      
      show() {
        this.tooltipEl = document.createElement('div');
        this.tooltipEl.id = this.tooltipId;
        this.tooltipEl.className = `tooltip tooltip-${this.position()}`;
        this.tooltipEl.textContent = this.text();
        this.tooltipEl.setAttribute('role', 'tooltip');
        document.body.appendChild(this.tooltipEl);
        this.positionTooltip();
      }
      
      hide() {
        this.tooltipEl?.remove();
        this.tooltipEl = null;
      }
      
      private positionTooltip() {
        // Position logic based on this.position() and this.el
      }
    }
    
    // Usage: <button appTooltip="Click to save" position="bottom">Save</button>
    

    Class and Style Manipulation

    @Directive({
      selector: '[appButton]',
      host: {
        'class': 'btn',
        '[class.btn-primary]': 'variant() === "primary"',
        '[class.btn-secondary]': 'variant() === "secondary"',
        '[class.btn-sm]': 'size() === "small"',
        '[class.btn-lg]': 'size() === "large"',
        '[class.disabled]': 'disabled()',
        '[attr.disabled]': 'disabled() || null',
      },
    })
    export class ButtonDirective {
      variant = input<'primary' | 'secondary'>('primary');
      size = input<'small' | 'medium' | 'large'>('medium');
      disabled = input(false, { transform: booleanAttribute });
    }
    
    // Usage: <button appButton variant="primary" size="large">Click</button>
    

    Event Handling

    @Directive({
      selector: '[appClickOutside]',
      host: {
        '(document:click)': 'onDocumentClick($event)',
      },
    })
    export class ClickOutsideDirective {
      private el = inject(ElementRef<HTMLElement>);
      
      clickOutside = output<void>();
      
      onDocumentClick(event: MouseEvent) {
        if (!this.el.nativeElement.contains(event.target as Node)) {
          this.clickOutside.emit();
        }
      }
    }
    
    // Usage: <div appClickOutside (clickOutside)="closeMenu()">...</div>
    

    Keyboard Shortcuts

    @Directive({
      selector: '[appShortcut]',
      host: {
        '(document:keydown)': 'onKeydown($event)',
      },
    })
    export class ShortcutDirective {
      key = input.required<string>({ alias: 'appShortcut' });
      ctrl = input(false, { transform: booleanAttribute });
      shift = input(false, { transform: booleanAttribute });
      alt = input(false, { transform: booleanAttribute });
      
      triggered = output<KeyboardEvent>();
      
      onKeydown(event: KeyboardEvent) {
        const keyMatch = event.key.toLowerCase() === this.key().toLowerCase();
        const ctrlMatch = this.ctrl() ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey;
        const shiftMatch = this.shift() ? event.shiftKey : !event.shiftKey;
        const altMatch = this.alt() ? event.altKey : !event.altKey;
        
        if (keyMatch && ctrlMatch && shiftMatch && altMatch) {
          event.preventDefault();
          this.triggered.emit(event);
        }
      }
    }
    
    // Usage: <button appShortcut="s" ctrl (triggered)="save()">Save (Ctrl+S)</button>
    

    Structural Directives

    Use structural directives for DOM manipulation beyond control flow (portals, overlays, dynamic insertion points). For conditionals and loops, use native @if, @for, @switch.

    Portal Directive

    Render content in a different DOM location:

    import { Directive, inject, TemplateRef, ViewContainerRef, OnInit, OnDestroy, input } from '@angular/core';
    
    @Directive({
      selector: '[appPortal]',
    })
    export class PortalDirective implements OnInit, OnDestroy {
      private templateRef = inject(TemplateRef<any>);
      private viewContainerRef = inject(ViewContainerRef);
      private viewRef: EmbeddedViewRef<any> | null = null;
      
      // Target container selector or element
      target = input<string | HTMLElement>('body', { alias: 'appPortal' });
      
      ngOnInit() {
        const container = this.getContainer();
        if (container) {
          this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef);
          this.viewRef.rootNodes.forEach(node => container.appendChild(node));
        }
      }
      
      ngOnDestroy() {
        this.viewRef?.destroy();
      }
      
      private getContainer(): HTMLElement | null {
        const target = this.target();
        if (typeof target === 'string') {
          return document.querySelector(target);
        }
        return target;
      }
    }
    
    // Usage: Render modal at body level
    // <div *appPortal="'body'">
    //   <div class="modal">Modal content</div>
    // </div>
    

    Lazy Render Directive

    Defer rendering until condition is met (one-time):

    @Directive({
      selector: '[appLazyRender]',
    })
    export class LazyRenderDirective {
      private templateRef = inject(TemplateRef<any>);
      private viewContainer = inject(ViewContainerRef);
      private rendered = false;
      
      condition = input.required<boolean>({ alias: 'appLazyRender' });
      
      constructor() {
        effect(() => {
          // Only render once when condition becomes true
          if (this.condition() && !this.rendered) {
            this.viewContainer.createEmbeddedView(this.templateRef);
            this.rendered = true;
          }
        });
      }
    }
    
    // Usage: Render heavy component only when tab is first activated
    // <div *appLazyRender="activeTab() === 'reports'">
    //   <app-heavy-reports />
    // </div>
    

    Template Outlet with Context

    interface TemplateContext<T> {
      $implicit: T;
      item: T;
      index: number;
    }
    
    @Directive({
      selector: '[appTemplateOutlet]',
    })
    export class TemplateOutletDirective<T> {
      private viewContainer = inject(ViewContainerRef);
      private currentView: EmbeddedViewRef<TemplateContext<T>> | null = null;
      
      template = input.required<TemplateRef<TemplateContext<T>>>({ alias: 'appTemplateOutlet' });
      context = input.required<T>({ alias: 'appTemplateOutletContext' });
      index = input(0, { alias: 'appTemplateOutletIndex' });
      
      constructor() {
        effect(() => {
          const template = this.template();
          const context = this.context();
          const index = this.index();
          
          if (this.currentView) {
            this.currentView.context.$implicit = context;
            this.currentView.context.item = context;
            this.currentView.context.index = index;
            this.currentView.markForCheck();
          } else {
            this.currentView = this.viewContainer.createEmbeddedView(template, {
              $implicit: context,
              item: context,
              index,
            });
          }
        });
      }
    }
    
    // Usage: Custom list with template
    // <ng-template #itemTemplate let-item let-i="index">
    //   <div>{{ i }}: {{ item.name }}</div>
    // </ng-template>
    // <ng-container 
    //   *appTemplateOutlet="itemTemplate; context: item; index: i"
    // />
    

    Host Directives

    Compose directives on components or other directives:

    // Reusable behavior directives
    @Directive({
      selector: '[focusable]',
      host: {
        'tabindex': '0',
        '(focus)': 'onFocus()',
        '(blur)': 'onBlur()',
        '[class.focused]': 'isFocused()',
      },
    })
    export class FocusableDirective {
      isFocused = signal(false);
      
      onFocus() { this.isFocused.set(true); }
      onBlur() { this.isFocused.set(false); }
    }
    
    @Directive({
      selector: '[disableable]',
      host: {
        '[class.disabled]': 'disabled()',
        '[attr.aria-disabled]': 'disabled()',
      },
    })
    export class DisableableDirective {
      disabled = input(false, { transform: booleanAttribute });
    }
    
    // Component using host directives
    @Component({
      selector: 'app-custom-button',
      hostDirectives: [
        FocusableDirective,
        {
          directive: DisableableDirective,
          inputs: ['disabled'],
        },
      ],
      host: {
        'role': 'button',
        '(click)': 'onClick($event)',
        '(keydown.enter)': 'onClick($event)',
        '(keydown.space)': 'onClick($event)',
      },
      template: `<ng-content />`,
    })
    export class CustomButtonComponent {
      private disableable = inject(DisableableDirective);
      
      clicked = output<void>();
      
      onClick(event: Event) {
        if (!this.disableable.disabled()) {
          this.clicked.emit();
        }
      }
    }
    
    // Usage: <app-custom-button disabled>Click me</app-custom-button>
    

    Exposing Host Directive Outputs

    @Directive({
      selector: '[hoverable]',
      host: {
        '(mouseenter)': 'onEnter()',
        '(mouseleave)': 'onLeave()',
        '[class.hovered]': 'isHovered()',
      },
    })
    export class HoverableDirective {
      isHovered = signal(false);
      
      hoverChange = output<boolean>();
      
      onEnter() {
        this.isHovered.set(true);
        this.hoverChange.emit(true);
      }
      
      onLeave() {
        this.isHovered.set(false);
        this.hoverChange.emit(false);
      }
    }
    
    @Component({
      selector: 'app-card',
      hostDirectives: [
        {
          directive: HoverableDirective,
          outputs: ['hoverChange'],
        },
      ],
      template: `<ng-content />`,
    })
    export class CardComponent {}
    
    // Usage: <app-card (hoverChange)="onHover($event)">...</app-card>
    

    Directive Composition API

    Combine multiple behaviors:

    // Base directives
    @Directive({ selector: '[withRipple]' })
    export class RippleDirective {
      // Ripple effect implementation
    }
    
    @Directive({ selector: '[withElevation]' })
    export class ElevationDirective {
      elevation = input(2);
    }
    
    // Composed component
    @Component({
      selector: 'app-material-button',
      hostDirectives: [
        RippleDirective,
        {
          directive: ElevationDirective,
          inputs: ['elevation'],
        },
        {
          directive: DisableableDirective,
          inputs: ['disabled'],
        },
      ],
      template: `<ng-content />`,
    })
    export class MaterialButtonComponent {}
    

    For advanced patterns, see references/directive-patterns.md.

    Recommended Servers
    StudioMeyer-Crew
    StudioMeyer-Crew
    Browser tool
    Browser tool
    Blockscout MCP Server
    Blockscout MCP Server
    Repository
    neversight/skills_feed