wake-up-neo.com

Winkel und entprellen

In AngularJS kann ich ein Modell mit den Optionen von ng-model entprellen.

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

Wie kann ich ein Modell in Angular entprellen? Ich habe versucht, in den Dokumenten nach Entprellung zu suchen, aber ich konnte nichts finden.

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

Eine Lösung wäre, meine eigene Debounce-Funktion zu schreiben, zum Beispiel:

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

Und mein HTML

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

Aber ich suche nach einer eingebauten Funktion, gibt es eine in Angular?

115
koningdavid

Aktualisiert für RC.5

Mit Angular 2 können wir mithilfe des RxJS-Operators debounceTime() auf den valueChanges eines Formularsteuerelements debounce:

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

Der obige Code enthält auch ein Beispiel für das Drosseln der Fenstergrößenänderung, wie von @albanx in einem Kommentar unten angefordert.


Obwohl der obige Code wahrscheinlich der Angular-Weg ist, ist er nicht effizient. Jeder Tastenanschlag und jedes Ereignis zur Größenänderung führt zur Ausführung der Änderungserkennung, auch wenn sie entwertet und gedrosselt werden. Mit anderen Worten, das Entprellen und Drosseln hat keinen Einfluss darauf, wie oft die Änderungserkennung ausgeführt wird . (Ich habe ein GitHub-Kommentar von Tobias Bosch gefunden, das dies bestätigt.) Sie können dies sehen, wenn Sie den Plunker ausführen, und Sie sehen, wie oft ngDoCheck() aufgerufen wird, wenn Sie in das Eingabefeld tippen oder die Größe ändern Fenster. (Verwenden Sie die blaue Schaltfläche "x", um den Plunker in einem separaten Fenster auszuführen und die Größenänderungsereignisse anzuzeigen.)

Eine effizientere Methode besteht darin, RxJS Observables außerhalb der "Zone" von Angular selbst aus den Ereignissen zu erstellen. Auf diese Weise wird die Änderungserkennung nicht bei jedem Auslösen eines Ereignisses aufgerufen. Lösen Sie dann in Ihren Methoden zum Abonnieren von Rückrufen die Änderungserkennung manuell aus, d. H. Sie steuern, wann die Änderungserkennung aufgerufen wird:

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

Ich verwende ngAfterViewInit() anstelle von ngOnInit(), um sicherzustellen, dass inputElRef definiert ist.

detectChanges() führt die Änderungserkennung für diese Komponente und ihre untergeordneten Komponenten aus. Wenn Sie die Änderungserkennung lieber über die Root-Komponente ausführen möchten (d. H. Eine vollständige Änderungserkennung durchführen), verwenden Sie stattdessen ApplicationRef.tick() . (Ich habe in Kommentaren im Plunker ApplicationRef.tick() angerufen.) Beachten Sie, dass durch das Aufrufen von tick()ngDoCheck() aufgerufen wird.

180
Mark Rajcok

Wenn Sie sich nicht mit @angular/forms beschäftigen möchten, können Sie einfach eine RxJS Subject mit Änderungsbindungen verwenden.

view.component.html

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

view.component.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);
    }
}

Dies löst eine Änderungserkennung aus. Für einen Weg, der keine Änderungserkennung auslöst, checke Marks Antwort.


Aktualisieren

.pipe(debounceTime(300), distinctUntilChanged()) wird für rxjs 6 benötigt.

Beispiel:

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

Nicht direkt zugänglich wie in angle1, aber Sie können problemlos mit NgFormControl und RxJS-Observables spielen:

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

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

Dieser Blogbeitrag erklärt es deutlich: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

Hier ist es für eine Autovervollständigung, aber es funktioniert in allen Szenarien.

28
bertrandg

Es könnte als Richtlinie umgesetzt werden

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

}

benutze es gerne

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

komponentenprobe

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);
    });
  }
} 
26
Oleg Polezky

Sie können create ein RxJS (v.6) Observable machen, das tut, was Sie möchten.

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


}
14
Matthias

Für jeden, der lodash verwendet, ist es extrem einfach, zu entprellen jede Funktion:

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

dann werfen Sie einfach so etwas in Ihre Vorlage:

<input [ngModel]="firstName" (ngModelChange)="changed()" />
8
br4d

Ich löste dieses Problem, indem ich einen Entprägungsdekorateur schrieb. Das beschriebene Problem könnte gelöst werden, indem der @debounceAccessor auf den Set-Accessor der Eigenschaft angewendet wird.

Ich habe auch einen zusätzlichen Entprägungsdekorator für Methoden bereitgestellt, der für andere Gelegenheiten nützlich sein kann.

Dies macht es sehr einfach, eine Eigenschaft oder eine Methode zu entprellen. Der Parameter gibt die Anzahl der Millisekunden an, die die Entprellung dauern soll, in dem folgenden Beispiel 100 ms.

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


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

Und hier ist der Code für die Dekorateure:

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

Ich habe einen zusätzlichen Parameter für den Method Decorator hinzugefügt, mit dem Sie die Methode NACH der Entprellungsverzögerung auslösen können. Ich habe das getan, damit ich es zum Beispiel in Verbindung mit Mouseover oder zur Größenänderung von Ereignissen verwenden konnte, wobei die Erfassung am Ende des Ereignisstroms erfolgen sollte. In diesem Fall gibt die Methode jedoch keinen Wert zurück.

3

Wir können eine Direktive [debounce] erstellen, die die standardmäßige viewToModelUpdate-Funktion von ngModel mit einer leeren Funktion überschreibt.

Richtliniencode

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

Wie man es benutzt

<div class="ui input">
  <input debounce [delay]=500 [(ngModel)]="myData" type="text">
</div>
3
BebbaPig

Eine einfache Lösung wäre die Erstellung einer Direktive, die Sie auf jedes Steuerelement anwenden können.

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


            });
    }
}

nutzung wäre 

<textarea [ngModel]="somevalue"   
          [debounce]="2000"
          (onDebounce)="somevalue = $event"                               
          rows="3">
</textarea>
2
Ashg

Da das Thema alt ist, funktionieren die meisten Antworten nicht auf Angular 6 .
Hier also eine kurze und einfache Lösung für Angular 6 mit RxJS.

Importiere zuerst notwendige Sachen: 

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

Initialisierung bei 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);
      });
  }
}

Verwenden Sie diesen Weg:

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

P.S .: Für komplexere und effizientere Lösungen möchten Sie vielleicht noch andere Antworten prüfen.

2
Just Shadow

Ich habe Stunden damit verbracht, hoffentlich kann ich jemandem etwas Zeit sparen. Für mich ist der folgende Ansatz zur Verwendung von debounce in einer Steuerung intuitiver und verständlicher. Es basiert auf der angle.io docs-Lösung für Autocomplete, hat aber die Möglichkeit, Anrufe abzufangen, ohne dass die Daten an das DOM gebunden werden müssen.

Plunker

Ein Anwendungsszenario für dieses Szenario besteht darin, einen Benutzernamen nach der Eingabe zu überprüfen, um festzustellen, ob er bereits vergeben ist. Anschließend wird der Benutzer gewarnt.

Hinweis: Vergessen Sie nicht, dass (blur)="function(something.value) für Sie je nach Ihren Bedürfnissen sinnvoller ist.

1
Helzgate

HTML-Datei:

<input [ngModel]="filterValue"
       (ngModelChange)="filterValue = $event ; search($event)"
        placeholder="Search..."/>

TS-Datei:

timer = null;
time = 250;
  search(searchStr : string) : void {
    clearTimeout(this.timer);
    this.timer = setTimeout(()=>{
      console.log(searchStr);
    }, time)
  }
1

DebounceTime in Angular 7 mit RxJS v6

Quelle Link

Demo Link

 enter image description here 

In HTML-Vorlage

<input type="text" #movieSearchInput class="form-control"
            placeholder="Type any movie name" [(ngModel)]="searchTermModel" />

In der Komponente

    ....
    ....
    export class AppComponent implements OnInit {

    @ViewChild('movieSearchInput') movieSearchInput: ElementRef;
    apiResponse:any;
    isSearching:boolean;

        constructor(
        private httpClient: HttpClient
        ) {
        this.isSearching = false;
        this.apiResponse = [];
        }

    ngOnInit() {
        fromEvent(this.movieSearchInput.nativeElement, 'keyup').pipe(
        // get value
        map((event: any) => {
            return event.target.value;
        })
        // if character length greater then 2
        ,filter(res => res.length > 2)
        // Time in milliseconds between key events
        ,debounceTime(1000)        
        // If previous query is diffent from current   
        ,distinctUntilChanged()
        // subscription for response
        ).subscribe((text: string) => {
            this.isSearching = true;
            this.searchGetCall(text).subscribe((res)=>{
            console.log('res',res);
            this.isSearching = false;
            this.apiResponse = res;
            },(err)=>{
            this.isSearching = false;
            console.log('error',err);
            });
        });
    }

    searchGetCall(term: string) {
        if (term === '') {
        return of([]);
        }
        return this.httpClient.get('http://www.omdbapi.com/?s=' + term + '&apikey=' + APIKEY,{params: PARAMS.set('search', term)});
    }

    }
1
Code Spy

Dies ist die beste Lösung, die ich bisher gefunden habe. Aktualisiert die ngModelon blur und 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);
        });
    }
}

wie von https://stackoverflow.com/a/47823960/3955513 geliehen

Dann in HTML:

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

Bei blur wird das Modell explizit mit einfachem Javascript aktualisiert.

Beispiel hier: https://stackblitz.com/edit/ng2-debounce-working

1
Shyamal Parikh

Lösung mit Initialisierungs-Teilnehmer direkt in Event-Funktion:

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

class MyAppComponent {
    searchTermChanged: Subject<string> = new Subject<string>();

    constructor() {
    }

    onFind(event: any) {
        if (this.searchTermChanged.observers.length === 0) {
            this.searchTermChanged.pipe(debounceTime(1000), distinctUntilChanged())
                .subscribe(term => {
                    // your code here
                    console.log(term);
                });
        }
        this.searchTermChanged.next(event);
    }
}

Und HTML:

<input type="text" (input)="onFind($event.target.value)">
0
Serhii Vasko