Kątowe i debounce

W AngularJS mogę zdemontować model używając opcji ng-model.

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

Jak mogę zdemontować model w Angular? Próbowałem szukać debounce w dokumentach, ale nic nie mogłem znaleźć.

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

Rozwiązaniem byłoby napisanie własnej funkcji debounce, na przykład:

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);

I mój html

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

Ale szukam funkcji wbudowanej, czy jest taka w Angular?

Author: ishandutta2007, 2015-08-17

13 answers

Aktualizacja dla RC.5

Z Angular 2 możemy debounce używając operatora RxJS debounceTime() na kontrolce postaci valueChanges obserwowalnej:

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

Powyższy kod zawiera również przykład, jak ograniczyć zdarzenia zmiany rozmiaru okna, o co prosi @albanx w komentarzu poniżej.


Chociaż powyższy kod jest prawdopodobnie kanciastym sposobem na zrobienie tego, nie jest wydajny. Każde naciśnięcie klawisza i każde zdarzenie zmiany rozmiaru, nawet jeśli są usuwane i dławione, skutkuje uruchomieniem wykrywania zmian. Innymi słowy, debouncing i dławienie nie mają wpływu na to, jak często działa wykrywanie zmian. (Znalazłem komentarz GitHub Tobiasa Boscha, który to potwierdza.) Możesz to zobaczyć, gdy uruchomisz plunker i widzisz, ile razy ngDoCheck() jest wywoływany po wpisaniu w polu wprowadzania lub zmianie rozmiaru okna. (Użyj niebieskiego przycisku "x", aby uruchomić plunker w osobnym oknie, aby zobaczyć zdarzenia zmiany rozmiaru.)

A bardziej efektywną techniką jest tworzenie Rxjs Obserwables siebie z wydarzeń, poza "strefą" Angular. W ten sposób wykrywanie zmian nie jest wywoływane przy każdym wywołaniu zdarzenia. Następnie, w metodach callback subscribe, ręcznie wyzwalaj wykrywanie zmian – tzn. kontroluj, kiedy zostanie wywołane wykrywanie zmian:

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

Używam ngAfterViewInit() zamiast ngOnInit(), aby upewnić się, że inputElRef jest zdefiniowana.

detectChanges() uruchomi wykrywanie zmian w tym komponent i jego dzieci. Jeśli wolisz uruchomić wykrywanie zmian z głównego komponentu (np. uruchomić pełną kontrolę wykrywania zmian), użyj ApplicationRef.tick() zamiast tego. (W komentarzach do:) Zauważ, że wywołanie tick() spowoduje wywołanie ngDoCheck().

 157
Author: Mark Rajcok,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-17 19:02:30

Jeśli nie chcesz mieć do czynienia z @angular/forms, możesz po prostu użyć RxJS Subject ze zmienianymi wiązaniami.

Widok.komponent.html

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

Widok.komponent.ts

import { Subject } from 'rxjs/Subject';
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);
    }
}

To powoduje wykrywanie zmian. aby znaleźć sposób, który nie uruchamia wykrywania zmian, sprawdź odpowiedź Marka.

 84
Author: 0xcaff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:39

Nie jest bezpośrednio dostępny jak w angular1, ale można łatwo grać z NgFormControl i rxjs observables:

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

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

Ten wpis na blogu wyjaśnia to wyraźnie: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

Tutaj jest to autouzupełnianie, ale działa we wszystkich scenariuszach.

 27
Author: bertrandg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-07 13:42:06

Może być wdrożona jako Dyrektywa

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 'rxjs/add/operator/takeUntil'; 

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

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

    private isFirstChange: boolean = true;
    private ngUnsubscribe: Subject<void> = new Subject<void>();

    constructor(public model: NgControl) {
    }

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

    ngOnDestroy() {
        this.ngUnsubscribe.next();
        this.ngUnsubscribe.complete();
    }

}

Użyj go jak

<input [(ngModel)]="model" [debounce]="500" (onDebounce)="doSomethingWhenModelIsChanged()">
 24
Author: Oleg Polezky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-03 05:30:12

Możesz utworzyć rxjs (V.6) obserwowalny , który robi, co chcesz.

Widok.komponent.html

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

Widok.komponent.ts

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

export class ViewComponent {
    searchChangeObserver;

  onSearchChange(searchValue: string) {

    if (!this.searchChangeObserver) {
      Observable.create(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);
  }  


}
 11
Author: Matthias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-05 00:06:52

Dla każdego, kto używa lodash, niezwykle łatwo jest usunąć dowolną funkcję:

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

To Wrzuć coś takiego do szablonu:

<input [ngModel]="firstName" (ngModelChange)="changed()" />
 6
Author: br4d,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-16 22:27:52

Rozwiązałem to, pisząc dekoratora debounce. Opisany problem można rozwiązać poprzez zastosowanie @ debounceAccessor do set accessor właściwości.

Dostarczyłem również dodatkowy dekorator debounce do metod, który może być przydatny na inne okazje.

To bardzo ułatwia usunięcie Właściwości lub metody. Parametr jest liczbą milisekund, które powinny trwać debounce, 100 ms W poniższym przykładzie.

@debounceAccessor(100)
set myProperty(value) {
  this._myProperty = value;
}


@debounceMethod(100)
myMethod (a, b, c) {
  let d = a + b + c;
  return d;
}

A oto kod do dekoratorzy:

function debounceMethod(ms: number, applyAfterDebounceDelay = false) {

  let timeoutId;

  return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor<any>) {
    let originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      if (timeoutId) return;
      timeoutId = window.setTimeout(() => {
        if (applyAfterDebounceDelay) {
          originalMethod.apply(this, args);
        }
        timeoutId = null;
      }, ms);

      if (!applyAfterDebounceDelay) {
        return originalMethod.apply(this, args);
      }
    }
  }
}

function debounceAccessor (ms: number) {

  let timeoutId;

  return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor<any>) {
    let originalSetter = descriptor.set;
    descriptor.set = function (...args: any[]) {
      if (timeoutId) return;
      timeoutId = window.setTimeout(() => {
        timeoutId = null;
      }, ms);
      return originalSetter.apply(this, args);
    }
  }
}

Dodałem dodatkowy parametr dla dekoratora metody, który pozwala na uruchomienie metody po opóźnieniu debounce. Zrobiłem to, aby móc na przykład używać go w połączeniu ze zdarzeniami mouseover lub resize, w których chciałem, aby przechwytywanie miało miejsce na końcu strumienia zdarzeń. W tym przypadku jednak metoda nie zwróci wartości.

 3
Author: Fredrik_Macrobond,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-16 16:40:22

Możemy utworzyć dyrektywę [debounce], która nadpisuje domyślną funkcję viewtomodelupdate ngModel na pustą.

Kod Dyrektywy

@Directive({ selector: '[debounce]' })
export class MyDebounce implements OnInit {
    @Input() delay: number = 300;

    constructor(private elementRef: ElementRef, private model: NgModel) {
    }

    ngOnInit(): void {
        const eventStream = Observable.fromEvent(this.elementRef.nativeElement, 'keyup')
            .map(() => {
                return this.model.value;
            })
            .debounceTime(this.delay);

        this.model.viewToModelUpdate = () => {};

        eventStream.subscribe(input => {
            this.model.viewModel = input;
            this.model.update.emit(input);
        });
    }
}

Jak go używać

<div class="ui input">
  <input debounce [delay]=500 [(ngModel)]="myData" type="text">
</div>
 3
Author: BebbaPig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-15 00:01:57

Prostym rozwiązaniem byłoby stworzenie dyrektywy, którą można zastosować do dowolnej kontroli.

import { Directive, ElementRef, Input, Renderer, HostListener, Output, EventEmitter } from '@angular/core';
import { NgControl } from '@angular/forms';

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

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


    private modelValue = null;

    constructor(public model: NgControl, el: ElementRef, renderer: Renderer)
    {

    }


    ngOnInit()
    {
         this.modelValue = this.model.value;

         if (!this.modelValue)
         {
            var firstChangeSubs = this.model.valueChanges.subscribe(v =>
            {
               this.modelValue = v;
               firstChangeSubs.unsubscribe()
            });
        }



        this.model.valueChanges
            .debounceTime(this.debounceTime)
            .distinctUntilChanged()
            .subscribe(mv =>
            {
                if (this.modelValue != mv)
                {
                    this.modelValue = mv;
                    this.onDebounce.emit(mv);
                }


            });
    }
}

Użycie byłoby

<textarea [ngModel]="somevalue"   
          [debounce]="2000"
          (onDebounce)="somevalue = $event"                               
          rows="3">
</textarea>
 2
Author: Ashg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-03 02:39:08

Spędziłem nad tym wiele godzin, mam nadzieję, że uda mi się uratować kogoś innego. Dla mnie poniższe podejście do używania debounce na kontrolerze jest bardziej intuicyjne i łatwiejsze do zrozumienia. Zbudowany jest na angular.io rozwiązanie docs dla autouzupełniania, ale z możliwością przechwytywania połączeń bez konieczności polegania na powiązaniu danych z DOM.

Plunker

Scenariuszem użycia może być sprawdzenie nazwy użytkownika po wpisaniu, aby sprawdzić, czy ktoś już zrobione, a następnie Ostrzeżenie użytkownika.

Uwaga: NIE ZAPOMNIJ, (blur)="function(something.value) może mieć dla Ciebie więcej sensu w zależności od twoich potrzeb.

 1
Author: Helzgate,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-23 18:50:33

To najlepsze rozwiązanie, jakie do tej pory znalazłem. Aktualizuje ngModel na blur i debounce

import { Directive, Input, Output, EventEmitter,ElementRef } from '@angular/core';
import { NgControl, NgModel } from '@angular/forms';
import 'rxjs/add/operator/debounceTime'; 
import 'rxjs/add/operator/distinctUntilChanged';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';

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

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

    private isFirstChange: boolean = true;

    constructor(private elementRef: ElementRef, private model: NgModel) {
    }

    ngOnInit() {
        const eventStream = Observable.fromEvent(this.elementRef.nativeElement, 'keyup')
            .map(() => {
                return this.model.value;
            })
            .debounceTime(this.debounceTime);

        this.model.viewToModelUpdate = () => {};

        eventStream.subscribe(input => {
            this.model.viewModel = input;
            this.model.update.emit(input);
        });
    }
}

Jako zapożyczone z https://stackoverflow.com/a/47823960/3955513

Następnie w HTML:

<input [(ngModel)]="hero.name" 
        [debounce]="3000" 
        (blur)="hero.name = $event.target.value"
        (ngModelChange)="onChange()"
        placeholder="name">

On blur model jest jawnie aktualizowany za pomocą zwykłego javascript.

Przykład tutaj: https://stackblitz.com/edit/ng2-debounce-working

 1
Author: Shyamal Parikh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-21 10:59:57

Ponieważ temat jest stary, większość odpowiedzi nie działa na .
Oto krótkie i proste rozwiązanie dla Angular 6 z RxJS.

Najpierw zaimportuj niezbędne rzeczy:

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

Initialize on ngOnInit:

export class MyComponent implements OnInit {
  notesText: string;
  notesModelChanged: Subject<string> = new Subject<string>();

  constructor() { }

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

Użyj w ten sposób:

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

P. S.: w przypadku bardziej złożonych i wydajnych rozwiązań możesz nadal chcieć sprawdzić inne odpowiedzi.

 0
Author: Just Shadow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-09 12:07:13

Dla form reaktywnych i obsługi pod kątem V2(najnowsze) plus v4 spójrz na:

Https://github.com/angular/angular/issues/6895#issuecomment-290892514

Mam nadzieję, że wkrótce pojawi się natywne wsparcie dla tego typu rzeczy...

 -1
Author: Matthew Erwin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-04-01 04:29:52