7

I'm wondering what would be the best way to set up configurable modules in angular2. In angular1 this was done usually via providers. With them being changed quite a bit, how would you go about passing config parameters to reusable ng2 modules/directives/components?

An ng1 example:

// configuring a (third party) module    
.config(function (angularPromiseButtonsProvider) {
  angularPromiseButtonsProvider.extendConfig({
    spinnerTpl: '<div class="other-class"></span>',
    disableBtn: false
  });
});

 // setting up the provider
.provider('angularPromiseButtons', function angularPromiseButtonsProvider() {
    var config = {
        spinnerTpl: '<span class="btn-spinner"></span>',
        priority: 0,
        disableBtn: true,
    };

    return {
        extendConfig: function(newConfig) {
            config = angular.extend(config, newConfig);
        },

        $get: function() {
            return {
                config: config
            };
        }
    };
})

// using the result in the directive, etc.
.directive('promiseBtn', function(angularPromiseButtons){
    var config = angularPromiseButtons.config;
})

This is basically the same question as this one but directed at angular2.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
hugo der hungrige
  • 12,382
  • 9
  • 57
  • 84
  • 1
    Can you expand your question? You shouldn't have to read another question just to figure out what you're asking here. You can continue to refer to the other question for additional context, but this question should be able to stand on its own – snorkpete May 06 '17 at 23:46
  • 1
    I really beginn to hate this community here that it is so hostile towards new questions. It's really annoying. @snorkpete Thanks for pointing that out. It's a valid point. – hugo der hungrige May 06 '17 at 23:51
  • 2
    it's the reason why i commented rather than voted - to allow you to update your question instead of jumping down your throat. I would say that the community does provide great content overall, even if the means used to get there isn't always the most friendly. Such is life with a big enough community – snorkpete May 07 '17 at 00:05
  • 2
    Providing some simple code of what exactly is your case would help. If you're not sure what you're trying to do in A2, it also makes sense provide A1 code as an example. It's a good strategy to keep the question constructive and not get 'too broad' close votes or downvotes. – Estus Flask May 07 '17 at 00:08
  • 2
    And dude, you have a high enough reputation to know all that already. The standard of questions is pretty dire right now. Lead by example. – Mike Chamberlain May 07 '17 at 05:57

1 Answers1

23

There are several recipes, which can be used separately or together.

Configuration service

Having a service to provide necessary configuration in key/value form is usually desirable.

There may be more than one configuration service to configure one application entity, e.g. someConfig for generic user-defined configuration, and someDefaultConfig for all default values that are supposed to be possibly changed. For example, someConfig may contain auth credentials that are always user-defined, and someDefaultConfig may contain default hook callbacks, deep settings for auth providers, etc. The most simple way to implement this is to merge configuration objects with Object.assign.

Mandatory configuration service

A recipe that requires the user to define configuration service explicitly, it basically uses DI to designate that some module won't work without proper configuration.

AngularJS

// third-party module
// will fail if someConfig wasn't defined by the user
angular.module('some', []).factory('someService', (someConfig) => { ... })

// user-defined module
angular.module('app', ['some']).constant('someConfig', { foo: 'foo' });

Angular

// third-party module
export const SOME_CONFIG = new InjectionToken('someConfig');

@Injectable
class SomeService {
  constructor(@Inject(SOME_CONFIG) someConfig) { ... }
}

@NgModule({ providers: [SomeService] })
export class SomeModule {}

// user-defined module
@NgModule({
  imports: [SomeModule],
  providers: [{ provide: SOME_CONFIG, useValue: { foo: 'foo' } }]
)
export class AppModule {}

Optional configuration service with overridable empty value

This is a slight variation of the previous recipe, the only difference is that there is empty default value that won't make the application to fail if configuration service wasn't defined by a user:

AngularJS

// third-party module
angular.module('some', [])
.constant('someConfig', {})
...

Angular

// third-party module
@NgModule({ providers: [..., { provide: SOME_CONFIG, useValue: {} }] })
export class SomeModule {}
...

Optional configuration service

Alternatively, configuration service can be made totally optional for injection.

AngularJS

// third-party module
angular.module('some', []).factory('someService', ($injector) => {
  const someConfig = $injector.has('someConfig') ? $injector.get('someConfig') : {};
  ...
})
...

Angular

// third-party module
export const SOME_CONFIG = new InjectionToken('someConfig');

@Injectable
class SomeService {
  constructor(@Inject(SOME_CONFIG) @Optional() someConfig) {
    this.someConfig = someConfig !== null ? someConfig : {};
    ...
  }
}

@NgModule({ providers: [SomeService] })
export class SomeModule {}
...

forRoot method

forRoot static module method is a convention that is followed by Angular router module and numerous third-party modules. As explained in the guide, the method returns an object that implements ModuleWithProviders.

Basically it gives an opportunity to define module providers dynamically, based on forRoot(...) arguments. This can be considered an alternative to AngularJS config and provider units that don't exist in Angular.

AngularJS

// third-party module
angular.module('some', [])
.constant('someDefaultConfig', { bar: 'bar' })
.provider('someService', function (someDefaultConfig) {
  let someMergedConfig;

  this.configure = (config) => {
    someMergedConfig = Object.assign({}, someDefaultConfig, config);
  };
  this.$get = ...
});

// user-defined module
angular.module('app', ['some']).config((someServiceProvider) => {
  someServiceProvider.configure({ foo: 'foo' });
});

Angular

// third-party module
export const SOME_CONFIG = new InjectionToken('someConfig');
export const SOME_DEFAULT_CONFIG = new InjectionToken('someDefaultConfig');

@Injectable
class SomeService {
  constructor(
    @Inject(SOME_CONFIG) someConfig,
    @Inject(SOME_DEFAULT_CONFIG) someDefaultConfig
  ) {
    this.someMergedConfig = Object.assign({}, someDefaultConfig, someConfig);
    ...
  }
}

@NgModule({ providers: [
  SomeService,
  { provide: SOME_DEFAULT_CONFIG, useValue { bar: 'bar' } }
] })
export class SomeModule {
  static forRoot(config): ModuleWithProviders {
    return {
      ngModule: SomeModule,
      providers: [{ provide: SOME_CONFIG, useValue: config }]
    };
  }
}

// user-defined module
@NgModule({ imports: [SomeModule.forRoot({ foo: 'foo' })] })
export class AppModule {}

APP_INITIALIZER multi-provider

Angular APP_INITIALIZER multi-provider allows to provide asynchronous initialization routines for the application.

APP_INITIALIZER shares some similarities with AngularJS config phase. APP_INITIALIZER routines are susceptible to race conditions, similarly to config and run blocks in AngularJS. For instance, Router is available for injection in root component but not in APP_INITIALIZER, due to circular dependency on another APP_INITIALIZER.

Synchronous initialization routine

AngularJS

...
// user-defined module
angular.module('app', ['some']).config((someServiceProvider) => {
  someServiceProvider.configure({ foo: 'foo' });
});

Angular

...
// user-defined module
export function someAppInitializer(someService: SomeService) {
  return () => {
    someService.configure({ foo: 'foo' });
  };
}

@NgModule({
  imports: [SomeModule],
  providers: [{
    provide: APP_INITIALIZER,
    multi: true,
    useFactory: someAppInitializer,
    deps: [SomeService]
  }]
})
export class AppModule {}

Asynchronous initialization routine

Initialization may involve fetching configuration from remote source to configure services; something that is not possible with single AngularJS application. This requires to have another application that initializes and bootstraps main module. This scenario is naturally handled by APP_INITIALIZER.

AngularJS

...
// user-defined module
angular.module('app', ['some']);

angular.module('appInitializer', [])
.factory('initializer', ($document, $http) => {
  return $http.get('data.json')
  .then((result) => result.data)
  .then((data) => {
    $document.ready(() => {
      angular.bootstrap($document.find('body'), ['app', (someServiceProvider) => {
        someServiceProvider.configure(data);
      }]);
    });
  });
});

angular.injector(['ng', 'appInitializer'])
.get('initializer')
.catch((err) => console.error(err));

Angular

...
// user-defined module
export function someAppInitializer(http: HttpClient, someService: SomeService) {
  return () => {
    return http.get('data.json').toPromise()
    .then(data => {
      someService.configure(data);
    });
  };
}

@NgModule({
  imports: [SomeModule],
  providers: [{
    provide: APP_INITIALIZER,
    multi: true,
    useFactory: someAppInitializer,
    deps: [HttpClient, SomeService]
  }]
})
export class AppModule {}
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 3
    I know the comments are not intended to be used like this, but in this case I have to point out that is an incredible answer! Thanks a lot! – hugo der hungrige May 07 '17 at 02:44
  • Why would you use injection for configurating a service? You can just set a property on it, or use a static property. – Ced Oct 28 '17 at 15:58
  • @Ced Because it's battle-proven. You can't configure a service by setting a property on its instance if the configuration is needed to construct it (and this happens often ). Static property is a potential mistake because there can be more than one instance of a service and more than one configuration, you lose the flexibility of DI by referring a class directly. – Estus Flask Oct 28 '17 at 16:16