Угловой и отказов
В AngularJS я могу отменить модель, используя параметры ng-model.
ng-model-options="{ debounce: 1000 }"
Как я могу разоблачить модель в Angular? Я пытался найти debounce в документах, но ничего не смог найти.
Решением было бы написать мою собственную функцию 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)">
Но я ищу встроенную функцию, есть ли такая в Angular?
14 ответов
Обновлено для RC.5
С Angular 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();
}
}
Приведенный выше код также включает в себя пример того, как регулировать события изменения размера окна, как это спросил @albanx в комментарии ниже.
Хотя приведенный выше код, вероятно, является Angular-способом сделать это, он неэффективен. Каждое нажатие клавиши и каждое событие изменения размера, даже если они отлажены и задушены, приводит к выполнению обнаружения изменений. Другими словами, устранение неполадок и регулирование не влияют на частоту выполнения обнаружения изменений. (Я нашел комментарий GitHub Тобиаса Босха, который подтверждает это.) Вы можете увидеть это, когда запустите поршень, и увидите, сколько раз ngDoCheck()
вызывается при вводе в поле ввода или изменении размера окна. (Используйте синюю кнопку "х", чтобы запустить поршень в отдельном окне, чтобы увидеть события изменения размера.)
Более эффективный метод заключается в создании RxJS Observables самостоятельно из событий за пределами "зоны" Angular. Таким образом, обнаружение изменений не вызывается каждый раз, когда происходит событие. Затем в ваших методах обратного вызова подписки инициируйте обнаружение изменений вручную, т.е. вы управляете, когда вызывается обнаружение изменений:
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();
}
}
я использую ngAfterViewInit()
вместо ngOnInit()
чтобы убедиться, что inputElRef
определено.
detectChanges()
запустит обнаружение изменений на этом компоненте и его дочерних элементах. Если вы предпочитаете запустить обнаружение изменений из корневого компонента (т. Е. Выполнить полную проверку обнаружения изменений), используйте ApplicationRef.tick()
вместо. (Я позвонил ApplicationRef.tick()
в комментариях в вантуз.) Обратите внимание, что зовет tick()
вызовет ngDoCheck()
быть названным.
Если вы не хотите иметь дело с @angular/forms
Вы можете просто использовать RxJS Subject
с изменениями привязок.
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);
}
}
Это вызывает обнаружение изменений. Для способа, который не вызывает обнаружение изменений, проверьте ответ Марка.
Обновить
.pipe(debounceTime(300), distinctUntilChanged())
нужен для rxjs 6.
Пример:
constructor() {
this.modelChanged.pipe(
debounceTime(300),
distinctUntilChanged())
.subscribe(model => this.model = model);
}
Поскольку тема старая, большинство ответов не работают на Angular 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)' />
PS: для более сложных и эффективных решений вы все еще можете проверить другие ответы.
Это может быть реализовано как директива
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()">
Непосредственно недоступен, как в angular1, но вы легко можете поиграть с наблюдаемыми NgFormControl и RxJS:
<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
Здесь это для автозаполнения, но это работает во всех сценариях.
Вы можете создать Observable RxJS (v.6), который делает все что угодно.
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);
}
}
Для тех, кто использует lodash, очень легко отменить любую функцию:
changed = _.debounce(function() {
console.log("name changed!");
}, 400);
затем просто добавьте что-то подобное в ваш шаблон:
<input [ngModel]="firstName" (ngModelChange)="changed()" />
Решение с инициализацией абонента непосредственно в функции события:
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);
}
}
И HTML:
<input type="text" (input)="onFind($event.target.value)">
Я решил это, написав декоратор debounce. Описанная проблема может быть решена путем применения @debounceAccessor к аксессору набора свойств.
Я также предоставил дополнительный декоратор 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], которая перезаписывает функцию 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>
HTML-файл:
<input [ngModel]="filterValue"
(ngModelChange)="filterValue = $event ; search($event)"
placeholder="Search..."/>
TS файл:
timer = null;
time = 250;
search(searchStr : string) : void {
clearTimeout(this.timer);
this.timer = setTimeout(()=>{
console.log(searchStr);
}, time)
}
Вы также можете решить эту проблему, используя декораторы, например, используя декоратор debounce из utils-decorator lib (npm install utils-decorators
):
import {debounce} from 'utils-decorators';
class MyAppComponent {
@debounce(500)
firstNameChanged($event, first) {
...
}
}
DebounceTime в Angular 7 с RxJS v6
Ссылка на источник
Демо- ссылка
В шаблоне HTML
<input type="text" #movieSearchInput class="form-control"
placeholder="Type any movie name" [(ngModel)]="searchTermModel" />
В компоненте
....
....
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)});
}
}
Простым решением было бы создать директиву, которую вы можете применить к любому элементу управления.
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>
Это лучшее решение, которое я нашел до сих пор. Обновляет ngModel
на 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);
});
}
}
как заимствовано из /questions/40545580/uglovoj-i-otkazov/40545590#40545590
Тогда в HTML:
<input [(ngModel)]="hero.name"
[debounce]="3000"
(blur)="hero.name = $event.target.value"
(ngModelChange)="onChange()"
placeholder="name">
На blur
модель явно обновляется с использованием простого javascript.
Пример здесь: https://stackblitz.com/edit/ng2-debounce-working
Потратил на это часы, надеюсь, я смогу спасти кого-то еще. Для меня следующий подход к использованию debounce
на элемент управления более интуитивно понятен и легче для меня. Он основан на решении angular.io docs для автозаполнения, но с возможностью перехвата вызовов без необходимости привязывать данные к DOM.
Сценарий использования для этого может быть проверка имени пользователя после его ввода, чтобы увидеть, если кто-то уже взял его, а затем предупреждение пользователя.
Примечание: не забывайте, (blur)="function(something.value)
может иметь больше смысла для вас в зависимости от ваших потребностей.
Для Реактивных форм и обработки в Angular v2(последняя версия) плюс v4 смотрите:
https://github.com/angular/angular/issues/6895
Надеюсь, скоро будет такая поддержка...