Repository navigation
Description
Context
So I found a nice way to compose my Directives/Components. I use a service that's not part of any module but just defined at Directive/Component level.
Here is an example :
@Component({
providers: [ NgrxService ]
})
export class MyComponent {
constructor( @Self() private ngrx: NgrxService) { }
}So NgrxService is not meant to be a singleton service but rather a way to compose my Component with reusable code. The service look like this :
@Injectable()
export class NgrxService {
constructor(private store: Store, private cdr: ChangeDetectionRef) {
}
select() { ... }
dispatch() { ... }
set() { ... }
ngOnDestroy(){
....
}
}I'm also talking about that kind of service here #17948.
This is really handy because you can inject Directive/Component related stuff like ViewContainerRef, ChangeDetectorRef... and you have even access to ngOnDestroy life cycle hook.
So it's wonderful and everything work great but....
Problem
The catch is that you have to always provide the service for each component, if you happen to forget it you'll probably end up with a parent service which you absolutely don't want. You can use @Self() to protect from it but well if you forget to provide it you'll probably also forget to use @Self().
Even if you have a really good memory it's still boilerplate and it feels like we're doing the job at the wrong end. The service is meant to work like that so that behavior should be determined during the service definition and not during its usage.
Proposals
There is probably multiple ways of achieving this but here is my proposal :
Create a new decorator for that kind of service:
- Those service won't be singleton but a new instance will be created for each injection.
- Can't be injected elsewhere than inside Component or Directive and get the same life cycle as the host Component / Directive.
- Must be provided by the component/directive's module or part of an imported module (do we need to export it ?). Their injection is scoped to the module and not global (like Component, Directive and Pipe).
Update
Since v6, this proposal is now about adding a component value to providedIn.
Activity
mlc-mlapis commentedon Jul 8, 2017
Yes, good idea. Probably this pattern is used already now but not as the first-order solution but rather at random according to a specific situation.
Some points:
ghetolay commentedon Jul 8, 2017
ngOnDestroyhook is available on services, this part is not from the proposal it's how it works at the moment.Component,DirectiveandPipeas they're much closer to that kind of injection rather than services.Maybe I shouldn't have talked about service, I really don't see them as service but just as a class than can be injected. They're like partial component.
mlc-mlapis commentedon Jul 8, 2017
They look like
helpersbut it is probably too general name.trotyl commentedon Jul 9, 2017
I guess the original use case can be easily solved by the Directive as a Service pattern.
Say that, your Component is configured with some
selector:Then made a Directive with the same
selector(or several different ones if needed):Make the Directive to implement your Service:
Provide the
MyServicetoken with itself in the Directive:Inject the Service in your Component (here make sure it's provided by companion Directive rather than parent):
It solves:
ChangeDetectionRef) in@Directive(), but not always OK in@Injectable();PS: The is how the
NgModeldirectives works with all different kinds ofinputsand custom components.ghetolay commentedon Jul 9, 2017
The aim of this proposal is to reduce some boilerplate and make this less error prone since it's currently working.
With your suggestion I'll have to do a double link, for each component you have to inject the directive (dunno why you use a service token we can directly inject the directive) and add the component selector to the directive selector.
It's less error-prone in the sense you can't end up injecting the wrong directive like you can with service. You'll either have the right directive or an error. But this makes the directive not really reusable as you need to edit it each time you want to use it on a component.
trotyl commentedon Jul 9, 2017
@ghetolay There may be several Directives implements the same Service, as a Component, you should not care about where it is from (even if you can), that's what called separation of concerns.
And that's all being avoided this way.
For the Component you don't need to change anything except
@Self()=>@Host().trotyl commentedon Jul 9, 2017
That's valid, maybe we can have a quick way to reuse the existing
selectorlike:To mark it as the same
selectorasMyComponent's.trotyl commentedon Jul 9, 2017
Possibly relates to:
ghetolay commentedon Jul 9, 2017
I really don't get why you would alias your directive and inject the alias rather than the directive directly, there is no interface, abstract class or multiple implementation, I'm just talking about a plain class doing stuff.
Anyway the trick on your solution is all about using the same selector twice to spread the code logic.
But again even we were to use
with: MyComponentinstead of selector we still need to reference all components using that directive. That's what I called the double reference, if C needs S, S is injected into C (we're good here) but we also needs to reference C into S (the selector). That's the killer.I prefer the way I'm doing it now even if I have to define the service inside
providersbecause it's all inside C. We create S once and add it as a dependency everywhere it's needed (on C1, C2...) without having to update S so it can be injected on (C1, C2...).I think only #17017 is related, seems to be the same source problem.
trotyl commentedon Jul 10, 2017
I appreciate your proposals here, but that's not sound enough as far as I know.
When saying Directive below, I mean the Directives other than Component.
Services never have a Decorator, is just a name for what being injected by DI.
And Injector has no restriction about how you provide them, wherever you can use
useClass, you can useuseFactoryor, but@Injectable()is just for theuseClass.So even if you put more information in the Decorator, it won't help with other cases.
What should happen when more than one Directives in the same host injects this Service? Should they get two different instances?
The Scope of a Service is determined by how it's provided, so for
providersit would be available in this Component and its view children, forviewProvidersit would only be available for its view children.So in this case you would need a new properties called
selfProvidersto provide those Service with Scope of just this Component. But then what about Directives? They don't have a way to declare any kind of providers.If a general life-cycle hooks sharing mechanism being introduced, then:
And since life-cycle hooks is only for
useClasscase, then what about the inconsistency betweenuseClassand other cases? Should we introduce other mechanism to compensate that?This means a imported module can easily break your application all the time, if a module declare
Storeas not singleton, what should it be? An separate store for each Component? Then how can a module cancel the non-singleton declaration made by a dependency module?There have been many thoughts about DI if you could have a search, but most of them just stay on the usability for their own use case, but never consider how many problems it would cause.
PS: The links above is for others who need to take action.
ghetolay commentedon Jul 10, 2017
There is a big misunderstanding here. I'm not introducing any new mechanism it's something that's working right now. I'm just proposing a different way to trigger that mechanism.
Exactly like
@Injectable, just a more declarative way of what we're doing and less error-prone because it would be handled by the framework under the hood rather than the user.Take the 2 first snippets, I would like to remove
providers: [ NgrxService ]and@Self()on the first snippet by adding something on the second snippet. Because I think the behavior induced byproviders: [ NgrxService ]should be defined at service level. That service was created to work that way, always everywhere.My proposal being replacing
@Injectable()with another decorator name but totally open for any suggestion like a new parameter on@Injectable().If this is rejected there'll probably be a way to do it using schematics and generate a third party pre-processing tool.
trotyl commentedon Jul 10, 2017
This would be a huge break of current DI mechanism, in the existing concept of DI, you can inject anything you want as long as the token can be found by the injector, it is why
that's working right nowfor Service being able to inject View-specific stuff. (may not work in 2.x but that can be considered bug)So this is a completely new DI mechanism for injectors to not being hierarchical anymore, so it may need to take a lot cases into consideration.
In short, back in old days (before the view engine), there're indeed some dependencies that can only be injected into Component, like ChangeDetectorRef, ElementRef, TemplateRef and ViewContainerRef (they're injected by compiler, not injector). But that's considered a bug due to the nature of DI and being fixed, after that your code can now work.
Again, you propose that some dependency should just be able to use in Component. Then questions come: Why shouldn't other consider this a bug as well? Why should your code work in first place?
Not exactly AFAIK, every other type is also
@Injectable(), it's capable to inject@Component(),@Directive()or@Pipe()as long as it's provided.And again, Decorator here would only work for
useClass, so whatever information added here would not fit the other ways, that's another break for current DI mechanism.@Injectable()just stands for@ThisIsAMeaninglessDecoratorButWeNeedThisDueToTypeScriptRestriction()at first then@ThisIsStillAMeaninglessDecoratorBugWeRequireItForConsistency(), what you proposed is not meaningless, so that's not something like@Injectable().igabesz commentedon Oct 4, 2017
Here is a question of mine on SO showing a possible case for usage. TLDR: I'd like to inject separate Logger instances into various components and services. This proposal would make the story a bit more convenient. We can solve it now but with a little more verbosity.
https://stackoverflow.com/questions/46573239/
1 remaining item
ghetolay commentedon May 4, 2018
With the new
providedIn: 'root'in v6 it looks like a first step toward this feat request was made. Allowing us to define where a service gets provided as part of the service metas instead of inside modules/components providers.So this proposal is now about adding a new
componentvalue to use with providedIn.Also the most common use case would probably be an
onDestroyservice to use withtakeUntil()when subscribing to observables inside components (until we get life cycle observables).trotyl commentedon May 6, 2018
What you thought is far from possible.
First of all, there's no concept of
componentin DI, onlydirectiveandview, what you think to be a component is just a normal directive on the root element of the specific view. So technically there can only beprovidedIn: 'directive'instead ofprovidedIn: 'component'. But that's still wrong, the injection scope is not the directive, but its host element, so it'sprovidedIn: 'element'instead.Even the API looks good now, what you want is the exact opposite behavior of
providedIn.providedInworks as the fallback option, it would only be used when the token not already provided. But you wantprovidedInto be used first and even ignore the normal providers. Even though technically achievable, almost require redesign from beginning.Then how would you like to write unit tests for them? More clearly:
"Let's just have integration tests then" is definitely not an option.
And given those service can only be used by directives, why should Angular restrict the life-cycle hooks only to
OnDestroy? Even you could say "I don't need" but others would still make the feature request as long as possible. So what's the difference between "element-specific service" and "dynamic directive on element"? Just same thing with different names?simeyla commentedon Aug 14, 2018
To solve the 'forgetting to add @self' or 'forgetting to provide at the component level' problems I did the following. This was sufficient for me at this time to keep moving and protect myself from bad cut-and-pasting.
remove
providedIn: 'root'from the servicecreated an
init(this)method in my service that must be called immediately in the constructor of the componentthe
init()method checks to see if that service instance has ever been initialized before (suggesting you forgot to 'provide' it locally)Methods on the service will fail if
init()hasn't been called.Is
tslintclever enough to be used to create a rule for any of this? Like a rule that saysparameter-of-type-X-must-be-provided-by-decorator?bryce-marshall commentedon Nov 21, 2018
I would also find such a feature useful. I see it as being analogous to Angular pure and impure pipes. A hypothetical "impure" injection candidate would result in a new instance being created (or a factory method being invoked afresh) each time the candidate is injected into a constructor. Theoretically, such a provider could still be overridden at different levels of the injector hierarchy, in which case new instances of the overriding provider type would be created for each injection at or below their definitions. As @ghetolay has already argued, this would be expected behavior for such a service/factory.
I my case, I want to take a state snapshot that is injected into components. I currently see no clean way of doing this within Angular.
I have asked a Stack Overflow question explaining the problem here, however I suspect that the answer is that it can't currently be done:
jsgoupil commentedon Feb 16, 2019
I also had a question on StackOverflow that would get fixed with this feature request. The
useFactorycaches the data on a node. Which to me is quite problematic, I want the service to be always new when the constructor is being called.https://stackoverflow.com/questions/54726855/how-to-clear-the-provider-cache-to-use-with-usefactory-in-angular-7
mlc-mlapis commentedon Feb 16, 2019
@jsgoupil ... but there is already the correct answer on SO, so where is the problem?
jsgoupil commentedon Feb 17, 2019
@mlc-mlapis The "correct" answer is a workaround. Only took me a few hours to figure this out. There no explanation of cache for factory usage. This came to me as a surprise.
pauldraper commentedon Apr 1, 2019
This sounds like a really long way of saying that you like the name
'element'over'component'.Regardless of the name, the intention is the same: that we be able to inject additional things at the same level as ElementRef, ChangeDetectorRef, etc.
trotyl commentedon Apr 16, 2019
@pauldraper I do agree this feature is more meaningful than I original thought, but this is not only about naming, but concept. Even
elementis still wrong name, butnodeis correct, a directive can be made on comment node without corresponding element, resulting a node-level injector.providedIn: 'component'is totally wrong as it's nothing special for component.providedIn: 'directive'is misleading as it won't create new instance for each directive, but still locally singleton service for that node injector (all directives in that node share the same service).I agree it's valuable even with selector-less directive landing simultaneously (with most functionality shared), a unique feature for
providedIn: 'node'is the availability in child injectors, like:alxhub commentedon Apr 14, 2022
Component DI is "closed" - the set of provided services are determined entirely when the component is compiled, and cannot be expanded at runtime. This is done for performance, as component creation requires a lot of injection operations.
This means that we cannot support adding a provided service to a component after initialization, which would be necessary to implement providedIn: component.
angular-automatic-lock-bot commentedon May 15, 2022
This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.
Read more about our automatic conversation locking policy.
This action has been performed automatically by a bot.