In one of my Angular 2 routes's templates (FirstComponent) I have a button
first.component.html
<div class="button" click="routeWithData()">Pass data and route</div>
My goal is to achieve:
Button click -> route to another component while preserving data and without using the other component as a directive.
This is what I tried...
1ST APPROACH
In the same view I am storing collecting same data based on user interaction.
first.component.ts
export class FirstComponent {
constructor(private _router: Router) { }
property1: number;
property2: string;
property3: TypeXY; // this a class, not a primitive type
// here some class methods set the properties above
// DOM events
routeWithData(){
// here route
}
}
Normally I'd route to SecondComponent by
this._router.navigate(['SecondComponent']);
eventually passing the data by
this._router.navigate(['SecondComponent', {p1: this.property1, p2: property2 }]);
whereas the definition of the link with parameters would be
@RouteConfig([
// ...
{ path: '/SecondComponent/:p1:p2', name: 'SecondComponent', component: SecondComponent}
)]
The issue with this approach is that I guess I can't pass complex data (e.g. an object like property3) in-url;
2ND APPROACH
An alternative would be including SecondComponent as directive in FirstComponent.
<SecondComponent [p3]="property3"></SecondComponent>
However I want to route to that component, not include it!
3RD APPROACH
The most viable solution I see here would be to use a Service (e.g. FirstComponentService) to
store the data (_firstComponentService.storeData()) on routeWithData() in FirstComponent
retrieve the data (_firstComponentService.retrieveData()) in ngOnInit() in SecondComponent
While this approach seems perfectly viable, I wonder whether this is the easiest / most elegant way to achieve the goal.
In general I'd like to know whether I'm missing other potential approaches to pass the data between components, particularly with the less possible amount of code
Pass data using Query Parameters
is what i was looking for. your link saved my day.
state
check the PR for more details. Some useful information here
Update 4.0.0
See Angular Angular Router - Fetch data before navigating for more details.
Original
Using a service is the way to go. In route params you should only pass data that you want to be reflected in the browser URL bar.
See Angular Angular Cookbook Component Communication - Bidirectional Service.
The router shipped with RC.4 re-introduces data
constructor(private route: ActivatedRoute) {}
const routes: RouterConfig = [
{path: '', redirectTo: '/heroes', pathMatch: 'full'},
{path: 'heroes', component: HeroDetailComponent, data: {some_data: 'some value'}}
];
class HeroDetailComponent {
ngOnInit() {
this.sub = this.route
.data
.subscribe(v => console.log(v));
}
ngOnDestroy() {
this.sub.unsubscribe();
}
}
See also the Plunker.
I think since we don't have $rootScope kind of thing in angular 2 as in angular 1.x. We can use angular 2 shared service/class while in ngOnDestroy pass data to service and after routing take the data from the service in ngOnInit function:
Here I am using DataService to share hero object:
import { Hero } from './hero';
export class DataService {
public hero: Hero;
}
Pass object from first page component:
ngOnDestroy() {
this.dataService.hero = this.hero;
}
Take object from second page component:
ngOnInit() {
this.hero = this.dataService.hero;
}
Here is an example: plunker
this.hero = this.dataService.hero
? Will I get the values?
Angular 7.2.0 introduced new way of passing the data when navigating between routed components:
@Component({
template: `<a (click)="navigateWithState()">Go</a>`,
})
export class AppComponent {
constructor(public router: Router) {}
navigateWithState() {
this.router.navigateByUrl('/123', { state: { hello: 'world' } });
}
}
Or:
@Component({
selector: 'my-app',
template: `
<a routerLink="/details" [state]="{ hello: 'world' }">Go</a>`,
})
export class AppComponent {}
To read the state, you can access window.history.state
property after the navigation has finished:
export class PageComponent implements OnInit {
state$: Observable<object>;
constructor(public activatedRoute: ActivatedRoute) {}
ngOnInit() {
this.state$ = this.activatedRoute.paramMap
.pipe(map(() => window.history.state))
}
}
window.history.state
returns something like {navigationId: 2}
instead of returning the object I passed-in.
<div class="button" click="routeWithData()">Pass data and route</div>
well the easiest way to do it in angular 6 or other versions I hope is to simply to define your path with the amount of data you want to pass
{path: 'detailView/:id', component: DetailedViewComponent}
as you can see from my routes definition, I have added the /:id
to stand to the data I want to pass to the component via router navigation. Therefore your code will look like
<a class="btn btn-white-view" [routerLink]="[ '/detailView',list.id]">view</a>
in order to read the id
on the component, just import ActivatedRoute
like
import { ActivatedRoute } from '@angular/router'
and on the ngOnInit
is where you retrieve the data
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
this.id = params['id'];
});
console.log(this.id);
}
you can read more in this article https://www.tektutorialshub.com/angular-passing-parameters-to-route/
I looked at every solution (and tried a few) from this page but I was not convinced that we have to kind of implement a hack-ish way to achieve the data transfer between route.
Another problem with simple history.state
is that if you are passing an instance of a particular class in the state
object, it will not be the instance while receiving it. But it will be a plain simple JavaScript object.
So in my Angular v10 (Ionic v5) application, I did this-
this.router.navigateByUrl('/authenticate/username', {
state: {user: new User(), foo: 'bar'}
});
https://i.stack.imgur.com/vIVig.png
And in the navigating component ('/authenticate/username'
), in ngOnInit()
method, I printed the data with this.router.getCurrentNavigation().extras.state
-
ngOnInit() {
console.log('>>authenticate-username:41:',
this.router.getCurrentNavigation().extras.state);
}
https://i.stack.imgur.com/bSx7S.png
And I got the desired data which was passed-
https://i.stack.imgur.com/JGWc6.png
extras
? is that something you just defined or an angular property?
state
(after routing to the new page) only worked on the constructor
for me, and not inside the ngOnInit
. That's because the getCurrentNavigation()
was null.
It is 2019 and many of the answers here would work, depending on what you want to do. If you want to pass in some internal state not visible in URL (params, query) you can use state
since 7.2 (as I have learned just today :) ).
From the blog (credits Tomasz Kula) - you navigate to route....
...from ts: this.router.navigateByUrl('/details', { state: { hello: 'world' } });
...from HTML template: <a routerLink="/details" [state]="{ hello: 'world' }">Go</a>
And to pick it up in the target component:
constructor(public activatedRoute: ActivatedRoute) {}
ngOnInit() {
this.state$ = this.activatedRoute.paramMap
.pipe(map(() => window.history.state))
}
Late, but hope this helps someone with recent Angular.
state
lost when user refreshes? It would be fun to be able to persist it natively.
I this the other approach not good for this issue. I thing the best approach is Query-Parameter by Router
angular that have 2 way:
Passing query parameter directly
With this code you can navigate to url
by params
in your html code:
<a [routerLink]="['customer-service']" [queryParams]="{ serviceId: 99 }"></a>
Passing query parameter by Router
You have to inject the router within your constructor
like:
constructor(private router:Router){
}
Now use of that like:
goToPage(pageNum) {
this.router.navigate(['/product-list'], { queryParams: { serviceId: serviceId} });
}
Now if you want to read from Router
in another Component
you have to use of ActivatedRoute
like:
constructor(private activateRouter:ActivatedRouter){
}
and subscribe
that:
ngOnInit() {
this.sub = this.route
.queryParams
.subscribe(params => {
// Defaults to 0 if no query param provided.
this.page = +params['serviceId'] || 0;
});
}
Some super smart person (tmburnell) that is not me suggests re-writing the route data:
let route = this.router.config.find(r => r.path === '/path');
route.data = { entity: 'entity' };
this.router.navigateByUrl('/path');
As seen here in the comments.
I hope someone will find this useful
Solution with ActiveRoute (if you want pass object by route - use JSON.stringfy/JSON.parse):
Prepare object before sending:
export class AdminUserListComponent {
users : User[];
constructor( private router : Router) { }
modifyUser(i) {
let navigationExtras: NavigationExtras = {
queryParams: {
"user": JSON.stringify(this.users[i])
}
};
this.router.navigate(["admin/user/edit"], navigationExtras);
}
}
Receive your object in destination component:
export class AdminUserEditComponent {
userWithRole: UserWithRole;
constructor( private route: ActivatedRoute) {}
ngOnInit(): void {
super.ngOnInit();
this.route.queryParams.subscribe(params => {
this.userWithRole.user = JSON.parse(params["user"]);
});
}
}
super.ngOnInit();
for?
Routes:
{ path: 'foo-route', component: FooComponent, data: { myData: false } },
In component access the data object once:
pipe(take(1))
unsubsrcibes immediately so there is no memory leak and no need to manually unsubscribe
constructor(private activatedRoute: ActivatedRoute) { ... }
ngOnInit(): void {
this.activatedRoute.data.pipe(take(1)).subscribe((data) => {
console.log(data); // do something with the data
});
}
remember to import needed stuff
Edit: the new firstValueFrom()
could be better
3rd approach is most common way to share data between components. you may inject the item service which you want to use in related component.
import { Injectable } from '@angular/core';
import { Predicate } from '../interfaces'
import * as _ from 'lodash';
@Injectable()
export class ItemsService {
constructor() { }
removeItemFromArray<T>(array: Array<T>, item: any) {
_.remove(array, function (current) {
//console.log(current);
return JSON.stringify(current) === JSON.stringify(item);
});
}
removeItems<T>(array: Array<T>, predicate: Predicate<T>) {
_.remove(array, predicate);
}
setItem<T>(array: Array<T>, predicate: Predicate<T>, item: T) {
var _oldItem = _.find(array, predicate);
if(_oldItem){
var index = _.indexOf(array, _oldItem);
array.splice(index, 1, item);
} else {
array.push(item);
}
}
addItemToStart<T>(array: Array<T>, item: any) {
array.splice(0, 0, item);
}
getPropertyValues<T, R>(array: Array<T>, property : string) : R
{
var result = _.map(array, property);
return <R><any>result;
}
getSerialized<T>(arg: any): T {
return <T>JSON.parse(JSON.stringify(arg));
}
}
export interface Predicate<T> {
(item: T): boolean
}
Pass using JSON
<a routerLink = "/link"
[queryParams] = "{parameterName: objectToPass| json }">
sample Link
</a>
use a shared service to store data with a custom index. then send that custom index with queryParam. this approach is more flexible.
// component-a : typeScript :
constructor( private DataCollector: DataCollectorService ) {}
ngOnInit() {
this.DataCollector['someDataIndex'] = data;
}
// component-a : html :
<a routerLink="/target-page"
[queryParams]="{index: 'someDataIndex'}"></a>
.
// component-b : typeScript :
public data;
constructor( private DataCollector: DataCollectorService ) {}
ngOnInit() {
this.route.queryParams.subscribe(
(queryParams: Params) => {
this.data = this.DataCollector[queryParams['index']];
}
);
}
say you have
component1.ts component1.html
and you want to pass data to component2.ts.
in component1.ts is a variable with data say //component1.ts item={name:"Nelson", bankAccount:"1 million dollars"} //component1.html //the line routerLink="/meter-readings/{{item.meterReadingId}}" has nothing to //do with this , replace that with the url you are navigating to View //component2.ts import { ActivatedRoute} from "@angular/router"; import 'rxjs/add/operator/filter'; /*class name etc and class boiler plate */ data:any //will hold our final object that we passed constructor( private route: ActivatedRoute, ) {} ngOnInit() { this.route.queryParams .filter(params => params.reading) .subscribe(params => { console.log(params); // DATA WILL BE A JSON STRING- WE PARSE TO GET BACK OUR //OBJECT this.data = JSON.parse(params.item) ; console.log(this.data,'PASSED DATA'); //Gives {name:"Nelson", bankAccount:"1 //million dollars"} }); }
You can use BehaviorSubject for sharing data between routed components. A BehaviorSubject holds one value. When it is subscribed it emits the value immediately. A Subject doesn't hold a value.
In the service.
@Injectable({
providedIn: 'root'
})
export class CustomerReportService extends BaseService {
reportFilter = new BehaviorSubject<ReportFilterVM>(null);
constructor(private httpClient: HttpClient) { super(); }
getCustomerBalanceDetails(reportFilter: ReportFilterVM): Observable<Array<CustomerBalanceDetailVM>> {
return this.httpClient.post<Array<CustomerBalanceDetailVM>>(this.apiBaseURL + 'CustomerReport/CustomerBalanceDetail', reportFilter);
}
}
In the component you can subscribe to this BehaviorSubject.
this.reportService.reportFilter.subscribe(f => {
if (f) {
this.reportFilter = f;
}
});
Note: Subject won't work here, Need to use Behavior Subject only.
By default i won't use a guard for this one for me it is more can i enter the route or can i leave it. It is not to share data betweenn them.
If you want to load data before we entered a route just add an resolver to this one this is also part of the Router.
As very basic example:
Resolver
import { Resolve, ActivatedRoute } from "@angular/router";
import { Observable } from "rxjs";
import { Injectable } from "@angular/core";
import { take } from "rxjs/operators";
@Injectable()
export class UserResolver implements Resolve<User> {
constructor(
private userService: UserService,
private route: ActivatedRoute
) {}
resolve(): Observable<firebase.User> {
return this.route.params.pipe(
switchMap((params) => this.userService.fetchUser(params.user_id)),
take(1)
);
}
}
put to the router:
RouterModule.forChild([
{
path: "user/:user_id",
component: MyUserDetailPage,
resolve: {
user: UserResolver
}
}
}]
get the data in our component
ngOnInit() {
const user: firebase.User = this.activatedRoute.snapshot.data.user;
}
The downside on this approach is, he will enter the route first if he get the user data not before, this ensures the data for the user has been loaded and is ready on start of the component, but you will stay on the old page as long the data has been loaded (Loading Animation)
One fine solution is to implement a Guard with canActivate method. In this scenario you can fetch data from a given api and let user access the component describe in the routing file. In the meantime one can set the data property of the route object and retrieve it in the component.
Let say you have this routing conf:
const routes: Routes = [
{ path: "/:projectName", component: ProjectComponent, canActivate: [ProjectGuard] }
]`
in your guard file you may have:
canActivate(next: ActivatedRouteSnapshot,state: RouterStateSnapshot)
: Observable<boolean> | Promise<boolean> | boolean {
return this.myProjectService.getProject(projectNameFoundElsewhere).pipe(
map((project) => {
if (project) {
next.data = project;
}
return !!project;
}),
);
}`
Then in your component
constructor(private route: ActivatedRoute) {
this.route.data.subscribe((value) => (this.project = value));
}
This way is a bit different than passing via a service since service keep the value in a behaviorSubject as long as it is not unset. Passing via tha guard make the data available for the current route. I havent check if the children routes keep the data or not.
In scenario where data needs to be passed to another Route, best and simplest solution is using { window.localStorage }. Also, do not remember to remove data from local storage once its use is over. I used ngOnDestroy's destroy() method to clean up this data. This also resolves a problem where data is lost by page refresh.
If you have a formula collection which handles a couple of ng components which are basely build on a collection / array of class objects hold approx. 10 props e.g. include input values, nominal value and at least units and Booleans …, so to keep the page status (input+results) ends into duplicate a lot of stuff.
Therefore, I simulate a routing by using *ngif to display the related parts (component s) of the single page but never change the url.
<div *ngIf="visibleComponentA>
... All part of ComponetA
></div>
CpmponetA.html
<div *ngIf="visibleComponentB>
... All part of ComponetB
></div>
CpmponetB.html
This Boolean will be set inside the relate code of the component:
@Input()visibleComponentA: boolean = true;
ComponetA.ts
Now in the top page
<div (click)="OnClickNav(visibleComponentA)" >ComponentA</div>
<div (click)="OnClickNav(visibleComponentB)" >ComponentB</div>
app.component.html
and the method OnClickNav(Selected:NavFlags) switching the correct visible status of the component.
OnClickNav(Selected:NavFlags){
Selected.NavStatus=!Selected.NavStatus
Selected.NavItem=='visibleComponetA'? this.visibleComponetA.NavStatus=Selected.NavStatus: this.visibleComponetA.NavStatus= false;
Selected.NavItem=='visibleComponetB'? this.visibleComponetB.NavStatus=Selected.NavStatus: this.visibleComponetB.NavStatus= false;
app.commonet.ts
The class NavFlags is simple
export class NavFlags {
NavItem: string = '';
NavStatus: boolean = false;
constructor(NavItem: string, NavStatus: boolean) {
this.NavItem = NavItem;
this.NavStatus = NavStatus;
}
}
nav-flags.ts
By this the "individual" pages will not leave an no data are lost. I have no duplicated store. The complete example can be visit on https://angulartool.de. By clicking the button, it is possible to navigate through the page in components without loss of data.
This hack is not perfect, so maybe there will be better way to solve this angular matter.
Success story sharing
ngOnInit() { this.myVar = this.route.snapshot.data['some_data']; }
NavigationExtras
- stackoverflow.com/a/54879389/1148107