Угловые и дребезга



в AngularJS я могу разоблачить модель, используя параметры ng-model.



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


как я могу разоблачить модель в Angular? Я попытался найти debounce в документах, но ничего не смог найти.



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



решением было бы написать мою собственную функцию debounce, например:



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


и мой html



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


но я ищу встроенную функцию, есть ли она в угловом?

591   13  

13 ответов:

обновлено для RC.5

С угловой 2 мы можем разоблачить с помощью оператора RxJS debounceTime() на форме управления valueChanges заметно:

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

приведенный выше код также включает в себя пример того, как дросселировать события изменения размера окна, как задано @albanx в комментарии ниже.


хотя приведенный выше код, вероятно, является угловым способом его выполнения, он не эффективен. Каждое нажатие клавиши и каждое событие изменения размера, даже если они отменяются и регулируются, приводит к запуску обнаружения изменений. Другими словами,отключение и дросселирование не влияют на то, как часто выполняется обнаружение изменений. (Я нашел GitHub комментарий Тобиас Бош это подтверждает.) Вы можете увидеть это, когда вы запускаете планкер, и вы видите, сколько раз ngDoCheck() вызывается при вводе в поле ввода или изменении размера окна. (Используйте синюю кнопку " x " для запуска plunker в отдельном окне, чтобы увидеть события resize.)

более эффективным методом является создание RxJS наблюдаемых самостоятельно из событий, вне "зоны"углового. Таким образом, обнаружение изменений не вызывается каждый раз при возникновении события. Затем в ваших методах обратного вызова subscribe вручную запускайте обнаружение изменений-т. е. вы контролируете, когда обнаружение изменений вызывается:

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

я использую ngAfterViewInit() вместо из ngOnInit() обеспечить inputElRef определяется.

detectChanges() запускает обнаружение изменений на этом компоненте и его дочерних элементах. Если вы предпочитаете запускать обнаружение изменений из корневого компонента (т. е. выполнять полную проверку обнаружения изменений), то используйте ApplicationRef.tick(). (Я позвонил в ApplicationRef.tick() в комментариях в plunker.) Обратите внимание, что вызов tick() вызывает ngDoCheck() называться.

если вы не хотите иметь дело с @angular/forms, вы можете просто использовать RxJS Subject с привязками изменения.

вид.деталь.HTML-код

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

вид.деталь.ТС

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

это вызывает обнаружение изменений. способ, который не вызывает обнаружение изменений, проверьте ответ Марка.

не доступны напрямую, как в angular1, но вы можете легко играть с ngformcontrol и RxJS observables:

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

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

это сообщение в блоге объясняет это ясно: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

здесь это для автозаполнения, но он работает во всех сценариях.

Это может быть реализовано как Директива

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

}

использовать его как

<input [(ngModel)]="model" [debounce]="500" (onDebounce)="doSomethingWhenModelIsChanged()">

Вы можете создать an RxJS (V. 6) наблюдаемых это делает все, что вам нравится.

вид.деталь.HTML-код

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

вид.деталь.ТС

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


}

для тех, кто использует lodash, это очень легко debounce функции:

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

тогда просто бросьте что-то вроде этого в свой шаблон:

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

Я решил эту проблему, написав декоратор дребезга. Описанная проблема может быть решена путем применения @debounceAccessor к свойству set accessor.

Я также поставил дополнительный декоратор debounce для методов, которые могут быть полезны для других случаев.

Это позволяет очень легко отказаться от свойства или метода. Параметр-это количество миллисекунд, которое должно длиться, в приведенном ниже примере, 100 мс.

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


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

и вот код для декораторов:

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

я добавил дополнительный параметр для декоратора метода, который позволяет вам запускать метод после задержки debounce. Я сделал это, чтобы я мог, например, использовать его в сочетании с наведением курсора мыши или изменением размера событий, где я хотел, чтобы захват происходил в конце потока событий. Однако в этом случае метод не возвращает значение.

мы можем создать директиву [debounce], которая перезаписывает функцию viewToModelUpdate по умолчанию ngModel с пустым.

Директива Код

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

Как использовать

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

простым решением было бы создать директиву, которую можно применить к любому элементу управления.

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


            });
    }
}

использование будет

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

потратил часы на это, надеюсь, я могу сэкономить кому-то еще некоторое время. Для меня следующий подход к использованию debounce на управление более интуитивно и легче понять для меня. Он построен на основе angular.io docs решение для автозаполнения, но с возможностью для меня перехватывать вызовы без необходимости зависеть от привязки данных к DOM.

Plunker

сценарий использования для этого может быть проверка имени пользователя после его ввода, чтобы увидеть, если кто-то уже взял его, а затем предупредил пользователя.

Примечание: не забудьте, (blur)="function(something.value) может сделать больше смысла для вас в зависимости от ваших потребностей.

это лучшее решение я нашел до сих пор. Обновляет ngModel on blur и 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);
        });
    }
}

как заимствовано из https://stackoverflow.com/a/47823960/3955513

затем в HTML:

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

On blur модель явно обновляется с помощью простого javascript.

пример здесь:https://stackblitz.com/edit/ng2-debounce-working

так как тема старая, большинство ответов не работают on Угловое 6.
Итак, вот короткое и простое решение для Angular 6 с RxJS.

сначала импортируйте необходимые вещи:

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

инициализировать на 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);
      });
  }
}

использовать таким образом:

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

P. S.: Для более сложных и эффективных решений, вы можете все еще хотите, чтобы проверить другие ответы.

для реактивных форм и обработки под угловым v2 (последний) плюс v4 посмотрите на:

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

надеюсь, что скоро будет встроенная поддержка для таких вещей...

Comments

    Ничего не найдено.