Angular 4 : how to pass an argument to a Service provided in a module file

Sophie Jobez picture Sophie Jobez · May 2, 2017 · Viewed 17.5k times · Source

I'm calling to you for help on what method to use here : I have tested many things to solve my problem and I have at least 4 working solutions, but I don't know which one(s) would be the best and if some of them work only because of a side effect of something else I did and should not be used...

I couldn't find anything in the documentation to point me to one of these solutions (or another one)

First, here's my situation : I'm building an app that connects to an API for data and builds different sections, each containing dataviz.

I built the sections of the site to be modules, but they share a service named DataService (each of these modules will use the DataService to get and process the data).

My DataService needs a config file with a number of options that are specific to each section and stored in it's own folder.

So I need to provide DataService separately for each section, in the providers section of the module.ts file, and it seemed like good practice to avoid copying DataService to each module...

So the architecture of the files would be :

--Dashboard module
----data.service.ts
----Section1 module
------Section1 config file
----Section2 module
------Section2 config file

I tried multiple things that all seem to work in the section module file :

Solution 1 : injection

(I couldn't get the injection to work without quotation marks, even if I didn't see this anywhere)

Module file :

import { DataService } from '../services/data.service';
import viewConfig from './view.config.json';

@NgModule({
    imports: [ ... ],
    declarations: [ ... ],
    providers: [
        { provide: 'VIEW_CONFIG', useValue: viewConfig },
        DataService,
    ]})
   export class Section1Module { }

Section file :

@Injectable()
export class DataService {

    constructor(@Inject('VIEW_CONFIG') private viewConfig : any) {}

}

Solution 2 : factory provider

Not working after ng serve reboot (see update)

Inspired from https://angular.io/docs/ts/latest/guide/dependency-injection.html#!#factory-provider

Module file :

import { DataService } from '../services/data.service';
import viewConfig from './view.config.json';

export let VIEW_CONFIG = new InjectionToken<any>('');
let dataServiceFactory = (configObject):DataService => {
    return new DataService(configObject)
}

@NgModule({
    imports: [ ... ],
    declarations: [ ... ],
    providers: [
        { provide: VIEW_CONFIG, useValue: viewConfig },
        {
            provide : DataService,
            useFactory : dataServiceFactory,
            deps : [VIEW_CONFIG]
        },
    ]})
   export class Section1Module { }

Section file :

@Injectable()
export class DataService {

    constructor(private viewConfig : any) {}

}

Solution 3 : custom provider with direct instanciation

Not working after ng serve reboot (see update)

Module file :

import { DataService } from '../services/data.service';
import viewConfig from './view.config.json';

@NgModule({
    imports: [ ... ],
    declarations: [ ... ],
    providers: [
        { provide: DataService, useValue : new DataService(viewConfig) },
    ]})
   export class Section1Module { }

Section file :

@Injectable()
export class DataService {

    constructor(private viewConfig : any) {}

}

Solution 4 : custom provider with factory

Not working after ng serve reboot (see update)

Very similar to Solution 3...

Module file :

import { DataService } from '../services/data.service';
import viewConfig from './view.config.json';

let dataServiceFactory = (configObject) => { return new DataService(configObject) }

@NgModule({
    imports: [ ... ],
    declarations: [ ... ],
    providers: [
         { provide: DataService, useValue : dataServiceFactory(viewConfig) }
    ]})
   export class Section1Module { }

Section file :

@Injectable()
export class DataService {

    constructor(private viewConfig : any) {}

}

Solution 5 Use of a factory exported from the service file AND and injection token

Module file :

export const VIEW_CONFIG = new InjectionToken<any>('');

 @NgModule({
    imports: [ ... ],
    declarations: [ ... ],
    providers: [
         { provide: VIEW_CONFIG, useValue: viewConfig },
         { provide : DataService,
           useFactory: dataServiceFactory,
           deps : [VIEW_CONFIG] }
    ]})
   export class Section1Module { }

Section file :

@Injectable()
export class DataService {
    constructor(private viewConfig : any) {}
}

export function dataServiceFactory(configObject) {
    return new DataService(configObject);
}

I'm open to any criticism, idea, new lead of any kind on "What would be the right solution ?" or even "Is the right solution among these ones ?"

Thanks a lot ! :D


UPDATE At one point, I realised my Angular-CLI was behaving strangely, and that some of these methods weren't working if I killed my local server and did an "ng serve". What is even stranger is, it doesn't work juste after killing the CLI local server and rebooting it, but next time I save a file and the CLI recompiles, it works fine...

Solution 1 : still working

Solution 2 / Solution 3 : fails with error :

Error encountered resolving symbol values statically. Calling function 
'DataService', function calls are not supported. Consider replacing the 
function or lambda with a reference to an exported function

Solution 4 : fails with error:

Error encountered resolving symbol values statically. Reference to a non-exported function

Added solution 5

Answer

SrAxi picture SrAxi · May 2, 2017

My approach of using the view's config data is this:

My config file:

export const IBO_DETAILS_GENERAL = {
    iboCode: 'IBOsFormCodeField',
    iboGivenName: 'IBOsFormGivenNameField',
    iboFamilyName: 'IBOsFormFamilyNameField',
    iboEmail: 'IBOsFormEmailField',
};

export const IBO_DETAILS_ACCOUNT = [];

My view component:

import { IBO_DETAILS_GENERAL } from './ibo-details-formfields';

@Component({
    selector: 'ibo-details-general',
    templateUrl: './ibo-details-general.component.html',
    styles: [],
})
export class IboDetailsGeneral extends FilterDataMappingManipulator implements OnInit, OnDestroy {
    // Initial constants,vectors and component mapping
    private formFields = IBO_DETAILS_GENERAL;

So, this way, I have my config in formFields. And I can use it anywhere within this component's scope.

I am currently using this for generating dynamic form fields (input, select, etc.) depending on which data I get from my API call.

But you can use that object (formFields in my case) to look for what you need and send it to your service as a parameter.

Also, good job on putting the shared service on a higher level. I believe is the best approach. But I would not add it to providers on each component, I would add it on the component's module. Like this:

@NgModule({
    declarations: [
        ChildComp1,
        ChildComp2
    ],
    imports: [
    ],
    providers: [
        SharedService
    ]
})
export class SectionModule {
}

This way, ChildComp1 and ChildComp2 will have access to SharedService without having to add it inside the component's code.

This works fine when, for example, you are defining a User's section. Inside, per say, UsersModule you declare in providers your UsersSharedService and then in declarations you declare your UsersLeftPanel and UsersRightPanel (examples).

Update 1:

Example of usage of config within service.

Immagine that the shared service has this method:

getData() {
  // do Stuff
  let myData = {}; // Result of the stuff we did
  this.dataSubject.next(myData);
}

In your component you call this like this:

this.myService.getData();

Right?

Now, remember we had our config declared? Add it to the call.

this.myService.getData(this.formFields);

And, in our service:

   getData(currentConfig) {
      // do Stuff depending on currentConfig obj received as parameter
      let myData = {}; // Result of the stuff we did
      this.dataSubject.next(myData);
    }

This way, you call getData() method of our shared service passing different configurations. This way, you don't have to include your service in many providers and you don't have to copy/paste the logic that handles the configuration, you have it in your shared service and, therefore, all your children have access to it.

Update 2:

Following your Solution 5 approach, I think you are missing multi: true.

Try this:

export function dataServiceFactory(configObject) {
    return () => new DataService(configObject);
}

providers: [
     { provide: VIEW_CONFIG, useValue: viewConfig },
     { provide : DataService,
       useFactory: dataServiceFactory,
       deps : [VIEW_CONFIG],
       multi: true }
]})

The return in the exported function is key: return () =>.

This is how I have it in my project:

export function initConfigFactory(userConfig: UserConfig) {
    return () => userConfig.getUsersConfig();
}

Where userConfig.getUsersConfig() is a service call that gets the user's config.

providers: [
    {
        provide: APP_INITIALIZER,
        useFactory: initConfigFactory,
        deps: [UserConfig],
        multi: true
    }
]

This is very close to your Solution 5, try it out!