46

I have a component that declares the MetricsService service. This service requires both HttpModule plus two strings that defines the host and the auth key to use.

The metrics service is as follows:

    @Injectable()
    export class MetricsService {
        constructor(
            private http: Http,
            public wsAuthKey: string,
            public wsHost: string
            ) {
            this.wsAuthKey  = wsAuthKey || "blahblahblahblahblahblahblahblah=";
            this.wsHost     = wsHost    || "https://preprod-admin.myservice.ws";
        }

The component that uses it is written as follows:

    export class DatavizComponent implements OnInit, OnChanges {
        constructor(
            private zms: MetricsService,
        ) { 
        }

My question is how should I write the component constructor so that the whole thing works, including passing the host and key (but not passing the http)?

Note : The code as currently written does not compile.

To be more precise, I would expect the component to provided app-depending data something like this:

     export class DatavizComponent implements OnInit, OnChanges {
            constructor(
                private zms = MetricsService("http://myhost.com", "mykey"),
            ) { 
            }

But if this works, how to pass http?

UPDATE AFTER PROPOSED SOLUTION:

    export class MetricsService {
    
        constructor(
            private http: Http,
            @Inject('wsAuthKey') @Optional() public wsAuthKey?: string,
            @Inject('wsHost') @Optional() public wsHost?: string
            ) {
            this.wsAuthKey  = wsAuthKey || "blahblah=";
            this.wsHost     = wsHost    || "https://preprod-admin.host.ws";
    
    
            console.log("MetricsService constructor="
                + " wsAuthKey="+this.wsAuthKey
                + ", wsHost="+this.wsHost
            );
    
        }

In the component:

    @Component({
        selector:    'dataviz-offers-volumes',
        templateUrl: 'app/dataviz.component.html',
        styleUrls:  ['app/dataviz.component.css'],
        encapsulation: ViewEncapsulation.None,
        providers: [
            {provide: 'wsAuthKey',  useValue: 'abc'}, 
            {provide: 'wsHost',     useValue: 'efg'}, 
        ],
    })
    export class DatavizComponent implements OnInit, OnChanges {
      
        @ViewChild('chart') private chartContainer: ElementRef;
        @Input() private graphId:string;
        @Input() private wsAuthKey:string;
        @Input() private wsHost:string;
        @Input() private maxSamples=12;
    
        constructor(
            private zms: MetricsService
        ) { 
        }

In the constructor, the log are as follows (value are not passed):

    MetricsService constructor= wsAuthKey=blahblah=, wsHost=https://preprod-admin.host.ws

where it should show 'abc' and 'efg'.

But I wonder if there is not an issue with the component that use dataviz componenet. In this component, the following information have been passed:

    @Input() private wsAuthKey:string;
    @Input() private wsHost:string;

As I would like the tag to optionally preset the host and key:

                    <h1>dataviz volume</h1>
                    <div class="chartContainer left" title="Simultaneous offers via dataviz directive">
                        <dataviz-offers-volumes 
                            id="dataviz-volumes1"
                            [graphId]="graphId"
                            [wsAuthKey]="'myauthkey'"
                            [wsHost]="'http://myhost.com'"
                            [maxSamples]="123"
                        >
                        </dataviz-offers-volumes>
                    </div>
paulhector
  • 85
  • 7
Stéphane de Luca
  • 12,745
  • 9
  • 57
  • 95
  • Where exactly do you intend to pass these wsAuthKey and wsHost? Are they specific to the app or to particular component? It doesn't look like you're trying to pass anything in your example, so it is not clear how this is supposed to work. – Estus Flask Feb 22 '17 at 16:20
  • Those data are specific to the app that uses the component (though the service provides default values). I was expecting to provide them from within the component? (I added a small section in the question) @estus – Stéphane de Luca Feb 22 '17 at 16:25
  • Of course, they will be undefined in constructor. They are bindings and aren't available on construction. Moreover, I see no reason for them to be bindings (Input), they aren't changed. This totally depends on how these values are used. Is there a real reason why these values should be passed through attrs, or you're trying to provide this feature in the case if you'll ever need it? This will make the whole thing substantially more complex. You need to provide the details on how MetricsService works - it probably can't be a singleton, for instance. – Estus Flask Feb 22 '17 at 18:23
  • Regarding how it should work: a page can contains several instances of dataviz of same or different class (class=same type of graphics but with potentially different attributes. Attributes can be provided in a page either to points to a specific infrastructure (preproduction, Devel, etc. served by the metrics service) or to compare the same metrics from different infrastructures. Also, one attribute serves as the period (daily, weekly, etc.) so you can have two dataviz of same class, same infra but different period on the same page. (I am porting my js to angular) – Stéphane de Luca Feb 22 '17 at 18:52
  • The attributes under question are specifically wsAuthKey and wsHost. Should they really be provided for each directive? Because they are obvious obstacles there. Without knowing how they are used in MetricsService , it is not possible to give a quality answer to updated question. – Estus Flask Feb 22 '17 at 19:45

3 Answers3

52

You can make the parameters optional by adding @Optional() (DI) and ? (TypeScript), and @Inject(somekey) for primitive values that are not supported as provider keys

@Injectable()
export class MetricsService {
    constructor(
        private http: Http,
        @Inject('wsAuthKey') @Optional() public wsAuthKey?: string,
        @Inject('wsHost') @Optional() public wsHost?: string
        ) {
        this.wsAuthKey  = wsAuthKey || "blahblahblahblahblahblahblahblah=";
        this.wsHost     = wsHost    || "https://preprod-admin.myservice.ws";
    }
providers: [
  {provide: 'wsAuthKey', useValue: 'abc'}, 
  {provide: 'wsHost', useValue: 'efg'}, 
]

If they are provided, they are passed, otherwise they are ignored, but DI still can inject the MetricsService.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • So far so good, except the providers section: I put that in the component that use the service, right? – Stéphane de Luca Feb 22 '17 at 17:26
  • 1
    You can put the providers in the component, if the service itself also is provided in the component. The values need to be provided at the same level as the service or higher. A service provided by `AppModule` can't inject values from `MyComponent`. – Günter Zöchbauer Feb 22 '17 at 17:53
  • As I said, this only can work if the MetricsService is also provided in the component. The service provided in NgModule can't inject values from a component. – Günter Zöchbauer Feb 22 '17 at 19:01
  • 1
    I tried it with Angular 6 and it is not working. I had to use `InjectorToken` as it is described in https://stackoverflow.com/a/49202291/9662881. – mgierw Jan 21 '20 at 12:39
21

This is a common recipe that is described in this question in particular. It should be a service that holds the configuration:

@Injectable()
export class MetricsConfig {
  wsAuthKey = "blahblahblahblahblahblahblahblah=";
  wsHost = "https://preprod-admin.myservice.ws";
}

@Injectable()
export class MetricsService {
    constructor(
        private http: Http,
        metricsConfig: MetricsConfig
    ) {
        this.wsAuthKey  = metricsConfig.wsAuthKey;
        this.wsHost     = metricsConfig.wsHost;
    }
}

In the case when it needs to changed, it can be overridden or extended for entire module or for particular component:

@Component(
  ...
  { provide: MetricsConfig, useClass: class ExtendedMetricsConfig { ... } }
)
export class DatavizComponent ...

There's no real need to make MetricsConfig a class in this case. It can be an OpaqueToken value provider as well. But a class can be conveniently extended, it is easier to inject and already provides an interface for typing.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
3

From the official docs: https://angular.io/guide/dependency-injection-in-action#injectiontoken

Use an @Optional decorator in the constructor:

export class MyService {
  constructor( @Optional() public var: type = value ) { }
}
Gus
  • 6,545
  • 4
  • 36
  • 39