Issue 1:
I'm submitting a ... (check one with "x")

[ ] bug report => search github for a similar issue or PR before submitting
[x] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

I'm not aware of any way to decorate existing injectable so one could use old version inside new one.

Expected behavior

I would be nice to have some way to decorate injectables. Maybe new type of provider - DecoratorProvider, which could work like this:

@Injectable()
class BetterFooService implements FooService {
constructor(
private fooService: FooService, // < not a cyclic dependency
private otherService: OtherService) {}
bar(): void {
console.log('better bar');
this.fooService.bar();
}
}

const PROVIDER: DecoratorProvider = { provide: FooService, useDecorator: BetterFooService };

@Directive({
selector: '[myDirective]',
providers: [PROVIDER]
})
class MyDirective {}

Minimal reproduction of the problem with instructions

Using FactoryProvider/ClassProvider to achieve similar effect causes Connot instantiate cyclic dependency! error:

https://plnkr.co/edit/YxGzVA1HNEjC1rXBmMQg?p=preview

What is the motivation / use case for changing the behavior?

Directive will have more freedom to modify behaviour of other components/directives.

Example use case:

Directive that decorates ViewContainerRef so it wraps every created component/embedded view in other component with animations. It could be used on RouterOutlet component to add animations when route changes.

Please tell us about your environment:

Angular version: 2.4.7

Browser: all

Language: all

Issue 2:
I'm submitting a...

[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Supersedes #14620 with more concrete summary and design.

Current behavior

It's currently not possible to wrap an existing provider in the same injector, which causing:

Class inheritance problems

#19181, #21697, #22011 (comment), ... (class extends ErrorHandler)

#18976 (class extends Location)

In Angular, the constructor is not consider public API due to the DI system, so extending class would be problematic and may require lots of boilerplate (declare deps again).

Customize existing service problems

#22024 (RendererFactory2)

#16184 (DefaultRouteReuseStrategy)

The declared class may not be same to the provided one, the latter may not be exposed to user.

Strategy pattern design problems

#3929 (Renderer)

#11309, #18554 (IterableDiffers)

#18155, #19852 (HttpInterceptor)

Extends behavior of some poor-designed services.

Expected behavior

API Design

The term decorator is not fit to this any more (due to other meanings), currently choose intercept for similar usage to HttpInterceptor. (still open to bikeshed)

An optional intercept property like multi applies to all kinds of providers:

@Injectable()
class CustomErrorHandler implements ErrorHandler {
constructor(private handler: ErrorHandler) {}

handleError(error: any) {
// custom error collecting or reporting
this.handler.handleError(error)
}
}

@NgModule({
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler, intercept: true }
]
})
class AppModule { }

Semantics

Ref: AngularJS Decorator API.

By setting intercept: true, it would allowed to inject (previous instance of) the token it providers, so it helps reusing existing logic (no need to rewrite) without modifying the consumer part (in same token).

So only combining with useClass or useFactory makes sense.

{ provide: TOKEN, useExisting: TOKEN, intercept: true } would not be allowed.

{ provide: TOKEN, useValue: VALUE, intercept: true } is technically valid but meaningless, could cause either warning or error.

Edge case: for multi: true provider, interceptor should transform the instances array, namely getting the existing array and returning a new array.

{ provide: TOKEN, useClass: CLASS, multi: true, intercept: true } would not be allowed, as it cannot return an array.

Inconsistent multi value between concrete provider and interceptor in same injector would not be allowed.

Scoping

Any interceptor would belong to a specific Injector thus supporting inheritance. For example:

@Component({
providers: [
{ provide: ErrorHandler, useFactory: func2, deps: [ErrorHandler], intercept: true },
]
})
class MyComponent {
constructor(handler: ErrorHandler) {
// handler -> func2(func1(/*original*/ErrorHandler))
}
}

@NgModule({
providers: [
{ provide: ErrorHandler, useFactory: func1, deps: [ErrorHandler], intercept: true },
]
})
class MyModule {
constructor(handler: ErrorHandler) {
// handler -> func1(/*original*/ErrorHandler)
}
}

Providing the normal instance won't be affected by interceptor in parent Injector. For example:

@Component({
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler },
]
})
class MyComponent {
constructor(handler: ErrorHandler) {
// handler -> CustomErrorHandler
}
}

@NgModule({
providers: [
{ provide: ErrorHandler, useFactory: func1, deps: [ErrorHandler], intercept: true },
]
})
class MyModule {
constructor(handler: ErrorHandler) {
// handler -> func1(/*original*/ErrorHandler)
}
}

Should intercepting multi: true providers at each level. For example:

(Without #18894)

@Component({
providers: [
{ provide: ERROR_HANDLERS, useValue: errorHandler3, multi: true },
{ provide: ERROR_HANDLERS, useValue: errorHandler4, multi: true },
{ provide: ERROR_HANDLERS, useFactory: func2, deps: [ErrorHandler], multi: true, intercept: true },
]
})
class MyComponent {
constructor(handler: ErrorHandler) {
// handler -> func2([errorHandler3, errorHandler4])
}
}

@NgModule({
providers: [
{ provide: ERROR_HANDLERS, useValue: errorHandler1, multi: true },
{ provide: ERROR_HANDLERS, useValue: errorHandler2, multi: true },
{ provide: ERROR_HANDLERS, useFactory: func1, deps: [ErrorHandler], multi: true, intercept: true },
]
})
class MyModule {
constructor(handler: ErrorHandler) {
// handler -> func1([errorHandler1, errorHandler2])
}
}

(With #18894)

@Component({
providers: [
{ provide: ERROR_HANDLERS, useValue: errorHandler3, multi: true },
{ provide: ERROR_HANDLERS, useValue: errorHandler4, multi: true },
{ provide: ERROR_HANDLERS, useFactory: func2, deps: [ErrorHandler], multi: true, intercept: true },
]
})
class MyComponent {
constructor(handler: ErrorHandler) {
// handler -> func2([...func1([errorHandler1, errorHandler2]), errorHandler3, errorHandler4])
}
}

@NgModule({
providers: [
{ provide: ERROR_HANDLERS, useValue: errorHandler1, multi: true },
{ provide: ERROR_HANDLERS, useValue: errorHandler2, multi: true },
{ provide: ERROR_HANDLERS, useFactory: func1, deps: [ErrorHandler], multi: true, intercept: true },
]
})
class MyModule {
constructor(handler: ErrorHandler) {
// handler -> func1([errorHandler1, errorHandler2])
}
}

Ordering

All interceptors are considered multi provider and applied last (local to Injector). For example:

@NgModule({
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler1 },
{ provide: ErrorHandler, useClass: CustomErrorHandler2, intercept: true },
{ provide: ErrorHandler, useClass: CustomErrorHandler3 },
{ provide: ErrorHandler, useClass: CustomErrorHandler4, intercept: true },
]
})
class AppModule {
constructor(handler: ErrorHandler) {
// handler -> new CustomErrorHandler4(new CustomErrorHandler2(new CustomErrorHandler3(/* ... */)))
}
}

Examples

Reduce class inheritance:

@Injectable()
class CustomErrorHandler implements ErrorHandler {
constructor(private handler: ErrorHandler) {}

handleError(error: any) {
// custom error collecting or reporting
this.handler.handleError(error)
}
}

@NgModule({
providers: [
{ provide: ErrorHandler, useClass: CustomErrorHandler, intercept: true }
]
})
class AppModule { }

Protect private APIs:

@NgModule({
providers: [
{ provide: RendererFactory2, useClass: CustomRendererFactory2, intercept: true }
]
})
class AppModule { }

Applying new strategy:

Actually this is not the best way to solve that problem, exposing DifferFactory as a multi provider would be better, but this could work without changing existing APIs.

@Injectable()
class CustomIterableDiffers implements IterableDiffers {
constructor(private differs: IterableDiffers) {}

get factories(): IterableDifferFactory[] { return this.differs.factories }

find(iterable: any): IterableDifferFactory {
if (iterable[SOME_SPECIAL_TOKEN]) {
return superPerformantDifferFactory
}
return this.differs.find(iterable)
}
}

@NgModule({
providers: [
{ provide: IterableDiffers, useClass: CustomIterableDiffers, intercept: true }
]
})
class AppModule { }

Custom control over existing strategy:

const disabledInterceptors = new WeakMap<HttpRequest, Set<Type<HttpInterceptor>>>()

@NgModule({
providers: [
{ provide: HTTP_INTERCEPTORS, useFactory: enableFiltering, multi: true, intercept: true },
InterceptorControlService,
]
})
class AppModule { }

function enableFiltering(interceptors: HttpInterceptor) {
return interceptors.map(interceptor => ({
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (disabledInterceptors.has(req) && disabledInterceptors.get(req).has(interceptor.constructor)) {
return next.handle(req)
}
return next.handle(interceptor.intercept(req, next))
}
}))
}

@Injectable()
class InterceptorControlService {
disable(request: HttpRequest, interceptorType: Type<HttpInterceptor>) {
if (!disabledInterceptors.has(request)) {
disabledInterceptors.set(request, new Set())
}
disabledInterceptors.get(request).add(interceptorType)
}
}

@Component({ /*...*/ })
class MyComponent {
constructor(http: HttpClient, interceptorControl: InterceptorControlService) {
const request = http.post(/*...*/)
interceptorControl.disable(request, SomeCustomInterceptor)
request.subscribe(/*...*/)
}
}

Implementation

Each Injector should hold a internal slot of interceptors, a naive logical demo could be:

// Didn't consider `multi: true` yet
class Injector_ {
private interceptedInjectables = new Map</*token*/any, /*instance*/any>()
private providerInterceptors = new Map</*token*/any, /*interceptors*/Provider[]>()

hasInterceptors(token: any) {
return this.providerInterceptors.has(token) || (!this.hasLocalProvider(token) && this.parent.hasInterceptors(token))
}

getInterceptors(token: any) {
return [...(this.hasLocalProvider(token) ? [] : this.parent.getInterceptors(token)), this.providerInterceptors.get(token)]
}

get(token: any) {
if (this.interceptedInjectables.has(token)) {
return this.interceptedInjectables.get(token)
}
if (this.hasInterceptors(token)) {
const raw = getRawProviderSomehow(token)
const interceptors = this.getInterceptors(token)
const res = interceptors.reduce(
(previousInstance, currentProvider) => instantiateSomehow(currentProvider, previousInstance), raw)
this.interceptedInjectables.set(token, res)
return res
}
// original logic
}
}

For performance consideration (static ones):

To get rid of mapping, the interceptors slot should be directly added to providerDef instance via Compiler;

If an injector has only provider interceptor but not concrete provider, use a providerDef with value of special symbol NO_VALUE to ship interceptors;

Tree-shakable consideration (static ones):

Extend @Inject() decorator to accept an additional optional parameter:

@Component({ /*...*/ })
class MyComponent {
constructor(
@Inject(HttpClient, logRequest) http: HttpClient
) {}
}

// makeParamDecorator('Inject', (token: any, ...interceptors: any[]) => ({token, interceptors}))

Or introduce a new decorator:

@Component({ /*...*/ })
class MyComponent {
constructor(
@Intercept(logRequest) http: HttpClient
) {}
}

// makeParamDecorator('Intercept', (...interceptors: any[]) => ({interceptors}))

Child injectors should not be affected in this way.

Minimal reproduction of the problem with instructions

Could go to #14620 for repro of current behavior.

What is the motivation / use case for changing the behavior?

Provide a demo of how to raise concrete feature request

Make the DI mechanism more powerful and useful.

Environment

Angular version: X.Y.Z

Browser:
- [ ] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX

For Tooling issues:
- Node version: XX
- Platform:

Others: