Trigger observables chain only if there was a button click - javascript

I would like to execute toggleButtonOnClick$() function from the service, only when I click on a button. This function will save current state, service will later receive this change and update usersObs$.
Whenever the above mentioned happens, the toggleButtonOnClick$ gets executed a few more times, even if I didn't click on the button again.
How can I prevent this and make this function only execute when I do .next() on the clickSubject and not when userObs$ changes?
I will write an example of the whole situation
ButtonComponent
private userClick = new Subject<null>();
private obs1$: Observable<string>();
private obs2$: Observable<string>();
private obs3$: Observable<boolean>();
ngOnInit(): void {
this.obs2$ = this.obs1$.pipe(
switchMap((value) => this.someService.getSomePropBasedOnValue$(value))
);
this.obs3$ = this.obs2$.pipe(
switchMap((value) => this.someService.checksAndReturnsBoolean$(value))
this.subscriptions.add(
this.someService.toggleButtonOnClick$(this.obs2$, this.userClick).subscribe()
)
}
ngOnDestroy(): void {
this.subscriptions.unsuscribe();
}
onClick(): void {
// emit a value when click on button to start observable chain
this.userClick.next();
}
HTML
<div
[attr.tooltip]="(obs3$ | async) ?
('text_translation_1' | transloco)
: ('text_translation_2' | transloco)"
>
<span
*ngIf="(obs3$ | async) && !isHovering"
>
Something here
</span>
<span
*ngIf="(obs3$ | async) && isHovering"
>
Something here
</span>
<span
*ngIf="!(obs3$ | async)"
>
Something here
</span>
</div>
SomeService
public checksAndReturnsBoolean$(id): Observable<boolean> {
return this.userObs$.pipe(
map((users) => { users.some(((user) => user.id === id)) }
);
}
public getSomePropBasedOnValue$(id): Observable<SomeObject | null> {
return this.userObs$.pipe(
map((users) => { users.find(((user) => user.id === id)) ?? null }
);
}
public toggleButtonOnClick$(obs2$, userClick): Observable<void> {
return userClick.pipe(
switchMap(() => obs3$),
switchMap((id) => combineLatest([this.getSomeDataById$(id), of(id)]))
).pipe(
map(([data, id]) => {
// some code block that gets executed everytime an observable emits new value
})
);
Once everything finishes, I try to store the users decision after a click is made. So the userObs$ gets updated, once that happens, the block within toggleButtonOnClick$ is executed again, and not once, but 2 sometimes 3 or 4.
Btw, in the component, the obs2$ im using it on the DOM with Async pipe to show/hide stuff. Maybe that is also triggering the calls after the service observable changes.
I've tried several things already without luck.
Any tip or help or guiding would be appreciated.
Thanks.

What I was needing was to use shareReplay(1) and take(1) on different functions on my service to make it work as expected without repeating unnecessary calls.
It would end up looking like this:
public checksAndReturnsBoolean$(id): Observable<boolean> {
return this.userObs$.pipe(
map((users) => { users.some((user) => user.id === id) }),
take(1)
);
}
public getSomePropBasedOnValue$(id): Observable<SomeObject | null> {
return this.userObs$.pipe(
map((users) => { users.find((user) => user.id === id) ?? null }),
shareReplay(1)
);
}

Related

How do I use async pipe instead of using subscribe?

I would like to use the async pipe "| async" instead of subscribing. This is what my subscription code currently looks like:
ngOnInit(): void {
this.activatedRoute.url
.pipe(takeUntil(this.unsubscribe$))
.subscribe(segments => {
this.quizName = segments[1].toString();
});
}
and in my template I have: <mat-card *ngIf="quiz.quizId === quizName">
Let's try this :
quizName$: Observable<any>;
ngOnInit(): void {
this.quizName$ = this.activatedRoute.url
.pipe(
takeUntil(this.unsubscribe$),
map(segments => segments[1].toString()); // Not sure about this part
);
}
<mat-card *ngIf="(quizName$ | async) === quiz.quizId">
Be careful, everytime you will use async pipe in your template, it will make a subscribe.
Add variable:
quizName$ = this.activatedRoute.url.pipe(map(segments => segments[1].toString()));
no need for takeUntil such as "| async" does it
optional(your IDE would know about it by itself)
quizName$: Observable<string> ...
in HTML:
*ngIf="(quizName$ | async) === quiz.quizId"
more "robust" solution
showQuiz$: Observable<boolean> = this.activatedRoute.url.pipe(
map(segments => segments[1].toString()),
map(quizName => quizName === this.quiz && this.quiz.id)
);
*ngIf="showQuiz$ | async"

How can I pipe two data streams/observables in the same pipe?

I have two forms in one view, each outputting changes. I have a global directive with two selectors to get both streams.
I can easily handle one form but when I have two, and place each in its own pipe or directive, they mess up each others save function, so to speak, so I need both in the same pipe. (Or another solution ;)
Form component
<form name="shop_info"
autocomplete="off"
#f1="ngForm"
(SubmitShop)="save($event)"
[SaveShop]="f1"
*ngIf="merchant">
...
</form>
...
<form name="region"
autocomplete="off"
#f2="ngForm"
(SubmitRegion)="save($event)"
[SaveRegion]="f2">
...
</form>
Directive with one stream:
#Directive({
selector: '[SaveShop], [SaveRegion]'
})
export class SaveMerchantDirective implements OnInit {
#Input() SaveShop: any;
#Input() SaveRegion: any;
#Output() SubmitShop: EventEmitter<any> = new EventEmitter<any>();
#Output() SubmitRegion: EventEmitter<any> = new EventEmitter<any>();
#Input() debounce = 350;
constructor(
private saveService: SaveService,
private $transitions: TransitionService
) { }
ngOnInit() {
this.saveRegion.form.valueChanges
.pipe(
debounceTime(this.debounce),
switchMap((data: Object) => {
if (!sessionStorage.getItem('dataRegion') || sessionStorage.getItem('dataRegion') === '{}') {
sessionStorage.setItem('dataRegion', JSON.stringify(data));
}
this.$transitions.onBefore({}, () => {
if (this.saveRegion.valid && !this.saveRegion.pristine) {
if (confirm(this.translate.instant(
'You are about to discard your REGION changes. Click "OK" to discard and navigate away! Click "Cancel" to stay.'
))) {
this.discard();
return true;
} else {
return false;
}
}
});
if (this.saveRegion.valid && !this.saveRegion.pristine) {
window.dispatchEvent(new CustomEvent('showHeader', { detail: true }));
return this.saveService.currentStatus$.pipe(
map(status => {
if (status === 'save') {
this.save(data);
} else if (status === 'discard') {
this.discard();
}
})
);
} else {
return NEVER;
}
})
)
.subscribe();
this.$transitions.onExit({}, () => {
sessionStorage.removeItem('dataRegion');
status = 'false';
this.saveService.changeStatus('false');
});
How do I handle this?
Can I simply do:
combineLatest(this.saveRegion.form.valueChanges, this.saveShop.form.valueChanges)
.pipe(
debounceTime(this.debounce),
switchMap((data: Array<Object>) => {...
Edit:
Doing
Observable.combineLatest(
this.saveShop.form.valueChanges,
this.saveRegion.form.valueChanges
)
.subscribe(...
gives Property 'combineLatest' does not exist on type 'typeof Observable'.
Doing
combineLatest(
this.saveShop.form.valueChanges,
this.saveRegion.form.valueChanges
)
.subscribe(...
gives me several problems where one is that 'form' cannot read undefined (since the two streams do not come in simultaneously one is undefined).
And if I add startWith(null) I get
this.saveShop.form.valueChanges.startWith is not a function
And if I add startWith(null) I get this.saveShop.form.valueChanges.startWith is not a function
That at least is easily solved: this use of operators is deprecated (possibly would still work, if you imported the "right" startWith from the right file), instead use .pipe (as you are doing with other operators: debounceTime, switchMap), so
this.saveRegion.form.valueChanges.pipe(startWith(null)) //...
(Alternatively, concat(of(null), this.saveRegion.form.valueChanges) would do).
Another thing: you can filter out any undefined/null values and just use "the good stuff", like this
combineLatest(
this.saveShop.form.valueChanges.pipe(filter(value => Boolean(value)),
this.saveRegion.form.valueChanges.pipe(filter(value => Boolean(value)),
//...

Rxjs from() operator doesn't send data

I have a simple app on Angular/rxjs/Ngrx which requests list of default films from the api.
component.ts
export class MoviesComponent implements OnInit {
private movies$: Observable<{}> =
this.store.select(fromRoot.getMoviesState);
private films = [];
constructor(public store: Store<fromRoot.State>) {}
ngOnInit() {
this.store.dispatch(new MoviesApi.RequestMovies());
this.movies$.subscribe(film => this.films.push(film));
console.log(this.films)
}
effects.ts
#Effect()
requestMovies$: Observable<MoviesApi.MoviesApiAction> = this.actions$
.pipe(
ofType(MoviesApi.REQUEST_MOVIES),
switchMap(actions => this.MoviesApiServ.getDefaultMoviesList()
.pipe(
mergeMap(movies => of(new MoviesApi.RecieveMovies(movies))),
catchError(err => {
console.log('err', err);
return of(new MoviesApi.RequestFailed(err));
})
)
)
);
service.ts
export class MoviesApiService {
private moviesList = [];
constructor(private http: HttpClient) { }
public getDefaultMoviesList(): Observable<{}> {
DEFAULT_MOVIES.map(movie => this.getMovieByTitle(movie).subscribe(item => this.moviesList.push(item)));
return from(this.moviesList);
}
public getMovieByTitle(movieTitle: string): Observable<{}> {
return this.http.get<{}>(this.buildRequestUrl(movieTitle))
.pipe(retry(3),
catchError(this.handleError)
);
}
}
DEFAULT_MOVIES is just array with titles.
So my getDefaultMoviesList method is not sending data. But if I replace this.moviesList to hardcoced array of values it works as expected.
What I'm doing wrong?
UPD
I wanted to loop over the default list of films, then call for each film getMovieByTitle and collect them in array and send as Observable. Is there any better solution?
1) You should probably move this line to the service contructor, otherwise you will push a second array of default movies every time you getDefaultMoviesList:
DEFAULT_MOVIES.map(movie => this.getMovieByTitle(movie).subscribe(item => this.moviesList.push(item)));
2) Actually you should probably merge the output of each http.get:
public getDefaultMoviesList(): Observable<{}> {
return merge(DEFAULT_MOVIES.map(movie => this.http.get<{}>(this.buildRequestUrl(movieTitle))
.pipe(retry(3),
catchError(this.handleError)
)))
}
3) You should actually only do that once and store it in BehaviorSubject not to make new HTTP request on each getDefaultMoviesList
private movies$: BehaviorSubject<any> = new BehaviorSubject<any>();
public getMovies$() {
return this.movies$.mergeMap(movies => {
if (movies) return of(movies);
return merge(DEFAULT_MOVIES.map(movie => this.http.get<{}>(this.buildRequestUrl(movieTitle))
.pipe(retry(3),
catchError(this.handleError)
)))
})
}
4) Your implementation shouldn't work at all since:
public getDefaultMoviesList(): Observable<{}> {
DEFAULT_MOVIES.map(movie => this.getMovieByTitle(movie).subscribe(item =>
this.moviesList.push(item))); // This line will happen after http get completes
return from(this.moviesList); // This line will happen BEFORE the line above
}
So you will always return an Observable of empty array.
5) You shouldn't use map if you don't want to map your array to another one. You should use forEach instead.
map is used like this:
const mappedArray = toMapArray.map(element => someFunction(element));
You can try creating the observable using of operator.
Ex: of(this.moviesList);
One intersting fact to note is that Observable.of([]) will be an empty array when you subscribe to it. Where as when you subscribe to Observable.from([]) you wont get any value.
Observable.of, is a static method on Observable. It creates an Observable for you, that emits value(s) that you specify as argument(s) immediately one after the other, and then emits a complete notification.

DebounceTime emits all the events which was captred during the time

I need to write the async validator for the reactive form type in angular.
I have implemented it trough promise. But the issue is the validator triggers for each keystroke it strike the server for every keystroke.For implementing the debounce i have implemented the setTimeout for the promise but the issue i faced is it triggers for after the certain millisecon i have defined.
Finally I have implemented the Observable inside the promise to achive all debounceTime, But the issue i faced here is the debounceTime emits all the events.
For example: If I type 'Prem' from input field the following code triggers the server for four time as timeout works.
If any issue in implemetation of the async validator please clarify me.
//Latest code
static hasDuplicateEmail(formControl: FormControl) {
return new Promise((resolve, reject) => {
return new Observable(observer =>
observer.next(formControl.value)).pipe(
debounceTime(600),
distinctUntilChanged(),
switchMap((value) => {
//server side
return MotUtil.fetch('checkForRegisterEmail', {e: formControl.value});
})
).subscribe((res) => {
return (JSONUtil.isEmpty(res)) ? resolve(null) : resolve({duplicate: true});
});
});
}
The debounceTime should work as mentioned in the Docs.
You are trying to approach it in a difficult way. Validator takes argument - AbstractControl. AbstractControl has property - valueChanges which return stream of changes in your formControl. So here you add debouceTime and later do other operations and finaly return this stream back to FormControl:
hasDuplicateEmail(control: AbstractControl) {
return control.valueChanges.pipe(
debounceTime(600),
switchMap(e =>
this.http.get('checkForRegisterEmail', {e}).pipe(
map((res: any) => JSONUtil.isEmpty(res) ? null : { duplicate: true })
)
)
)
}
As you notice I use HttpClient as it is the way you make HTTP calls in Angular (it is designed to work on streams rather then Promises)
emailValidator(/** params that you need inside switchMap */): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
return of(control.value).pipe(
delay(500), //for me works with delay, debounceTime are not working with asyncValidator
filter(email=> !!email), //length or another logic there is not emails with less than 10 characters
distinctUntilChanged(),
switchMap(/** as you need */),
map(exist => (exist ? {duplicate: true} : null)),
catchError(() => of(null))
);
};
}

Angular6 polling not returning data

I have an Angular application where I am trying to check an external data service for changes every few seconds, and update the view.
I've tried to implement Polling from rxjs but I'm not able to access the object, conversely it seems the polling function isn't working but assume this is because the returned object is inaccessible.
app.component.ts
export class AppComponent {
polledItems: Observable<Item>;
items : Item[] = [];
title = 'site';
landing = true;
tap = false;
url:string;
Math: any;
getScreen(randomCode) {
const items$: Observable<any> = this.dataService.get_screen(randomCode)
.pipe(tap( () => {
this.landing = false;
this.Math = Math
}
));
const polledItems$ = timer(0, 1000)
.pipe(( () => items$))
console.log(this.polledItems);
console.log(items$);
}
excerpt from app.component.html
<h3 class="beer-name text-white">{{(polledItems$ | async).item_name}}</h3>
excerpt from data.service.ts
get_screen(randomCode) {
return this.httpClient.get(this.apiUrl + '/tap/' + randomCode)
}
assuming that you want an array of items you could go with something like this.
// dont subscribe here but use the
// observable directly or with async pipe
private readonly items$: Observable<Item[]> = this.dataService.get_screen(randomCode)
// do your side effects in rxjs tap()
// better move this to your polledItems$
// observable after the switchMap
.pipe(
tap( () => { return {this.landing = false; this.Math = Math}; })
);
// get new items periodicly
public readonly polledItems$ = timer(0, 1000)
.pipe(
concatMap( () => items$),
tap( items => console.log(items))
)
the template:
// pipe your observable through async and THEN access the member
<ng-container *ngFor="let polledItem of (polledItems$ | async)>
<h3 class="item-name text-white">{{polledItem.item_name}}</h3>
</ng-container>
take a look at: https://blog.strongbrew.io/rxjs-polling/
if you are not awaiting an array but a single than you dont need the ngFor but access your item_name like:
<h3 class="item-name text-white">{{(polledItems$ | async).item_name}}</h3>

Categories