Angular: How to check change in queryParams before ngOnDestroy is called - javascript

I would like to conditionally execute some code in ngOnDestroy based on changes in current route.
Route is changed from /foo to /login?logout=true, and this change is triggered outside of Foo component.
In ngOnInit I am subscribing to queryParam changes, to update correctly loggingOut flag.
My problem is, that ngOnDestroy is called before next handler of queryParam, so the loggingOut has incorrect value.
export class FooComponent implements OnInit, OnDestroy {
loggingOut = false;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.queryParamMap.subscribe(queryParams => {
this.loggingOut = queryParams.get('logout') === 'true';
});
}
ngOnDestroy(): void {
if (this.loggingOut) {
// do this
} else {
// do that
}
}
}
Seems this is intended behavior from lifecycle POV, so have following question:
Is there a way to check route changes before ngOnDestory is called?
If possible, please add link to documentation describing, how are lifecycle hooks (especially ngOnDestory) called with respect to navigation changes?
Thanks.

My problem is, that ngOnDestroy is called before next handler of queryParam
componentDestroyed = false;
ngOnInit(): void {
this.route.queryParamMap.subscribe(queryParams => {
if (!this.componentDestroyed)
this.loggingOut = queryParams.get('logout') === 'true';
else {
// Do here what you wanted to do in ngOnDestroy
}
});
}
ngOnDestroy(): void {
this.componentDestroyed = true;
}
Would this fix your problem?

Related

Passing void events from parent component to child component

Quick overview
I have a "Terminal" Component which should be able to be used multiple times all over my application. This component should also be able to put into a "read only" mode where you pass a single command you'd like the terminal to fire and it will display the output. I am trying to have this component be able to be refreshed by many things; data updating else where, user events, etc. To achieve this, currently, I am using RxJS subjects as an #Input into the terminal component which when updated fires the subscribed functions. This works for the first user click (see bellow) but after that the subject doesn't update again. I suspect this is due to the "object" not updating, there for angular doesn't register the change and my whole idea falls apart.
Can I fix this? or do I need to redesign this "Terminal" component?
Code
terminal.component.ts
export class TerminalComponent implements OnInit, OnDestroy {
constructor() {}
$destroy = new Subject();
terminalOutput = '';
// Command input (if you want the terminal to only fire one command)
#Input() command = '';
$command: BehaviorSubject<string> = new BehaviorSubject('');
// Refresh terminal input
$refresh: Subject<void> = new Subject();
#Input() set refresh(value: Subject<void>) {
this.$refresh = value;
}
// ReadOnly Input
#Input() readOnly = false;
ngOnInit(): void {
this.$refresh
.pipe(
takeUntil(this.$destroy),
tap(() => {
const lastCommand = this.$command.getValue();
if (lastCommand) {
console.log('Refreshing, last command is:', lastCommand);
}
})
)
.subscribe();
//...
}
//...
}
parent.component.html
<h1>Home</h1>
<app-terminal command="ls" [refresh]="$refreshSubject"></app-terminal>
<button (click)="refreshTest()">Refresh</button>
parent.component.ts
export class ParentComponent implements OnInit {
$refreshSubject: Subject<void> = new Subject();
constructor() {}
ngOnInit(): void {}
refreshTest(): void {
console.log('Refreshing');
this.$refreshSubject.next();
}
}
So I found the problem, it was another bug in my code that was causing the RxJS tap to not fire after the first time.
ngOnInit(): void {
this.$refresh
.pipe(
takeUntil(this.$destroy),
tap(() => {
const lastCommand = this.$command.getValue();
if (lastCommand) {
console.log('Refreshing, last command is:', lastCommand);
throw new Error("Example Error");
// ^^^^ This will cause future Subject emits to not fire as this function has failed.
}
})
)
.subscribe();
//...
}
Side note: I think the question title should be changed to better suit the real problem with my code, and there for have better SEO. However if this is a completely rubbish question then I think it should be deleted.

Angular does not detect changes made from window global function

During the work I encountered a very strange behaviour.
Here is the link for a similar problem: stackblitz
From index.html file I have raised some click event
function createClause(event) {
Office.context.document.getSelectedDataAsync(
Office.CoercionType.Text,
(asyncResult) => {
window.sendSelectedTextCallback({selectedText: asyncResult.value});
event.completed();
});
}
In the app.component.ts I'm listening to the sendSelectedTextCallback function.
(window as any).sendSelectedTextCallback = (params: any) => {
clauseCommunicationService.addClause({name: params.selectedText});
};
clauseCommunicationService.addClause method calls next function for a subject.
In some component I'm listening for the changes.
this.clauseAddedSubscription = clauseCommunicationService.clauseAdded$.subscribe(
(clause) => {
this.clauses.push(clause);
console.log(this.clauses);
}
);
The issue i'm facing is that console.log(this.clauses) command shows me the list updated, but this is not reflected on the UI.
If I'm replacing
(window as any).sendSelectedTextCallback = (params: any) => {
clauseCommunicationService.addClause({name: params.selectedText});
}
with
setTimeout(() => {
clauseCommunicationService.addClause({name: 'helloooo'});
}, 4000);
i can see that the changes are reflected on the UI.
I tried to use ngZone and ChangeDetector features but without success.
The issue is because the button is outside of angular, so angular doesn't test for change detection on the button so when the button gets clicked it doesn't know something happened. We can add change detection to that button using a #HostListener Stack Blitz example.
export class HelloComponent {
public elements = [1, 2, 3, 4];
#HostListener('window:click')
private onWindowClick() {
this.cdr.detectChanges();
}
constructor(
#Inject(CommunicationService)
private communicationService: CommunicationService,
private cdr: ChangeDetectorRef
) {}
ngOnInit() {
this.communicationService.elemAdded$.subscribe(elem => {
this.elements.push(elem);
console.log(this.elements);
});
}
}
You can force angular to "refresh" and take into consideration events and things that are out of its world by using ngZone : try this
import { NgZone } from '#angular/core';
// In the constructor, inject NgZone
constructor(private zone: NgZone) { }
// Then :
clauseCommunicationService.clauseAdded$.subscribe(
(clause) => {
this.zone.run(() => { // Where the magic happens
this.clauses.push(clause);
console.log(this.clauses);
})
}
);

ionic page data does not update

So say i have page one:
This page contains multiple variables and a constructor. it could look something like this:
export class TestPage implements OnInit {
testInt: number;
testString: string;
constructor(private someService: SomeService) {
}
ngOnInit() {
this.testInt = this.someService.getInt();
this.testString = this.someService.getLongText();
}
}
Now when this page loads it correctly sets the values.
Now say that I change page and on this page, I change some of the values in the service.
When I then come pack to this TestPage it hasn't updated the values.
Does this have something to do with caching? or with push state?
How can I make sure that the page is "reloaded" ?
Try using RxJS.
#Injectable({...})
class SomeService {
private _testInt: BehaviorSubject<number> = new BehaviorSubject<number>(0); // initial value 0
setTestInt(value: number) {
this._testInt.next(value);
}
getTestInt(): Observable<number> {
return this._testInt.asObservable();
}
}
#Component({...})
class TestPage implements OnInit {
public testInt: number;
public testInt$: Observable<number>;
private subscription: Subscription;
constructor(private someService: SomeService) {}
ngOnInit() {
// one way
this.testInt$ = this.someService.getTestInt();
// or another
this.subscription = this.someService.getTestInt()
.subscribe((value: number) => {
this.testInt = value;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
in the HTML:
<p>{{ testInt }}</p>
<p>{{ testInt$ | async }}</p>
If you are subscribing to a Observable, make sure you unsubscribe after the usage (usually On Destroy lifecycle hook).
Async Pipe does that out of the box.
Or try the ionViewWillEnter lifecycle hook.
As you can see in the official documentation:
ngOnInit will only fire each time the page is freshly created, but not when navigated back to the page.
For instance, navigating between each page in a tabs interface will only call each page's ngOnInit method once, but not on subsequent visits.
ngOnDestroy will only fire when a page "popped". link That means that Page is cached, yes. Assigning value On Init will set the value only the first time page is visited and therefore not updated.

Subscribing to Observable not triggering change detection

I am using 'angular2-virtual-scroll' to implement load on demand. The items used to be driven by observable's using the async pipe triggered by the parent component. Now i am trying to call my service from the child. The call is successful and i get my data, i need to use the subscribe event to apply other logic. The issue is change detected does not appear to be working when i update my arrays in the subscribe function. I have read other similar issues but i have had no luck finding a solution.
This is the main component where the service calls are used. The inital request is done from the onInit. And then when you scroll down fetchMore is called.
import { Component, OnInit, Input, OnDestroy } from '#angular/core';
import { Store } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { User } from './../models/user';
import { Role } from './../../roles/models/role';
import { UsersService } from './../services/users.service';
import { ChangeEvent } from 'angular2-virtual-scroll';
import { promise } from 'selenium-webdriver';
import { VirtualScrollComponent } from 'angular2-virtual-scroll';
import { Subscription } from 'rxjs/Subscription';
#Component({
selector: 'app-users-list',
template: `
<div class="status">
Showing <span class="">{{indices?.start + 1}}</span>
- <span class="">{{indices?.end}}</span>
of <span class="">{{users?.length}}</span>
<span>({{scrollItems?.length}} nodes)</span>
</div>
<virtual-scroll [childHeight]="75" [items]="users" (update)="scrollItems = $event" (end)="fetchMore($event)">
<div #container>
<app-user-info *ngFor="let user of scrollItems" [roles]="roles" [user]="user">
<li>
<a [routerLink]="['/users/edit/', user.id]" class="btn btn-action btn-edit">Edit</a>
</li>
</app-user-info>
<div *ngIf="loading" class="loader">Loading...</div>
</div>
</virtual-scroll>
`
})
export class UsersListComponent implements OnInit, OnDestroy {
users: User[] = [];
#Input() roles: Role[];
currentPage: number;
scrollItems: User[];
indices: ChangeEvent;
readonly bufferSize: number = 20;
loading: boolean;
userServiceSub: Subscription;
constructor(private usersService: UsersService) {
}
ngOnInit() {
this.reset();
}
ngOnDestroy() {
if(this.userServiceSub) {
this.userServiceSub.unsubscribe();
}
}
reset() {
this.loading=true;
this.currentPage = 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = users;
});
}
fetchMore(event: ChangeEvent) {
if (event.end !== this.users.length) return;
this.loading=true;
this.currentPage += 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = this.users.concat(users);
});
}
}
From what i have read this could be a context issue but i am not sure. Any suggestions would be great.
"EDIT"
Looking at the source code for the plugin component i can see where the change event is captured.
VirtualScrollComponent.prototype.ngOnChanges = function (changes) {
this.previousStart = undefined;
this.previousEnd = undefined;
var items = changes.items || {};
if (changes.items != undefined && items.previousValue == undefined || (items.previousValue != undefined && items.previousValue.length === 0)) {
this.startupLoop = true;
}
this.refresh();
};
If i put a breakpoint in this event it fires on the initial load, so when we instantiate the array to []. It fires when i click on the page. But it does not fire when the array is update in the subscribe event. I have even put a button in that sets the array to empty, and that updates the view so the subscribe function must be breaking the change detection.
So when you say the change detection does not appear to be working, I assume you are referring to this: *ngFor="let user of scrollItems"?
I have not used that particular component nor do I have any running code to work with ... but I'd start by taking a closer look at this:
<virtual-scroll [childHeight]="75"
[items]="currentBuffer"
(update)="scrollItems = $event"
(end)="fetchMore($event)">
Maybe change the (update) to call a method just to ensure it is emitting and that you are getting what you expect back from it.
EDIT:
Here is an example subscription that updates the primary bound property showing the data for my page:
movies: IMovie[];
getMovies(): void {
this.movieService.getMovies().subscribe(
(movies: IMovie[]) => {
this.movies = movies;
this.performFilter(null);
},
(error: any) => this.errorMessage = <any>error
);
}
The change detection works fine in this case. So there is most likely something else going on causing the issue you are seeing.
Note that your template does need to bind to the property for the change detection to work. In my example, I'm binding to the movies property. In your example, you'd need to bind to the users property.
So change detection was not firing. I had to use "ChangeDetectorRef" with the function "markForCheck" to get change detection to work correctly. I am not sure why so i definitely have some research to do.

how can i call a function when a template loads in angular2?

I am new to angular2. I have a requirement to call a function when a template loads/initializes. I know how to do this in angular1.x., but I am not able to find out how it can be done in angular-2.
This is how I tried in angular1.x
In html
<div ng-init="getItems()">
//some logic to get my items
</div>
In controller
getItems = function(){
console.log('in the getitems function call');
//logic to get my items from db/localStorage
}
This is how I used ng-init in angular1.x, but there is no ng-init in angular-2?Please help me on this issue.
#Component({
...
})
class MyComponent {
constructor() {
// when component class instance is created
}
ngOnChanges(...) {
// when inputs are updated
}
ngOnInit() {
// after `ngOnChanges()` was called the first time
}
ngAfterViewInit() {
// after the view was created
}
ngAfterContentInit() {
// after content was projected
}
}
See also https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#hooks-overview for the full list
Check lifecycle events of a component https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html . From what you are saying you probably needs ngAfterViewInit
In angular2 you can use component phase ngOnInit it is equal to on-init in angularJS. Here is more information about lifecycle in angular.
Example:
export class PeekABoo implements OnInit {
constructor(private logger: LoggerService) { }
// implement OnInit's `ngOnInit` method
ngOnInit() {
this.logIt(`OnInit`);
}
protected logIt(msg: string) {
this.logger.log(`#${nextId++} ${msg}`);
}
}

Categories