0

I'm trying to make a hierarchy of components. There's a global "CONTEXT" object (provided at NgModule level), but I want one component to be able to override this object for their children. The "CONTEXT" object is currently obtained by injection, like this:

NgModule           ---> Provides default "CONTEXT" for injection
<comp1>
   <comp2>
       <comp3>     ---> Overrides CONTEXT.someProp
           <comp4>
               <comp5>
               <comp5>
           <comp4>
       <comp3>
   <comp2>
<comp1>

No component is aware of the others' existence (they're created dynamically), but comp3 should be able to control/change what context"see" its appointed children (comp4 and comp5 in this case).

I guess injection is not the best pattern here (how can I do it? Should comp3 extend CONTEXT?), at the very least because it would be pseudo-static (could Angular detect and propagate the change of a setting after creation?). Perhaps an Observable could help here.

I've seen this answer but I don't think it's the same case.

What would be the best way to achieve this? I'd like to avoid using explicit @Input properties because the intermediate components should be as agnostic as possible. Is there a common pattern to follow, here?

I. Ahmed
  • 2,438
  • 1
  • 12
  • 29
Guillermo Prandi
  • 1,537
  • 1
  • 16
  • 31
  • You've tried to generalize it, but the description is too vague. Please, provide more specifics and http://stackoverflow.com/help/mcve . Providing real entity names instead of abstract ones would probably give some idead what this is all about. *CONTEXT.someProp* - isn't it the whole CONTEXT that should be overridden? *propagate the change of a setting after creation* - the change of what and when and where? Generally the recommendation would be to define own CONTEXT in comp3 `providers`, but it's not clear if this is expected behaviour. – Estus Flask Feb 20 '18 at 05:12
  • As I understand.You could either: 1). Pass the **Context** from root to children via _@Input_ binding. 2). Have **Context** as an observable property in a _service_ called **CarrierService** inject it in the root, Consume in the __ and then bind to children (comp4, comp5..) via _@Input_ . 3). Consume and use the **CarrierService** service wherever needed. – Thirueswaran Rajagopalan Feb 20 '18 at 06:18
  • @estus thanks for your answer. The example is as generic as described. For instance, I'd like to activate debug/tracing from one component and below. Or use different credentials than their ancestors, or become iconized, or read-only (not disabled!), etc. – Guillermo Prandi Feb 20 '18 at 14:13
  • I see. The generalization is not desirable here because you can possibly perceive cases as similar while they are different. But both debug and auth you mentioned are similar indeed, they are solved with DI alone and config provider, I provided the explanation. – Estus Flask Feb 20 '18 at 14:22

1 Answers1

0

Strict component hierarchy suggests that this can be solved with hierarchical injectors.

A provider that needs to be configured differently in different components can be specified in child injector in order to be reinstantiated. The need for provider configuration suggests that there should be another configuration provider (see this answer for configuration recipes). For example, a provider with mandatory configuration, like auth service:

export const SOME_CONFIG = new InjectionToken('someConfig');

@Injectable
export class SomeService {
  config: IConfig;
  constructor(@Optional() @Inject(SOME_CONFIG) someConfig) {
    this.config = {
      /* default config goes here */,
      ...someConfig
    };
  }

  updateConfig(config) {
    return Object.assign(this.config, config);
  }

  ...
}

@NgModule({
  ...
  providers: [
    SomeService,
    { provide: SOME_CONFIG, useValue: { /* global config */ }
  ] 
})
export class AppModule {}

And a new instance can be created with another configuration for a component:

@Component({
  providers: [
    SomeService,
    { provide: SOME_CONFIG, useValue: { /* local config */ }
  ] 
})
class FooComponent {...}
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • Thank you. This, however is not under control of FooComponent. In my scenario, FooComponent needs to decide (dinamically) what local config its children see. As much as I've tried, I've couldn't make FooComponent to receive the SOME_CONFIG from the NgModule AND override it for its children. It's either a provider or a consumer, but I couldn't make it be both. – Guillermo Prandi Feb 20 '18 at 23:08
  • As it was said, the question is too generalized. For generalized question the approach above is applicable. Please, provide [http://stackoverflow.com/help/mcve](http://stackoverflow.com/help/mcve) that reflects your real case as close as possible. It's unclear how comes that FooComponent needs to decide that. There may be XY problem, or there may be suitable workarounds. – Estus Flask Feb 21 '18 at 01:46
  • FooComponent has a checkbox. Or receives some value from a subscription. Or has self awareness. Perhaps the most useful analogy is the Windows Explorer interface: you have many folders and none is concerned about the others. But the user may mark one as holding pictures, and then every folder below it now defaults to a picture view. Or sets compression, or inheritable permissions, etc. Although in my case the settings are volatile. Sorry if my examples are lacking. – Guillermo Prandi Feb 21 '18 at 05:23
  • I believe configuration merging was already mentioned in linked answer. I updated the answer with some example. Just provide a method on service instance that allows to update config that is defined on service construction. That's all. – Estus Flask Feb 21 '18 at 05:25
  • I'm still struggling to understand the modern Javascript/TypeScript paradigm (I come from C/C++), but I'll study your example to make it work in my project. I need FooComponent to inherit the config from its hierarchy and change only some values. Perhaps `{ provide: SOME_CONFIG, useFactory: xxx` and make the factory take the previous config and change it. – Guillermo Prandi Feb 21 '18 at 14:40
  • You don't have to use DI for things that don't need it, there may be no need for useFactory. It's more like `useValue: {...previousConfigConstant, a: 1, b: 2}`. Again, as suggested here https://stackoverflow.com/a/43827191/3731501 , you can have more than one config provider, SOME_DEFAULT_CONFIG for global defaults and SOME_CONFIG for specific overrides. – Estus Flask Feb 21 '18 at 15:11
  • I know I can have many providers, but as far as I understand (please correct me) I must pick the provider at the child component, which is not what I want. I want the child component to be unaware of the parent's messing with the configuration. Whether or not the configuration was modified is not the child's concern. Anyway, I think I'll go with the @Input() option. It's much more cumbersome, but has the ability to update dinamically. – Guillermo Prandi Feb 21 '18 at 19:02
  • I believe the answer covers that, so I'm not sure what the problem is. If you list only `providers: [SomeService]` in component `providers` SOME_CONFIG will be 'inherited' from global config. You can patch it dynamically with updateConfig method. – Estus Flask Feb 21 '18 at 23:27