How to change data in one component relatively to others on route event in Angular?
For e.g. if I have three components: "nav.component", "about.component" and "service.component".
So I want to display different text in "nav.component" when I switch between about and service pages in my app.
My "app.router.ts" file:
import { ModuleWithProviders } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import { AppComponent } from './app.component';
import { AboutComponent } from './about/about.component';
import { ServiceComponent } from './service/service.component';
export const router: Routes = [
{ path: '', redirectTo: 'about', pathMatch: 'full' },
{ path: 'about', component: AboutComponent },
{ path: 'service', component: ServiceComponent }
];
export const routes: ModuleWithProviders = RouterModule.forRoot(router);
I don't want to display just page name text in my nav bar while switching between these pages, it would be a custom text for each component.
Also I would like to store this data/text directly in "about.component.ts" and "service.component.ts" but not in the "app.router.ts" due to maintainability and scalability.
Is it possible?
U.P.D.
This is my "app.component.html" file:
<div class="container">
<!-- Nav Bar (text changes here) -->
<app-nav></app-nav>
<!-- Pages (components which are included in app.router.ts) -->
<router-outlet></router-outlet>
</div>
For e.g. this is "about.component.ts" file:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-about',
templateUrl: './about.component.html',
styleUrls: ['./about.component.scss']
})
export class AboutComponent implements OnInit {
const text_for_nav_bar = "This is my new About page."; // <-- text that should be displayed in nav component for this page on router event.
constructor() { }
ngOnInit() {
}
}
Using below code you will able to subscribe router change events. You need to add this code on nav bar.
Import router and Navigation start
import { Router, ActivatedRoute, NavigationStart } from '#angular/router';
import "rxjs/add/operator/filter";
import "rxjs/add/operator/pairwise";
Add below code in constrictor.
this.router.events
.filter(event => event instanceof NavigationStart)
.pairwise()
.subscribe((value: [NavigationStart, NavigationStart]) => {
let nextUrl = value[1].url;
if (nextUrl == '/about') {
// your code here for next url
}
},
(err) => {
},
() => { });
}
});
One way would be with *ngIf (or [hidden] if you want to load all contents to the DOM at once). And to catch current route, inject Router module:
class NavComponent {
constructor(private router: Router){
}
}
and in nav.component.html:
<div *ngIf="router.url === '/some/route'">
text for this route
</div>
<div *ngIf="router.url === '/other/route'">
text for other route
</div>
Doing same in component.ts, could be:
nav.component.html:
<h1>{{yourText}}</h1>
component.ts:
ngOnInit() {
if(this.router.url == '/some/route') {
yourText = 'Text'
} elseif(this.router.url == '/other/route') {
yourText = 'Other text'
}
}
You should use router datas
export const router: Routes = [
{ path: '', redirectTo: 'about', pathMatch: 'full' },
{ path: 'about', component: AboutComponent, data: {navigationText: 'Some text'} },
{ path: 'service', component: ServiceComponent, data: {navigationText: 'Some other text'} }
];
and in app.component.html
<div class="container">
<app-nav text="outlet.activatedRouteData.navigationText"></app-nav>
<router-outlet #outlet="outlet"></router-outlet>
</div>
Of course you need to add a "#Input text: string" property in nav.component.ts
Related
I am working on a navigation system for an Angular 14 app.
In app-routing.module.ts I have:
import { NgModule } from '#angular/core';
import { RouterModule, Routes } from '#angular/router';
import { AboutComponent } from './components/pages/about/about.component';
import { TermsComponent } from './components/pages/terms/terms.component';
const routes: Routes = [
{ path: '', component: AboutComponent },
{ path: 'about', component: AboutComponent },
{ path: 'terms', component: TermsComponent },
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
In navbar.component.ts I have:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-navbar',
templateUrl: './navbar.component.html',
styleUrls: ['./navbar.component.css'],
})
export class NavbarComponent implements OnInit {
constructor() {}
public menuItems: any[] = [
{
route: '',
text: 'Home',
},
{
route: '/about',
text: 'About',
},
{
route: '/terms',
text: 'Terms and Conditions',
},
];
ngOnInit() {}
}
In navbar.component.html I have:
<nav>
Logo
<ul class="navigation">
<li *ngFor="let item of menuItems">
<a [routerLink]="[item.route]" [routerLinkActive]="'active'">
{{ item.text }}
</a>
</li>
</ul>
</nav>
There is a Stackblitz HERE with all the code.
The goal
The goal is to be able to choose which of the pages (components) should reload upon clicking its corresponding router link, even if the component is already loaded.
For example, if I want (only) the "Terms and Conditions" to always reload, I would have:
public menuItems: any[] = [
{
route: '',
text: 'Home',
reload: false
},
{
route: '/about',
text: 'About',
reload: false
},
{
route: '/terms',
text: 'Terms and Conditions',
reload: true
},
];
EDIT
Here is a solution that works but is redundant and it lacks flexibility:
In navbar.component.html I have added a reloadComponent()method:
public menuItems: any[] = [
{
route: '',
text: 'Home',
reload: false
},
{
route: '/about',
text: 'About',
reload: false
},
{
route: '/terms',
text: 'Terms and Conditions',
reload: true
},
];
public reloadComponent(item: any){
if (item.reload && window.location.pathname == '/terms' ) {
window.location.reload();
}
}
In navbar.component.html I have added:
<li *ngFor="let item of menuItems">
<a [routerLink]="[item.route]" [routerLinkActive]="'active'" (click)="reloadComponent(item)">
{{ item.text }}
</a>
</li>
I consider this solution, to be honest, dumb. It's only quality its that it works, for the particular case it is needed for.
I wish there was a way that I could reload the component that corresponds to the clicked menu item and not the current page.
Questions
How can I achieve this goal?
Is there a flexible and "elegant" solution to this issue?
Add this in your router config
imports: [RouterModule.forRoot(routes, {
onSameUrlNavigation: 'reload'
})],
Along with this you will have to disable click with condition reload true/false.
Docs link: https://angular.io/api/router/OnSameUrlNavigation
You should implement RouteReuseStrategy, which allows you to control the reusability of the components, and you can choose which components should be cached and reused when the user navigates to a route they have already visited.
import { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '#angular/router';
export class CustomRouteReuseStrategy implements RouteReuseStrategy {
private storedRouteHandles = new Map<string, DetachedRouteHandle>();
shouldReuseRoute(current: ActivatedRouteSnapshot, next: ActivatedRouteSnapshot): boolean {
return true;
}
store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
this.storedRouteHandles.set(route.routeConfig.path, handle);
}
retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
return this.storedRouteHandles.get(route.routeConfig.path);
}
shouldAttach(route: ActivatedRouteSnapshot): boolean {
return this.storedRouteHandles.has(route.routeConfig.path);
}
shouldDetach(route: ActivatedRouteSnapshot): boolean {
return true;
}
deleteRouteSnapshot(path: string): void {
this.storedRouteHandles.delete(path);
}
}
You need to register it into app module:
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { RouterModule, RouteReuseStrategy } from '#angular/router';
import { AppComponent } from './app.component';
import { CustomRouteReuseStrategy } from './custom-route-reuse-strategy';
#NgModule({
imports: [
BrowserModule,
RouterModule.forRoot([
// ...
])
],
declarations: [AppComponent],
providers: [
{ provide: RouteReuseStrategy, useClass: CustomRouteReuseStrategy }
],
bootstrap: [AppComponent]
})
export class AppModule { }
In the ActivatedRouteSnapshot you should have access to that custom route information:"reload: true":
shouldReuseRoute(current: ActivatedRouteSnapshot, next: ActivatedRouteSnapshot): boolean {
// Check if the custom parameter "myParam" exists in the route's data.
const myParam = current.data && current.data['myParam'];
// If "myParam" is set to "reuse", reuse the current route.
if (myParam === 'reuse') {
return true;
}
// Otherwise, create a new component.
return false;
}
Maybe you could set state data to your route, and in it's components you could validate if the route state snapshot has the flag you refresh the page.
Example: When you render your menu, you could do something like this:
navbar.component.html
<nav>
Logo
<ul class="navigation">
<li *ngFor="let item of menuItems">
<a [routerLink]="[item.route]" [routerLinkActive]="'active'" [state]="{ reload:item.reload}">
{{ item.text }}
</a>
</li>
</ul>
</nav>
And on each component belonging to your route, do something like this.
constructor(private router: Router) {}
ngOnInit() {
boolean shouldPageReload = this.router.getCurrentNavigation()?.extras?.state["reload"]
if (shouldPageReload) {
// reload your component
}
}
For more information please check this article about passing data to your routes
https://www.tektutorialshub.com/angular/angular-pass-data-to-route/
I need a way to update a variable in my header component (always displayed) whenever a param changes in the URL.
My routing:
const routes: Routes = [
{
path: '',
component: DefaultLayoutComponent,
data: {
title: 'Home'
},
children: [
{
path: 'home',
component: HomeComponent
},
{
path: 'dashboard/:id',
component: DashboardComponent,
},
]
},
{path: '**', redirectTo: 'home'}
];
The dashboard component has an ID attached to it and this is what I need to track. In the event a user goes to a page without an ID (such as the home route) I want the ID to show as 0.
I have this working to where it will track the ID as long as they continue to hit routes with an ID but as soon as they navigate to home all tracking is stopped.
header.component
import { Component, Input } from '#angular/core';
import { ClassToggleService, HeaderComponent } from '#coreui/angular-pro';
import { ActivatedRoute } from '#angular/router';
import { Subscription } from 'rxjs';
#Component({
selector: 'app-default-header',
templateUrl: './default-header.component.html',
})
export class DefaultHeaderComponent extends HeaderComponent {
private issuer: Subscription;
constructor(private classToggler: ClassToggleService,
private route: ActivatedRoute) {
super();
}
issuerID: number;
ngOnInit(): void {
this.route.children.forEach(child => {
child.params.subscribe(params => {
const id = params['id'] || 0
console.log('id ' + id)
this.issuerID = id
})
})
}
//To prevent memory leak
ngOnDestroy(): void {
if (this.issuer)
this.issuer.unsubscribe()
}
}
I am using the same component for my router, on the first click the component affected, but on the next click the component still in the first state.
Here is the script for changing the route
<a [routerLink]="['react/1']">link 1</a>
<a [routerLink]="['react/2']">link 2</a>
Here is my router module
panel-routing.module.ts
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router'
import { PanelCoursesComponent } from 'src/app/components/panel-courses/panel-courses.component';
import { PanelHomeComponent } from 'src/app/components/panel-home/panel-home.component';
import { PanelIntroComponent } from 'src/app/components/panel-intro/panel-intro.component';
const routes: Routes = [
{ path: '', component: PanelHomeComponent },
{ path: 'react', component: PanelIntroComponent },
{ path: 'react/:no', component: PanelCoursesComponent } //the target
]
#NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PanelRoutingModule { }
panel-course.component.ts
import { Component, OnInit } from '#angular/core';
import { Router, ActivatedRoute } from '#angular/router'
#Component({
selector: 'app-panel-courses',
templateUrl: './panel-courses.component.html',
styleUrls: ['./panel-courses.component.scss']
})
export class PanelCoursesComponent implements OnInit {
url!: any
constructor(private route: ActivatedRoute, private router: Router) {
console.log('route')
}
ngOnInit(): void {
this.url = this.router.url
console.log(this.route.snapshot.params) //the test script
}
}
On the PanelCourseComponent I try to console log the params, but that's only executed one time on the first click.
Am I missing something?
You can use this.route.params.subscribe method for this case
Here is the example
ngOnInit(): void {
this.route.params.subscribe(params => {
console.log(params) // It will be executed whenever you click the link
})
}
by default pathMatch is set to 'prefix'. so paths will be matched against your current location and the first one witch "matches" will render its component. to make your paths match only "exact" match add pathMatch: 'full' for your routes
const routes: Routes = [
{ path: '', component: PanelHomeComponent, pathMatch: 'full' },
{ path: 'react', component: PanelIntroComponent, pathMatch: 'full' },
{ path: 'react/:no', component: PanelCoursesComponent } //the target
]
In my Angular 10 application I have a route like this
http://localhost:4200/employee/enrollments?number=189930097&city=Chicago
Sometimes the URL is being encoded as
http://localhost:4200/employee/enrollments%3Fnumber%3D189930097%26city=Chicago
and router fails to find a match. Is there a way to fix this decoding issue and make it resolve always?
Update:
I added my footer component in which I am using routerLink that updates the current URL
EmployeeRoutingModule:
export const enrollmentManagementRoutes: Routes = [
{
path: 'enrollments',
component: EnrollmentSearchComponent,
canActivate: [EmployeeAuthGuardService],
}
];
#NgModule(
{
imports: [
RouterModule.forChild(enrollmentManagementRoutes)
],
exports: [
RouterModule
]
})
export class EmployeeRoutingModule
{
}
FooterComponent
import {Component, OnInit} from '#angular/core';
import {NavigationEnd, Router} from '#angular/router';
import {environment} from '../../../environments/environment';
#Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss']
})
export class FooterComponent implements OnInit
{
appVersion: any;
currentUrl='/';
constructor(private router: Router)
{
}
ngOnInit()
{
this.appVersion = environment.VERSION;
//Update Need Assistance link URL, this prevents default URL being '/'
this.router.events.subscribe(data=>
{
if(data instanceof NavigationEnd)
{
this.currentUrl=data.url+'';
}
});
}
navigateByUrl()
{
this.router.navigateByUrl(this.currentUrl);
}
}
Footer Component HTML:
<a class=" col-sm-12 col-xs-12 col-md-auto request-help-link" id="request-help-link" rel="noopener noreferrer"
[routerLink]="currentUrl" style="font-size: 20px" >
Need Assistance? Click here
</a>
Would it not make more sense to use a route parameter than a query string?
export const enrollmentManagementRoutes: Routes = [
{
path: 'enrollments:number',
component: EnrollmentSearchComponent,
canActivate: [EmployeeAuthGuardService],
}
];
and then route to http://localhost:4200/employee/enrollments/189930097
and in your component you can use the ActivatedRoute service to get the param.
https://angular.io/api/router/ActivatedRoute
Have you tried to use something like custom serializer?
serializer.ts
export class CustomUrlSerializer implements UrlSerializer {
parse(url: any): UrlTree {
const dus = new DefaultUrlSerializer();
return dus.parse(url);
}
serialize(tree: UrlTree): any {
const dus = new DefaultUrlSerializer();
const path = dus.serialize(tree);
// use your regex to replace as per your requirement.
path.replace(/%3F/g, '?');
path.replace(/%3D/g, '=');
return path;
}
}
and then in App module
...
providers: [
{provide: UrlSerializer, useClass: CustomUrlSerializer}
],
...
Yesterday I asked a question about an another specific thing of angular 2 routing and the answer was satisfying for me Angular 2 — navigate through web pages without reloading a component that is common for those pages . But when I got back to examining these things, I encountered a problem again. Here's the new version of the app: http://ivan-khludov.com/ . What if I want the pages of the private section to have a shared component (a counter in my example), don't reload it each time I navigate withing the section and at the same time display different components at different pages - the dashboard component at private/dashboard and the inbox component at private/inbox? Is it possible to do without reloading the counter and without storing the last value of the counter in memory? This is the entry point of the application and the root module:
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { HttpModule } from '#angular/http';
import { RouterModule } from '#angular/router';
import { BrowserModule } from '#angular/platform-browser';
import { platformBrowserDynamic } from '#angular/platform-browser-dynamic';
import { ROUTES } from './routes';
import { AppWrapper } from './components/app-wrapper';
import { PublicSection } from './components/public';
import { PrivateSection } from './components/private';
import { Counter } from './components/counter';
import { Dashboard } from './components/private/dashboard';
import { Inbox } from './components/private/inbox';
#NgModule({
imports: [
BrowserModule,
CommonModule,
HttpModule,
RouterModule.forRoot(ROUTES)
],
declarations: [
AppWrapper,
PublicSection,
PrivateSection,
Counter,
Dashboard,
Inbox
],
providers: [
],
bootstrap: [
AppWrapper
]
})
class RootModule {}
platformBrowserDynamic().bootstrapModule(RootModule);
Routing:
import { Routes } from '#angular/router';
import { AppWrapper } from '../components/app-wrapper';
import { PublicSection } from '../components/public';
import { PrivateSection } from '../components/private';
export const ROUTES: Routes = [
{
path: '',
redirectTo: '/public/1',
pathMatch: 'full'
},
{
path: 'section-1',
redirectTo: '/public/1',
pathMatch: 'full'
},
{
path: 'public/:page',
component: PublicSection
},
{
path: 'private',
redirectTo: '/private/dashboard',
pathMatch: 'full'
},
{
path: 'private/:page',
component: PrivateSection
}
];
The private section component:
import { Component } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
#Component({
selector: 'private',
template: `
<h2>Private section — {{page}}</h2>
<counter></counter>
<dashboard></dashboard>
<inbox></inbox>
`
})
export class PrivateSection {
private page: string;
private sub: any;
constructor(
private route: ActivatedRoute
) {
}
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
this.page = params['page'];
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
The dashboard component:
import { Component } from '#angular/core';
#Component({
selector: 'dashboard',
template: `
<div>dashboard text: lorem ipsum</div>
`
})
export class Dashboard {
}
The inbox component:
import { Component } from '#angular/core';
#Component({
selector: 'inbox',
template: `
<div>inbox text: dolor sit amet</div>
`
})
export class Inbox {
}
Thanks in advance for your answers.