10

In Angular 2, you can specify a @CanActivate annotation for a component where you can determine if the component should be activated or not. The reason it's not an interface is because the callback is called before the component is even instantiated. The problem is, I can't figure out a way to get dependencies injected into that callback. And I need my service that tells me whether I'm logged in or not (and as whom) to determine whether routing to a particular component is allowed or not.

How can I inject a dependency into a @CanActivate callback? I'm using ES5, and this doesn't work:

app.ListsComponent = ng.router.CanActivate([
    app.SessionService,
    function(session, instruction) {
        console.log(session);
        return true;
    }
])(app.ListsComponent);

Edit: Alternatively, I can use the routerOnActivate lifecycle event on the component, and use this.router.navigate to send the user away if they're not supposed to be there. The downside there is that it breaks browser history: If I redirect you asynchronously every time you arrive at a particular URL, you can't use the Back button in your browser very usefully. Is there a way to have router.navigate use history.replaceState instead of history.pushState for this kind of situation?

Ben Dilts
  • 10,535
  • 16
  • 54
  • 85
  • DI should be possible once issue [Router hooks are injectable and consistent with core](https://github.com/angular/angular/issues/7485) is resolved. Milestone is currently Angular 2 Release Candidate. – Philip Bulley Mar 25 '16 at 18:11

5 Answers5

8

Most solutions here will present problems with loading sub-dependencies from elsewhere in the hierarchy, because they create a new injector. Also, this results in additional (non-shared) services being instanced. I recommend following the pattern provided by Brandon in https://github.com/angular/angular/issues/4112

He references this Plunk: http://plnkr.co/edit/SF8gsYN1SvmUbkosHjqQ?p=preview

Its main idea is a singleton injector, which he saves when the app initializes. This provides access to the root dependencies you already have configured, and further allows your services to be shared as a singleton as they were probably intended:

import {Injector} from 'angular2/angular2';
let appInjectorRef: Injector;

export const appInjector = (injector?: Injector):Injector => {
    if (injector) {
      appInjectorRef = injector;
    }

    return appInjectorRef;
};

bootstrap([ServiceYouNeed]).then((appRef) => {
    // store a reference to the injector
    appInjector(appRef.injector);
});
shannon
  • 8,664
  • 5
  • 44
  • 74
2

You have to inject it using injector. Just a quick copy paste from a project I'm doing:

@CanActivate((next: ComponentInstruction, prev: ComponentInstruction) => {

  console.info('CanActivate hook! - can return boolean or Promise');

  var injector = Injector.resolveAndCreate([HTTP_PROVIDERS, YourService]);
  var yourService = injector.get(YourService);
  // do something with yourService
  return new Promise(function(resolve, reject) {
      resolve(true);
  });

})

HTTP_PROVIDERS you have to pass along if your service is for example injecting the HTTP service in the constructor.

I use it to put an observable on the params of the next object. And the next object is your next "Component/state":

@CanActivate((next: ComponentInstruction, prev: ComponentInstruction) => {

  console.info('properties component CanActivate hook! - can return boolean or Promise');
  var injector = Injector.resolveAndCreate([HTTP_PROVIDERS, PropertiesService]);
  var propertiesService = injector.get(PropertiesService);
  return new Promise(function(resolve, reject) {
      next.params['properties'] = propertiesService.getProperties();
      resolve(true);
  });

})

The PropertiesService calls a backend and returns an Observable that represents the data with properties

Sam Vloeberghs
  • 1,082
  • 5
  • 18
  • 29
  • This way you get a new instance of `YourService` don't you. This might not be desired. A workaround would be to store the injector form your `AppComponent` using a static field and then using this injector in `@CanActivate()` instead creating a new one. – Günter Zöchbauer Jan 12 '16 at 10:34
  • you mean implement the service as a singleton? – Sam Vloeberghs Jan 12 '16 at 10:35
  • in my case I'm thinking about if that really matters.. I don't store data in the service. It just provides a way to call a backend. What do you think? – Sam Vloeberghs Jan 12 '16 at 10:36
  • 1
    Yes, I guess you usually don't want new instances of services each time you access them. You might want to cache values fetched from the server. If you don't need it then your solution should work well of course. – Günter Zöchbauer Jan 12 '16 at 10:37
  • I'm just wondering now: Will every class I write need to be implemented as a singleton? Coming from AngularJS that just sounds silly. Using a service there was basically always a singleton, unless you used the factory. So isn't there, or shouldn't there be a way for Angular2 to annotate this behaviour or something? – Sam Vloeberghs Jan 12 '16 at 10:42
  • 1
    There is no need from an Angular perspective. This only depends on how you want it as an software architect. It's a singleton as long as you use the same injector. In your answer you create a new injector which has its own singleton. – Günter Zöchbauer Jan 12 '16 at 10:43
  • In your first comment you are implying it. So the question is, how and when to implement a class for a service in Angular2. – Sam Vloeberghs Jan 12 '16 at 10:46
  • regarding your first comment: where would you get the instance? The annotation @CanActivate is around the component. How can you rely there on something you have to fetch/instantiate in the constructor of the component? – Sam Vloeberghs Jan 12 '16 at 10:50
  • 2
    I don't understand "how and when to implement a class for a service in Angular2" - always of course. What I try to point out is that `bootstrap(AppComponent, [YourService])` creates an injector and `Injector.resolveAndCreate([HTTP_PROVIDERS, YourService])` creates another insjector where both have one singleton (which sums up to 2 instances in your application). – Günter Zöchbauer Jan 12 '16 at 10:50
  • 1
    aha that makes sense! :) I'm also still in the learning process. But than still: how to access it in the @CanActivate annotation – Sam Vloeberghs Jan 12 '16 at 10:52
1

I don't know if its the best way, but these guys do it by extending and using its own <router-outlet> and override the CanActivate method:

https://auth0.com/blog/2015/05/14/creating-your-first-real-world-angular-2-app-from-authentication-to-calling-an-api-and-everything-in-between/

Also you could use routerOnActivate instead.

https://angular.io/docs/js/latest/api/router/OnActivate-interface.html I hope this helps.

Langley
  • 5,326
  • 2
  • 26
  • 42
  • 1
    Thanks, I'd run into that before, but I think the routing API has changed a lot since then; I couldn't get their example working. – Ben Dilts Jan 10 '16 at 23:44
  • I see, why don't you try `routerOnActivate` instead? You can still avoid the component from getting instantiated but you do have access to its injected properties somehow. https://angular.io/docs/js/latest/api/router/OnActivate-interface.html – Langley Jan 11 '16 at 01:10
1

Here (https://gist.github.com/clemcke/c5902028a1b0934b03d9) is how to test the addInjector() solution that @shannon references:

beforeEachProviders(()=>[PERSON_SERVICE_PROVIDERS]);

beforeEach(inject([PersonService],()=>{
  let injector = Injector.resolveAndCreate([PERSON_SERVICE_PROVIDERS]);
  appInjector(injector);
}));
clemcke
  • 43
  • 7
0

Angular 2.0 final solution:

Since we now define a separate class which implements CanActivate, that class can be @Injectable, and another dependency can be provided in its constructor as per this answer.

Community
  • 1
  • 1
Stephen Paul
  • 37,253
  • 15
  • 92
  • 74