Skip to content

Instantly share code, notes, and snippets.

@roymanigley
Last active October 3, 2023 21:27
Show Gist options
  • Save roymanigley/7ff897b573bdb7516f1f5bfd83f040de to your computer and use it in GitHub Desktop.
Save roymanigley/7ff897b573bdb7516f1f5bfd83f040de to your computer and use it in GitHub Desktop.
Angular Content Projection and debounce time

Angular Content Projection and debounce time

dropdown.component.ts

import { Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { EMPTY, Observable, Subject, catchError, debounceTime, map, tap } from 'rxjs';

@Component({
  selector: 'app-dropdown',
  templateUrl: './dropdown.component.html',
  styleUrls: ['./dropdown.component.scss']
})
export class DropdownComponent implements OnInit, OnDestroy {

  @Input('filter') filter: (query: string) => Observable<any[]> = (query: string) => EMPTY;
  @ContentChild('item',{static: false}) itemTemplateRef: TemplateRef<any> | null = null;

  queryUpdate = new Subject<Event | null>();
  $items?: Observable<any[]>;
  isLoading = false;

  ngOnInit(): void {
    this.queryUpdate.pipe(
      tap(() => this.isLoading = true),
      debounceTime(1000),
      map(event => (event?.target as HTMLInputElement)?.value ?? ''),
      map(query => this.filter(query ?? '')
        .pipe(
          catchError(err => {
            console.error(err)
            return []
          })
        )
      ),
    ).subscribe(items => {
      this.$items = items;
      this.isLoading = false;
    });
    this.queryUpdate.next(null)
  }

  ngOnDestroy(): void {
      this.queryUpdate.unsubscribe()
  }
}

dropdown.component.html

<input type="text" (keyup)="queryUpdate.next($event)"> {{ isLoading }}
<ul *ngFor="let item of $items | async">
    <li>
        <ng-container
            *ngIf="itemTemplateRef; else elseBlock"
            [ngTemplateOutlet]="itemTemplateRef"
            [ngTemplateOutletContext]="{$implicit:item}">
        </ng-container>
        <ng-template #elseBlock>
            {{ item }}
        </ng-template>
    </li>
</ul>

app.component.ts

import { Component } from '@angular/core';
import { Observable, filter, map, mergeMap, of, tap } from 'rxjs';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'content-projection-experiment';

  public fetchItems(query: string): Observable<string[]> {
    console.log("AppComponent", query);
    
    return of(["Foxtrot", "Uniform", "Charlie", "Kilo"])
      .pipe(
        map(items => items
          .filter(
            item => item.toLowerCase().indexOf(query.toLowerCase()) >= 0
          )
        ),
        tap(item => { if (Math.floor(Math.random() * 1000) % 2 == 0) throw new Error("AAAAAAAAAA") })
      )
  }
}

app.component.html

<router-outlet></router-outlet>

<app-dropdown 
  [filter]="fetchItems">
  <ng-template #item let-item>
    {{ item | uppercase }}
  </ng-template>
</app-dropdown>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment