Property change caused by route change won't update view - *ngIf - javascript

So I have a kind of custom select bar with products-header__select expanding the list on click. To do so I created the property expanded which is supposed to describe its current state. With *ngIf I either display it or not.
It works fine clicking the products-header__select. But a click on one of the expanded list's items changes the route, the path and some other element changes, but the products-header__select remains visible.
All good, but I want to collapse the list on route change - my approach was to listen to router events and then run expanded = false when the navigation has ended. - But somehow the view won't update and the list remains expanded, even though running console.log(this.expanded) inside of the router event returns false. Why won't it update then?
View:
<div class="products-header__select" (click)="expanded = !expanded">
<ul>
<li class="basic-text__small custom-select">{{mobileCategories ? (mobileCategories[0].name | transformAllProducts) : ''}}</li>
<div class="select-options" *ngIf="expanded">
<li class="basic-text__small" *ngFor="let category of mobileCategories.slice(1, mobileCategories.length); let i = index" routerLink="/products/{{category.name.toLowerCase()}}">
{{category?.name | transformAllProducts}}
</li>
</div>
</ul>
</div>
import {Component, Input, OnInit} from '#angular/core';
import {NavigationEnd, Router, RouterEvent} from '#angular/router';
#Component({
selector: 'app-products-header',
templateUrl: './products-header.component.html',
styleUrls: ['./products-header.component.scss']
})
export class ProductsHeaderComponent implements OnInit {
expanded = false;
url: string;
$categories;
#Input() set categories(value) {
if (value) {
this.$categories = value;
this.createArrayForMobile();
this.getActiveRoute();
}
}
mobileCategories: any[];
constructor(private router: Router) {
router.events.subscribe((event: RouterEvent) => {
if (event instanceof NavigationEnd) {
this.expanded = false;
this.url = event.url;
this.getActiveRoute();
}
});
}
ngOnInit(): void {
}
getActiveRoute() {
if (!this.mobileCategories) { return; }
const decodedUrl = decodeURI(this.url);
const index = this.mobileCategories.findIndex(item => decodedUrl.includes(item.name.toLowerCase()));
const obj = this.mobileCategories[index];
this.mobileCategories.splice(index, 1);
this.mobileCategories.unshift(obj);
}
createArrayForMobile() {
this.mobileCategories = [...this.$categories, {name: 'all'}];
}
}
That's how I use it:
<app-products-header [categories]="categories"></app-products-header>

Don't know the answer but things I would try would be:
Try subscribing to the router events in ngOnInit() {} rather than the constructor.
Try specifically calling change detection.
constructor(private router: Router, private cdr: ChangeDetectorRef) {
router.events.subscribe((event: RouterEvent) => {
if (event instanceof NavigationEnd) {
this.expanded = false;
this.url = event.url;
this.getActiveRoute();
this.cdr.detectChanges();
}
});}

Had to wrap the router event's code with a timeout:
constructor(private router: Router) {
router.events.subscribe((event: RouterEvent) => {
if (event instanceof NavigationEnd) {
setTimeout(() => {
this.expanded = false;
this.url = event.url;
this.getActiveRoute();
});
}
});
}

Related

Angular component aware of event emitter's result

I have a generic button component:
#Component({
selector: "debounced-submit-button"
template: `
<button (click)="debounceClick.emit()" [disabled]="disabled">
<ng-content></ng-content>
</button>
`})
export class DebouncedSubmitButton {
#Input() disabled: boolean = false;
#Output() debounceClick = new EventEmitter();
}
And I use it like:
#Component({
selector: "example-component",
template: `
<debounced-submit-button (debounceClick)="makeBackendCall()" disabled="loading">
</debouncedSubmitButton>
`})
export class ExampleComponent {
loading = false;
makeBackendCall(): Promise<any> {
this.loading = true;
return apiService.makeCall()
.then(result => useResult(result))
.finally(() => this.loading = false);
}
}
So you can't click on the button again while the HTTP call is in progress. However, this requires me include a lot of boilerplate call to track "loading" wherever I have a button.
Is there any way to communicate back the results of the (debounce-click) event to the debounced-submit-button, so I can centrally locate my disabling code? Like, ideally, I just want
<debounced-submit-button (debounce-click)="makeBackendCall()">
and have the component be something like
#Component({
selector: "debounced-submit-button"
template: `
<button (click)="onClick($event)" [disabled]="disabled">
<ng-content></ng-content>
</button>
`})
export class DebouncedSubmitButton {
disabled: boolean = false;
#Output() debounceClick = new EventEmitter();
onClick() {
this.disabled = true;
// I don't think this works, but I want the return value of the callback function
httpCall = debounceClick.emit();
httpCall.finally(() => this.disabled = false);
}
}
Like, obviously this doesn't work, because the debounceClick event emitter could be subscribed to by multiple listeners, or no listeners. I'm just looking for a less-boilerplate-y way to communicate to the DebouncedSubmitButton that the API call is done, and the user should be able to interact with it again. Is there a way to do this?
Sounds to me like you just want to pass a function as an input.
template: `
<button (click)="_onClick($event)" [disabled]="disabled">
<ng-content></ng-content>
</button>
`})
export class DebouncedSubmitButton {
disabled: boolean = false;
#Input() onClick: () => Promise<any>;
_onClick() {
this.disabled = true;
this.onClick().finally(() => this.disabled = false;)
}
}
<debounced-submit-button [onClick]="makeBackendCall">
Just make sure you pass arrow functions instead of regular functions to maintain the lexical context of this.
export class ExampleComponent {
makeBackendCall = () => {
return this.apiService.makeCall()
.then(result => this.useResult(result))
}
}
create a flag in the api service, inside the service, fundamentally there will be four implementations mostly! ( GET,POST,PUT,DELETE ) so we just need to toggle the flag, we use the tap operator, which will execute after the API call happens, but will not do anything to the response. then use that flag to enable/disable buttons in any components. Please check the implementation below!
import { Injectable } from '#angular/core';
import { tap } from 'rxjs/operators';
#Injectable()
export class ApiService {
apiCallInProgress = false;
constructor(private http: HttpClient) { }
get(): Observable<any> {
this.apiCallInProgress = true;
return this.http.get('https://jsonplaceholder.typicode.com/users').pipe(
tap(() => {this.apiCallInProgress = false;}
);
}
}
Use the service variable in the component.
#Component({
selector: "debounced-submit-button"
template: `
<button (click)="debounceClick.emit()" [disabled]="getDisabled()">
<ng-content></ng-content>
</button>
`})
export class DebouncedSubmitButton {
#Output() debounceClick = new EventEmitter();
constructor(apiService: ApiService){}
getDisabled() {
return this.apiService.apiCallInProgress;
}
}

Child component's EventEmitter "loses" parent component's observer

I have a parent component which observes child component's Output event emitter (topicsChanged).
Parent component:
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output
} from "#angular/core";
import { Language } from "../language";
import { TemplateTopic } from "../template-topic";
#Component({
selector: "at-main-topics",
templateUrl: "./main-topics.component.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MainTopicsComponent implements OnInit {
#Input() templates: string[] = [];
#Input() templateTopics: TemplateTopic[] = [];
#Input() languages: Language[] = [];
#Output() templateTopicChanged = new EventEmitter<TemplateTopic>();
constructor() {}
ngOnInit(): void {}
get availableTemplateTopics(): TemplateTopic[] {
return this.templates
.map(x => +x)
.map(template => {
const existingTopic = this.templateTopics.find(
x => x.template === template
);
return (
existingTopic ||
{ //observer will disappear for this empty created object.
template: template,
topics: []
}
);
});
}
onTopicsChanged(templateTopic: TemplateTopic) {
// This will not be triggered for 3rd template which is created in availableTemplateTopics getter, because it doesn't exist in initial data (templateTopics)
this.templateTopicChanged.emit(templateTopic);
}
}
<at-template-topic *ngFor="let templateTopic of availableTemplateTopics"
[templateTopic]="templateTopic"
[languages]="languages"
(topicsChanged)="onTopicsChanged($event)">
</at-template-topic>
In one strange case, this event emitter loses it's parent component's observer. That is - in child component I am opening a dialog. Before dialog is opened, if I inspect the emitter, the observer is there, but once the dialog is closed, observer is gone.
Child component:
import { Component, EventEmitter, Input, OnInit, Output } from '#angular/core';
import { MatDialog } from '#angular/material/dialog';
import { Language } from '../language';
import { TemplateTopic } from '../template-topic';
import { Topic } from '../topic';
import { TranslationDialogModel } from '../translation-dialog.model';
import { TranslationDialogComponent } from '../translation-dialog/translation-dialog.component';
#Component({
selector: 'at-template-topic',
templateUrl: './template-topic.component.html'
})
export class TemplateTopicComponent implements OnInit {
#Input() templateTopic: TemplateTopic;
#Input() languages: Language[] = [];
#Output() topicsChanged = new EventEmitter<TemplateTopic>();
private dialogTitle: string = 'lorem ipsum'
constructor(
private dialog: MatDialog
) { }
ngOnInit(): void {
}
onCreateTopic(): void {
this.openDialog();
}
onEditTopic(topic: Topic): void {
this.openDialog(topic);
}
private openDialog(topic?: Topic): void {
// this.topicsChanged always has observer at this point
const dialogRef = this.dialog.open(TranslationDialogComponent, {
data: {
pageTitle: this.dialogTitle,
availableLanguages: this.languages,
translations: topic?.translations
} as TranslationDialogModel
});
dialogRef
.beforeClosed()
.subscribe(translations => {
if (translations) {
if (topic) {
topic.translations = translations;
topic.title = translations[0].title;
} else {
this.templateTopic.topics.push({ translations, title: translations[0].title })
}
// When called via onCreateTopic method for a category which was created as an empty placeholder, this.topicsChanged has lost it's observer. However if category had initial data, then observer is still there.
this.topicsChanged.emit(this.templateTopic);
}
})
}
}
There is nothing shady going in the dialog, it simply returns some data and that's it. This is somehow connected to the getter get availableTemplateTopics in parent component from which list of child components are created. In getter there is a list of templates representing each child component which is either populated from already existing data or an empty placeholder is created. And the issue is with the empty placeholder objects.
Snippet:
get availableTemplateTopics(): TemplateTopic[] {
return this.templates
.map(x => +x)
.map(template => {
const existingTopic = this.templateTopics.find(
x => x.template === template
);
return (
existingTopic ||
{ //observer will disappear for this empty created object.
template: template,
topics: []
}
);
});
}
I found that I can solve all of this simply by moving the getter logic one level up, but I would still like to understand this weird behavior. How can observer disappear just like that and how is it connected to the getter?
Stackblitz for full code: https://stackblitz.com/edit/angular-kjewu7?file=src/app/main-topics/main-topics.component.ts

Angular: How to kinda refresh the ngOnInit method

I have a sidebar with different redirects of specific products categories, when these buttons are clicked it redirects to a component that gets the URL params and makes a consult to service and retrieves the data of that specific category, the thing is, when a click it the first time, it works, but the second time it does not, it only changes the URL but does not refresh the data
sidebar.component.html
<div class="list-group">
<a [routerLink]="['products/category']" [queryParams]="{name:category.name}" class="list-group-item"
*ngFor="let category of categories">{{category.name}}</a>
</div>
And the component that makes the magic
export class ViewAllProductsByCategoryComponent implements OnInit {
searchCategory: any;
products: Product;
constructor(private activatedRoute: ActivatedRoute, private productsService: ProductsService) {
}
ngOnInit(): void {
this.activatedRoute.queryParams.subscribe(res => {
this.searchCategory = res.name;
});
this.productsService.searchCategoryProducts(this.searchCategory).subscribe(res => {
this.products = res;
console.log(this.products);
});
}
}
So, how do I refresh the data?
Angular by default doesn't re-initialize an already loaded component.
But there is a way to bypass that feature:
let newLocation = `/pathName/5110`;
// override default re use strategy
this.router
.routeReuseStrategy
.shouldReuseRoute = function () {
return false;
};
this.router
.navigateByUrl(newLocation)
.then(
(worked) => {
// Works only because we hooked
// routeReuseStrategy.shouldReuseRoute
// and explicitly told it don't reuse
// route which forces a reload.
// Otherwise; the url will change but new
// data will not display!
},
(error) => {
debugger;
}
);
Just set the .shouldReuseRoute function to return false, that way the component will reload.
Here's more detail on that topic.
https://dev.to/jwp/angular-s-naviation-challenges-20i2
You can also configure the router to reuse the route.
I've modified a bit john's answer, this is how I fixed it
export class ViewAllProductsByCategoryComponent implements OnInit, OnDestroy {
searchCategory: any;
products: Product;
mySubscription: any;
constructor(private activatedRoute: ActivatedRoute,
private productsService: ProductsService,
private router: Router,
) {
this.router.routeReuseStrategy.shouldReuseRoute = () => {
return false;
};
this.mySubscription = this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.router.navigated = false;
}
});
}
ngOnInit(): void {
this.activatedRoute.queryParams.subscribe(res => {
this.searchCategory = res.name;
console.log(this.searchCategory);
});
this.productsService.searchCategoryProducts(this.searchCategory).subscribe(res => {
this.products = res;
console.log(this.products);
});
}
ngOnDestroy(): void {
if (this.mySubscription) {
this.mySubscription.unsubscribe();
}
}
}

Method some() doesn't work as expected in Angular 8

I have an Angular app and I want to add follow/unfollow functionality for users. I'm trying to add isFollowed flag, so I will be able to know if user is followed or no, and depending on that I will show 2 different buttons: Follow and Unfollow. I'm using some() method for this purposes but it doesn't work. It shows me that isFollowed flag is undefined although it should show true or false. I don't understand where the problem is, here is my HTML relevant part:
<button *ngIf="!isFollowing; else unfollowBtn" class="btn" id="btn-follow" (click)="follow(id)">Follow </button>
<ng-template #unfollowBtn><button class="btn" id="btn-follow" (click)="unFollow(id)">Unfollow</button></ng-template>
TS component relevant part:
import { Component, OnInit } from '#angular/core';
import { Router, ActivatedRoute } from "#angular/router";
import { AuthenticationService } from '#services/auth.service';
import { FollowersService } from '#services/followers.service';
#Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
user;
id;
followers;
isFollowing: boolean;
constructor(
private authenticationService: AuthenticationService,
private followersService: FollowersService,
private router: Router,
private route: ActivatedRoute,
) { }
ngOnInit() {
this.id = this.route.snapshot.paramMap.get("id");
this.authenticationService.getSpecUser(this.id).subscribe(
(info => {
this.user = info;
})
);
this.followersService.getFollowing().subscribe(
data => {
this.followers = data;
this.isFollowing = this.followers.some(d => d.id == this.user.id);
}
);
}
follow(id) {
console.log('follow btn');
this.followersService.follow(id).subscribe(
(data => console.log(data))
)
this.isFollowing = true;
}
unFollow(id) {
console.log('unFollow btn');
this.followersService.unFollow(id).subscribe(
(data => console.log(data))
)
this.isFollowing = false;
}
}
Any help would be appreciated.
If you want it called everytime and to make sure this.user is populated. Then you could use a forkJoin
forkJoin(
this.authenticationService.getSpecUser(this.id),
this.followersService.getFollowing()
).pipe(
map(([info, data]) => {
// forkJoin returns an array of values, here we map those values to the objects
this.user = info;
this.followers = data;
this.isFollowing = this.followers.some(d => d.id == this.user.id);
})
);
Not tested this because I didn't have time. If you make a StackBlitz we could see it in action and try from there.
Hope this helps.

Display Loading Icon on Route Resolver / Route Change

Im trying to show a loading icon while I the route resolver gets the data from the DB.
I've tried the below option:
Root Component:
_router.events.subscribe((routerEvent: RouterEvent) => {
if (routerEvent instanceof NavigationStart) {
console.log("start");
this.loading = true;
} else if (routerEvent instanceof NavigationError || NavigationCancel || NavigationEnd) {
console.log("end");
this.loading = false;
}
});
Root Component HTML:
<h1 *ngIf="loading">Loading</h1>
The loading icon does not show at all.
The following is displayed on console log on every route change:
Update:
Below is the output after applying the following changes:
public loading: boolean = true;
console.log(routerEvent);
console.log("Loading is " + this.loading);
Update 2:
app.component.html:
<div class="uk-offcanvas-content">
<h1>{{loading}}</h1>
<h1 *ngIf="loading">Loading</h1>
<app-root-nav></app-root-nav>
<app-notifications></app-notifications>
<router-outlet></router-outlet>
</div>
app.component.ts:
import {Component, OnInit, AfterViewInit} from '#angular/core';
import {AuthenticationService} from "../../authentication/services/authentication.service";
import {Router, Event, NavigationStart, NavigationEnd, NavigationCancel, NavigationError} from "#angular/router";
import {RouterEvent} from "#angular/router";
import UIkit from 'uikit'
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, AfterViewInit {
isLoggedIn: boolean;
public loading: boolean = true;
UIkit: any;
constructor(private _router: Router, private _authService: AuthenticationService) {
_router.events.subscribe((routerEvent: RouterEvent) => {
if (routerEvent instanceof NavigationStart) {
this.loading = true;
console.log(routerEvent);
console.log("Loading is " + this.loading);
} else if (routerEvent instanceof NavigationError || NavigationCancel || NavigationEnd) {
this.loading = false;
}
});
}
ngAfterViewInit() {
}
ngOnInit() {
UIkit.notification({
message: 'my-message!',
status: 'primary',
pos: 'top-right',
timeout: 5000
});
}
}
The problem here is pretty simple but easy to miss. you're improperly checking for the router event type, it should be like :
else if (routerEvent instanceof NavigationError || routerEvent instanceof NavigationCancel || routerEvent instanceof NavigationEnd)
the way you have it is just returning true always because your second clause is basically "or if NavigationCancel is truthy", which it is by definition since it's an existing type. so loading sets to false immediately when the route resolve starts since there are a lot of intermediate router events before NavigationEnd event and it sets to false on all of them due to the way you're checking.
plunk: https://plnkr.co/edit/7UKVqKlRY0EPXNkx0qxH?p=preview
Try this code to show a loading icon while the route resolver gets the data from the DB :
constructor(private router: Router){
router.events.subscribe(e => {
if (e instanceof ChildActivationStart) {
this.loaderservice.show();
} else if (e instanceof ChildActivationEnd) {
this.loaderservice.hide();
}
});
}
I'd similar situation and i resolved in the following way:
public loading = true;
constructor(private router: Router) {
}
public onClick(): void {
this.loading = true;
this.router.navigate(['/test']).then(_ => {
this.loading = false;
});
}
I managed navigation in the programmatically way. I put loading variable to true before start navigation and i switch its value to false when routing is finished.

Categories

Resources