Mastering Angular Directives using illustrative patterns

Component is a Directive

Directive is a Component

🤔

Lets dig into it ⛏️

Ctrl + click

Directive

Component

  • A Directive is subset of component
  • A component is a Directive
@Component({
  standalone: true,
  selector: 'app-awesome',
  templateUrl: './awesome.component.html',
  styleUrl: './awesome.component.scss',
  imports: [],
})
export class AwesomeComponent {
}
@Directive({
  selector: '[app-my-custom]',
  standalone: true,
})
export class MyCustomDirective {
}

Simple example Component and Directive

Directive API

export interface Directive {
  selector?: string;
  inputs?: ({
    name: string,
    alias?: string,
    required?: boolean,
    transform?: (value: any) => any,
  }|string)[];
  outputs?: string[];
  providers?: Provider[];
  exportAs?: string;
  queries?: {[key: string]: any};
  host?: {[key: string]: string};
  jit?: true;
  standalone?: boolean;
  hostDirectives?: (Type<unknown>|{
    directive: Type<unknown>,
    inputs?: string[],
    outputs?: string[],
  })[];
}

- selector

- inputs

- outputs

- providers

- exportAs

- queries

- host

- jit

- standalone

- hostDirectives

Component API

export declare interface Component
    extends Directive {
  changeDetection?: ChangeDetectionStrategy;
  viewProviders?: Provider[];
  moduleId?: string;
  templateUrl?: string;
  template?: string;
  styleUrl?: string;
  styleUrls?: string[];
  styles?: string | string[];
  animations?: any[];
  encapsulation?: ViewEncapsulation;
  interpolation?: [string, string];
  preserveWhitespaces?: boolean;
  standalone?: boolean;
  imports?: (Type<any> | ReadonlyArray<any>)[];
  schemas?: SchemaMetadata[];
}

- changeDetection

- viewProviders

- moduleId

- templateUrl

- styleUrls / style

- animations

- encapsulation

- interpolation

- preserveWhitespaces

- standalone

- imports

- schemas

- selector

- inputs

- outputs

- providers

- exports

- queries

- host

- jit

- standalone

- hostDirectives

Component

- changeDetection

- viewProviders

- templateUrl / template

- styleUrls/styles

- preserveWithWhitespace

- animations

- encapsulation

- interpolation

- imports

- schemas

 

Directive

Component is a Directive with a Template

Template / View

Types of Directive

Structural

Attribute

(wider)

  • Composition
  • Validation
  • Event based
  • Exportable
  • Extension
  • Helper

*(Conditional Rendering)

  • ngIf (@if)
  • ngFor (@for)
  • ngSwitch (@switch)
  • cdkVirtualFor

Disclaimer: This types are given to better understanding of angular directive API.

  • Composition
  • Validation
  • Event based
  • Exportable
  • Extension
  • Helper

- selector

- inputs

- outputs

- providers

- exportAs

- queries

- host

- jit

- standalone

- hostDirectives

Match the following

Pankaj P. Parkar

Principal Application Devloper @AON

  • Angular GDE

  • Ex- Microsoft MVP (2015-22)

  • @ngx-lib/multiselect 📦 

About Me!

pankajparkar

#devwhorun 🏃‍♂️ 

Structural Directive

*(Conditional Rendering)

Feature Flag

<div *featureFlag="isFastLoginEnabled; else notEnabled">
  <button>Login with one click</button>
</div>
<ng-template #notEnabled>
  <div>Contact support</div>
</ng-template>
if (this.isFastLoginEnabled) {
  // perform specific action
}
@Directive({
  selector: '[featureFlag]',
  standalone: true,
})
export class FeatureFlagDirective {
  templateRef = inject(TemplateRef);
  viewContainer = inject(ViewContainerRef);
  featureFlagService = inject(FeatureFlagService);
  hasView = false;

  @Input() set featureFlag(feature: FeatureFlagKeys) {
    if (this.featureFlagService.getFeature(feature) && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }

  @Input() set featureFlagElse(elseTemplateRef: TemplateRef<any>) {
    if (!this.hasView()) {
      this.viewContainer.createEmbeddedView(elseTemplateRef);
    }
  }
}

1. Composition

@Component({
  selector: 'my-menu-item',
  standalone: true,
  imports: [],
  hostDirectives: [CdkMenuItem],
  template: `
    <ng-content></ng-content>
  `,
})
export class MenuItemComponent { }
@Component({
  selector: 'my-menu',
  standalone: true,
  imports: [],
  hostDirectives: [CdkMenu],
  template: `
    <ng-content></ng-content>
  `,
})
export class MenuComponent { }
<button [cdkMenuTriggerFor]="menu">
  Click me!
</button>
<ng-template #menu>
  <my-menu>
    <my-menu-item>Tes M</my-menu-item>
    <my-menu-item>Tes M</my-menu-item>
    <my-menu-item>Tes M</my-menu-item>
  </my-menu>
</ng-template>
<select>
  <option>Option 1</option>
  <option>Option 2</option>
  <option>Option 3</option>
</select>

Create a new element with behaviour using multiple directives

pankajparkar

1. Composition

Create a new element with behaviour using multiple directives

pankajparkar

Applying directives directly on component

 <app-ribbon appBold appBlink>
   Flash⚡️ Sell
 </app-ribbon>

Bold Directive

@pankajparkar

www.pankajparkar.com

@Directive({
  selector: '[appBold]',
  standalone: true
})
export class BoldDirective {
  @HostBinding('style.font-weight')
  bold = 'bolder';
}

My Text

My Text

Blink Directive

@pankajparkar

@Directive({
  selector: '[appBlink]',
  standalone: true,
})
export class BlinkDirective {
  @HostBinding('style.animation')
  animation = 'blinker 1s step-start infinite';

  @Input() theme = 'red';
  @Output() elementClick = new EventEmitter<MouseEvent>();

  @HostListener('click', ['$event'])
  click(event: MouseEvent) {
    this.elementClick.emit(event);
  }
}

1. Composition

@Component({
  selector: 'app-ribbon',
  standalone: true,
  imports: [
 	BoldDirective,
    FlashDirective,
  ],
  hostDirectives: [
	BoldDirective,
    FlashDirective,
  ],
})
export class RibbonComponent {
  ...
}

Create a new element with behaviour using multiple directives

pankajparkar

2. Validation

export function validatePhone(control: AbstractControl): ValidationErrorOrNull {
  if (control.value && control.value.length != 10) {
    return { 'phoneNumberInvalid': true };
  }
  return null;
}

@Directive({
  selector: '[appPhoneNumberValidator]',
  providers: [{
    provide: NG_VALIDATORS,
    useExisting: PhoneNumberValidatorDirective,
    multi: true
  }]
})
export class PhoneNumberValidatorDirective implements Validator {
  validate(control: AbstractControl) : ValidationErrorOrNull {
    return validatePhone(control);
  }
}
<input appPhoneNumberValidator
  type="text" name="phone" 
  [(ngModel)]="phone" />

For validating form field / control

pankajparkar

3. Event-Based

@Directive({
  selector: '[appBlockCopyPaste]'
})
export class BlockCopyPasteDirective {
  constructor() { }

  @HostListener('paste', ['$event'])
  blockPaste(e: KeyboardEvent) {
    e.preventDefault();
  }

  @HostListener('copy', ['$event'])
  blockCopy(e: KeyboardEvent) {
    e.preventDefault();
  }

  @HostListener('cut', ['$event'])
  blockCut(e: KeyboardEvent) {
    e.preventDefault();
  }
}
@Directive({
 selector: '[appBlockCopyPaste]',
 host: {
  '[class.default-input]': 'true',
  '(paste)': 'preventEvent($event)',
  '(copy)': 'preventEvent($event)',
  '(cut)': 'preventEvent($event)',
 }
})
export class BlockCopyPasteDirective {
  preventEvent(e: KeyboardEvent) {
    e.preventDefault()
  }
}
<input type="text" appBlockCopyPaste />

listen and react to the event

4. Exportable

<input
  type="text"
  name="name"
  ngModel
  #userName="ngModel"
  minlength="2"
  required />
<div *ngIf="userName.errors?.required">
  Name is required
</div>
<div *ngIf="userName.errors?.minlength">
  Minimum of 2 characters
</div>
@Directive({
  selector: '[ngModel]:not([formControlName]):not([formControl])',
  providers: [formControlBinding],
  exportAs: 'ngModel'
})
export class NgModel extends NgControl {
  ...
}

easily extract directive instance on template

pankajparkar

4. Exportable

@ViewChildren(NgModel) models: QueryList<NgModel>;
@ViewChild(NgForm) form: NgForm;
<form (ngSubmit)="save()" ngForm>
  <input type="text" name="phone" [(ngModel)]="phone" />
  
  <input type="text" name="phone" [(ngModel)]="address" />
  ...
  <button [disabled]="form.invalid">
    SAVE
  </button>
</form>

without `exportAs` how it could have been done

pankajparkar

5. Extension

<div class="mat-elevation-z8">
  <table mat-table [dataSource]="dataSource" matSort>
    <!-- ID Column -->
    <ng-container matColumnDef="id">
      <th mat-header-cell *matHeaderCellDef> ID </th>
      <td mat-cell *matCellDef="let row"> {{row.id}} </td>
    </ng-container>
    <!-- Other columns hidden for brevity -->
  </table>

  <mat-paginator
  	[pageSizeOptions]="[5, 10, 25, 100]">
  </mat-paginator>
</div>

extend existing behaviour of an component

5. Extension

@Directive({
  selector: 'mat-paginator',
  standalone: true,
})
export class MatPaginatorDefaultDirective {
  // private paginator = inject(MatPaginator, 
  //	{ self: true });
  
 constructor(
 	@Self() private paginator: MatPaginator,
 ) {}
  
  ngOnInit() {
    if (!this.paginator.pageSizeOptions) {
      this.paginator.pageSizeOptions = [5, 10];
    }
  }
}

Reference: https://stackblitz.com/edit/fepvih

pankajparkar

6. Helper

<div class="page-container">
  <user-details #userDetails />
</div>

<button (click)="print(userDetails)">
  Print
</button>
@ViewChild(UserDetails, {read: ElementRef})
userDetails: ElementRef;

print(element) {
  window.print(element);
}
print(element) {
  window.print(element);
}

Helps to extract stuff out of host component

pankajparkar

@Directive({
  selector: '[elementUtil]',
  exportAs: 'elementUtil',
  queries: {
  	form: new ViewChild(NgForm),
  }
})
export class ComponentAsDomDirective {
  dom = inject(ElementRef).nativeElement;
  form! NgForm;
  // form = viewChild(NgForm);
}
<div class="page-container">
  <user-details #component="elementUtil" />
</div>

<button (click)="print(component.dom)">
  Print
</button>

6. Helper

Helps to extract stuff out of host component

pankajparkar

  • Composition
  • Validation
  • Event based
  • Exportable
  • Extension
  • Helper

- selector

- inputs

- outputs

- providers

- exportAs

- queries

- host

- jit

- standalone

- hostDirectives

pankajparkar

(~ @HostBinding, @HostListener)

(~ viewChildren, contentChildren, etc)

Match the following

@pankajparkar

pankajparkar.dev

Say Hi 👋

References

  • https://timdeschryver.dev/blog/use-angular-directives-to-extend-components-that-you-dont-own#default-directive
  • https://medium.com/@iamjustin/feature-flags-in-angular-d50a2b8437fe
  • https://ultimatecourses.com/blog/angular-2-forms-reactive

Mastering Angular Directives using illustrative patterns

By Pankaj Parkar

Mastering Angular Directives using illustrative patterns

Mastering Angular Directives: A Comprehensive Guide

  • 216