[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6F2Z59DR2694BQBPP29CGG7":3,"skill-same:01M6F2Z59DR2694BQBPP29CGG7":66},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":43,"commitSha":44,"skillDirectory":45,"forksCount":46,"repositoryHtmlUrl":47,"repositoryHomepageUrl":48,"ownerHtmlUrl":49,"ownerBlogUrl":6,"ownerDescription":50,"updatedAt":51,"install":52,"resources":65},"01M6F2Z59DR2694BQBPP29CGG7","angular-directives",null,"Create custom directives in Angular v20+ for DOM manipulation and behavior extension. Use for attribute directives that modify element behavior\u002Fappearance, structural directives for portals\u002Foverlays, and host directives for composition. Triggers on creating reusable DOM behaviors, extending element functionality, or composing behaviors across components. Note - use native @if\u002F@for\u002F@switch for control flow, not custom structural directives.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",67,"MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fanalogjs\u002Fangular-skills\u002Fangular-directives\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"angular directives","angular","across","note","native","element","extension","host","triggers","control","structural","components","switch","reusable","use","behavior","Composition","flow","create","2026-11-10 10:12:30",false,"---\nname: angular-directives\ndescription: Create custom directives in Angular v20+ for DOM manipulation and behavior extension. Use for attribute directives that modify element behavior\u002Fappearance, structural directives for portals\u002Foverlays, and host directives for composition. Triggers on creating reusable DOM behaviors, extending element functionality, or composing behaviors across components. Note - use native @if\u002F@for\u002F@switch for control flow, not custom structural directives.\n---\n\n# Angular Directives\n\nCreate custom directives for reusable DOM manipulation and behavior in Angular v20+.\n\n## Attribute Directives\n\nModify the appearance or behavior of an element:\n\n```typescript\nimport { Directive, input, effect, inject, ElementRef } from '@angular\u002Fcore';\n\n@Directive({\n  selector: '[appHighlight]',\n})\nexport class Highlight {\n  private el = inject(ElementRef\u003CHTMLElement>);\n  \n  \u002F\u002F Input with alias matching selector\n  color = input('yellow', { alias: 'appHighlight' });\n  \n  constructor() {\n    effect(() => {\n      this.el.nativeElement.style.backgroundColor = this.color();\n    });\n  }\n}\n\n\u002F\u002F Usage: \u003Cp appHighlight=\"lightblue\">Highlighted text\u003C\u002Fp>\n\u002F\u002F Usage: \u003Cp appHighlight>Default yellow highlight\u003C\u002Fp>\n```\n\n### Using host Property\n\nPrefer `host` over `@HostBinding`\u002F`@HostListener`:\n\n```typescript\n@Directive({\n  selector: '[appTooltip]',\n  host: {\n    '(mouseenter)': 'show()',\n    '(mouseleave)': 'hide()',\n    '[attr.aria-describedby]': 'tooltipId',\n  },\n})\nexport class Tooltip {\n  text = input.required\u003Cstring>({ alias: 'appTooltip' });\n  position = input\u003C'top' | 'bottom' | 'left' | 'right'>('top');\n  \n  tooltipId = `tooltip-${crypto.randomUUID()}`;\n  private tooltipEl: HTMLElement | null = null;\n  private el = inject(ElementRef\u003CHTMLElement>);\n  \n  show() {\n    this.tooltipEl = document.createElement('div');\n    this.tooltipEl.id = this.tooltipId;\n    this.tooltipEl.className = `tooltip tooltip-${this.position()}`;\n    this.tooltipEl.textContent = this.text();\n    this.tooltipEl.setAttribute('role', 'tooltip');\n    document.body.appendChild(this.tooltipEl);\n    this.positionTooltip();\n  }\n  \n  hide() {\n    this.tooltipEl?.remove();\n    this.tooltipEl = null;\n  }\n  \n  private positionTooltip() {\n    \u002F\u002F Position logic based on this.position() and this.el\n  }\n}\n\n\u002F\u002F Usage: \u003Cbutton appTooltip=\"Click to save\" position=\"bottom\">Save\u003C\u002Fbutton>\n```\n\n### Class and Style Manipulation\n\n```typescript\n@Directive({\n  selector: '[appButton]',\n  host: {\n    'class': 'btn',\n    '[class.btn-primary]': 'variant() === \"primary\"',\n    '[class.btn-secondary]': 'variant() === \"secondary\"',\n    '[class.btn-sm]': 'size() === \"small\"',\n    '[class.btn-lg]': 'size() === \"large\"',\n    '[class.disabled]': 'disabled()',\n    '[attr.disabled]': 'disabled() || null',\n  },\n})\nexport class Button {\n  variant = input\u003C'primary' | 'secondary'>('primary');\n  size = input\u003C'small' | 'medium' | 'large'>('medium');\n  disabled = input(false, { transform: booleanAttribute });\n}\n\n\u002F\u002F Usage: \u003Cbutton appButton variant=\"primary\" size=\"large\">Click\u003C\u002Fbutton>\n```\n\n### Event Handling\n\n```typescript\n@Directive({\n  selector: '[appClickOutside]',\n  host: {\n    '(document:click)': 'onDocumentClick($event)',\n  },\n})\nexport class ClickOutside {\n  private el = inject(ElementRef\u003CHTMLElement>);\n  \n  clickOutside = output\u003Cvoid>();\n  \n  onDocumentClick(event: MouseEvent) {\n    if (!this.el.nativeElement.contains(event.target as Node)) {\n      this.clickOutside.emit();\n    }\n  }\n}\n\n\u002F\u002F Usage: \u003Cdiv appClickOutside (clickOutside)=\"closeMenu()\">...\u003C\u002Fdiv>\n```\n\n### Keyboard Shortcuts\n\n```typescript\n@Directive({\n  selector: '[appShortcut]',\n  host: {\n    '(document:keydown)': 'onKeydown($event)',\n  },\n})\nexport class Shortcut {\n  key = input.required\u003Cstring>({ alias: 'appShortcut' });\n  ctrl = input(false, { transform: booleanAttribute });\n  shift = input(false, { transform: booleanAttribute });\n  alt = input(false, { transform: booleanAttribute });\n  \n  triggered = output\u003CKeyboardEvent>();\n  \n  onKeydown(event: KeyboardEvent) {\n    const keyMatch = event.key.toLowerCase() === this.key().toLowerCase();\n    const ctrlMatch = this.ctrl() ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey;\n    const shiftMatch = this.shift() ? event.shiftKey : !event.shiftKey;\n    const altMatch = this.alt() ? event.altKey : !event.altKey;\n    \n    if (keyMatch && ctrlMatch && shiftMatch && altMatch) {\n      event.preventDefault();\n      this.triggered.emit(event);\n    }\n  }\n}\n\n\u002F\u002F Usage: \u003Cbutton appShortcut=\"s\" ctrl (triggered)=\"save()\">Save (Ctrl+S)\u003C\u002Fbutton>\n```\n\n## Structural Directives\n\nUse structural directives for DOM manipulation beyond control flow (portals, overlays, dynamic insertion points). For conditionals and loops, use native `@if`, `@for`, `@switch`.\n\n### Portal Directive\n\nRender content in a different DOM location:\n\n```typescript\nimport { Directive, inject, TemplateRef, ViewContainerRef, OnInit, OnDestroy, input } from '@angular\u002Fcore';\n\n@Directive({\n  selector: '[appPortal]',\n})\nexport class Portal implements OnInit, OnDestroy {\n  private templateRef = inject(TemplateRef\u003Cany>);\n  private viewContainerRef = inject(ViewContainerRef);\n  private viewRef: EmbeddedViewRef\u003Cany> | null = null;\n  \n  \u002F\u002F Target container selector or element\n  target = input\u003Cstring | HTMLElement>('body', { alias: 'appPortal' });\n  \n  ngOnInit() {\n    const container = this.getContainer();\n    if (container) {\n      this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef);\n      this.viewRef.rootNodes.forEach(node => container.appendChild(node));\n    }\n  }\n  \n  ngOnDestroy() {\n    this.viewRef?.destroy();\n  }\n  \n  private getContainer(): HTMLElement | null {\n    const target = this.target();\n    if (typeof target === 'string') {\n      return document.querySelector(target);\n    }\n    return target;\n  }\n}\n\n\u002F\u002F Usage: Render modal at body level\n\u002F\u002F \u003Cdiv *appPortal=\"'body'\">\n\u002F\u002F   \u003Cdiv class=\"modal\">Modal content\u003C\u002Fdiv>\n\u002F\u002F \u003C\u002Fdiv>\n```\n\n### Lazy Render Directive\n\nDefer rendering until condition is met (one-time):\n\n```typescript\n@Directive({\n  selector: '[appLazyRender]',\n})\nexport class LazyRender {\n  private templateRef = inject(TemplateRef\u003Cany>);\n  private viewContainer = inject(ViewContainerRef);\n  private rendered = false;\n  \n  condition = input.required\u003Cboolean>({ alias: 'appLazyRender' });\n  \n  constructor() {\n    effect(() => {\n      \u002F\u002F Only render once when condition becomes true\n      if (this.condition() && !this.rendered) {\n        this.viewContainer.createEmbeddedView(this.templateRef);\n        this.rendered = true;\n      }\n    });\n  }\n}\n\n\u002F\u002F Usage: Render heavy component only when tab is first activated\n\u002F\u002F \u003Cdiv *appLazyRender=\"activeTab() === 'reports'\">\n\u002F\u002F   \u003Capp-heavy-reports \u002F>\n\u002F\u002F \u003C\u002Fdiv>\n```\n\n### Template Outlet with Context\n\n```typescript\ninterface TemplateContext\u003CT> {\n  $implicit: T;\n  item: T;\n  index: number;\n}\n\n@Directive({\n  selector: '[appTemplateOutlet]',\n})\nexport class TemplateOutlet\u003CT> {\n  private viewContainer = inject(ViewContainerRef);\n  private currentView: EmbeddedViewRef\u003CTemplateContext\u003CT>> | null = null;\n  \n  template = input.required\u003CTemplateRef\u003CTemplateContext\u003CT>>>({ alias: 'appTemplateOutlet' });\n  context = input.required\u003CT>({ alias: 'appTemplateOutletContext' });\n  index = input(0, { alias: 'appTemplateOutletIndex' });\n  \n  constructor() {\n    effect(() => {\n      const template = this.template();\n      const context = this.context();\n      const index = this.index();\n      \n      if (this.currentView) {\n        this.currentView.context.$implicit = context;\n        this.currentView.context.item = context;\n        this.currentView.context.index = index;\n        this.currentView.markForCheck();\n      } else {\n        this.currentView = this.viewContainer.createEmbeddedView(template, {\n          $implicit: context,\n          item: context,\n          index,\n        });\n      }\n    });\n  }\n}\n\n\u002F\u002F Usage: Custom list with template\n\u002F\u002F \u003Cng-template #itemTemplate let-item let-i=\"index\">\n\u002F\u002F   \u003Cdiv>{{ i }}: {{ item.name }}\u003C\u002Fdiv>\n\u002F\u002F \u003C\u002Fng-template>\n\u002F\u002F \u003Cng-container \n\u002F\u002F   *appTemplateOutlet=\"itemTemplate; context: item; index: i\"\n\u002F\u002F \u002F>\n```\n\n## Host Directives\n\nCompose directives on components or other directives:\n\n```typescript\n\u002F\u002F Reusable behavior directives\n@Directive({\n  selector: '[focusable]',\n  host: {\n    'tabindex': '0',\n    '(focus)': 'onFocus()',\n    '(blur)': 'onBlur()',\n    '[class.focused]': 'isFocused()',\n  },\n})\nexport class Focusable {\n  isFocused = signal(false);\n  \n  onFocus() { this.isFocused.set(true); }\n  onBlur() { this.isFocused.set(false); }\n}\n\n@Directive({\n  selector: '[disableable]',\n  host: {\n    '[class.disabled]': 'disabled()',\n    '[attr.aria-disabled]': 'disabled()',\n  },\n})\nexport class Disableable {\n  disabled = input(false, { transform: booleanAttribute });\n}\n\n\u002F\u002F Component using host directives\n@Component({\n  selector: 'app-custom-button',\n  hostDirectives: [\n    Focusable,\n    {\n      directive: Disableable,\n      inputs: ['disabled'],\n    },\n  ],\n  host: {\n    'role': 'button',\n    '(click)': 'onClick($event)',\n    '(keydown.enter)': 'onClick($event)',\n    '(keydown.space)': 'onClick($event)',\n  },\n  template: `\u003Cng-content \u002F>`,\n})\nexport class CustomButton {\n  private disableable = inject(Disableable);\n  \n  clicked = output\u003Cvoid>();\n  \n  onClick(event: Event) {\n    if (!this.disableable.disabled()) {\n      this.clicked.emit();\n    }\n  }\n}\n\n\u002F\u002F Usage: \u003Capp-custom-button disabled>Click me\u003C\u002Fapp-custom-button>\n```\n\n### Exposing Host Directive Outputs\n\n```typescript\n@Directive({\n  selector: '[hoverable]',\n  host: {\n    '(mouseenter)': 'onEnter()',\n    '(mouseleave)': 'onLeave()',\n    '[class.hovered]': 'isHovered()',\n  },\n})\nexport class Hoverable {\n  isHovered = signal(false);\n  \n  hoverChange = output\u003Cboolean>();\n  \n  onEnter() {\n    this.isHovered.set(true);\n    this.hoverChange.emit(true);\n  }\n  \n  onLeave() {\n    this.isHovered.set(false);\n    this.hoverChange.emit(false);\n  }\n}\n\n@Component({\n  selector: 'app-card',\n  hostDirectives: [\n    {\n      directive: Hoverable,\n      outputs: ['hoverChange'],\n    },\n  ],\n  template: `\u003Cng-content \u002F>`,\n})\nexport class Card {}\n\n\u002F\u002F Usage: \u003Capp-card (hoverChange)=\"onHover($event)\">...\u003C\u002Fapp-card>\n```\n\n## Directive Composition API\n\nCombine multiple behaviors:\n\n```typescript\n\u002F\u002F Base directives\n@Directive({ selector: '[withRipple]' })\nexport class Ripple {\n  \u002F\u002F Ripple effect implementation\n}\n\n@Directive({ selector: '[withElevation]' })\nexport class Elevation {\n  elevation = input(2);\n}\n\n\u002F\u002F Composed component\n@Component({\n  selector: 'app-material-button',\n  hostDirectives: [\n    Ripple,\n    {\n      directive: Elevation,\n      inputs: ['elevation'],\n    },\n    {\n      directive: Disableable,\n      inputs: ['disabled'],\n    },\n  ],\n  template: `\u003Cng-content \u002F>`,\n})\nexport class MaterialButton {}\n```\n\nFor advanced patterns, see [references\u002Fdirective-patterns.md](references\u002Fdirective-patterns.md).\n\n","# Angular Directives\n\nCreate custom directives for reusable DOM manipulation and behavior in Angular v20+.\n\n## Attribute Directives\n\nModify the appearance or behavior of an element:\n\n```typescript\nimport { Directive, input, effect, inject, ElementRef } from '@angular\u002Fcore';\n\n@Directive({\n  selector: '[appHighlight]',\n})\nexport class Highlight {\n  private el = inject(ElementRef\u003CHTMLElement>);\n  \n  \u002F\u002F Input with alias matching selector\n  color = input('yellow', { alias: 'appHighlight' });\n  \n  constructor() {\n    effect(() => {\n      this.el.nativeElement.style.backgroundColor = this.color();\n    });\n  }\n}\n\n\u002F\u002F Usage: \u003Cp appHighlight=\"lightblue\">Highlighted text\u003C\u002Fp>\n\u002F\u002F Usage: \u003Cp appHighlight>Default yellow highlight\u003C\u002Fp>\n```\n\n### Using host Property\n\nPrefer `host` over `@HostBinding`\u002F`@HostListener`:\n\n```typescript\n@Directive({\n  selector: '[appTooltip]',\n  host: {\n    '(mouseenter)': 'show()',\n    '(mouseleave)': 'hide()',\n    '[attr.aria-describedby]': 'tooltipId',\n  },\n})\nexport class Tooltip {\n  text = input.required\u003Cstring>({ alias: 'appTooltip' });\n  position = input\u003C'top' | 'bottom' | 'left' | 'right'>('top');\n  \n  tooltipId = `tooltip-${crypto.randomUUID()}`;\n  private tooltipEl: HTMLElement | null = null;\n  private el = inject(ElementRef\u003CHTMLElement>);\n  \n  show() {\n    this.tooltipEl = document.createElement('div');\n    this.tooltipEl.id = this.tooltipId;\n    this.tooltipEl.className = `tooltip tooltip-${this.position()}`;\n    this.tooltipEl.textContent = this.text();\n    this.tooltipEl.setAttribute('role', 'tooltip');\n    document.body.appendChild(this.tooltipEl);\n    this.positionTooltip();\n  }\n  \n  hide() {\n    this.tooltipEl?.remove();\n    this.tooltipEl = null;\n  }\n  \n  private positionTooltip() {\n    \u002F\u002F Position logic based on this.position() and this.el\n  }\n}\n\n\u002F\u002F Usage: \u003Cbutton appTooltip=\"Click to save\" position=\"bottom\">Save\u003C\u002Fbutton>\n```\n\n### Class and Style Manipulation\n\n```typescript\n@Directive({\n  selector: '[appButton]',\n  host: {\n    'class': 'btn',\n    '[class.btn-primary]': 'variant() === \"primary\"',\n    '[class.btn-secondary]': 'variant() === \"secondary\"',\n    '[class.btn-sm]': 'size() === \"small\"',\n    '[class.btn-lg]': 'size() === \"large\"',\n    '[class.disabled]': 'disabled()',\n    '[attr.disabled]': 'disabled() || null',\n  },\n})\nexport class Button {\n  variant = input\u003C'primary' | 'secondary'>('primary');\n  size = input\u003C'small' | 'medium' | 'large'>('medium');\n  disabled = input(false, { transform: booleanAttribute });\n}\n\n\u002F\u002F Usage: \u003Cbutton appButton variant=\"primary\" size=\"large\">Click\u003C\u002Fbutton>\n```\n\n### Event Handling\n\n```typescript\n@Directive({\n  selector: '[appClickOutside]',\n  host: {\n    '(document:click)': 'onDocumentClick($event)',\n  },\n})\nexport class ClickOutside {\n  private el = inject(ElementRef\u003CHTMLElement>);\n  \n  clickOutside = output\u003Cvoid>();\n  \n  onDocumentClick(event: MouseEvent) {\n    if (!this.el.nativeElement.contains(event.target as Node)) {\n      this.clickOutside.emit();\n    }\n  }\n}\n\n\u002F\u002F Usage: \u003Cdiv appClickOutside (clickOutside)=\"closeMenu()\">...\u003C\u002Fdiv>\n```\n\n### Keyboard Shortcuts\n\n```typescript\n@Directive({\n  selector: '[appShortcut]',\n  host: {\n    '(document:keydown)': 'onKeydown($event)',\n  },\n})\nexport class Shortcut {\n  key = input.required\u003Cstring>({ alias: 'appShortcut' });\n  ctrl = input(false, { transform: booleanAttribute });\n  shift = input(false, { transform: booleanAttribute });\n  alt = input(false, { transform: booleanAttribute });\n  \n  triggered = output\u003CKeyboardEvent>();\n  \n  onKeydown(event: KeyboardEvent) {\n    const keyMatch = event.key.toLowerCase() === this.key().toLowerCase();\n    const ctrlMatch = this.ctrl() ? event.ctrlKey || event.metaKey : !event.ctrlKey && !event.metaKey;\n    const shiftMatch = this.shift() ? event.shiftKey : !event.shiftKey;\n    const altMatch = this.alt() ? event.altKey : !event.altKey;\n    \n    if (keyMatch && ctrlMatch && shiftMatch && altMatch) {\n      event.preventDefault();\n      this.triggered.emit(event);\n    }\n  }\n}\n\n\u002F\u002F Usage: \u003Cbutton appShortcut=\"s\" ctrl (triggered)=\"save()\">Save (Ctrl+S)\u003C\u002Fbutton>\n```\n\n## Structural Directives\n\nUse structural directives for DOM manipulation beyond control flow (portals, overlays, dynamic insertion points). For conditionals and loops, use native `@if`, `@for`, `@switch`.\n\n### Portal Directive\n\nRender content in a different DOM location:\n\n```typescript\nimport { Directive, inject, TemplateRef, ViewContainerRef, OnInit, OnDestroy, input } from '@angular\u002Fcore';\n\n@Directive({\n  selector: '[appPortal]',\n})\nexport class Portal implements OnInit, OnDestroy {\n  private templateRef = inject(TemplateRef\u003Cany>);\n  private viewContainerRef = inject(ViewContainerRef);\n  private viewRef: EmbeddedViewRef\u003Cany> | null = null;\n  \n  \u002F\u002F Target container selector or element\n  target = input\u003Cstring | HTMLElement>('body', { alias: 'appPortal' });\n  \n  ngOnInit() {\n    const container = this.getContainer();\n    if (container) {\n      this.viewRef = this.viewContainerRef.createEmbeddedView(this.templateRef);\n      this.viewRef.rootNodes.forEach(node => container.appendChild(node));\n    }\n  }\n  \n  ngOnDestroy() {\n    this.viewRef?.destroy();\n  }\n  \n  private getContainer(): HTMLElement | null {\n    const target = this.target();\n    if (typeof target === 'string') {\n      return document.querySelector(target);\n    }\n    return target;\n  }\n}\n\n\u002F\u002F Usage: Render modal at body level\n\u002F\u002F \u003Cdiv *appPortal=\"'body'\">\n\u002F\u002F   \u003Cdiv class=\"modal\">Modal content\u003C\u002Fdiv>\n\u002F\u002F \u003C\u002Fdiv>\n```\n\n### Lazy Render Directive\n\nDefer rendering until condition is met (one-time):\n\n```typescript\n@Directive({\n  selector: '[appLazyRender]',\n})\nexport class LazyRender {\n  private templateRef = inject(TemplateRef\u003Cany>);\n  private viewContainer = inject(ViewContainerRef);\n  private rendered = false;\n  \n  condition = input.required\u003Cboolean>({ alias: 'appLazyRender' });\n  \n  constructor() {\n    effect(() => {\n      \u002F\u002F Only render once when condition becomes true\n      if (this.condition() && !this.rendered) {\n        this.viewContainer.createEmbeddedView(this.templateRef);\n        this.rendered = true;\n      }\n    });\n  }\n}\n\n\u002F\u002F Usage: Render heavy component only when tab is first activated\n\u002F\u002F \u003Cdiv *appLazyRender=\"activeTab() === 'reports'\">\n\u002F\u002F   \u003Capp-heavy-reports \u002F>\n\u002F\u002F \u003C\u002Fdiv>\n```\n\n### Template Outlet with Context\n\n```typescript\ninterface TemplateContext\u003CT> {\n  $implicit: T;\n  item: T;\n  index: number;\n}\n\n@Directive({\n  selector: '[appTemplateOutlet]',\n})\nexport class TemplateOutlet\u003CT> {\n  private viewContainer = inject(ViewContainerRef);\n  private currentView: EmbeddedViewRef\u003CTemplateContext\u003CT>> | null = null;\n  \n  template = input.required\u003CTemplateRef\u003CTemplateContext\u003CT>>>({ alias: 'appTemplateOutlet' });\n  context = input.required\u003CT>({ alias: 'appTemplateOutletContext' });\n  index = input(0, { alias: 'appTemplateOutletIndex' });\n  \n  constructor() {\n    effect(() => {\n      const template = this.template();\n      const context = this.context();\n      const index = this.index();\n      \n      if (this.currentView) {\n        this.currentView.context.$implicit = context;\n        this.currentView.context.item = context;\n        this.currentView.context.index = index;\n        this.currentView.markForCheck();\n      } else {\n        this.currentView = this.viewContainer.createEmbeddedView(template, {\n          $implicit: context,\n          item: context,\n          index,\n        });\n      }\n    });\n  }\n}\n\n\u002F\u002F Usage: Custom list with template\n\u002F\u002F \u003Cng-template #itemTemplate let-item let-i=\"index\">\n\u002F\u002F   \u003Cdiv>{{ i }}: {{ item.name }}\u003C\u002Fdiv>\n\u002F\u002F \u003C\u002Fng-template>\n\u002F\u002F \u003Cng-container \n\u002F\u002F   *appTemplateOutlet=\"itemTemplate; context: item; index: i\"\n\u002F\u002F \u002F>\n```\n\n## Host Directives\n\nCompose directives on components or other directives:\n\n```typescript\n\u002F\u002F Reusable behavior directives\n@Directive({\n  selector: '[focusable]',\n  host: {\n    'tabindex': '0',\n    '(focus)': 'onFocus()',\n    '(blur)': 'onBlur()',\n    '[class.focused]': 'isFocused()',\n  },\n})\nexport class Focusable {\n  isFocused = signal(false);\n  \n  onFocus() { this.isFocused.set(true); }\n  onBlur() { this.isFocused.set(false); }\n}\n\n@Directive({\n  selector: '[disableable]',\n  host: {\n    '[class.disabled]': 'disabled()',\n    '[attr.aria-disabled]': 'disabled()',\n  },\n})\nexport class Disableable {\n  disabled = input(false, { transform: booleanAttribute });\n}\n\n\u002F\u002F Component using host directives\n@Component({\n  selector: 'app-custom-button',\n  hostDirectives: [\n    Focusable,\n    {\n      directive: Disableable,\n      inputs: ['disabled'],\n    },\n  ],\n  host: {\n    'role': 'button',\n    '(click)': 'onClick($event)',\n    '(keydown.enter)': 'onClick($event)',\n    '(keydown.space)': 'onClick($event)',\n  },\n  template: `\u003Cng-content \u002F>`,\n})\nexport class CustomButton {\n  private disableable = inject(Disableable);\n  \n  clicked = output\u003Cvoid>();\n  \n  onClick(event: Event) {\n    if (!this.disableable.disabled()) {\n      this.clicked.emit();\n    }\n  }\n}\n\n\u002F\u002F Usage: \u003Capp-custom-button disabled>Click me\u003C\u002Fapp-custom-button>\n```\n\n### Exposing Host Directive Outputs\n\n```typescript\n@Directive({\n  selector: '[hoverable]',\n  host: {\n    '(mouseenter)': 'onEnter()',\n    '(mouseleave)': 'onLeave()',\n    '[class.hovered]': 'isHovered()',\n  },\n})\nexport class Hoverable {\n  isHovered = signal(false);\n  \n  hoverChange = output\u003Cboolean>();\n  \n  onEnter() {\n    this.isHovered.set(true);\n    this.hoverChange.emit(true);\n  }\n  \n  onLeave() {\n    this.isHovered.set(false);\n    this.hoverChange.emit(false);\n  }\n}\n\n@Component({\n  selector: 'app-card',\n  hostDirectives: [\n    {\n      directive: Hoverable,\n      outputs: ['hoverChange'],\n    },\n  ],\n  template: `\u003Cng-content \u002F>`,\n})\nexport class Card {}\n\n\u002F\u002F Usage: \u003Capp-card (hoverChange)=\"onHover($event)\">...\u003C\u002Fapp-card>\n```\n\n## Directive Composition API\n\nCombine multiple behaviors:\n\n```typescript\n\u002F\u002F Base directives\n@Directive({ selector: '[withRipple]' })\nexport class Ripple {\n  \u002F\u002F Ripple effect implementation\n}\n\n@Directive({ selector: '[withElevation]' })\nexport class Elevation {\n  elevation = input(2);\n}\n\n\u002F\u002F Composed component\n@Component({\n  selector: 'app-material-button',\n  hostDirectives: [\n    Ripple,\n    {\n      directive: Elevation,\n      inputs: ['elevation'],\n    },\n    {\n      directive: Disableable,\n      inputs: ['disabled'],\n    },\n  ],\n  template: `\u003Cng-content \u002F>`,\n})\nexport class MaterialButton {}\n```\n\nFor advanced patterns, see [references\u002Fdirective-patterns.md](references\u002Fdirective-patterns.md).\n\n",{"name":5,"description":7},"main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Fanalogjs\u002Fangular-skills\u002Fangular-directives",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":53,"prompt":63,"zipAvailable":64},[54,57,60],{"tool":55,"command":56},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill angular-directives",{"tool":58,"command":59},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill angular-directives",{"tool":61,"command":62},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill angular-directives","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fanalogjs\u002Fangular-skills\u002Fangular-directives\u002FSKILL.md",true,[],[67,81,91,100,106,113],{"skillNo":68,"name":69,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":70,"categories":71,"keywords":77,"publishedAt":38},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[72],{"categoryNo":73,"categoryName":74,"taxonomyType":75,"taxonomyTypeDesc":76},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[78,79,80],"model","model hierarchy","model hierarchy skill",{"skillNo":82,"name":83,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":87,"publishedAt":38},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[86],{"categoryNo":73,"categoryName":74,"taxonomyType":75,"taxonomyTypeDesc":76},[88,89,90],"MCP","zoom","zoom mcp",{"skillNo":92,"name":93,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":96,"publishedAt":38},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[97,98,99,89],"sdk","zoom apps sdk","apps",{"skillNo":101,"name":102,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":103,"categories":104,"keywords":105,"publishedAt":38},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[102],{"skillNo":107,"name":108,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":109,"categories":110,"keywords":112,"publishedAt":38},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[111],{"categoryNo":73,"categoryName":74,"taxonomyType":75,"taxonomyTypeDesc":76},[108],{"skillNo":114,"name":115,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":116,"categories":117,"keywords":118,"publishedAt":38},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[115]]