Debounce functionality for multiple checkboxes in Angular - javascript

I am trying to apply debounce time for multiple check boxes in Angular 7. The idea is to delay the api calls for x seconds in order to have better performing application (checking/unchecking the checkboxes invokes a call to the API).
Each checkbox corresponds to a filter which is sent to the backend when checked/unchecked and that determines the result returned from the server. I have tried using a custom debounce directive suggested in the article mentioned here,
https://coryrylan.com/blog/creating-a-custom-debounce-click-directive-in-angular
The problem addresses debounce for a single input field, in my case if I check and uncheck a single checkbox multiple times within the debounce time, only of call to the backend is made no matter how many times I click it. But my problem is with checking/unchecking multiple checkboxes and have only one request made to the API with in the debounce time.
Right now a call to the api is made every time I click on the checkbox which does not address my issue.
Following is the directive I am using which is similar to the example in the article:
import { Output, EventEmitter, OnInit, OnDestroy, Directive, ElementRef } from '#angular/core';
import { debounceTime } from 'rxjs/operators';
import { HostListener } from '#angular/core';
import { Subscription, Subject } from 'rxjs';
#Directive({
selector: '[appDebounceClick]'
})
export class DebounceDirective implements OnInit, OnDestroy{
#Output() debounceClick = new EventEmitter();
private clicks = new Subject();
private subscription: Subscription;
constructor(private elementRef: ElementRef) { }
ngOnInit() {
this.subscription = this.clicks.pipe(
debounceTime(2000)
).subscribe(e => this.debounceClick.emit(e));
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
#HostListener('change', ['$event'])
clickEvent(event) {
this.clicks.next(event);
}
}
Following is the code in my template:
<ng-container *ngFor="let checkbox of checkboxList">
<mat-checkbox *ngIf="checkbox.name == 'test'" [(ngModel)]="checkbox.selected" appDebounceClick (debounceClick)="onChange($event,checkbox)" >
{{checkbox.name}}
</mat-checkbox>
</ng-container>
I feel the issue here is the Directive is applied to every checkbox which in turn creates a new Observable/Subject which only associated with an individual checkbox . I think there should be just one Subject to which the click events on the checkboxes be subscribes to, but I am not entirely sure how to implement that. Would appreciate any thoughts on this issue or any better ideas on solving this problem.

You can use a Service to centralize the Subject, obviously the Service has to be Singleton so that all the checkboxes emits to one Subject and the debounceTime operator acts for all elements.
On the other hand, you can implement the same behavior but in the parent component, I mean, the Subject has to be placed in the parent component.
The decision depends on reusability matters.

Related

How can I make a function run only when I reload the page with angular?

What I want to do is shuffle or riffle a number and then stock it in an array, to later use it in the view, but I only want this when I reload the page. The problem is that every time I go to the component it generate a new number. This number I am using to shuffle the order of my products in the view, order:
{{number}}.
IN THE COMPONENT:
public accesoriosJson:any
private current_value:any
constructor(private accesoriosService: AccesoriosService){
this.accesoriosService.getAccesorios().subscribe(respuesta =>{
this.accesoriosJson = respuesta;
this.accesoriosJson.map(currentValue =>{
this.current_value = currentValue;
this.current_value.random = Math.ceil(Math.random()*10);
})
})
}
IN THE VIEW:
<div class="div" *ngFor='let accesorios of accesoriosJson' style="order:{{accesorios.random}};" routerLink="{{accesorios.name}}">
<div class="lupa"><i class="fas fa-search"></i></div>
<img src="{{accesorios.logo}}">
</div>
</section>
I tried to do something with window.onload, but clearly I do not know how to use angular, and also I do not have so much experience with typescript, if somebody could help me I would really appreciate it! Thank you!
It's a new number, because each time the page is revisited, the component is re-created (a new instance of the component is created).
What you need is a singleton service. Singleton services are the ones that only have a single instance across the application.
So you can:
Create the service
Move your code to the service
Inject the service into your component.
That way, your number will instantiate only once, when the application loads, and each time you revisit the page, you will see the same number.
Documentation on singleton services.
Documentation on how to inject a service.
This is a very broad description, but as you can see from the information in the links provided, the full answer won't fit here.
A simple way is to subscribe to the router events and that will get you the page refreshed or not. NavigationStart is used to tell if the page loaded the first time.
import { Component, OnDestroy } from '#angular/core';
import { NavigationStart, Router } from '#angular/router';
import { Subscription } from 'rxjs';
export let browserRefresh = false;
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnDestroy {
name = 'Angular 6';
subscription: Subscription;
constructor(private router: Router) {
this.subscription = router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
browserRefresh = !router.navigated;
}
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Working Example:
https://stackblitz.com/edit/angular-r6-detect-browser-refresh
A solution can be to generate your random number into the root component (propably app-component if you didn't changed it) in the ngOnInit() function. Please avoid put code in constructor() function.
After your number as been generated, you can pass it to all the components you want : by using #Input/#Output if you have a parent/child hierarchy, or by using a service if you want to share it to every component wherever you want

Get the number of instance of a component in angular 2/4/6?

I want to get the number of occurrence of a component. Menas want to know how many times this particular component is rendering?
Actually, I have made a toaster and want to know how many times it's displaying?
My codes are:
main.component.ts:
<toster-component [message]="'this is toster message'" [type]="'danger'" [duration]="'15'"> </toster-component>
<toster-component [message]="'this is toster message 2'" [type]="'info'
[duration]="'5'"> </toster-component>
toster.component.ts:
import { Component,Input,OnInit,ContentChildren,ElementRef,QueryList } from '#angular/core';
#Component({
selector: 'toster-component',
templateUrl: './toster.component.html',
styleUrls: ['./toster.component.css'],
});
export class TosterComponent implements OnInit {
public isShow:any=true;
public TabItem:any;
#Input() message:string; // Will show the message.
#Input() type:string; // What type will decide the style of this toster.
#Input() duration:number; // If duration ( In second ) is available the it will show till duration otherwise it will show permanently till manually closed
#ContentChildren(TosterComponent, {read: ElementRef}) tosterComponents:QueryList<TosterComponent>;
ngAfterContentInit() {
console.log("Length: ",this.tosterComponents.toArray().length);
}
ngOnInit(){
this.message=this.message?this.message:'Test Message';
this.type=this.type?this.type:'info';
if(this.duration>0){
setTimeout(()=>{
this.isShow=false;
},this.duration*1000);
}
}
}
In the above code I am using below code:
#ContentChildren(TosterComponent, {read: ElementRef}) tosterComponents:QueryList<TosterComponent>;
ngAfterContentInit() {
console.log("Length: ",this.tosterComponents.toArray().length); // I want to print the instance of this component.
}
to achieve the same, But its showing only 1. But it should be 2 because its using 2 times in main.component.ts.
You can declare a static class variable, and increase its count everytime the constructor() runs.
I have prepared THIS here.
I don't know how you handle your toaster, but when I did one and used one, I was using a service.
When you have a service, it is decorated with #Injectable, meaning it's a singleton.
And because of that, you can consider it as the only source of truth, and use it to store your toaster informations.
So, to answer your question : create a service that will create your toasters. When you display one, add 1 to the counter, and when it disappears, remove 1 from the counter.

Angular 4 - "Expression has changed after it was checked" error while using NG-IF

I setup a service to keep track of logged in users. That service returns an Observable and all components that subscribe to it are notified (so far only a single component subscribe to it).
Service:
private subject = new Subject<any>();
sendMessage(message: boolean) {
this.subject.next( message );
}
getMessage(): Observable<any> {
return this.subject.asObservable();
}
Root App Component: (this component subscribes to the observable)
ngAfterViewInit(){
this.subscription = this._authService.getMessage().subscribe(message => { this.user = message; });
}
Welcome Component:
ngOnInit() {
const checkStatus = this._authService.checkUserStatus();
this._authService.sendMessage(checkStatus);
}
App Component Html: (this is where the error occurs)
<div *ngIf="user"><div>
What I'm trying to do:
I want every component (except the Root App Component) to send the users logged-in state to the Root App Component so I can manipulate the UI within the Root App Component Html.
The issue:
I get the following error when the Welcome Component is initialised.
Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'true'.
Please note this error occurs on this *ngIf="user" expression which is located within Root App Components HTML file.
Can someone explain the reason for this error and how I can fix this?
On a side note: If you think theres a better way to achieve what I'm trying to do then please let me know.
Update 1:
Putting the following in the constructor solves the issue but don't want to use the constructor for this purpose so it seems it's not a good solution.
Welcome Component:
constructor(private _authService: AuthenticationService) {
const checkStatus = this._authService.checkUserStatus();
this._authService.sendMessage(checkStatus);
}
Root App Component:
constructor(private _authService: AuthenticationService){
this.subscription = this._authService.getMessage().subscribe(message => { this.usr = message; });
}
Update 2:
Here's the plunkr. To see the error check the browser console. When the app loads a boolean value of true should be displayed but I get the error in the console.
Please note that this plunkr is a very basic version of my main app. As the app is bit large I couldn't upload all the code. But the plunkr demonstrates the error perfectly.
What this means is that the change detection cycle itself seems to have caused a change, which may have been accidental (ie the change detection cycle caused it somehow) or intentional. If you do change something in a change detection cycle on purpose, then this should retrigger a new round of change detection, which is not happening here. This error will be suppressed in prod mode, but means you have issues in your code and cause mysterious issues.
In this case, the specific issue is that you're changing something in a child's change detection cycle which affects the parent, and this will not retrigger the parent's change detection even though asynchronous triggers like observables usually do. The reason it doesn't retrigger the parent's cycle is becasue this violates unidirectional data flow, and could create a situation where a child retriggers a parent change detection cycle, which then retriggers the child, and then the parent again and so on, and causes an infinite change detection loop in your app.
It might sound like I'm saying that a child can't send messages to a parent component, but this is not the case, the issue is that a child can't send a message to a parent during a change detection cycle (such as life cycle hooks), it needs to happen outside, as in in response to a user event.
The best solution here is to stop violating unidirectional data flow by creating a new component that is not a parent of the component causing the update so that an infinite change detection loop cannot be created. This is demonstrated in the plunkr below.
New app.component with child added:
<div class="col-sm-8 col-sm-offset-2">
<app-message></app-message>
<router-outlet></router-outlet>
</div>
message component:
#Component({
moduleId: module.id,
selector: 'app-message',
templateUrl: 'message.component.html'
})
export class MessageComponent implements OnInit {
message$: Observable<any>;
constructor(private messageService: MessageService) {
}
ngOnInit(){
this.message$ = this.messageService.message$;
}
}
template:
<div *ngIf="message$ | async as message" class="alert alert-success">{{message}}</div>
slightly modified message service (just a slightly cleaner structure):
#Injectable()
export class MessageService {
private subject = new Subject<any>();
message$: Observable<any> = this.subject.asObservable();
sendMessage(message: string) {
console.log('send message');
this.subject.next(message);
}
clearMessage() {
this.subject.next();
}
}
This has more benefits than just letting change detection work properly with no risk of creating infinite loops. It also makes your code more modular and isolates responsibility better.
https://plnkr.co/edit/4Th7m0Liovfgd1Z3ECWh?p=preview
Declare this line in constructor
private cd: ChangeDetectorRef
after that in ngAfterviewInit call like this
ngAfterViewInit() {
// it must be last line
this.cd.detectChanges();
}
it will resolve your issue because DOM element boolean value doesnt get change. so its throw exception
Your Plunkr Answer Here Please check with AppComponent
import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
import { MessageService } from './_services/index';
#Component({
moduleId: module.id,
selector: 'app',
templateUrl: 'app.component.html'
})
export class AppComponent implements OnDestroy, OnInit {
message: any = false;
subscription: Subscription;
constructor(private messageService: MessageService,private cd: ChangeDetectorRef) {
// subscribe to home component messages
//this.subscription = this.messageService.getMessage().subscribe(message => { this.message = message; });
}
ngOnInit(){
this.subscription = this.messageService.getMessage().subscribe(message =>{
this.message = message
console.log(this.message);
this.cd.detectChanges();
});
}
ngOnDestroy() {
// unsubscribe to ensure no memory leaks
this.subscription.unsubscribe();
}
}
Nice question, so, what causes the problem? What's the reason for this error? We need to understand how Angular change detection works, I'm gonna explain briefly:
You bind a property to a component
You run an application
An event occurs (timeouts, ajax calls, DOM events, ...)
The bound property is changed as an effect of the event
Angular also listens to the event and runs a Change Detection Round
Angular updates the view
Angular calls the lifecycle hooks ngOnInit, ngOnChanges and ngDoCheck
Angular run a Change Detection Round in all the children components
Angular calls the lifecycle hooks ngAfterViewInit
But what if a lifecycle hook contains a code that changes the property again, and a Change Detection Round isn't run? Or what if a lifecycle hook contains a code that causes another Change Detection Round and the code enters into a loop? This is a dangerous eventuality and Angular prevents it paying attention to the property to don't change in the while or immediately after. This is achieved performing a second Change Detection Round after the first, to be sure that nothing is changed. Pay attention: this happens only in development mode.
If you trigger two events at the same time (or in a very small time frame), Angular will fire two Change Detection Cycles at the same time and there are no problems in this case, because Angular since both the events trigger a Change Detection Round and Angular is intelligent enough to understand what's happening.
But not all the events cause a Change Detection Round, and yours is an example: an Observable does not trigger the change detection strategy.
What you have to do is to awake Angular triggering a round of change detection. You can use an EventEmitter, a timeout, whatever causes an event.
My favorite solution is using window.setTimeout:
this.subscription = this._authService.getMessage().subscribe(message => window.setTimeout(() => this.usr = message, 0));
This solves the problem.
To understand the error, read:
Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error
You case falls under the Synchronous event broadcasting category:
This pattern is illustrated by this plunker. The application is
designed to have a child component emitting an event and a parent
component listening to this event. The event causes some of the parent
properties to be updated. And these properties are used as input
binding for the child component. This is also an indirect parent
property update.
In your case the parent component property that is updated is user and this property is used as input binding to *ngIf="user". The problem is that you're triggering an event this._authService.sendMessage(checkStatus) as part of change detection cycle because you're doing it from lifecycle hook.
As explained in the article you have two general approaches to working around this error:
Asynchronous update - this allows triggering an event outside of change detection process
Forcing change detection - this adds additional change detection run between the current run and the verification stage
First you have to answer the question if there's any need to trigger the even from the lifecycle hook. If you have all the information you need for the even in the component constructor I don't think that's the bad option. See The essential difference between Constructor and ngOnInit in Angular for more details.
In your case I would probably go with either asynchronous event triggering instead of manual change detection to avoid redundant change detection cycles:
ngOnInit() {
const checkStatus = this._authService.checkUserStatus();
Promise.resolve(null).then(() => this._authService.sendMessage(checkStatus););
}
or with asynchronous event processing inside the AppComponent:
ngAfterViewInit(){
this.subscription = this._authService.getMessage().subscribe(Promise.resolve(null).then((value) => this.user = message));
The approach I've shown above is used by ngModel in the implementation.
But I'm also wondering how come this._authService.checkUserStatus() is synchronous?
I recently encountered the same issue after migration to Angular 4.x, a quick solution is to wrap each part of the code which causes the ChangeDetection in setTimeout(() => {}, 0) // notice the 0 it's intentional.
This way it will push the emit AFTER the life-cycle hook therefore not cause change detection error.
While I am aware this is a pretty dirty solution it's a viable quickfix.
Don't change the var in ngOnInit, change it in constructor
constructor(private apiService: ApiService) {
this.apiService.navbarVisible(false);
}

Why the component does not detect internal state change without `changeDetetectorRef.markForCheck`?

General
I have a problem. My component does not re-render without calling changeDetectorRef.markForCheck method.
I have an autocomplete. When input changes I send some async request (just simple HttpClient service and get method). After that, I fill in some internal state.
The code
Note markForCheck call. If I remove this line: nothing works. I noticed that if I remove it and if I click somewhere outside of the component, I see re-render. Right in time when I click somewhere component is re-rendered.
By the way, I realized that markForCheck is working by accident. I just tried something and it worked. I got info about CD mechanisms and CD service from some articles.
Here is my main component:
#Component({
selector: 'tags-auto-complete',
template: `
<tags-internal-auto-complete-input
// ....
(inputChanged)="onInputChange($event);"
></tags-internal-auto-complete-input>
<tags-internal-auto-complete-results
[data]="queryResultsTags"
// ....
></tags-internal-auto-complete-results>
`,
})
export class TagsAutoCompleteContainerComponent implements OnInit {
inputChanged = new Subject<string>();
queryResultsTags: Tag[] = [];
constructor(
private tagsService: TagsService,
private changeDetectorRef: ChangeDetectorRef,
) {}
onInputChange(query: string): void {
this.inputChanged.next(query);
}
ngOnInit() {
this.inputChanged
.filter(inputValue => inputValue.length > 0)
.debounceTime(400)
.switchMap(query => this.tagsService.getTagsList({ query }))
.do(() => this.changeDetectorRef.markForCheck()); // note this
.subscribe((tags: Tag[]) => (this.queryResultsTags = tags)) // here I change the input of inner component
}
// ...
Here is child component (tags-internal-auto-complete-results):
#Component({
selector: 'tags-internal-auto-complete-results',
template: `
<div class="container">
<span *ngFor="let tag of data" (click)="selectTag.emit(tag);" class="tag">
{{tag.name}}
</span>
</div>
`,
styleUrls: ['./results.styles.css'],
})
export class TagsAutoCompleteResultsComponent {
#Input() data: Tag[] = [];
#Output() selectTag = new EventEmitter<Tag>();
}
These are just fragments. Whole code is available on GitHub.
main component
inner component
By the way, I have another component (selected tags block) and I have input showLoader in it. It has exactly same problem.
My thoughts
Probably problem somehow connected to the zones mechanism. From some articles I know, that zone.js monkey-patches some events or XHR calls. And my case is XHR call (I didn't dive deep into HttpClient but it must just make an HTTP call).
What I want
I want to understand why changes are not detecting out of the box there (so I will use markForCheck and I will be ok) or I want to find a mistake in my code.
Hope you will help me there.
It's due to a ChangeDetectionStrategy.OnPush added on a parent component.
In that parent, if the references of his inputs don't change, his subtree components will not be checked for change.

Angular 2 reload the current component based on user input

In my Angular2 app, on UI input a component is loaded which pulls data from a web service.
I want to reload the aptSearchComponent when the user input changes. Although the new data is fetched from the service base on the input, the component is not reloaded.
The input is in the headerComponent, when the user inputs some search criteria and hits enter, data is passed to the sharedService and routed to aptSearchComponent, where data is pulled from the shared service and results are displayed.
The headerComponent template stays at the top and the aptSearchcomponent template is displayed below it.
#Component({
selector: 'app-header',
template: `
<div class="mdl-textfield__expandable-holder">
<input class="mdl-textfield__input" type="text" id="search" (keyup.enter)="Search($event)">
</div>
`,
})
export class HeaderComponent {
public apartments: Object[];
constructor(private apartmentService: ApartmentService,private router: Router,private sharedService: SharedService) {
this.apartmentService=apartmentService;
this.sharedService=sharedService;
}
Search(event){
this.apartmentService.searchApt2(event.target.value).subscribe(res => {this.sharedService.temp = res
this.router.navigate(['AptSearch'])});
}
}
How can I reload the component in Angular 2. Basically the data in this.aptDetails is changed, but template is still shows the old data.
export class AptSearchComponent implements OnInit {
aptDetails: any;
constructor(private apartmentService: ApartmentService, private sharedService: SharedService,private zone:NgZone) {
this.apartmentService = apartmentService;
}
ngOnInit(){
this.aptDetails = this.sharedService.temp;
JSON.stringify(console.log(this.aptDetails)); //the data here is changed based on input, but the template is not refreshed and I still see the previous result.
}
}
I tried the following in constructor() but no luck
this.zone.run(()=>this.aptDetails=this.sharedService.temp);
I am using RC4, and polyfills in not imported.
I resolved this by using #Input and ngOnChanges() hook in the child component. will share the detailed answer if anybody needs it.
To reload it you can remove it with a simple trick.
Put an *ngIf on the component and set it to true initially.
When you want to remove it set it to false, and then using setTimeout flick it back to true instantly. This will remove it and then recreate it.
When you recreate it pass the new parameters you want to pass in from the parent component.
(Angular2 used to use this trick to reset a form, I'm not sure if a better way is available now but during RC this was the correct approach).
Change detection only work if the property reference changed.
You must reset aptDetails before updating it.
this.aptDetails = {} // or whatever type it is
this.aptDetails = this.sharedService.temp;

Categories