Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create configurable service Angular 4

I would like to create a service to be re-used throughout a couple of angular applications.

I'm using angular-cli.

I want the service to be configurable. In other words, give each app the ability to instantiate the service with different config parameters, using DI.

The service will sit in a shared library, so it cannot import a specific file path from the parent application in order to get the config params, it has to get them via DI.

I tried following the instructions here, since the article describes exactly the problem I am facing, however I am getting an error:

ERROR Error: No provider for String!
at injectionError (core.es5.js:1169)
at noProviderError (core.es5.js:1207)
at ReflectiveInjector_.webpackJsonp.../../../core/@angular/core.es5.js.ReflectiveInjector_._throwOrNull (core.es5.js:2649)
at ReflectiveInjector_.webpackJsonp.../../../core/@angular/core.es5.js.ReflectiveInjector_._getByKeyDefault (core.es5.js:2688)
at ReflectiveInjector_.webpackJsonp.../../../core/@angular/core.es5.js.ReflectiveInjector_._getByKey (core.es5.js:2620)
at ReflectiveInjector_.webpackJsonp.../../../core/@angular/core.es5.js.ReflectiveInjector_.get (core.es5.js:2489)
at resolveNgModuleDep (core.es5.js:9533)
at _createClass (core.es5.js:9572)
at _createProviderInstance$1 (core.es5.js:9544)
at resolveNgModuleDep (core.es5.js:9529)

Also, from zone.js:

zone.js:643 Unhandled Promise rejection: No provider for String! ; Zone: <root> ; Task: Promise.then ; Value: Error: No provider for String!

Here is my relevent code:

main.ts:

...
import { provideAuthService } from 'shared-library';
...
platformBrowserDynamic().bootstrapModule(AppModule, [provideAuthService('Good Day')]);

app.module.ts:

...
import { AuthService } from 'shared-library';

@NgModule({
  declarations: [
      ...
  ],
  imports: [
    ...
  ],
  providers: [
    AuthService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

and auth.service.ts:

...

export function provideAuthService(settings:string) {  
  return { provide: AuthService, useFactory: () => new AuthService(settings) 
}

@Injectable()
export class AuthService {
mgr: UserManager = new UserManager(this.settings);

    constructor(private settings:string='Hello') {
       ...
    }
}

Thanks in advance to anyone who might be able to help out here!

like image 655
Basya Rosemann Avatar asked Jul 03 '17 01:07

Basya Rosemann


1 Answers

You can follow a pattern done by the Angular router where a configuration object is defined as injectable and you can only declare it once in your main app's module.

Define a new token to identify the config object:

export const APP_CONFIG = new InjectionToken<string>('AppConfig');

Create an interface or abstract class that your config objects can use. This just makes it easier to maintain your config objects. Here's an example but you can define your own properties or methods as needed.

export interface AppConfig {
     name: string;
     baseUrl: string;
}

Create a new module to hold the configurable service. This module will have a forRoot() method that allows you to set the config object. It will also have a safety check to ensure that the module is only configured once which is proper (via the constructor).

import { Injectable, NgModule, SkipSelf, Optional } from '@angular/core';
@NgModule({
     providers: [
         YourServiceClass
     ]
})
export class ServiceModule {
    public static forRoot(config: AppConfig) {
         return {
              ngModule: ServiceModule,
              providers: [
                  {provide: APP_CONFIG, useValue: config}
              ]
         };
    }

    public constructor(@Optional() @SkipSelf() parentModule: ServiceModule) {
         if(parentModule) {
             throw new Error('ServiceModule has already been imported.');
         }
    }
}

You can then inject the config object into your service class.

@Injectable()
export class YourServiceClass {
     public constructor(@Inject(APP_CONFIG) config: AppConfig) {
     }
}

To configure the service you have to use the forRoot method in your main module.

@NgModule({
    imports: [
        ServiceModule.forRoot({
             name: 'FooBar',
             baseUrl: 'http://www.example.com/'
        })
    ]
})
export class MainModule {}

So what happens here is the forRoot method creates module metadata manually. There is a token APP_CONFIG that hasn't been declared anywhere. So the module returned by forRoot declares that it references a specific value that was passed to the function.

Order or module imports becomes important, because if a module imports ServiceModule before the main module the APP_CONFIG will be undefined. Therefore, we check for this case in the module constructor. If ServiceModule can be injected then it's been imported already. This adds a restriction that only one instance of the module can be used, and that's the one configured for the service.

It's a good pattern and I use this pattern, but I've run into cases where lazy loaded modules want to load the ServiceModule. So if you want to avoid headaches later in the project. Try to limit your configurable service as the only provider in the module. Once you start adding other stuff to the module it becomes more likely that you'll need flexibility in the loading order.

like image 138
Reactgular Avatar answered Nov 04 '22 19:11

Reactgular