I have a method which is called everytime some input text is change. (Basically it is a search).
I want to delay post/get request so if user quickly types, only one request will be send to server.
I was thinking something like that:
public partnerFilterChange(value)
{
if (this.partnersSubscriptions)
this.partnersSubscriptions.unsubscribe();
this.partnersSubscriptions = this.partnersService.list({ filter: value })
.debounceTime(5000)
.subscribe(partners =>
{
delete this.partnersSubscriptions;
this.partners = partners;
});
}
but it is not working.
Http request is executed immediately, not after 5 seconds. I also try delay instead debounceTime.
Edited:
I am using kendo drop down list component and its change event, so I have no control over the function call, only on subscribing to http request.
As per my comment. You can't use form.get('fieldName').valueChanges directly since you're using Kendo. But you CAN push the values received from Kendo to your own, custom observable, thus replicating the behavior of valueChanges:
class AppComponent {
// This is your own, custom observable that you'll subscribe to.
// It will contain a stream of filter values received from Kendo.
private _filterValues: Subject<string> = new Subject<string>();
constructor() {
// Start from your custom stream, debounce values, and run http query
this._filterValues.asObservable()
.debounceTime(400)
.mergeMap(value => this.partnersService.list({ filter: value }))
.subscribe(partners => this.partners = partners);
}
// This method is called by Kendo every time the field value changes.
handleFilter(value) {
// Push the value in the custom stream.
this._filterValues.next(value);
}
}
NB. This code assumes that this.partnersService.list() returns an observable.
With this code, every time you update the field, the list of partners should be refreshed and the debounce should be applied. (I haven't tested the code, you might need to adapt it to your own use case.)
import { Component, Output, HostListener, ChangeDetectionStrategy } from '#angular/core';
import { Observable } from 'rxjs/Observable';
import { FormGroup, FormControl } from '#angular/forms';
#Component({
selector: 'my-user-search',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<form role="search" class="navbar-form-custom form-inline" [formGroup]="searchForm">
<div class="form-group">
<label for="user-search">Search</label>
<input
id="user-search"
class="form-control"
name="input"
type="text"
placeholder="Search Co-worker"
formControlName="search"
/>
</div>
</form>
`
})
export class UserSearchComponent {
searchForm = new FormGroup({
search: new FormControl('')
});
#Output() search: Observable<string> = this.searchForm.valueChanges
.map(form => form.search)
.debounceTime(700)
.distinctUntilChanged();
#HostListener('window:keyup', ['$event'])
cancelSearch(event) {
if (event.code === 'Escape') {
this.searchForm.reset();
// this.searchControl.setValue('', {emitEvent: true});
}
}
}
Usage
Where the $event value is the search term and (search) will be called first when 700ms has passed and the input is not the same
<my-user-search (search)="handleSearch($event)"></my-user-search>
I had a similar issue in my Angular 2 app, I am just going to paste my solution:
subscribeToSearchQueryChanges(){
const sub = Observable.fromEvent(this.panelSuggestionBox.nativeElement, 'keyup')
.debounceTime(300)
.filter((kbE: KeyboardEvent) => {
return !(kbE['code'] === 'Space' || kbE.key === 'ArrowDown' || kbE.key === 'ArrowUp' || kbE.key === 'Enter' || kbE.key === 'Tab' || kbE.key === 'Shift')
})
.map(() => _.trim(this.panelSuggestionBox.nativeElement.value) )
.filter((term: string) => term.length > 2 )
.switchMap((term: string) => this.suggestionService.getSuggestions(this.suggestionTypes, term))
.subscribe((suggestions: Suggestion[]) => {
this.suggestionsData = this.suggestionService.groupSuggestions(suggestions);
this.toggleSuggestionList();
}, err => {
console.error('suggestions failed', err);
this.removeSubscription(sub);
this.subscribeToSearchQueryChanges();
});
this.addSubscription(sub);
}
Related
I cannot define specific type when using FromEventsfrom RxJS. When I pipe the 'keyup' event and map it to get the inputed value, I can only use (e: any => return (e.target!).value;). Even though the browser's debuggers identifies the e as a KeyboardEvent, if I define e: KeyboardEvent I get an error stating that "e does not contain property target". The current code works, however, since I am new to RxJS + Angular, and the community always advise to avoid using any, so I wonder what can I do to avoid using it in this situation ?
Component.ts
import { TagInterface } from './../../../interfaces/tag/tag-interface';
import { Component, OnInit, Output, EventEmitter, ViewChild, ElementRef, AfterViewInit } from '#angular/core';
import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
import { fromEvent, Observable } from 'rxjs';
#Component({
selector: 'app-tag-search-bar',
templateUrl: './tag-search-bar.component.html',
styleUrls: ['./tag-search-bar.component.scss']
})
export class TagSearchBarComponent implements OnInit,AfterViewInit {
#Output() tags = new EventEmitter<TagInterface[]>();
#ViewChild('tagInput') tagInputChild!: ElementRef;
mappedAndFilteredInput!: Observable<any>;
eventKeyUp! : Observable<any>;
constructor() { }
ngOnInit(): void {
}
ngAfterViewInit() {
this.eventKeyUp = fromEvent(this.tagInputChild.nativeElement, 'keyup');
this.mappedAndFilteredInput = this.eventKeyUp.pipe(
map((e: any) => {
return (e.target!).value;
}),
debounceTime(100),
distinctUntilChanged(),
filter(value => (value != "" && value != undefined))
);
this.mappedAndFilteredInput.subscribe(
value => console.log(value),
error => console.log(`error: ${error}`),
() => console.log('completed maping and filtering tag input term'));
}
}
HTML
<div class="input-group-prepend">
<span class="input-group-text">
<i class="far fa-filter"></i>
</span>
</div>
<input #tagInput type="text" class="filter-tag-list">
Here's the type-safe way to get the target from a KeyboardEvent
fromEvent<KeyboardEvent>(document.documentElement, 'keyup').subscribe(e => {
const [target] = e.composedPath()
console.log(target)
})
However, in your case it would be simpler to just do
fromEvent(this.tagInputChild.nativeElement, 'keyup').subscribe(_ => {
console.log(this.tagInputChild.nativeElement.value)
})
Or better still use a form control, and listen to valueChanges on the form control.
I am trying to create a reusable form editor component that shows/hides a toolbar at the bottom of the form based on if the form is dirty. The toolbar has a save button and a reset form button. Both of the buttons on the toolbar need to be controlled based on the validity of the form. I used this tutorial here which is great... but I want to be able to reuse the component by passing in any form.
Here is my code (notice I am creating a FormGroup and passing it directly to ContentEditorComponent as an Input. My custom dirtyCheck operator is at the bottom.)
content-editor.component.ts
#Component({
selector: 'content-editor',
template: `
<mat-card class="card">
<ng-content></ng-content>
<div class="footer" *ngIf="isDirty$ | async">
<button type="submit" [disabled]="form.invalid$">
</div>
</mat-card>
`,
})
export class ContentEditorComponent implements OnInit, OnDestroy
{
#Input('formGroup') form: FormGroup;
isDirty$: Observable<boolean>;
source: Observable<any>;
ngOnInit(): void {
this.source = of(cloneDeep(this.form.value));
this.isDirty$ = this.form.valueChanges.pipe(
dirtyCheck(this.source)
);
}
initForm(): void {
this.source = of(cloneDeep(this.form.value));
this.isDirty$ = this.form.valueChanges.pipe(
takeUntil(this.unsubscribeAll),
dirtyCheck(this.source)
);
}
reset(): void {
this.form.patchValue(this.source);
this.initForm();
}
}
service-area-editor.component.html
<content-editor [formGroup]="form">
... my form
</content-editor>
dirty-check.ts
export function dirtyCheck<U>(source: Observable<U>) {
let subscription: Subscription;
let isDirty = false;
return function <T>(valueChanges: Observable<T>): Observable<boolean> {
const isDirty$ = combineLatest([
source,
valueChanges,
]).pipe(
debounceTime(300),
map(([a, b]) => {
console.log(a, b);
return isDirty = isEqual(a, b) === false;
}),
finalize(() => this.subscription.unsubscribe()),
startWith(false)
);
subscription = fromEvent(window, 'beforeunload').subscribe(event => {
isDirty && (event.returnValue = false);
});
return isDirty$;
};
}
How do I get the dirtyCheck operator to work correctly when the form can be anything? I cant supply the dirtyCheck operator with a source stream because the source stream comes from the parent component.
I am trying to call to a service on input key-up event.
The HTML
<input placeholder="enter name" (keyup)='onKeyUp($event)'>
Below is the onKeyUp() function
onKeyUp(event) {
let observable = Observable.fromEvent(event.target, 'keyup')
.map(value => event.target.value)
.debounceTime(1000)
.distinctUntilChanged()
.flatMap((search) => {
// call the service
});
observable.subscribe((data) => {
// data
});
}
It was found from the network tab of the browser that, it is calling the key-up function on every key-up event(as it is supposed to do), but what I am trying to achieve is a debounce time of 1sec between each service call. Also, the event is triggered if I move the arrow key move.
plunkr link
So the chain is really correct but the problem is that you're creating an Observable and subscribe to it on every keyup event. That's why it prints the same value multiple times. There're simply multiple subscriptions which is not what you want to do.
There're obviously more ways to do it correctly, for example:
#Component({
selector: 'my-app',
template: `
<div>
<input type="text" (keyup)='keyUp.next($event)'>
</div>
`,
})
export class App implements OnDestroy {
public keyUp = new Subject<KeyboardEvent>();
private subscription: Subscription;
constructor() {
this.subscription = this.keyUp.pipe(
map(event => event.target.value),
debounceTime(1000),
distinctUntilChanged(),
mergeMap(search => of(search).pipe(
delay(500),
)),
).subscribe(console.log);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
See your updated demo: http://plnkr.co/edit/mAMlgycTcvrYf7509DOP
Jan 2019: Updated for RxJS 6
#marlin has given a great solution and it works fine in angular 2.x but with angular 6 they have started to use rxjs 6.0 version and that has some slight different syntax so here is the updated solution.
import {Component} from '#angular/core';
import {Observable, of, Subject} from 'rxjs';
import {debounceTime, delay, distinctUntilChanged, flatMap, map, tap} from 'rxjs/operators';
#Component({
selector: 'my-app',
template: `
<div>
<input type="text" (keyup)='keyUp.next($event)'>
</div>
`,
})
export class AppComponent {
name: string;
public keyUp = new Subject<string>();
constructor() {
const subscription = this.keyUp.pipe(
map(event => event.target.value),
debounceTime(1000),
distinctUntilChanged(),
flatMap(search => of(search).pipe(delay(500)))
).subscribe(console.log);
}
}
Well, here's a basic debouncer.
ngOnInit ( ) {
let inputBox = this.myInput.nativeElement;
let displayDiv = this.myDisplay.nativeElement;
let source = Rx.Observable.fromEvent ( inputBox, 'keyup' )
.map ( ( x ) => { return x.currentTarget.value; } )
.debounce ( ( x ) => { return Rx.Observable.timer ( 1000 ); } );
source.subscribe (
( x ) => { displayDiv.innerText = x; },
( err ) => { console.error ( 'Error: %s', err ) },
() => {} );
}
}
If you set up the two indicated view children (the input and the display), you'll see the debounce work. Not sure if this doesn't do anything your does, but this basic form is (as far as I know) the straightforward way to debounce, I use this starting point quite a bit, the set of the inner text is just a sample, it could make a service call or whatever else you need it to do.
I would suggest that you check the Angular2 Tutorial that show a clean and explained example of something similar to what you're asking.
https://angular.io/docs/ts/latest/tutorial/toh-pt6.html#!#observables
I am using 'angular2-virtual-scroll' to implement load on demand. The items used to be driven by observable's using the async pipe triggered by the parent component. Now i am trying to call my service from the child. The call is successful and i get my data, i need to use the subscribe event to apply other logic. The issue is change detected does not appear to be working when i update my arrays in the subscribe function. I have read other similar issues but i have had no luck finding a solution.
This is the main component where the service calls are used. The inital request is done from the onInit. And then when you scroll down fetchMore is called.
import { Component, OnInit, Input, OnDestroy } from '#angular/core';
import { Store } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { User } from './../models/user';
import { Role } from './../../roles/models/role';
import { UsersService } from './../services/users.service';
import { ChangeEvent } from 'angular2-virtual-scroll';
import { promise } from 'selenium-webdriver';
import { VirtualScrollComponent } from 'angular2-virtual-scroll';
import { Subscription } from 'rxjs/Subscription';
#Component({
selector: 'app-users-list',
template: `
<div class="status">
Showing <span class="">{{indices?.start + 1}}</span>
- <span class="">{{indices?.end}}</span>
of <span class="">{{users?.length}}</span>
<span>({{scrollItems?.length}} nodes)</span>
</div>
<virtual-scroll [childHeight]="75" [items]="users" (update)="scrollItems = $event" (end)="fetchMore($event)">
<div #container>
<app-user-info *ngFor="let user of scrollItems" [roles]="roles" [user]="user">
<li>
<a [routerLink]="['/users/edit/', user.id]" class="btn btn-action btn-edit">Edit</a>
</li>
</app-user-info>
<div *ngIf="loading" class="loader">Loading...</div>
</div>
</virtual-scroll>
`
})
export class UsersListComponent implements OnInit, OnDestroy {
users: User[] = [];
#Input() roles: Role[];
currentPage: number;
scrollItems: User[];
indices: ChangeEvent;
readonly bufferSize: number = 20;
loading: boolean;
userServiceSub: Subscription;
constructor(private usersService: UsersService) {
}
ngOnInit() {
this.reset();
}
ngOnDestroy() {
if(this.userServiceSub) {
this.userServiceSub.unsubscribe();
}
}
reset() {
this.loading=true;
this.currentPage = 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = users;
});
}
fetchMore(event: ChangeEvent) {
if (event.end !== this.users.length) return;
this.loading=true;
this.currentPage += 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = this.users.concat(users);
});
}
}
From what i have read this could be a context issue but i am not sure. Any suggestions would be great.
"EDIT"
Looking at the source code for the plugin component i can see where the change event is captured.
VirtualScrollComponent.prototype.ngOnChanges = function (changes) {
this.previousStart = undefined;
this.previousEnd = undefined;
var items = changes.items || {};
if (changes.items != undefined && items.previousValue == undefined || (items.previousValue != undefined && items.previousValue.length === 0)) {
this.startupLoop = true;
}
this.refresh();
};
If i put a breakpoint in this event it fires on the initial load, so when we instantiate the array to []. It fires when i click on the page. But it does not fire when the array is update in the subscribe event. I have even put a button in that sets the array to empty, and that updates the view so the subscribe function must be breaking the change detection.
So when you say the change detection does not appear to be working, I assume you are referring to this: *ngFor="let user of scrollItems"?
I have not used that particular component nor do I have any running code to work with ... but I'd start by taking a closer look at this:
<virtual-scroll [childHeight]="75"
[items]="currentBuffer"
(update)="scrollItems = $event"
(end)="fetchMore($event)">
Maybe change the (update) to call a method just to ensure it is emitting and that you are getting what you expect back from it.
EDIT:
Here is an example subscription that updates the primary bound property showing the data for my page:
movies: IMovie[];
getMovies(): void {
this.movieService.getMovies().subscribe(
(movies: IMovie[]) => {
this.movies = movies;
this.performFilter(null);
},
(error: any) => this.errorMessage = <any>error
);
}
The change detection works fine in this case. So there is most likely something else going on causing the issue you are seeing.
Note that your template does need to bind to the property for the change detection to work. In my example, I'm binding to the movies property. In your example, you'd need to bind to the users property.
So change detection was not firing. I had to use "ChangeDetectorRef" with the function "markForCheck" to get change detection to work correctly. I am not sure why so i definitely have some research to do.
I would like to build a directive that can mutate values being passed to and from an input, bound with ngModel.
Say I wanted to do a date mutation, every time the model changes, the mutator first gets to change the value to the proper format (eg "2017-05-03 00:00:00" is shown as "2017/05/03"), before ngModel updates the view. When the view changes, the mutator gets to change the value before ngModel updates the model (eg entering "2017/08/03" sets the model to "2017-08-03 00:00:00" [timestamp]).
The directive would be used like this:
<input [(ngModel)]="someModel" mutate="date:YYYY/MM/DD" />
My first instinct was to get a reference to the ControlValueAccessor and NgModel on the Host component.
import { Directive, ElementRef, Input,
Host, OnChanges, Optional, Self, Inject } from '#angular/core';
import { NgModel, ControlValueAccessor,
NG_VALUE_ACCESSOR } from '#angular/forms';
#Directive({
selector: '[mutate]',
})
export class MutateDirective {
constructor(
#Host() private _ngModel: NgModel,
#Optional() #Self() #Inject(NG_VALUE_ACCESSOR)
private _controlValueAccessor: ControlValueAccessor[]
){
console.log('mutute construct', _controlValueAccessor);
}
}
Then I realized that the Angular 2 Forms classes are complicated and I have no idea what I'm doing. Any ideas?
UPDATE
Based on the answer below I came up with the solution: see gist
Usage (requires Moment JS):
<input mutate="YYYY/MM/DD" inputFormat="YYYY-MM-DD HH:mm:ss" [(ngModel)]="someDate">
Short answer: you need to implement ControlValueAccessor in some class and provide it as a NG_VALUE_ACCESSOR for the ngModel with some directive. This ControlValueAccessor and directive can actually be the same class.
TL;DR
It's not very obvious but still not very complicated. Below is the skeleton from one of my date controls. This thing acts as a parser/formatter pair for the angular 1 ng-model.
It all starts with ngModel injecting all NG_VALUE_ACCESSOR's into itself. There are bunch of default providers as well, and they all get injected into ngModel constructor, but ngModel can distinguish between default value accessors and the ones provided by the user. So it picks one to work with. Roughly it looks like this: if there's user's value accessor then it will be picked, otherwise it falls back to choosing from default ones. After that initial setup is done.
Control value accessor should subscribe to the 'input' or some other similar event on input element to process input events from it.
When value is changed externally ngModel calls writeValue() method on value accessor picked during initialization. This method is responsible for rendering display value that will go into an input as string shown to user.
At some point (usually on blur event) control can be marked as touched. This is shown as well.
Please note: code below is not real production code, it has not been tested, it can contain some discrepancies or inaccuracies, but in general it shows the whole idea of this approach.
import {
Directive,
Input,
Output,
SimpleChanges,
ElementRef,
Renderer,
EventEmitter,
OnInit,
OnDestroy,
OnChanges,
forwardRef
} from '#angular/core';
import {Subscription, Observable} from 'rxjs';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '#angular/forms';
const DATE_INPUT_VALUE_ACCESSOR_PROVIDER = [
{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateInputDirective), multi: true}
];
#Directive({
// [date-input] is just to distinguish where exactly to place this control value accessor
selector: 'input[date-input]',
providers: [DATE_INPUT_VALUE_ACCESSOR_PROVIDER],
host: { 'blur': 'onBlur()', 'input': 'onChange($event)' }
})
export class DateInputDirective implements ControlValueAccessor, OnChanges {
#Input('date-input')
format: string;
model: TimeSpan;
private _onChange: (value: Date) => void = () => {
};
private _onTouched: () => void = () => {
};
constructor(private _renderer: Renderer,
private _elementRef: ElementRef,
// something that knows how to parse value
private _parser: DateParseTranslator,
// something that knows how to format it back into string
private _formatter: DateFormatPipe) {
}
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
if (changes['format']) {
this.updateText(this.model, true);
}
}
onBlur = () => {
this.updateText(this.model, false);
this.onTouched();
};
onChange = ($event: KeyboardEvent) => {
// the value of an input - don't remember exactly where it is in the event
// so this part may be incorrect, please check
let value = $event.target.value;
let date = this._parser.translate(value);
this._onChange(date);
};
onTouched = () => {
this._onTouched();
};
registerOnChange = (fn: (value: Date) => void): void => {
this._onChange = fn;
};
registerOnTouched = (fn: () => void): void => {
this._onTouched = fn;
};
writeValue = (value: Date): void => {
this.model = value;
this.updateText(value, true);
};
updateText = (date: Date, forceUpdate = false) => {
let textValue = date ? this._formatter.transform(date, this.format) : '';
if ((!date || !textValue) && !forceUpdate) {
return;
}
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', textValue);
}
}
Then in the html template:
<input date-input="DD/MM/YYYY" [(ngModel)]="myModel"/>
You shouldn't have to do anything with Forms here. As an example, I made a credit card masking directive that formats the user input into a credit card string (a space every 4 characters, basically).
import { Directive, ElementRef, HostListener, Input } from '#angular/core';
#Directive({
selector: '[credit-card]' // Attribute selector
})
export class CreditCard {
#HostListener('input', ['$event'])
confirmFirst(event: any) {
let val = event.target.value;
event.target.value = this.setElement(val);
}
constructor(public element: ElementRef) { }
setElement(val) {
let num = '';
var v = val.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
var matches = v.match(/\d{4,16}/g);
var match = matches && matches[0] || '';
var parts = [];
for (var i = 0, len = match.length; i < len; i += 4) {
parts.push(match.substring(i, i + 4));
}
if (parts.length) {
num = parts.join(' ').trim();
} else {
num = val.trim();
}
return num;
}
}
Then I used it in a template like so:
<input credit-card type="text" formControlName="cardNo" />
I am using form control in this example, but it doesnt matter either way. It should work fine with ngModel binding.