Angular setTimeout running too many times - javascript

I have a log viewer component implemented in two or more other components. This log viewer uses a setTimeout to create an interval loop to fetch data from a file. My problem is since this component is imported in other components the timer runs for every component separately thus making multiple file reads per second.
Can this be avoided and run the timer only once regardless of the number of components using this component?
Here's the code for the log viewer component where the setTimeout interval is created:
import { Component, ElementRef, OnInit, ViewChild } from '#angular/core';
import { LogsService } from '../../services/logs.service';
import { HelperService } from '../../services/helper.service';
#Component({
selector: 'app-logger',
templateUrl: './logger.component.html',
styleUrls: ['./logger.component.scss']
})
export class LoggerComponent implements OnInit {
#ViewChild('scroller', {static: false}) scroller: ElementRef;
logClassName: string = 'logs shadow close';
logs: string = '';
logTS: number = 0;
logTimer;
scrollTop: number = 0;
constructor(
private logsService: LogsService,
private h: HelperService
){}
ngOnInit(): void {
this.getLogs();
}
ngOnDestroy(): void {
if (this.logTimer) window.clearTimeout(this.logTimer);
}
toggle(type){
switch (type)
{
case 'open': this.logClassName = 'logs shadow open'; break;
case 'close': this.logClassName = 'logs shadow close'; break;
case 'full': this.logClassName = 'logs shadow full'; break;
}
}
getLogs(){
this.logsService.fetch(this.logTS).subscribe(
response => {
this.logs += response.data.join('');
this.logTS = response.ts;
window.setTimeout(() => {
this.scrollTop = this.scroller.nativeElement.scrollHeight;
}, 100);
this.setLogTimer();
},
error => {
this.h.lg('unable to fetch logs', 'error');
this.logs = '<p>Unable to fetch logs</p>';
this.setLogTimer();
}
);
}
setLogTimer(){
if (this.logTimer) window.clearTimeout(this.logTimer);
this.logTimer = window.setTimeout(() => {
this.getLogs();
}, 1000);
}
}

The problem is this.logTimer is associated with component instance and it will be different for each instance and never get cancelled solution would be to use some shared service to perform your file read like
#Injectable(provideIn:'root')
export class SomeService{
setLogTimer(){
if (this.logTimer) window.clearTimeout(this.logTimer);
this.logTimer = window.setTimeout(() => {
this.getLogs();
}, 1000);
}
}
Then use this service in your component like
this.someService.setLogTimer()

To solve this problem Angular has Singleton services. You can move the setLogTimer() and logTimer in your LogsService.

Related

Angular 6 - Back button press trigger more than once

I have the following code to detect the back button press using angular 6.
import { Location } from '#angular/common';
export class ProductsComponent implements OnInit {
constructor( private location: Location){
this.handleBackButtonPress();
}
handleBackButtonPress() {
this.subscribed = true;
this.location.subscribe(redirect => {
if (redirect.pop === true) {
alert('this is a backbutton click');
}
});
}
}
This is working and we got alert on back button press. The problem is If we visit the same page more than once it will trigger the alert with the number of time we visited the route with the same component.
Note:
I have checked for a solution like this.location.unsubscribe(), But failed to find a function like that for location.
You just need to unsubscribe when the component is destroyed by the ngOnDestroy lifecycle hook.
import { Location } from '#angular/common';
import { SubscriptionLike } from 'rxjs';
export class ProductsComponent implements OnInit, OnDestroy {
public subscription: SubscriptionLike;
constructor(private location: Location){
this.handleBackButtonPress();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
handleBackButtonPress() {
this.subscription = this.location.subscribe(redirect => {
if (redirect.pop === true) {
alert('this is a backbutton click');
}
});
}
}
As mentioned by briosheje in the comments the lifecycle hook does not run on browser refreshes. For that you'll need to handle the unsubscription on the document's onbeforereload event.
The problem, I analyzed here is, every time whenever constructor will run. It will call your function for sure. So you have to check whether this function has been run previously or not.
Simplest answer is
constructor( private location: Location){
const PopCalled = localStorage.getItem('PopCalled')
if(!PopCalled)
this.handleBackButtonPress();
}
handleBackButtonPress() {
this.subscribed = true;
this.location.subscribe(redirect => {
if (redirect.pop === true) {
localStorage.setItem('PopCalled', true);
alert('this is a backbutton click');
}
});
}
Basically, you have to manage the state of PopCalled its up to you which way you want to choose, as per my knowledge this is the simplest way.

Showing the loading spinner icon on all components

I have written the code to show the loading spinner on all components when any event is triggered. It works fine on a single component but the issue with it, I have to show the same loading spinner on the around multiple components when certain event is triggered. See below code:
tasks() {
this.handler.activateLoader();
this.tasksService.get(this.page, this.pageSize).subscribe(results => {
this.handler.hideLoader();
if (this.handler.handle(results)) {
return;
}
this.tasksRes = results['data'];
for (let i = 0; i < this.tasksRes.length; i++) {
if (this.tasksRes[i].status == 'In_progress' && this.tasksRes[i].eventType == 'Sync' &&
this.tasksRes[i].entityId == this.id) {
this.progressFlag = true;
break;
} else {
this.progressFlag = false;
}
}
this.length = results['totalElements'];
}, error => {
this.handler.hideLoader();
this.handler.error(error);
});
}
connect() {
let source = new EventSource('/api/v1/events/register');
source.addEventListener('message', message => {
this.tasks();
});
}
And on ngOnInit(), I have called these 2 methods as below then its working fine.
ngOnInit() {
this.tasks();
this.connect();
}
The actual requirement is when I run a particular event the button is going to be disabled and at the same time the spinner loading will come. I have achieved this one. But how to show the same spinner on multiple components so that the user can know that the task is running.
This is how I am showing the loading spinner. See below:
<span class="text-warning pull-right" *ngIf="progressFlag">
<i class="fa fa-spinner fa-spin fa-2x"></i>
</span>
In my code, I have many components at around 17-18 where I need to show the loading spinner. If I want to show the spinner globally means I can show it on either header and footer component which is common to my entire template. Can any one provide any ideas on it.
Thanks.
Please search keyword HttpInterceptor learn details. One simple example below:
// siteHttpInterceptor.ts
import { Injectable } from '#angular/core';
import { HttpRequest, HttpInterceptor, HttpHandler, HttpEvent, HttpResponse } from '#angular/common/http';
import { throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { LoadingService } from './loading.service';
#Injectable()
export class SiteHttpInterceptor implements HttpInterceptor {
constructor(private loadingService: LoadingService){}
intercept(request: HttpRequest<any>, httpHandler: HttpHandler): Observable<any> {
/* Start loading here */
this.loadingService.startLoading();
return httpHandler.handle(request).pipe(
tap((event: HttpEvent<any>) => {
/* End loading */
this.loadingService.endLoading();
},
(err: any) => {
/* End loading */
this.loadingService.endLoading();
}),
catchError(err => {
return throwError(err);
})
);
}
}
//loading.service.ts LoadingService base on Ionic framework, you can instead it
import { Injectable } from '#angular/core';
import { LoadingController } from '#ionic/angular';
#Injectable({
providedIn: 'root'
})
export class LoadingService {
private loaders = [];
//sometimes, the request so quickly then close event earlier than open loading bar
private badLoaders = 0;
constructor(
private loadingController: LoadingController
) {
}
async startLoading() {
if (this.badLoaders > 0) {
this.badLoaders --;
} else {
await this.loadingController.create({
message: 'Loading ...',
}).then(loader => {
this.loaders.push(loader);
loader.present().then(() => {
//if it is bad loader, close
if (this.badLoaders > 0) {
this.badLoaders --;
this.endLoading();
}
});
});
}
}
endLoading() {
let loader = this.loaders.pop();
if (loader) {
loader.dismiss();
} else {
// it is mean close event earlier
this.badLoaders ++;
}
}
}
Use it then you not need manage loader handle each request method.
Put your spinner at the main component.. in most cases its the AppComponent
Then put a these on your shared service
private LoadingStatus = new Subject<boolean>();
// Observable string streams
IsLoading$ = this.LoadingStatus.asObservable();
// Service message commands
triggerLoading(status: boolean) {
this.LoadingStatus.next(mission);
}
Then at your sender component call triggerLoading(true) or triggerLoading(false) from the service and subscribe at your main component (AppComponent):
this.shareService.IsLoading$.subscribe( data => progressFlag = data )
Or Add your logic as this:
this.shareService.IsLoading$.subscribe(
data => {
if(data) {
// start loading logic here
} else {
// end loading logic here
}
}
)
Source: Angular - Component Interaction

Angular 2 - http.get never being call

Im learning Angular 4 and have run into a problem that I cannot seem to find a solution to. Here is the context:
I have a simple app that displays info about US Presidents.
The backend is a rest API provided by webapi...this works fine.
The front end is an Angular app.
Ive distilled the problem down to 3 components, 1 data service and 1 model.
Here is the model:
export class President {
constructor(
public id: number,
public presidentName: string,
public presidentNumber: number,
public yearsServed: string,
public partyAffiliation: string,
public spouse: string) {}
}
The 3 components are
1. SearchComponent
2. HomeComponent
3. PresidentComponent
When the app bootstraps, it loads the ApplicationComponent - it is the root component:
import { Component, ViewEncapsulation } from '#angular/core';
#Component({
selector: 'my-app',
template: `
<search-component></search-component>
<home-component></home-component>
`
})
export class ApplicationComponent {}
PresidentComponent is a child component of HomeComponent. When home component loads, it makes an http call to the api to get a list of presidents and renders 1 presidentComponent for each row returned. This works fine.
What Im trying to do is implement a search feature where the dataService exposes an EventEmitter and provides the search method as shown here:
import { Injectable, EventEmitter, Output } from '#angular/core'
import { President } from '../models/President'
import { Http } from '#angular/http';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
#Injectable()
export class DataService {
constructor(private http: Http) {
}
searchEvent: EventEmitter<any> = new EventEmitter();
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any): Observable<President[]> {
let encParams = encodeParams(params);
console.log(encParams);
return this.http
.get(this.presidentUrl, {search: encParams})
.map(response => response.json());
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this.http.get(this.presidentUrl)
.map(response => response.json());
}
}
/**
* Encodes the object into a valid query string.
* this function is from the book Angular2 Development with TypeScript
*/
function encodeParams(params: any): URLSearchParams {
return Object.keys(params)
.filter(key => params[key])
.reduce((accum: URLSearchParams, key: string) => {
accum.append(key, params[key]);
return accum;
}, new URLSearchParams());
}
The Search Component houses the search form and when the search button is clicked, it executes the onSearch() function and calls emit on the data service:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.searchEvent.emit(this.formModel.value);
}
}
Then, in the HomeComponent, I want to subscribe to this event and execute a search via the dataservice when it fires:
ngOnInit(): void {
//when component loads, get list of presidents
this.dataService.getPresidents()
.subscribe(
presidents => {
console.log('sub');
this.presidents = presidents;
},
error => console.error(error)
)
//when search event is fired, do a search
this.dataService.searchEvent
.subscribe(
params => {
console.log('in home.ts subscribe ' + JSON.stringify(params));
this.result = this.dataService.search(params);
},
err => console.log("cant get presidents. error code: %s, URL: %s"),
() => console.log('done')
);
}
When I run this in the browser, everything works except the http call is never executed. If I subscribe() to the http.get call in the dataservice itself, it executes but why should I have to do that when I have a subscription being setup on the HomeComponent?
I want to handle the Observable in the HomeComponent and update the list of presidents that is being displayed in the UI based on the search result.
Any advice is greatly appreciated.
The entire idea of using EventEmitter in the service is not right. The EventEmitter should be used with #Output properties to send data from the child component to its parent.
Even though the EventEmitter is a subclass of the Subject, you shouldn't be using it in services. So inject the service into your component, subscribe to its observable in the component, and emit an event using EventEmitter to the parent component if need be.
In the code this.result = this.dataService.search(params);, result is an observable. You have not made a subscription.
In that case you should have used the async pipe to display the data.
Why not use Subject from rxjs. Here is what i am proposing:
DataService:
import { Observable, Subject } from "rxjs";
import 'rxjs/add/operator/catch';
#Injectable()
export class DataService {
private _dataSubject = new Subject();
constructor(private http: Http) {
this.http.get(this.presidentUrl)
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
);
}
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any){
let encParams = encodeParams(params);
console.log(encParams);
this.http
.get(this.presidentUrl, {search: encParams})
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this._dataSubject;
}
SearchComponent:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.search(this.formModel.value);
}
}
With these modifications you should be able to have only 1 subscriber in homeCompoent and then get new data emitted every time onSearch() is called.

RxJS Observable :'Skip is not a function' (or any other function)

I am getting a weird error with RxJS, getting a 'Method is not a function'.
I do have import the Observable library.
I already got Observable array with no errors raised.
But when I do a skip, or take,I get this error.
If I read correctly :
Returns an Observable that skips the first count items emitted by the
source Observable.
As my observable contains Article, it should skip x Article on the Observable right?
Here is the code
import { Component, OnInit } from '#angular/core';
import { Article} from '../../model/article';
import { ArticleService} from '../../model/article.service';
import { Router } from '#angular/router';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
#Component({
selector: 'app-liste-article',
templateUrl: './liste-article.component.html',
styleUrls: ['./liste-article.component.css']
})
export class ListeArticleComponent implements OnInit {
articles: Observable<Article[]>;
articlesPage: Observable<Article[]>;
selectedArticle: Article;
newArticle: Article;
page = 1;
itemPerPage = 2;
totalItems = 120;
constructor(private router: Router, private articleService: ArticleService) {
}
ngOnInit() {
this.articles = this.articleService.getArticles();
this.articlesPage = this.articles.skip((this.page - 1) * this.itemPerPage ).take(this.itemPerPage);
//this.articlesPage = this.articles.slice( (this.page - 1) * this.itemPerPage , (this.page) * this.itemPerPage );
}
onSelect(article: Article) {
this.selectedArticle = article;
this.router.navigate(['../articles', this.selectedArticle.id ]);
}
onPager(event: number): void {
console.log('Page event is' , event);
this.page = event;
this.articlesPage = this.articles.skip((this.page - 1) * this.itemPerPage ).take(this.itemPerPage);
console.log('tab' , this.articlesPage.count());
//this.articleService.getArticles().then(articles => this.articles = articles);
}
getArticles(): void {
this.articles = this.articleService.getArticles();
}
createArticle(article: Article): void {
//this.articleService.createArticle(article)
//.then(articles => {
// this.articles.push(articles);
// this.selectedArticle = null;
//});
}
deleteArticle(article: Article): void {
//this.articleService
// .deleteArticle(article)
// .then(() => {
// this.articles = this.articles.filter(b => b !== article);
// if (this.selectedArticle === article) { this.selectedArticle = null; }
//});
}
}
I tried to use it directly with .skip(1), but getting the same error.
I checked official documentations RxJS Observable and it looks OK to me. It seems the case is OK, as my call to the function. Both arryas are Observable so I should be able to use those function on one, and put the result in the other one right?
I don't see what is wrong here, probably a small detail, but can't see it myself (as I am starting with this, I see less details).
You'll want to import skip and take the same way as you have done with map and catch.
import 'rxjs/add/operator/skip';
import 'rxjs/add/operator/take';
For rxjs#6.5.5 I had to make the following changes:
import skip
import { skip } from "rxjs/operators";
change
return this.udpstream.asObservable().skip(1);
to
return this.udpstream.asObservable().pipe(skip(1));

DevExpress' DevExtreme for Angular2 and pre-selection

I'm using DevExpress' DevExtreme with Angular2. I have a data grid (below) that lists states and asks the user to select some states. It is possible that some states have already been stored in the database. How do I set the previously selected states? I can see in the documentation that I should use dataGrid.instance.selectRows(arrayOfPreviouslySelectedStates) but dataGrid is instantiated sometime after I try to set it which is in the ngOnInit().
My HTML grid:
<dx-data-grid #statesGrid id="statesContainer" [dataSource]="states" [selectedRowKeys]="[]" [hoverStateEnabled]="true" [showBorders]="true" [showColumnLines]="true" [showRowLines]="true" [rowAlternationEnabled]="true">
<dxo-sorting mode="multiple"></dxo-sorting>
<dxo-selection mode="multiple" [deferred]="true"></dxo-selection>
<dxo-paging [pageSize]="10"></dxo-paging>
<dxo-pager [showPageSizeSelector]="true" [allowedPageSizes]="[5, 10, 20]" [showInfo]="true"></dxo-pager>
<dxo-filter-row [visible]="true"></dxo-filter-row>
<dxi-column dataField="abbreviation" [width]="100"></dxi-column>
<dxi-column dataField="name"></dxi-column>
</dx-data-grid>
My componenet:
import 'rxjs/add/operator/switchMap';
import { Component, OnInit, ViewContainerRef, ViewChild } from '#angular/core';
import { CompanyService } from './../../../shared/services/company.service';
import { StateService } from './../../../shared/services/state.service';
import notify from 'devextreme/ui/notify';
import { DxDataGridModule, DxDataGridComponent } from 'devextreme-angular';
#Component({
selector: 'app-company-detail',
templateUrl: './company-detail.component.html'
})
export class CompanyDetailComponent implements OnInit {
#ViewChild(DxDataGridComponent) dataGrid: DxDataGridComponent;
companyStates: Array<ICompanyState>;
states: Array<IState>;
constructor(private CompanyService: CompanyService, private StateService: StateService) { }
ngOnInit() {
this.StateService.getStates().subscribe((states) => {
this.getSelectedStates();
this.states = states
});
}
public getSelectedStates = (): void => {
this.CompanyService.getStates(id).subscribe((states) => {
let preselectedStates: Array<IState> = this.companyStates.map((state) => {
return { abbreviation: state.Abbreviation, name: state.Name }
});
// I get an error here that says that dataGrid is undefined.
this.dataGrid.instance.selectRows(preselectedStates, false);
}
}
}
Thanks to #yurzui 's comment I was able to figure out my problems in the following way. [selectedRowKeys] deals with all preselection. It's "problem" is that it doesn't update itself when additional selections are made. So, I listened for onSelectionChanged and passed the event, which contains data about many things regarding selection, into my custom function which updates the selectedStates which I then use to save the data to the database when the save button is clicked.
Gets the preselected states from the database
public getCompanyStates = (): void => {
this.CompanyService.getStates().subscribe((states) => {
this.selectedStates = states;
});
}
Event handler
public onSelectionChanged = (e): void => {
this.selectedStates = e.selectedRowKeys;
}
The dx-data-grid portion of the HTML
<dx-data-grid #statesGrid id="statesContainer"
(onSelectionChanged)="onSelectionChanged($event)"
[selectedRowKeys]="selectedStates"
[dataSource]="states">
...
</dx-data-grid>

Categories

Resources