2

I am trying to create a generic directive which will take a class type for rule validation and according to the rule in the class the directive will either show or hide an element.

This is my attempt so far.

PLUNKER Demo

myIf-Directive.ts

@Directive({
  selector: '[myIf]'
})
export class MyIfDirective {
  constructor(private _viewContainer: ViewContainerRef,
    private _template: TemplateRef<Object>) 
  { }

  @Input() set myIf(rule: string) {
    //rule class type will come as string
    //how can I use string token to get dependency from injector?
    //currently harcoded
    //will the injector create new instance or pass on instance from parent?
    let injector = ReflectiveInjector.resolveAndCreate([AdminOnly]);
    let adminOnly : IRule = injector.get(AdminOnly);
    let show = adminOnly.shouldShowElement();
    show ? this.showItem() : this.hideItem();
  }
  private showItem() {
    this._viewContainer.createEmbeddedView(this._template);
  }

  private hideItem() {
    this._viewContainer.clear();
  }
}


app-component.ts

@Component({
  selector: 'my-app',
  template: `
    <div *myIf="'AdminOnly'">
      <h2>Hello {{name}}</h2>
    </div>
  `,
})
export class App {
  name:string;
  constructor() {
    this.name = 'Angular2'
  }
}

But I am stuck in 2 places:

  1. I keep getting the error No Provider for AuthService
  2. I do not know how I can get the dependency from Injector using class name as string rather than the type

Any suggestion whether this is the right way to do it or where I am going wrong is highly appreciated.

Logan H
  • 3,383
  • 2
  • 33
  • 50
lbrahim
  • 3,710
  • 12
  • 57
  • 95
  • *I do not know how I can get the dependency from Injector using class name as string rather than the type* You can't. See http://stackoverflow.com/a/40063568/3731501 – Estus Flask Nov 10 '16 at 20:30
  • @estus So, is there anyway I can pass the type of `AdminOnly` to the `my-if` directive rather than as a string? – lbrahim Nov 11 '16 at 07:12
  • I'm not sure how this directive should work. But you have to pass component constructor function, not a string. If you want to pass it as a string, a map of components and their string representations should be used like it was shown above. – Estus Flask Nov 11 '16 at 10:34
  • @estus Basically, what I am expecting is that I will send a rule class/type to the directive and it will get the instance from the injector with all its dependencies and invoke a certain method to determine whether to show or hide the element in question. If this is not possible could you recommend another suitable API for this to work? – lbrahim Nov 11 '16 at 11:02
  • You need to use an injector as the answer suggests and resolve a component through map. It doesn't look like a good design solution to me. I'm not sure what would be a good one. For limited amount of components it should be a template with `ngSwitch`, it's as simple as that. – Estus Flask Nov 11 '16 at 11:15

2 Answers2

4

You need to pass the parent injector like

export class MyIfDirective {
  constructor(private injector:Injector, private _viewContainer: ViewContainerRef,
    private _template: TemplateRef<Object>) 
  { }

  @Input() set myIf(rule: string) {
    let resolvedProviders = ReflectiveInjector.resolve([AdminOnly]);
    let childInjector = ReflectiveInjector.fromResolvedProviders(resolvedProviders, this.injector);

    let adminOnly : IRule = childInjector.get(AdminOnly);
    let show = adminOnly.shouldShowElement();
    show ? this.showItem() : this.hideItem();
  }
  private showItem() {
    this._viewContainer.createEmbeddedView(this._template);
  }

  private hideItem() {
    this._viewContainer.clear();
  }
}

See also Inject service with ReflectiveInjector without specifying all classes in the dependency tree

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • This solves No.1 of my problems. Thanks. Could you also suggest some option for the second problem? – lbrahim Nov 11 '16 at 11:03
  • 2
    You need to register it by string like `{provide: 'someName', useClass: SomeClass}`, then you can get it by name like `injector.get('someName')`. Alternatively you can maintain a map of types like `let types = {'someName': SomeClass', 'foo': Foo, 'bar': Bar}`, then you can use it like `injector.get(this.types['someName'])` – Günter Zöchbauer Nov 11 '16 at 11:05
  • I like the first way. But in that case this will not work `let resolvedProviders = ReflectiveInjector.resolve(['AdminOnly']);` – lbrahim Nov 11 '16 at 14:11
  • 1
    `let resolvedProviders = ReflectiveInjector.resolve([{provide: 'AdminOnly', useClass: AdminOnly]);` or `let resolvedProviders = ReflectiveInjector.resolve([{provide: 'AdminOnly', useClass: this.types['AdminOnly']}]);` – Günter Zöchbauer Nov 11 '16 at 14:14
0

Just update for Angular version 10+:

  • From your service:
  @Injectable({
    providedIn: 'any'
  })
  export class AdminOnly { ... }
  • In your directive or a pure function, ...:
 import { Injector } from '@angular/core';

 ...
 const injector: Injector = Injector.create({
   providers: [{provide: AdminOnly, deps: []}]
 });
 const adminOnly: AdminOnly = injector.get(AdminOnly);

 let show = adminOnly.shouldShowElement();
 ...

See more

Hieu Tran AGI
  • 849
  • 9
  • 9