Angular auto refesh in time intervals - javascript

In my Angular2 application there are two components for form view and graph view. In the form, there is a spinner and checkbox for auto refresh handle.
Form Component html
<div class="refresh-spinner">
<my-checkbox
[selected]=autoRefresh
[disabled]="false"
[id]="'autoRefresh'"
[name]="'Auto Refresh Every'"
[code]="'autoRefresh'"
(status)="checkAutoRefresh($event)">
</my-checkbox>
<my-spinner [title]="''"
[category]="'duration'"
[maxCount]=maxRefreshTime
[minCount]=minRefreshTime
[value]=minRefreshTime //default refresh value is 1 min
[editable]="true"
(count)="getAutoRefreshInterval($event)">
</my-spinner>
<span class="post__text"> Mins</span>
</div>
Form Component ts
// form view emits selected criteria
#Output() criteria = new EventEmitter<any>();
checkAutoRefresh(ele) {
this.autoRefresh = ele.selected;
localStorage.setItem('autoRefresh', ele.selected);
}
getAutoRefreshInterval(interval) {
localStorage.setItem('refreshInterval', interval.value);
}
Refresh interval and checkbox value (autoRefresh true/fasle) are set is local storage on spinner event and checkbox select event.
Graph components ts
alive: boolean; // used to unsubscribe from the IntervalObservable when OnDestroy is called.
#Input() criteria: FilteringCriteria;
constructor(private element: ElementRef, private myService: MyService) {
this.alive = true;
}
ngOnInit() {
let interval: number = JSON.parse(localStorage.getItem('refreshInterval'));
console.log(Date());
this.getStatistics();
IntervalObservable.create(interval * 60 * 1000)
.takeWhile(() => this.alive)
.subscribe((e) => {
// console.log(e)
if (JSON.parse(localStorage.getItem('autoRefresh'))) {
console.log(Date());
this.getStatistics();
}
});
}
ngOnDestroy() {
this.alive = false;
}
These form view and graph view are used in main component as below.
<search-criteria (criteria)="emitCriteria($event)"></search-criteria> //filteringCriteria emits from here
<ng-template ngFor let-result [ngForOf]="servers" let-i="index">
<my-grid-item row [class]="'graph--view'"
[colspan]="4">
<graph-view [criteria]="filteringCriteria"></graph-view>
</my-grid-item>
</ng-template>
Two Questions:
1. Once I check auto refresh checkbox graphs are refresh in 1 minute. But time interval is calculating from the time component is initialized not from the time the checkbox is selected.
2 If I change the value of the spinner (from 1 min to 2 min) local storage value is changed to new value 2. But graphs are refreshing in 1 min time intervals. But if I hit on form done button, then the graphs are refreshing in new time interval(2 min).
Any suggestions are appreciated.
Thank You!

It is happening because you are initializing the Observable as part of the init process of the graph component. So the time is taken from the moment it is initialized and when you update the interval it does not know about that and keeps using the one with which it was initialized.
You can declare a variable to hold the subscription and move all your code to subscribe to a different method. Something like
subscription;
constructor(private element: ElementRef, private myService: MyService) {
}
ngOnInit() {
this.getThreadPoolStatistics();
this.autoUpdateInit();
// subscribe to autoRefresh and interval changes
// autoRefreshChange.subscribe( () => this.autoUpdateInit());
// intervalChange.subscribe( () => this.autoUpdateInit());
}
autoUpdateInit(){
let interval: number = JSON.parse(localStorage.getItem('refreshInterval'));
console.log(Date());
// remove the old subscription
if(subscription) {
subscription.unsubscribe();
subscription = null;
}
// create one only when you need it. check for autorefresh and alive?
if(<subscription required>){
subscription = IntervalObservable.create(interval * 60 * 1000)
.subscribe((e) => {
// console.log(e)
if (JSON.parse(localStorage.getItem('autoRefresh'))) {
console.log(Date());
this.getStatistics();
}
});
}
}
You have to make sure, your Graph component gets the updates to the autorefresh and interval and call the autoUpdateInit again to make sure it updates the IntervalObservable. You can use a service to make sure both the components are looking at the same values as per this answer. If they are having a parent-child relation, then you can emit the changes via the #Output.

Related

Using Angular Material Progress Bar For Loading of Page

I have an Angular project where I am extensively using the Angular Material library. I would like to use the Progress Bar Component to show when a page is loading or when I make an api call. For example, while I'm waiting for a response from Stripe.
Starting the progress bar seems simple, just use a global variable in a service to signal when the page is loading. I'm thinking of using the router for that.
However, I would like to show the actual progress of the page load. An example would be when you go to a youtube video.
The component api uses a value property to display the amount of progress. But how to get the progress of the page load?
I know there are other libraries such as ngx that use this but I would like to use the Angular Material library if possible.
Any ideas how to achieve this?
Too late but maybe someone else needs it:
There are packages for this, for example ng2-slim-loading-bar.
But if you want to do it manually with Material Progress Bar then take a look at this example.
It really gives a false illusion of progress because it increases over time, and in case it reaches 95% without the load being finished then it stops until that happens. I don't know if there is any way to calculate the true progress of a request, that would be perfect.
Edit: Check Angular docs about Tracking and showing request progress, with that you may be able to implement a fairly real progress bar.
Component:
import { Component } from '#angular/core';
import {
NavigationCancel,
Event,
NavigationEnd,
NavigationError,
NavigationStart,
Router,
} from '#angular/router';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
progressValue = 0;
progressColor = 'primary';
progressTimer: number;
// This will be used to force stop (if an error occurs, or the user stops loading)
stopProgress = false;
constructor(private router: Router) {
this.router.events.subscribe((event: Event) => {
this.navigationObserver(event);
});
}
private navigationObserver(event: Event): void {
if (event instanceof NavigationStart) {
// Increase 1% every 25 milliseconds, adjust it to your preference
this.progressTimer = setInterval(() => {
this.loading();
}, 25);
}
if (event instanceof NavigationEnd) {
// When the navigation finishes, fill the bar completely
this.progressValue = 100;
/*
* Uncomment this block to simulate a delay (for testing), because if you
* are in a local environment or the request is to a 'light' or very fast resource,
* the progress bar will appear at 100%.
*/
/*
setTimeout(() => {
this.progressValue = 100;
}, 2000);
*/
}
/*
* If the navigation is canceled or an error occurs,
* stop the progress bar and change its color.
*/
if (event instanceof NavigationCancel) {
this.stopProgress = true;
this.progressColor = 'accent';
}
if (event instanceof NavigationError) {
this.stopProgress = true;
this.progressColor = 'warn';
}
}
// Function to increase the value of the progress bar
private loading(): void {
/*
* Leave 5% in case an unusual delay occurs, in the previous
* function it is filled to 100% if the load ends successfully
*/
if (this.progressValue >= 95 || this.stopProgress) {
clearInterval(this.progressTimer);
} else {
this.progressValue++;
}
}
}
Template:
<mat-progress-bar [value]="progressValue" [color]="progressColor">
</mat-progress-bar>
<div *ngIf="progressValue == 100; else elseBlock">
<h1>Loaded!</h1>
</div>
<ng-template #elseBlock>
<h1>Loading...</h1>
</ng-template>
if you see their example they have given the solution here
export class ProgressBarConfigurableExample {
color = 'primary';
mode = 'determinate';
value = 100; // this value from 0 to 100 changes progess bar
bufferValue = 100;
}

Take N values from Observable until its complete based on an event. Lazy loading a multi select list

I'm new to rxjs, and I'm developing an angular multiselect list component that should render a long list of values (500+).
I'm rendering the list based on an UL, I'm iterating over an observable that will render the LI's.
I'm thinking about my options to avoid impacting the performance by rendering all the elements at once. But I don't know whether this is possible or not, and if it's possible what is the best operator to use.
The proposed solution:
On init I load all the data into an Observable. (src) and I'll take 100 elements from it and will put them on the target observable (The one that will be used to render the list)
Everytime that the user reaches the end of the list (the scrollEnd event fires) I'll load 100 elements more, until there are no more values in the src observable.
The emission of new values in the target observable will be triggered by the scrollEnd event.
Find my code below, I still need to implement the proposed solution, but I'm stuck at this point.
EDIT: I'm implementing #martin solution, but I'm still not able to make it work in my code. My first step was to replicate it in the code, to get the logged values, but the observable is completing immediately without producing any values.
Instead of triggering an event, I've added a subject. Everytime the scrollindEnd output emits, I will push a new value to the subject. The template has been modified to support this.
multiselect.component.ts
import { Component, AfterViewInit } from '#angular/core';
import { zip, Observable, fromEvent, range } from 'rxjs';
import { map, bufferCount, startWith, scan } from 'rxjs/operators';
import { MultiSelectService, ProductCategory } from './multiselect.service';
#Component({
selector: 'multiselect',
templateUrl: './multiselect.component.html',
styleUrls: ['./multiselect.component.scss']
})
export class MultiselectComponent implements AfterViewInit {
SLICE_SIZE = 100;
loadMore$: Observable<Event>;
numbers$ = range(450);
constructor() {}
ngAfterViewInit() {
this.loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');
zip(
this.numbers$.pipe(bufferCount(this.SLICE_SIZE)),
this.loadMore$.pipe(),
).pipe(
map(results => console.log(results)),
).subscribe({
next: v => console.log(v),
complete: () => console.log('complete ...'),
});
}
}
multiselect.component.html
<form action="#" class="multiselect-form">
<h3>Categories</h3>
<input type="text" placeholder="Search..." class="multiselect-form--search" tabindex="0"/>
<multiselect-list [categories]="categories$ | async" (scrollingFinished)="lazySubject.next($event)">
</multiselect-list>
<button class="btn-primary--large">Proceed</button>
</form>
multiselect-list.component.ts
import { Component, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'multiselect-list',
templateUrl: './multiselect-list.component.html'
})
export class MultiselectListComponent {
#Output() scrollingFinished = new EventEmitter<any>();
#Input() categories: Array<string> = [];
constructor() {}
onScrollingFinished() {
this.scrollingFinished.emit(null);
}
}
multiselect-list.component.html
<ul class="multiselect-list" (scrollingFinished)="onScrollingFinished($event)">
<li *ngFor="let category of categories; let idx=index" scrollTracker class="multiselect-list--option">
<input type="checkbox" id="{{ category }}" tabindex="{{ idx + 1 }}"/>
<label for="{{ category }}">{{ category }}</label>
</li>
</ul>
NOTE: The scrollingFinished event is being triggered by the scrollTracker directive that holds the tracking logic. I'm bubbling the event from multiselect-list to the multiselect component.
Thanks in advance!
This example generates an array of 450 items and then splits them into chunks of 100. It first dumps the first 100 items and after every button click it takes another 100 and appends it to the previous results. This chain properly completes after loading all data.
I think you should be able to take this and use to for your problem. Just instead of button clicks use a Subject that will emit every time user scrolls to the bottom:
import { fromEvent, range, zip } from 'rxjs';
import { map, bufferCount, startWith, scan } from 'rxjs/operators';
const SLICE_SIZE = 100;
const loadMore$ = fromEvent(document.getElementsByTagName('button')[0], 'click');
const data$ = range(450);
zip(
data$.pipe(bufferCount(SLICE_SIZE)),
loadMore$.pipe(startWith(0)),
).pipe(
map(results => results[0]),
scan((acc, chunk) => [...acc, ...chunk], []),
).subscribe({
next: v => console.log(v),
complete: () => console.log('complete'),
});
Live demo: https://stackblitz.com/edit/rxjs-au9pt7?file=index.ts
If you're concerned about performance you should use trackBy for *ngFor to avoid re-rendering existing DOM elements but I guess you already know that.
Here is a live demo on Stackblitz.
If your component subscribes to an observable holding the whole list to be displayed, your service will have to hold this whole list and send a new one every time an item is added. Here is an implementation using this pattern. Since lists are passed by reference, each list pushed in the observable is simply a reference and not a copy of the list, so sending a new list is not a costly operation.
For the service, use a BehaviorSubject to inject your new items in your observable. You can get an observable from it using its asObservable() method. Use another property to hold your current list. Each time loadMore() is called, push the new items in your list, and then push this list in the subject, which will push it in the observable as well, and your components will rerender.
Here I am starting with a list holding all items (allCategories), every time loadMore() is called, a block of 100 items if taken and placed on the current list using Array.splice():
#Injectable({
providedIn: 'root'
})
export class MultiSelectService {
private categoriesSubject = new BehaviorSubject<Array<string>>([]);
categories$ = this.categoriesSubject.asObservable();
categories: Array<string> = [];
allCategories: Array<string> = Array.from({ length: 1000 }, (_, i) => `item #${i}`);
constructor() {
this.getNextItems();
this.categoriesSubject.next(this.categories);
}
loadMore(): void {
if (this.getNextItems()) {
this.categoriesSubject.next(this.categories);
}
}
getNextItems(): boolean {
if (this.categories.length >= this.allCategories.length) {
return false;
}
const remainingLength = Math.min(100, this.allCategories.length - this.categories.length);
this.categories.push(...this.allCategories.slice(this.categories.length, this.categories.length + remainingLength));
return true;
}
}
Then call the loadMore() method on your service from your multiselect component when the bottom is reached:
export class MultiselectComponent {
categories$: Observable<Array<string>>;
constructor(private dataService: MultiSelectService) {
this.categories$ = dataService.categories$;
}
onScrollingFinished() {
console.log('load more');
this.dataService.loadMore();
}
}
In your multiselect-list component, place the scrollTracker directive on the containing ul and not on the li:
<ul class="multiselect-list" scrollTracker (scrollingFinished)="onScrollingFinished()">
<li *ngFor="let category of categories; let idx=index" class="multiselect-list--option">
<input type="checkbox" id="{{ category }}" tabindex="{{ idx + 1 }}"/>
<label for="{{ category }}">{{ category }}</label>
</li>
</ul>
In order to detect a scroll to bottom and fire the event only once, use this logic to implement your scrollTracker directive:
#Directive({
selector: '[scrollTracker]'
})
export class ScrollTrackerDirective {
#Output() scrollingFinished = new EventEmitter<void>();
emitted = false;
#HostListener("window:scroll", [])
onScroll(): void {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight && !this.emitted) {
this.emitted = true;
this.scrollingFinished.emit();
} else if ((window.innerHeight + window.scrollY) < document.body.offsetHeight) {
this.emitted = false;
}
}
}
Hope that helps!

Primeng paginator cannot reset page 1 after call API

I have 2 functions to load data: when init page and search page.
When init page, the data display with 5 pages. I click page 3, the data show with paging is Ok. After that, enter data search. The data table is reload, but the page number does not reset to 1, it's still page 3.
in html:
<p-paginator rows="5" totalRecords="{{totalRecords}}" (onLazyLoad)="paginate($event)"
(onPageChange)="paginate($event)" [rowsPerPageOptions]="[2,5,10,20]"></p-paginator>
ts:
defaultPage:0;
defaultSize:2
paginate(event) {
let currentPage: number;
this.defaultPage = event.first / event.rows;
this.defaultSize = event.rows;
this.listData = [];
if (this.isSearch == 1) {
this.getLoadPageSearch(this.defaultSize, this.defaultPage);
} else {
this.getLoadPage(this.defaultSize, this.defaultPage);
}
}
Please adivice me how to reset the paginator after call another API
The p-paginator component contains the changePageToFirst function. To access this function, we will need to obtain a ViewChild reference to the component. For example, in our template we define the component:
<p-paginator rows="10" totalRecords="100" #p></p-paginator>
<button (click)="reset($event)">Reset</button>
And in our component, we can handle the reset event as follows:
#ViewChild('p', {static: false}) paginator: Paginator;
reset($event) {
this.paginator.changePageToFirst($event);
}
Here is a demo: https://plnkr.co/edit/FxwTHK0W2WuTCjuqhLp6?p=preview
EDIT: The above demo is no longer working on plunkr. Here is an updated version:
https://stackblitz.com/edit/angular-u9nqf5
Another useful method is changePage.
In template:
<p-paginator #pp></p-paginator>
In the component:
#ViewChild('pp') paginator: Paginator;
this.paginator.changePage(0);
For those using the dataTable primeng component and using its lazy pagination feature,
we can reset the page to '1' by using
this.tableTemplateName.first = 0;
where setting first=0 means first page

How to save state to var/const and stop it updating?

I'm working on a filter for a ListView, a way to be able to sort/order/etc the items. Basically I'm saving the parameters in state and they're updated via some toggles/select-fields on a <Modal>.
The modal has a cancel & apply button. If you select apply after changing filters, the ListView's contents would be updated. However if they were to select cancel after changing settings, they would be reverted to whatever it was before the filter modal was launched.
So I'm doing this:
// Update filterValues state
adjustFilterValue(filterSection, newValue) {
if ( this.state.hasAdjustedFilters === false ) {
const filterValues = this.state.filterValues;
this.setState({
hasAdjustedFilters: true
})
}
var newFilterValues = defaultFilterValues;
newFilterValues[filterSection] = newValue;
this.setState({
filterValues: newFilterValues
})
}
However whenever I adjust this.state.filterValues - newFilterValues get's updated too.
How can I save & isolate an object from state?
You can store your initial state by using lifecycle hooks:
componentDidMount() {
this._initialState = this.state;
}
Later on, in a case of revert, simply setState(this._initialState);

angular 2.0 equivalent to $scope.$apply

I am trying to get started with angular 2.0, now I was wondering how I can initiate an update to the view after some external event changed data.
In details I have a google map and a handler for a click-event on the map. After the user clicks on the map I store latitude and longitude of the click in to variables on the controller
this.lat = event.latLng.lat();
this.lon = event.latLng.lon();
In the view I want to display these values
<div> this is my spot: {{lat}} and {{lon}} </div>
In angular 1 I would simply wrap the assignment in the controller in a call to $scope.$apply().
What is the preferred way to go about updating views in angluar 2.0 ?
Try to import ChangeDetectorRef from angular core as follow
import {Component, ChangeDetectorRef} from '#angular/core';
in constructor
constructor(private chRef: ChangeDetectorRef){
chRef.detectChanges(); //Whenever you need to force update view
}
Mostly, you don't need to do anything to update the view. zone.js will do everything for you.
But if for some reason you want to fire change detection manually (for example if your code is running outside of an angular zone) you can use LifeCycle::tick() method to do it. See this plunker
import {Component, LifeCycle, NgZone} from 'angular2/angular2'
#Component({
selector: 'my-app',
template: `
<div>
Hello world!!! Time: {{ time }}.
</div>
`
})
export class App {
time: number = 0;
constructor(lc: LifeCycle, zone: NgZone) {
zone.runOutsideAngular(() => {
setInterval(() => {
this.time = Date.now();
lc.tick(); // comment this line and "time" will stop updating
}, 1000);
})
}
doCheck() {
console.log('check', Date.now());
}
}
setTimeout(function(){
//whatever u want here
},0)
ref : http://blog.mgechev.com/2015/04/06/angular2-first-impressions/

Categories