Angular 2.0.2: ActivatedRoute is empty in a Service
Asked Answered
S

5

48

I want to use ActivatedRoute to get route params in a service like I would do in a Component. However, when I inject the ActivatedRoute object in a Service it contains an empty params variable

I've created a plunker that reproduces the behaviour: http://plnkr.co/edit/sckmDYnpIlZUbqqB3gli

Note that the intention is to use the parameter in the service and not in the component, the way the plunker is set up is purely to demonstrate the issue.

Component (test is retrieved):

export class Component implements OnInit {
  result: string;

  constructor(private route: ActivatedRoute) {
  }

  ngOnInit() {
    this.route.params.subscribe(params => {
      this.result = params['test'];
    });
  }
}

Service (test is not retrieved):

export class Service {
  result: string;

  constructor(private route: ActivatedRoute) {
    this.getData();
  }

  getData() {
    this.route.params.subscribe(params => {
      this.result = params['test'];
    });
  }
}
Sept answered 11/10, 2016 at 13:6 Comment(3)
I'm not sure that it is a good idea to inject route into a service. Service is a singleton. ActivatedRoute is not. I guess that ActivatedRoute instances are just different in Component and Service.Pellitory
@estus Makes sense, I could pass the param to the service via a function. But this seems clunky.Sept
I used an rxjs ReplaySubject to store/observe the required values. Felt like a simple solution at least for me: https://mcmap.net/q/371796/-how-can-a-service-subscribe-to-the-parammap-of-the-current-routeLanding
P
45

Service here is a singleton that belongs to root injector and is injected with root ActivatedRoute instance.

Outlets get their own injector and own ActivatedRoute instance.

The solution here is to let route components have their own Service instances:

@Component({
  ...
  providers: [Service]
})
export class MainComponent { ... }
Pellitory answered 11/10, 2016 at 14:53 Comment(4)
This is definitely a solution but creating multiple singleton services has its downsides. For example, if this service performs a http request and then caches the result it would do so for every service instanceSept
Then they should be two different services, I guess. The one is singleton, another one is specific to current route. The whole pattern was pretty much the same in Angular 1. If you have some real-world design issue, feel free to update the question with the context.Pellitory
Well, you solved the issue I described here so I'll keep it as it is. I'll figure out if splitting the service up will do the trick and create a new issue when it does not. Many thanks for your helpSept
Creating a singleton is also not possible if the sole purpose of the service is to keep specific state across multiple components (using the url as the single source of truth).Registrant
S
19

The answer of estus provides a good solution when multiple service instances are not an issue.

The following solution gets a parameter straight from the router and allows the service to have one instance:

Update

export class Service {
  url: string = this._router.url;

  constructor(private _router: Router) {}
}

or as an observable:

export class Service {
  private _url$ = this._router.events.pipe(
    startWith(new NavigationEnd(0, '', '')),
    map((event) => {
      return event instanceof NavigationEnd ? location.pathname : undefined;
    }),
    distinctUntilChanged()
  );

  constructor(private _router: Router) {}
}

Before Angular 2.0.0

export class Service {
  result: string;

  constructor(private router: Router) {
    this.result = router.routerState.snapshot.root.children[0].url[index].path
  }
}

or as an observable:

export class Service {
  result: string;

  constructor(private router: Router) {
    this.router.routerState.root.children[0].url.map((url) => {
      this.result = url[index].path;
    });
  }
}

alternatively, when routerState is not available:

export class Service {
  result: string;

  constructor(private router: Router) {
    this.router.parseUrl(this.router.url).root.children.primary.segments[index].toString();
  }
}

Index is the position of the param in the url.

Sept answered 12/10, 2016 at 17:16 Comment(1)
Is there a way to unit test the service with RouterTestingModule when using this approach? router.routerState.snapshot.root.children is empty by default in the injected Router.Etienne
C
2

I came across this issue and the working solution I end with is the following.

@Component({
  selector: 'app-sample',
  styleUrls: [`../sample.component.scss`],
  templateUrl: './sample.component.html',
})
export class AppSampleComponent {
  constructor(private route: ActivatedRoute,
              private someService: SomeService){}
  public callServiceAndProcessRoute(): void {
    this.someService.processRoute(route);
  }
}

@Injectable()
export class SomeService {
  public processRoute(route: ActivatedRoute): void {
  // do stuff with route
  }
}

So you will pass the ActivatedRoute to the service as a param.

Commentate answered 5/2, 2019 at 3:47 Comment(0)
I
1

I found the following code from the Angular documentation (as of 2022) which suggests to use the following code in a service:

You can compute all params (or data) in the router state or to get params outside of an activated component by traversing the RouterState tree as in the following example:

collectRouteParams(router: Router) {
    let params = {};
    let stack: ActivatedRouteSnapshot[] = [router.routerState.snapshot.root];
    while (stack.length > 0) {
        const route = stack.pop()!;
        params = {...params, ...route.params};
        stack.push(...route.children);
    }
    return params;
}

See also the discussion here and related issues: https://github.com/angular/angular/pull/40306

Isochronal answered 25/12, 2022 at 18:31 Comment(1)
"outside of an activated component" and therefore suitable for a service... 👍Variorum
D
-3

I can't see a routerLink directive in your Plunker code you provided.

You need to use a routerLink directive to navigate to your component, where you supply the param, then only you can retrieve it. For example:

<a routerLink="/myparam" >Click here to go to main component</a>
Deprecatory answered 11/10, 2016 at 14:17 Comment(1)
The plunker redirects to /success which is then retrieved, there is no need for an extra navigationSept

© 2022 - 2024 — McMap. All rights reserved.