I am trying to make a modal in Angular 9 that returns a Promise as result. I don't know how to move the promise logic outside of the declaration.
<a class="button-primary" (click)="yes()">Yes</a>
<a class="button-default" (click)="no()">No</a>
This is the modal controller
import { Component, OnInit, HostBinding } from '#angular/core';
#Component({
selector: 'change-username-modal',
templateUrl: './change-username-modal.component.html',
styleUrls: ['./change-username-modal.component.less']
})
export class ChangeUsernameModalComponent implements OnInit {
#HostBinding('class.show')
show: boolean = false;
constructor() { }
ngOnInit(): void {
console.log('init');
}
public open(): Promise<boolean> {
return new Promise(function(resolve, reject) {
resolve(true);
});
}
yes() {
//this.myPromise.resolve(true);
this.show = false;
}
no() {
//this.myPromise.reject(false);
this.show = false;
}
}
I need to make the Promise resolve or reject when calling the yes() or no() functions.
Thank you in advance!
You could use Observable approach instead of promise. you need a simple subject which will emit and complete immediately (for avoiding memory leak). the code should look like this
export class Component{
#HostBinding('class.show')
show: boolean = false;
private _emitter$ = new Subject<boolean>();
constructor() { }
ngOnInit(): void {
console.log('init');
}
public open(): Observable<boolean> {
return this._emitter.asObservable();
}
yes() {
//this.myPromise.resolve(true);
this.show = false;
this.emitAndClose(true);
}
emitAndClose(answer:boolean){
this._emitter.next(answer);
this._emitter.complete();
}
no() {
this.emitAndClose(false);
this.show = false;
}
}
now whenever answer is clicked, it will emit the value and complete the subject so you don't need unsubscribe outside
Related
I have a SPA that ultimately lists out a lot of data, but in batches.
I created a component at the bottom of the list, with a 'Visibility' directive so that when it is visible we make a new request to the dataset in a SQL server to get the next batch.
html-tag-for-component
<app-infinity-scroll
[(pageNumber)]="page"
[displayPage]="displayPage"
[authd]="authd"
[done]="done"
[numResults]="displayPage == 'tiles-hub' ? hubs.length : wallets.length"
class="{{scrollVisible ? '' : 'hiddenDisplay'}}"
trackVisibility
></app-infinity-scroll>
component-to-trigger-data-call
import { outputAst } from '#angular/compiler';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '#angular/core';
import { DbSqlService } from 'services/db-sql.service';
import { TokenAuthService } from 'services/token-auth.service';
import { TrackVisibilityDirective } from 'src/app/directives/track-visibility.directive';
import { SortStyle } from 'src/app/interfaces/mvlot';
import { MatProgressBar } from '#angular/material/progress-bar';
#Component({
selector: 'app-infinity-scroll',
templateUrl: './infinity-scroll.component.html',
styleUrls: ['./infinity-scroll.component.scss']
})
export class InfinityScrollComponent implements OnInit {
#Input() pageNumber: number;
#Input() displayPage: string;
#Input() authd: boolean;
#Input() done: boolean;
#Input() numResults: number;
#Output() pageNumberChange = new EventEmitter<number>();
lastDisplay = '';
loading: boolean = true;
constructor(
private visTrack: TrackVisibilityDirective
, private cdr: ChangeDetectorRef
, private dbApi: DbSqlService
, private authService: TokenAuthService
) {
}
ngOnInit(): void {
this.authService.UserAuthd.subscribe((res) => {
// if (res) {
this.dbApi.initGetWalletsHandler(0, 50, SortStyle.scoreDesc);
this.pageNumber = 1;
// }
})
this.visTrack.visibile.subscribe((val) => {
if (!this.done) {
this.loading = true;
if (val) {
if (this.displayPage == 'tiles') {
this.dbApi.initGetWalletsHandler((this.pageNumber) * 50, 50, SortStyle.default);
this.pageNumber += 1;
}
if (this.displayPage == 'tiles-hub') {
this.dbApi.initGetHubsHandler((this.pageNumber) * 50, 50);
this.pageNumber += 1;
}
}
}
})
}
}
Some functions run, call out to a back-end, respond with data, where a listener is waiting.
this.dbApi.resultObs.subscribe(val => {
if (val.append != true) {
this.results = [];
}
if (val.reset) {
this.page = 1;
}
val.data.data.forEach((b: any) => {
var result: Collection;
var existingResults = this.results.filter(w => w.ownerId == b.ownerId);
if (existingResults.length == 0) {
result = {
ownerId: b.ownerId
, totalScore: b.modifiedLandScore
, filteredCount: b.filteredCount
, totalLots: b.totalLots
, totalPrice: b.totalPrice
, name: ''
, lands: []
, type: CollectionType.b
}
result.bs.push(b);
this.results.push(result);
} else {
result = existingResults[0];
result.bs.push(b);
}
});
this.resultDataSource = new MatTableDataSource(this.results);
this.collectionType = CollectionType.b;
this.uiService.loadingBar(false);
this.done = val.data.data.length == 0;
this.cdr.detectChanges();
})
And, finally this is laid out for the user:
<tr *ngFor="let result of results">
<td>
<display-block
[collection]="b"
[displayVertical]="displayVertical"
[displayCaseCount]="displayCaseCount"
[gridClassName]="gridClassName"
[authd]="authd"
[type]="result.type"
[expanded]="results.length == 1"
[isPhonePortrait]="isPhonePortrait"
></display-block>
</td>
</tr>
Everything works fine on the first grab of data.
And everything appears to work fine on the second pull, but for any of the items appended to the view with the second pull, ChangeDetector just seems to give up. I'll trigger an action, that should modify the view, but nothing happens, unless I manully put in cdr, or I flip to a new window, or something, then they respond.
I'm going to continue trying to find a root cause, but at the moment, I'm out of ideas. There's no prominent error message that would imply something broke. The items fromt the first batch still work. But the ones from the second will appear to lock up. until CDR is forced by an outside event.
I wanted to check here to see if anyone had any ideas on what may be causing this.
Also, here's the declaration code for 'trackVisibility'
import {
Directive,
ElementRef,
EventEmitter,
NgZone,
OnDestroy,
OnInit,
Output,
} from '#angular/core';
#Directive({
selector: '[trackVisibility]',
})
export class TrackVisibilityDirective implements OnInit, OnDestroy {
observer!: IntersectionObserver;
#Output()
visibile = new EventEmitter<boolean>();
constructor(private el: ElementRef<HTMLElement>, private ngZone: NgZone) {}
ngOnInit(): void {
this.ngZone.runOutsideAngular(() => {
this.observer = new IntersectionObserver((entries) => {
entries.forEach((e) => {
this.visibile.emit(e.isIntersecting);
});
});
this.observer.observe(this.el.nativeElement);
});
}
ngOnDestroy(): void {
this.observer.disconnect();
}
}
here is the solution
You used runOutsideAngular function in your Directive.
"Running functions via runOutsideAngular allows you to escape Angular's zone and do work that doesn't trigger Angular change-detection or is subject to Angular's error handling. Any future tasks or microtasks scheduled from within this function will continue executing from outside of the Angular zone."
I also changed some parts of the code for more readability.
I'm trying to implement a global loading indicator that can be reused in the entire application. I have an injectable service that has the show and hide functions:
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
#Injectable()
export class SpinnerOverlayService {
private loaderSubject = new Subject<any>();
public loaderState = this.loaderSubject.asObservable();
constructor() { }
/**
* Show the spinner
*/
show(): void {
this.loaderSubject.next(<any>{ show: true });
}
/**
* Hide the spinner
*/
hide(): void {
this.loaderSubject.next(<any>{ show: false });
}
}
And this is the code of the spinner overlay component. I'll exclude details about the HTML and CSS implementation as they're not important here.
import { Component, OnInit } from '#angular/core';
import { Subscription } from 'rxjs';
import { SpinnerOverlayService } from '../spinner-overlay.service';
#Component({
selector: 'spinner-overlay',
templateUrl: './spinner-overlay.component.html',
styleUrls: ['./spinner-overlay.component.scss']
})
export class SpinnerOverlayComponent implements OnInit {
show = false;
private _subscription: Subscription;
constructor(private spinnerOverlayService: SpinnerOverlayService) { }
ngOnInit(): void {
this._subscription = this.spinnerOverlayService.loaderState.subscribe((state) => {
console.log("Subscription triggered.");
this.show = state.show;
});
}
ngOnDestroy(): void {
this._subscription.unsubscribe();
}
}
The problem: In the code of the overlay component I'm subscribing to the observable loaderState of the service. However when I call the show() function which triggers the next() of the observable, the subscription callback is not triggered.
This is how I call the show() function in the app.component.ts:
ngOnInit() {
this.spinnerOverlayService.show();
}
What could I be missing? Seems really strange that the callback is not triggered.
Here is an example in Stackblitz: https://stackblitz.com/edit/angular-7-registration-login-example-2qus3f?file=app%2Fspinner-overlay%2Fspinner-overlay.component.ts
The problem is you call this.spinnerOverlayService.show(); before spinner-overlay is initialized. Subjects do not hold previous emitted value, so late subscribers won't get any value unless there is a new value.
One thing you can do is to change Subject to BehaviorSubject which emits the last value to new subscribers.
Or, you can call this.spinnerOverlayService.show(); within ngAfterViewInit.
This way, you'll know spinner-overlay will get initialized and subscribe to spinnerOverlayService.loaderState
ngAfterViewInit() {
this.spinnerOverlayService.show();
}
Check it out
In addition to the above answer you can have a state in your spinnerOverlayService service to check the show hide and also have a subject to subscribe if new value is ready:
public state = { show: false };
constructor() { }
/**
* Show the spinner
*/
show():void {
this.state = { show: true };
this.loaderSubject.next(<any>{ show: true })
}
/**
* Hide the spinner
*/
hide():void {
this.state = { show: false };
this.loaderSubject.next(<any>{ show: false })
}
and in your ngOnInit:
ngOnInit(): void {
if(this.spinnerOverlayService.state.show){
console.log('Subscription triggeredd.');
};
this._subscription = this.spinnerOverlayService.loaderState.subscribe((state) => {
console.log("Subscription triggered.");
this.show = state.show;
});
}
OR you can use:
private loaderSubject = new ReplaySubject(1); // to cache last value
demo.
In the template component AppComponent, depending on the value, the variable this.loggedInService.isLoggedIn switches between the logIn() and logout() methods, which in the application component AppComponent are subscribed to these methods in the service LoggedinService and depending on the method, change the value of the variable to true or false.
Also in the Guard's method checkLogin (url: string) I return true or false depending on the value of the variable this.loggedInService.isLoggedIn
Everything works, but when I reset the page, I need to keep the value of the input or output button. I try to do this in the login() and logout() methods in the service, but after reloading the page, the changes are still not saved. Help solve this problem so that the changes remain after the page reboot.
template of AppComponent:
<li class="nav-item">
<a class="btn btn-outline-success"
[class.btn-outline-success]="!this.loggedInService.isLoggedIn"
[class.btn-outline-danger]="this.loggedInService.isLoggedIn"
(click)="this.loggedInService.isLoggedIn ? logout() : logIn()">
{{this.loggedInService.isLoggedIn ? 'Exit' : 'Enter'}}
</a>
</li>
code of AppComponent:
export class AppComponent implements OnInit {
constructor(private loggedInService: LoggedinService,
private router: Router) {
}
ngOnInit() {}
logIn(): void {
this.loggedInService.login();
if (this.loggedInService.isLoggedIn) {
let redirect = this.loggedInService.redirectUrl ? this.loggedInService.redirectUrl :
'/gallery';
this.router.navigate([redirect]);
}
}
logout(): void {
this.loggedInService.logout();
this.router.navigate(['/']);
}
}
LoggedinService:
export class LoggedinService implements OnInit {
isLoggedIn: boolean = false;
redirectUrl: string;
constructor() {
}
ngOnInit() {
this.CheckAuthentication();
}
enter code here
CheckAuthentication(): boolean {
if (localStorage.getItem('login') === 'true') {
return this.isLoggedIn = true;
} else if (localStorage.getItem('login') === 'false') {
return this.isLoggedIn = false;
}
}
login() {
localStorage.setItem('login', 'true')
}
logout() {
localStorage.removeItem('login');
localStorage.setItem('login', 'false')
}
AuthGuard:
export class AuthGuard implements CanActivate {
constructor(private loggedInService: LoggedinService) {
}
canActivate(next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean{
let url: string = state.url;
return this.checkLogin(url);
}
checkLogin(url: string): boolean {
if (this.loggedInService.isLoggedIn) {
return true;
} else {
this.loggedInService.redirectUrl = url;
return false;
}
}
}
Change is isLoggedIn to be get method base on localStorage item
export class LoggedinService implements OnInit {
redirectUrl: string;
constructor() {}
get isLoggedIn(): boolean {
return localStorage.getItem('login') ? true : false;
}
login(){
localStorage.setItem('login','true')
}
logout(){
localStorage.removeItem('login')
}
}
app.component
export class AppComponent {
constructor(private loggedInService: LoggedinService,
private router: Router) {
}
logIn(): void {
this.loggedInService.login(); // set the state as login
let redirect = this.loggedInService.redirectUrl ? this.loggedInService.redirectUrl :
'/gallery';
this.router.navigate([redirect]);
}
logout(): void {
this.loggedInService.logout(); //// set the state as logout
this.router.navigate(['/']);
}
}
stackblitz demo
I have a doubt with your code.
In LoggedInService onInit why are you calling login() and logout() directly?
this.CheckAuthentication();
this.login();
this.logout();
Doing that is adding and deleting from your localStorage. Also, you can check data in your local storage by typing localStorage in browser console.I think you should comment or remove onInit method
What I am trying to do is to create an anchor link. This link will navigate to a specific scroll point in my page. I have Angular version 5.
Html:
<mat-list>
<mat-list-item><a [routerLink]="['/']"> Intro </a></mat-list-item>
<mat-list-item><a [routerLink]="['/']" fragment="mobile"> Mobile </a></mat-list-item>
...
</mat-list>
In home.componets.ts:
export class HomeGrComponent implements OnInit {
private fragment: string;
constructor(private route: ActivatedRoute) { }
ngOnInit() {
this.route.fragment.subscribe(fragment => { this.fragment = fragment; });
}
ngAfterViewInit(): void {
try {
setTimeout(()=> {
document.querySelector('#' + this.fragment).scrollIntoView();
}, 1000);
} catch (e) { }
}
}
I took this code from this question but it doesn't work. Url is changed to
http://localhost:4200/#mobile
but it didn't scroll to my point.
Also in console there is an error:
Cannot read property 'scrollIntoView' of null
What can be possible goes wrong? If you need some additional information please ask me to reply. Also it could be great the scroll navigates smoothly (optional).
You can use the following Code:
import { Component, OnDestroy } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { filter } from 'rxjs/operators';
import { Subscription } from 'rxjs';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnDestroy {
private sub: Subscription;
constructor(activeRoute: ActivatedRoute) {
this.sub = activeRoute.fragment.pipe(filter(f => !!f)).subscribe(f => document.getElementById(f).scrollIntoView());
}
public ngOnDestroy(): void {
if(this.sub) this.sub.unsubscribe();
}
}
Working example and Code behind
The reason why it's not working is that ngAfterViewInit is being called before the Observable is resolved, and therefore this.fragment is null, so no element is found
ngOnInit() {
this.route.fragment.subscribe(fragment => {
this.fragment = fragment;
});
}
ngAfterViewInit(): void {
let interval = setInterval(()=> {
let elem = document.getElementById(this.fragment);
if(elem) {
elem.scrollIntoView();
clearInterval(interval);
}
}, 1000);
}
Another option is to use setTimout(). So you don't need clearInterval().
You can also access the fragment with the help of the ActivatedRoute
constructor(private route: ActivatedRoute) {}
ngAfterViewInit(): void {
setTimeout(() => document.querySelector(this.route.snapshot.fragment).scrollIntoView(), 1000);
}
I have a Visual Studio 2017 .NET Core 2.0 project with Angular template. Angular prerendering times out when I uncomment either of the "this.SetUpRefreshInterval()" lines below. The app returns error "NodeInvocationException: Prerendering timed out after 30000ms because the boot function in 'ClientApp/dist/main-server' returned a promise that did not resolve or reject. Make sure that your boot function always resolves or rejects its promise." Any ideas?
Here's the component code:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-currentdatetime',
templateUrl: './currentdatetime.component.html',
styleUrls: ['./currentdatetime.component.css']
})
export class CurrentDateTimeComponent implements OnInit {
private currentDateTime: Date;
constructor() {
this.refreshDate();
//this.setUpRefreshInterval();
}
get currentDateFormatted(): String {
return this.currentDateTime.toLocaleDateString();
};
get currentTimeFormatted(): String {
return this.currentDateTime.toLocaleTimeString();
};
ngOnInit(): void {
//this.setUpRefreshInterval();
}
private setUpRefreshInterval(): void {
setInterval(() => {
this.refreshDate();
}, 1000);
}
private refreshDate(): void {
this.currentDateTime = new Date();
}
}
I also tried using Observable.timer and got the same result:
private setUpRefreshInterval(): void {
let timer = Observable.timer(0, 1000);
timer.subscribe(t => this.currentDateTime = new Date());
}
As a workaround, I used Krishnanunni's suggestion and made this change, which works:
import { Component, OnInit, Inject } from '#angular/core';
import { isPlatformBrowser } from '#angular/common';
import { PLATFORM_ID } from '#angular/core';
constructor(#Inject(PLATFORM_ID) private platformId: Object) {
this.refreshDate();
}
ngOnInit(): void {
// setting up the refresh interval caused a timeout in prerendering, so only set up interval if rendering in browser
if (isPlatformBrowser(this.platformId)) {
this.setUpRefreshInterval();
}
}
Wrap the interval code in isPlatformBrowser condition so that it is not pre-rendered