328
votes

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

18
thanks @Prashobh. Pass data using Query Parameters is what i was looking for. your link saved my day. - Raj
Angular 7.2 has now new feature to pass data between routes using state check the PR for more details. Some useful information here - AzizKap21
@Prashobh Thanks a lot. The link which you have shared is very useful - vandu

18 Answers

232
votes

update 4.0.0

See Angular docs for more details https://angular.io/guide/router#fetch-data-before-navigating

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 also https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#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 at https://github.com/angular/angular/issues/9757#issuecomment-229847781

78
votes

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

46
votes

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))
  }
}
35
votes
<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/

20
votes

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.

19
votes

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

15
votes

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;
      });
  }
12
votes

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'}
});

enter image description here

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);
}

enter image description here

And I got the desired data which was passed-

enter image description here

9
votes

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"]);
      });
  }

}
5
votes

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
}
3
votes

Pass using JSON

  <a routerLink = "/link"
   [queryParams] = "{parameterName: objectToPass| json }">
         sample Link                   
  </a>
3
votes

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']];
        }
    );
}
2
votes

say you have

  1. component1.ts
  2. 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
      <a
        mat-button
        [queryParams]="{ params: item | json}"
        routerLink="/meter-readings/{{item.meterReadingId}}"
        routerLinkActive="router-link-active">
        View
      </a>
    
      //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"}
       });
      }
    
2
votes

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.

0
votes

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.

0
votes

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)

0
votes

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.

0
votes

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