javascript - Angular 2+ ve geciktirme


AngularJS'de bir modeli ng-model seçeneklerini kullanarak erteleyebilmiştim.

ng-model-options="{ debounce: 1000 }"

Angular'da bir modeli nasıl geciktirebilirim?
Belgelerde debounce için arama yapmayı denedim ama hiçbir şey bulamadım.

https://angular.io/search/#stq=debounce&stp=1

Bir çözüm, kendi debounce fonksiyonumu yazmak olurdu, örneğin:

import {Component, Template, bootstrap} from 'angular2/angular2';

// Annotation section
@Component({
  selector: 'my-app'
})
@Template({
  url: 'app.html'
})
// Component controller
class MyAppComponent {
  constructor() {
    this.firstName = 'Name';
  }
    
  changed($event, el){
    console.log("changes", this.name, el.value);
    this.name = el.value;
  }

  firstNameChanged($event, first){
    if (this.timeoutId) window.clearTimeout(this.timeoutID);
    this.timeoutID = window.setTimeout(() => {
        this.firstName = first.value;
    }, 250)
  }
    
}
bootstrap(MyAppComponent);

Ve benim html'im

<input type=text [value]="firstName" #first (keyup)="firstNameChanged($event, first)">

Ama yerleşik bir fonksiyon arıyorum, Angular'da böyle bir şey var mı?

Cevap #1

RC.5 için güncellendi

Angular 2 ile bir form kontrolünün valueChanges gözlemlenebilirinde RxJS operatörü debounceTime() kullanarak gecikme uygulayabiliriz:

import {Component}   from '@angular/core';
import {FormControl} from '@angular/forms';
import {Observable}  from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';

@Component({
  selector: 'my-app',
  template: `<input type=text [value]="firstName" [formControl]="firstNameControl">
    <br>{{firstName}}`
})
export class AppComponent {
  firstName        = 'Name';
  firstNameControl = new FormControl();
  formCtrlSub: Subscription;
  resizeSub:   Subscription;
  ngOnInit() {
    // debounce keystroke events
    this.formCtrlSub = this.firstNameControl.valueChanges
      .debounceTime(1000)
      .subscribe(newValue => this.firstName = newValue);
    // throttle resize events
    this.resizeSub = Observable.fromEvent(window, 'resize')
      .throttleTime(200)
      .subscribe(e => {
        console.log('resize event', e);
        this.firstName += '*';  // change something to show it worked
      });
  }
  ngDoCheck() { console.log('change detection'); }
  ngOnDestroy() {
    this.formCtrlSub.unsubscribe();
    this.resizeSub  .unsubscribe();
  }
} 

Plunker

Yukarıdaki kod ayrıca, altta bir yorumda @albanx tarafından istendiği gibi, pencere yeniden boyutlandırma olaylarını nasıl sınırlayabileceğinize dair bir örnek de içerir.

Yukarıdaki kod muhtemelen bunu Angular tarzında yapmanın yoludur, ancak verimli değildir. Her tuş vuruşu ve her yeniden boyutlandırma olayı, ne kadar debounce ve throttled olursa olsun, değişiklik tespitinin çalışmasına neden olur. Başka bir deyişle, debounce ve throttling, değişiklik tespitinin ne sıklıkla çalıştığını etkilemez. (Bunu doğrulayan Tobias Bosch tarafından GitHub'da bir yorum buldum.) Bunu, plunker'ı çalıştırdığınızda ve giriş kutusuna yazı yazarken veya pencereyi yeniden boyutlandırırken ngDoCheck()'in kaç kez çağrıldığını gördüğünüzde fark edebilirsiniz. (Yeniden boyutlandırma olaylarını görmek için mavi "x" butonunu kullanarak plunker'ı ayrı bir pencerede çalıştırın.)

Daha verimli bir teknik, Angular'ın "zone"unun dışında olaylardan kendiniz RxJS Observables oluşturmaktır. Bu şekilde, her olay tetiklendiğinde değişiklik algılama çağrılmaz. Daha sonra, subscribe geri arama yöntemlerinizde değişiklik algılamayı manuel olarak tetikleyebilirsiniz - yani, değişiklik algılamanın ne zaman çağrılacağını siz kontrol edersiniz:

import {Component, NgZone, ChangeDetectorRef, ApplicationRef, 
        ViewChild, ElementRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';

@Component({
  selector: 'my-app',
  template: `<input #input type=text [value]="firstName">
    <br>{{firstName}}`
})
export class AppComponent {
  firstName = 'Name';
  keyupSub:  Subscription;
  resizeSub: Subscription;
  @ViewChild('input') inputElRef: ElementRef;
  constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef,
    private appref: ApplicationRef) {}
  ngAfterViewInit() {
    this.ngzone.runOutsideAngular( () => {
      this.keyupSub = Observable.fromEvent(this.inputElRef.nativeElement, 'keyup')
        .debounceTime(1000)
        .subscribe(keyboardEvent => {
          this.firstName = keyboardEvent.target.value;
          this.cdref.detectChanges();
        });
      this.resizeSub = Observable.fromEvent(window, 'resize')
        .throttleTime(200)
        .subscribe(e => {
          console.log('resize event', e);
          this.firstName += '*';  // change something to show it worked
          this.cdref.detectChanges();
        });
    });
  }
  ngDoCheck() { console.log('cd'); }
  ngOnDestroy() {
    this.keyupSub .unsubscribe();
    this.resizeSub.unsubscribe();
  }
} 

Plunker

inputElRef'in tanımlı olduğundan emin olmak için ngOnInit() yerine ngAfterViewInit() kullanıyorum.

detectChanges(), bu bileşende ve çocuklarında değişiklik algılamayı çalıştıracaktır. Eğer değişiklik algılamayı kök bileşenden (yani, tam bir değişiklik algılama kontrolü çalıştırmak) çalıştırmak istiyorsanız, bunun yerine ApplicationRef.tick() kullanın. (Plunker'da, ApplicationRef.tick() çağrısını yorum satırlarına ekledim.) tick() çağırmanın, ngDoCheck() fonksiyonunun çağrılmasına neden olacağını unutmayın.

Cevap #2

Eğer @angular/forms ile uğraşmak istemiyorsanız, sadece değişiklik bağlamalarıyla bir RxJS Subject kullanabilirsiniz.

view.component.html

<input [ngModel]='model' (ngModelChange)='changed($event)' />

view.component.ts

import { Subject } from 'rxjs';
import { Component }   from '@angular/core';
import 'rxjs/add/operator/debounceTime';

export class ViewComponent {
    model: string;
    modelChanged: Subject<string> = new Subject<string>();

    constructor() {
        this.modelChanged
            .debounceTime(300) // wait 300ms after the last event before emitting last event
            .distinctUntilChanged() // only emit if value is different from previous value
            .subscribe(model => this.model = model);
    }

    changed(text: string) {
        this.modelChanged.next(text);
    }
}

Bu, değişiklik algılamayı tetikler. Değişiklik algılamayı tetiklemeyen bir yol için Mark'ın cevabına bakın.


Güncelle

.pipe(debounceTime(300), distinctUntilChanged()) rxjs 6 için gereklidir.

Örnek :

   constructor() {
        this.modelChanged.pipe(
            debounceTime(300), 
            distinctUntilChanged())
            .subscribe(model => this.model = model);
    }

Cevap #3

Konu eski olduğundan, çoğu cevap Angular 6-13 üzerinde çalışmıyor ve/veya başka kütüphaneler kullanıyor.
Bu yüzden Angular 6+ ile RxJS için kısa ve basit bir çözüm burada.

Önce gerekli şeyleri içe aktarın:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

ngOnInit ve ngOnDestroy metodlarını uygula:

export class MyComponent implements OnInit, OnDestroy {
  public notesText: string;
  public notesModelChanged: Subject<string> = new Subject<string>();
  private notesModelChangeSubscription: Subscription

  constructor() { }

  ngOnInit() {
    this.notesModelChangeSubscription = this.notesModelChanged
      .pipe(
        debounceTime(2000),
        distinctUntilChanged()
      )
      .subscribe(newText => {
        this.notesText = newText;
        console.log(newText);
      });
  }

  ngOnDestroy() {
    this.notesModelChangeSubscription.unsubscribe();
  }
}

Bu şekilde kullan:

<input [ngModel]='notesText' (ngModelChange)='notesModelChanged.next($event)' />

P.S. Daha karmaşık ve verimli çözümler için yine de diğer cevapları kontrol etmek isteyebilirsiniz.

Cevap #4

Yönerge olarak uygulanabilir

import { Directive, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { NgControl } from '@angular/forms';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { Subscription } from 'rxjs';

@Directive({
  selector: '[ngModel][onDebounce]',
})
export class DebounceDirective implements OnInit, OnDestroy {
  @Output()
  public onDebounce = new EventEmitter<any>();

  @Input('debounce')
  public debounceTime: number = 300;

  private isFirstChange: boolean = true;
  private subscription: Subscription;

  constructor(public model: NgControl) {
  }

  ngOnInit() {
    this.subscription =
      this.model.valueChanges
        .debounceTime(this.debounceTime)
        .distinctUntilChanged()
        .subscribe(modelValue => {
          if (this.isFirstChange) {
            this.isFirstChange = false;
          } else {
            this.onDebounce.emit(modelValue);
          }
        });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

onu gibi kullan

<input [(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">

bileşen örneği

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

@Component({
  selector: 'app-sample',
  template: `
<input[(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">
<input[(ngModel)]="value" (onDebounce)="asyncDoSomethingWhenModelIsChanged($event)">
`
})
export class SampleComponent {
  value: string;

  doSomethingWhenModelIsChanged(value: string): void {
    console.log({ value });
  }

  async asyncDoSomethingWhenModelIsChanged(value: string): Promise<void> {
    return new Promise<void>(resolve => {
      setTimeout(() => {
        console.log('async', { value });
        resolve();
      }, 1000);
    });
  }
} 

Cevap #5

Angular1’de olduğu gibi doğrudan erişilebilir değil ama NgFormControl ve RxJS gözlemlerle kolayca oynayabilirsiniz:

<input type="text" [ngFormControl]="term"/>

this.items = this.term.valueChanges
  .debounceTime(400)
  .distinctUntilChanged()
  .switchMap(term => this.wikipediaService.search(term));

Bu blog yazısı bunu açıkça açıklıyor: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

İşte bu bir otomatik tamamlama için ama tüm senaryolarda çalışıyor.

Cevap #6

İstediğiniz şeyi yapan bir RxJS (v.6) Observable oluşturabilirsiniz.

view.component.html

<input type="text" (input)="onSearchChange($event.target.value)" />

view.component.ts

import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

export class ViewComponent {
    searchChangeObserver;

  onSearchChange(searchValue: string) {

    if (!this.searchChangeObserver) {
      new Observable(observer => {
        this.searchChangeObserver = observer;
      }).pipe(debounceTime(300)) // wait 300ms after the last event before emitting last event
        .pipe(distinctUntilChanged()) // only emit if value is different from previous value
        .subscribe(console.log);
    }

    this.searchChangeObserver.next(searchValue);
  }  


}

Cevap #7

Lodash kullanan herkes için herhangi bir fonksiyonu debounce etmek son derece kolaydır:

changed = _.debounce(function() {
    console.log("name changed!");
}, 400);

o zaman bunu şablonuna şöyle at:

<(input)="changed($event.target.value)" />

İlgili makaleler