Angular

Basics

Rachèl Heimbach

Principal Consultant @ OpenValue

Solution Architect @ Rabobank

Frank Merema

Lead Frontend developer @ FrontValue

Frontend development

HTML

CSS

JavaScript

Web components

Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps.

Webcomponent tech.

  • Custom elements
  • Shadow DOM
  • HTML templates

Architecture

Component-based frameworks

and more...

Angular

HTML

SCSS

TypeScript

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.

TypeScript

  • Type Assignment

 

 

  • Type Inference

 

 

  • Combined
let n = 1; // number

n = '123';

Redundant

let n: number = 1;

let n: number;

n = 1;

export class ProductsService {
  private products: Product[];
  
  constructor() {}
  
  getProducts() {
    return this.products;
  }
  
  public setProducts(products: Product[]) {
    this.products = products;
  }
}

  • Interfaces

 

 

 

  • Access modifiers
interface Product {
  name: string;
  price: number;
}

Redundant

TypeScript Playground

tsconfig.json

  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitReturns": true,
    "strictPropertyInitialization": false,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2017",
    "module": "es2020",
    "lib": [
      "es2018",
      "dom"
    ]
  },

Generics<T>

export class DataService<T> {
  private data: T;
  
  constructor() {}
  
  get(): T {
    return this.data;
  }
  
  set(data: T): void {
    this.data = data;
  }
}

const numberService = new DataService<number>();
numberService.set(1);

const textService = new DataService<string>();
textService.set('Text');

Decorators

Angular

... is a command-line interface tool that you use to initialize, develop, scaffold, and maintain Angular applications directly from a command shell.

Angular CLI

Component based

Dependency Injection

Modular

Component based

Dependency Injection

Modular

Web components

Products list

A list of products where each product consists of a title, category, image, rating, description and price.

Hierarchy

A list of products where each product consists of a title, category, image, rating, description and price.

Products

Product

<section class="product">
  <a>
    <img
      class="square-image"
      src="https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
      alt="Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops"
    />
  </a>
  <div class="info">
    <p class="category">men's clothing</p>
    <a class="title">Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops</a>
    <p class="rating">
      <span class="fa fa-star"></span>
      <span class="fa fa-star"></span>
      <span class="fa fa-star"></span>
      <span class="fa fa-star"></span>
      <span class="fa fa-star-o"></span>
      <span class="count">(120)</span>
    </p>
    <p class="description">
      Your perfect pack for everyday use and walks in the forest. Stash your
      laptop (up to 15 inches) in the padded sleeve, your everyday
    </p>
    <p class="price">109.95</p>
  </div>
</section>

Product

(plain html)

<section class="product">
  <a>
    <ov-square-image
      src="https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
      alt="Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops"
    ></ov-square-image>
  </a>
  <div class="info">
    <p class="category">men's clothing</p>
    <a class="title">Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops</a>
    <ov-rating rating="4" count="120"></ov-rating>
    <p class="description">
      Your perfect pack for everyday use and walks in the forest. Stash your
      laptop (up to 15 inches) in the padded sleeve, your everyday
    </p>
    <p class="price">109.95</p>
  </div>
</section>

Product

(webcomponents)

Hierarchy

A list of products where each product consists of a title, category, image, rating, description and price.

Products

Product

Product

SquareImage

Rating

Components

Angular App

A very simple Angular app that renders a list of products.

Products

Product

Product

SquareImage

Rating

App

AppComponent

Root of the component tree.

import { Component } from '@angular/core';

@Component({
  selector: 'ov-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
}

...

App

<!DOCTYPE html>
<html lang="en">
  <head>
    ...
  </head>
  <body>
    <ov-root></ov-root>
  </body>
</html>

index.html

Checkout repo

# Install deps
npm i

# Start app
npm start

# Run docs (assignments)
npm run docs

# Run tests
npm test

Let's create a component

Product data model

A list of products where each product consists of a title, category, image, rating, description and price.

// ng generate interface shared/product
export interface Product {
  id: number;
  title: string;
  price: number;
  description: string;
  category: string;
  image: string;
  rating: Rating;
}

// ng generate interface shared/rating
export interface Rating {
  rate: number;
  count: number;
}

Product TS

// ng generate component product
import { Component, Input, OnInit } from '@angular/core';
import { Product } from '../shared/product';

@Component({
  selector: 'ov-product',
  templateUrl: './product.component.html',
  styleUrls: ['./product.component.scss'],
})
export class ProductComponent implements OnInit {
  @Input() product: Product;
  
  constructor() {}

  ngOnInit(): void {}
}

...

Product

Product

product

<ov-product [product]="product"></ov-product>

[binding]

<!-- Binds property value to the result of expression firstName. -->
<input [value]="firstName">	

<!-- Binds attribute role to the result of expression myAriaRole. -->
<div [attr.role]="myAriaRole">	

<!-- Binds the presence of the CSS class extra-sparkle on the element
 to the truthiness of the expression isDelightful. -->
<div [class.extra-sparkle]="isDelightful">
  
<!-- Bind product to ProductComponent -->
<ov-product [product]="product">

Product HTML

<section class="product">
  <a>
    <ov-square-image
      [src]="product.image"
      [alt]="product.title"
    ></ov-square-image>
  </a>
  <div class="info">
    <p class="category">{{ product.category }}</p>
    <a class="title">{{ product.title }}</a>
    <p class="rating">
      <ov-rating 
        [rate]="product.rating.rate"
        [count]="product.rating.count"
      ></ov-rating>
    </p>
    <p class="description">{{ product.description }}</p>
    <p class="price">{{ product.price }}</p>
  </div>
</section>

...

Product

Product

SquareImage

Rating

[product]

[src], [alt]

[rate], [count]

Assignment 1.1-1.3

Components

Directives

ViewEncapsulation

Assignment 1.4-1.5

*ngFor - ViewEncapsulation

Search TS

// ng generate component search
import { Component, Input, OnInit
        EventEmitter } from '@angular/core';

@Component({
  selector: 'ov-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.scss'],
})
export class SearchComponent implements OnInit {
  @Input() query: string;
  @Output() search = new EventEmitter<string>();
  
  constructor() {}

  ngOnInit(): void {}
  
  onChange(input: HTMLInputElement) {
    this.search.emit(input.value);
  }
}

...

Search

search($event)

<ov-search 
  [query]="searchValue" 
  (search)="onSearch($event)"
></ov-search>

(event)

<!-- 
  Calls method readRainbow when a click event is triggered 
  on this button element (or its children) and passes in the 
  event object. 
-->
<button (click)="readRainbow($event)">

<!--
  Sets up two-way data binding. Equivalent to:
  <my-cmp [title]="name" (titleChange)="name=$event">
-->
<my-cmp [(title)]="name">

Lifecycle hooks

  • ngOnChanges: When an input/output binding value changes.
  • ngOnInit: After the first ngOnChanges.
  • ngDoCheck: Developer's custom change detection.
  • ngAfterContentInit: After component content initialized.
  • ngAfterContentChecked: After every check of component content.
  • ngAfterViewInit: After a component's views are initialized.
  • ngAfterViewChecked: After every check of a component's views.
  • ngOnDestroy: Just before the directive is destroyed.

Component based

Dependency Injection

Modular

It describes how to compile a component's template and how to create an injector at runtime

It identifies the module's own components, directives, and pipes

What is a module?

Angular modules

CommonModule

FormsModule

...

RouterModule

BrowserModule

HttpClientModule

Main.ts

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

AppModule

App

AppModule

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Contains core dependencies that are always needed on boot.

Products

Product

SquareImage

Rating

App

AppModule

Products

Product

SquareImage

Rating

App

AppModule

ProductsModule

SharedModule

Products

Product

SquareImage

App

AppModule

ProductsModule

SharedModule

Rating

RatingModule

?

Angular Material

Module architecture

Assignment 2

Modules

TestBed

Component testing

E2E

(UI)

Integration

Unit

Critical user flow testing

Render checks + Edge case testing

Isolated testing

Configures and initializes environment for unit testing and provides methods for creating components and services in unit tests.

TestBed is the primary api for writing unit tests for Angular applications and libraries.

What is TestBed?

TestBed example

@Component({
  selector: 'app-banner',
  template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
  title = 'Test Tour of Heroes';
}
describe('BannerComponent', () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ BannerComponent ],
    });
    fixture = TestBed.createComponent(BannerComponent);
    component = fixture.componentInstance; // BannerComponent test instance
  });
  
  it('should display original title', () => {
    const h1 = fixture.nativeElement.querySelector('h1');
    expect(h1.textContent).toContain(component.title);
  });
});

Module

Fixture

TestBed fixture

export declare class ComponentFixture<T> {
  /**
   * The instance of the root component class.
   */
  componentInstance: T;
  /**
   * The native element at the root of the component.
   */
  nativeElement: any;
  /**
   * The DebugElement associated with the root element of this component.
   */
  debugElement: DebugElement;
  // ...
}

TestBed example

@Component({
  selector: 'app-banner',
  template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
  title = 'Test Tour of Heroes';
}
describe('BannerComponent', () => {
  let component: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ BannerComponent ],
    });
    fixture = TestBed.createComponent(BannerComponent);
    component = fixture.componentInstance; // BannerComponent test instance
  });
  
  it('should display original title', () => {
    const h1 = fixture.nativeElement.querySelector('h1');
    expect(h1.textContent).toContain(component.title);
  });
});

Smart

  • Feature

  • Service access

  • Controls state

  • App specific

  • Dumb children

  • Deep test

Smart

Dumb

Dumb

Dumb

Dumb

Dumb

  • Piece of UI

  • No service access

  • No state

  • Portable/reusable

  • Dumb children

  • Shallow test

Service

Data

Data

Event

Event

Smart vs Dumb

Shallow test

  • Mock children
  • Test UI based on input
  • Test child bindings
  • Test child event handlers
  • Test event emitting

Dumb

Dumb

Dumb

Data

Event

Event

Data

Product test

  • Mock children
  • Test UI based on input
  • Test child bindings
  • Test child event handler
  • Test event emitting

Product

SquareImage

Rating

product

rate, count

src, alt

NgMocks

Products test

  let component: ProductsComponent;
  let fixture: ComponentFixture<ProductsComponent>;

  const PRODUCTS_MOCK: Product[] = [...];

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ProductsComponent, MockComponent(ProductComponent)],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(ProductsComponent);
    component = fixture.componentInstance;
    component.products = PRODUCTS_MOCK;
    
    fixture.detectChanges();
  });

  it('should render 3 products', () => {
    const productComponents = fixture.debugElement.queryAll(
      By.directive(ProductComponent)
    );

    expect(productComponents.length).toBe(3);

    productComponents.forEach(({ componentInstance }, index) => {
      expect(componentInstance.product).toEqual(PRODUCTS_MOCK[index]);
    });
  });

Mock

Get mock instances

Assign mock data

Call change detection

Assert binding

Assignment 3

Unit testing

Assignment 4

Homework

Routing

The Router enables navigation by interpreting a browser URL as an instruction to change the view.

Router?

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'products', component: ProductsComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

App

RouterOutlet

Home

/

Products

/products

const routes: Routes = [
  { path: 'products', component: ProductsComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)]
})
export class ProductsModule { }

RouterModule

<nav>
  <a routerLink="/" 
     routerLinkActive="active" 
     [routerLinkActiveOptions]="{exact:true}">
    Home
  </a>
  <a routerLink="/products" 
     routerLinkActive="active" 
     [routerLinkActiveOptions]="{exact:true}">
    Products
  </a>
</nav>

<router-outlet></router-outlet>

Router outlet

app.component.html

.active {
  background-color: grey;
  cursor: auto;
}

app.component.scss

const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'products',
    loadChildren: () =>
      import('./products-overview/products-overview.module')
        .then(m => m.ProductsOverviewModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

App

RouterOutlet

Home

/

Products

/products

const routes: Routes = [
  { path: '', component: ProductsComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)]
})
export class ProductsOverviewModule { }

Lazy loading

Products

Product

SquareImage

Rating

App

AppModule

ProductsModule

RouterOutlet

Home

...

HomeModule

/

/products

SharedModule

...

Assignment 5

Routing

Component based

Dependency Injection

Modular

It provides dependencies to a class upon instantiation. Use it to increase flexibility and modularity in your applications.

Dependencies are services or objects that a class needs to perform its function.

Angular DI

Components shouldn't fetch or save data directly [...] They should focus on presenting data and delegate data access to a service.

Service

Service

@Injectable()
export class ProductService {
  get(): Product[] {
    // ...
  }
}

Decorator that marks a class as available to be provided and injected as a dependency.

@Injectable()

An injector uses the provider to create a new instance of a dependency for a class that requires it.

A provider object defines how to obtain an injectable dependency associated with a DI token.

What is a provider?

Custom provider

import { InjectionToken } from '@angular/core';

export const API_URL = new InjectionToken<string>('api url');
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [
    {
      provide: API_URL,
      useValue: '/api',
    },
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

1. Define an injection token

2. Provide the token with a value/class/factory

export class AppComponent implements OnInit {
  constructor(
    @Inject(API_URL) 
    private apiUrl: string
  ) {}

  ngOnInit() {
    console.log(this.apiUrl); // /api
  }
}

3. Inject the token where needed

@Injectable() (old)

1. Define a service

@Injectable()
export class ProductService {
  get(): Product[] {
    // ...
  }
}

2. Provide it to a module

@NgModule({
  declarations: [FeatureComponent],
  providers: [ProductService]
})
export class FeatureModule {}
export class AppComponent implements OnInit {
  constructor(
    private productService: ProductService
  ) {}
  // ...
}

3. Inject the service where needed

Root injector

The injector scope where all sync providers are put by default.

ProductService

@NgModule({
  declarations: [FeatureComponent],
  providers: [ProductService]
})
export class FeatureModule {}

Root injector

@Injectable() (today)

1. Define a service

@Injectable({
  provideIn: 'root'
})
export class ProductService {
  get(): Product[] {
    // ...
  }
}

2. Inject the service where needed

export class AppComponent implements OnInit {
  constructor(
    private productService: ProductService
  ) {}
  // ...
}

Benefits:

  • Lazy loadable -> smaller bundles

Example architectures

ProductService

Root injector

ProductsPage

...

Products

Product

All over the place pattern

Pros:

  • Easy

Cons:

  • Complex UI
  • Harder to debug       

ProductService

Root injector

ProductsPage

...

Products

Product

Page controller pattern

Pros:

  • Complexity page level
  • Simple UI components
  • Clear separation between UI and App logic

Cons:

  • "Prop drilling"       

ProductService

Root injector

ProductsPage

...

Products

Product

Smart component pattern

Pros:

  • Standalone features
  • Few smart components
  • Many dumb components

Cons:

  • UI is not completely separated from business logic        

History lesson

$scope

function Controller($scope) {
  // ...
 
  $scope.watch(productService.products,  () => { ... });
                                              
  // ...
}

Observables are lazy Push collections of multiple values. They fill the missing spot in the following table:

Observable

SINGLE MULTIPLE
Pull Function Iterator
Push Promise Observable

An Observer is a consumer of values delivered by an Observable.

Observer

Observers are simply a set of callbacks, one for each type of notification delivered by the Observable: next, error, and complete.

Single

Multiple

import { Observable } from 'rxjs';

const number$ = Observable.of(1);

number$
  .subscribe(
    n => console.log(n),
    e => console.error(e),
    () => console.log('Completed')
  );

// 1
// Completed


import { Observable } from 'rxjs';

const numbers$ = Observable.from([1, 2, 3]);

numbers$
  .subscribe(
    n => console.log(n),
    e => console.error(e),
    () => console.log('Completed')
  );

// 1
// 2
// 3
// Completed

A Subject is a special type of Observable which shares a single execution path among observers.

Subject

You can think of this as a single speaker talking at a microphone in a room full of people. Their message (the subject) is being delivered to many (multicast) people (the observers) at once.

Single sub

import { Subject } from 'rxjs';

const numbersSubject = new Subject();

numbersSubject.subscribe(...);

numbersSubject.next(1);

// 1

numbersSubject.next(2);

// 2   

numbersSubject.complete();

// Completed

Multiple subs

import { Subject } from 'rxjs';

const numbersSubject = new Subject();

numbersSubject.subscribe(...);
numbersSubject.subscribe(...);

numbersSubject.next(1);

// 1
// 1

numbersSubject.next(2);

// 2   
// 2   

numbersSubject.complete();

// Completed
// Completed

RxJS is mostly useful for its operators, even though the Observable is the foundation.

Operators

Operators are the essential pieces that allow complex asynchronous code to be easily composed in a declarative manner.

Cold

Hot

import { Observable } from 'rxjs';

const number$ = Observable.create(
  observer => {
    observer.next(1);
    observer.next(2);
    observer.complete();
  }  
);

number$.subscribe(...);

// 1
// 2
// Completed

number$.subscribe(...);

// 1
// 2
// Completed



import { Observable } from 'rxjs';

const numbersSubject = new Subject();

const number$ = Observable.create(
  observer => 
    numbersSubject.subscribe(observer)
);

number$.subscribe(...);

numbersSubject.next(1);

// 1

number$.subscribe(...);

numbersSubject.next(2);

// 2   
// 2

A Subscription is an object that represents a disposable resource, usually the execution of an Observable.

Subscription

A Subscription has one important method, unsubscribe, that takes no argument and just disposes the resource held by the subscription.

Subscription

Subscription

import { Observable } from 'rxjs';
import { delay } from 'rxjs/operators';

const number$ = Observable.from([1, 2, 3])
                  .pipe(
                    delay(1000)
                  );

const subscription = number$.subscribe(...);

setTimeout(() => {
  subscription.unsubscribe();
  console.log('Unsubscribed');
}, 2001);
                  
// 1
// 2
// Unsubscribed


import { Observable } from 'rxjs';

const numbersSubject = new Subject();

const number$ = Observable.create(
  observer => 
    numbersSubject.subscribe(observer)
);

const subscription = number$.subscribe(...);
number$.subscribe(...);

numbersSubject.next(1);

// 1
// 1

subscription.unsubscribe();

numbersSubject.next(2);

// 2

Data fetching

@Injectable({
  providedIn: 'root'
})
export class ProductsService {
  
  constructor(private httpClient: HttpClient) {}
  
  get(): Observable<Product[]> {
    return this.httpClient
      .get<Product[]>('/api/products');
  }
}
@Component({
  ...
})
export class ProductsComponent {
  products: Product[];
  
  constructor(private productsService: ProductsService) {}

  ngOnInit() {
    this.productsService.get()
      .subscribe(
        products => this.products = products,
        error => console.error(error)
      );
  }
}

ProductsService

Products

data

Data fetching

@Injectable({
  providedIn: 'root'
})
export class ProductsService {
  products$: Observable<Product[]>;

  private productsSubject = new Subject<Product[]>();

  constructor(private httpClient: HttpClient) {
    this.products$ = this.productsSubject.asObservable();
  }

  get(): void {
    this.httpClient.get<Product[]>('/products')
      .subscribe(
        products => this.productsSubject.next(products),
          error => console.log(error)
      );
  }
}

ProductsService

Products

data

Memory leaks

@Component({
  selector: 'ov-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit {
  // ... 
  constructor(private productsService: ProductsService) {
  }

  ngOnInit(): void {
    this.productsService.products$
      .subscribe(products => {
        console.log(products);
      });

    this.productsService.get();
  }
  // ... 
}

Subscriptions

@Component({
  selector: 'ov-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit, OnDestroy {
  // ... 
  
  private subscription: Subscription;
  
  constructor(private productsService: ProductsService) {
  }

  ngOnInit(): void {
    this.subscription = this.productsService.products$
      .subscribe(products => {
        console.log(products);
      });

    this.productsService.get();
  }
  
  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
  // ... 
}

Async Pipe

@Component({
  selector: 'ov-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit, OnDestroy {
  products$: Observable<Product[]>;
  
  constructor(private productsService: ProductsService) {
    this.products$ = productsService.products$;
  }
  // ... 
}

<ng-container *ngIf="products$ | async as products else loading">
  {{ products | json }}
</ng-container>
<ng-template #loading>
  Loading...
</ng-template>

Data fetching

@Component({
  selector: 'ov-products',
  templateUrl: './products.component.html',
  styleUrls: ['./products.component.scss']
})
export class ProductsComponent implements OnInit {
  products$: Observable<Product[]>;
  
  constructor(private productsService: ProductsService) {
    this.products$ = productsService.products$;
  }
  
  ngOnInit(): void {
    this.productsService.get();
  }
  
  onSearch(query: string) {
    this.productsService.get({
      filter: query
    });
  }
  // ... 
}

ReactiveForms

Data fetching

@Component({
  selector: 'ov-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit, OnDestroy {
  @Output()
  search = new EventEmitter<string>();
  searchFormControl: FormControl;
  private subscription: Subscription;

  ngOnInit(): void {
    this.searchFormControl = new FormControl('')

    this.subscription = this.searchFormControl.valueChanges
      .pipe(
        debounceTime(300)
      )
      .subscribe(value => this.search.emit(value))
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
  // ...
}
<input matInput type="text" [formControl]="searchFormControl">

Data fetching

@Component({
  selector: 'ov-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit, OnDestroy {
  @Output()
  search = new EventEmitter<string>();
  searchFormControl: FormControl;
  private subscription: Subscription;

  ngOnInit(): void {
    this.searchFormControl = new FormControl('')

    this.subscription = this.searchFormControl.valueChanges
      .pipe(
        debounceTime(300),
      	distinctUntilChanged()
      )
      .subscribe(value => this.search.emit(value))
  }

  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
  // ...
}
Made with Slides.com