Injection token for a custom generic component constructor?

68 views Asked by At

I made a generic material autocomplete, which I would like to use for different API data e.g. countries, people, positions etc. Each of these data share common properties: id, name. Hence, I defined an interface:

export interface AutocompleteValue {
    id?: number,
    name: string
}

I also use services to fetch this data, each extending the same structure. There is always a getAll() method, which fetches the data. For this I used generic types, which are extended.

export class APIService<GetType> {
    getAll(): Observable<T[]> {...};
}

Finally, I have a generic autocomplete angular material component to be used for each of these types.

@Component({...})
export class AutoCompleteInput<Type extends AutocompleteValue, ServiceType extends APIService<Type>> implements ControlValueAccessor, MatFormFieldControl<Type | undefined> {
    ...
    constructor(protected service: ServiceType, ...) {}
    ...
}

If I want to use it somewhere:

export class PositionsAutoComplete extends AutoCompleteInput<GetDto, PositionService> {
    constructor(
        @Inject(POSITION_SERVICE_TOKEN) protected override service: PositionService, ...){}
}

It turns out that I need to provide injection tokens... @Inject(POSITION_SERVICE_TOKEN) and export const POSITION_SERVICE_TOKEN = new InjectionToken<PositionService>('');.

And in the app.component.ts:

providers: [{ provide: POSITION_SERVICE_TOKEN, useClass: PositionService }]

This works fine so far. However, AutocompleteInput also required an injection token and I don't know why? I have the following error at the constructor of AutoCompleteInput:

No suitable injection token for parameter 'service' of class 'AutoCompleteInput'. Consider using the @Inject decorator to specify an injection token.(-992003)

Update: I had a similar issue with generic services, where I only had to remove the @Injectable decorator from the generic class to resolve the issue. I think that this is similar and @Component instantiates the generic class, which requires an InjectionToken. If I remove the @Component, I can't use the template in the generic class (autocomplete-input.component.html).

3

There are 3 answers

1
M G On

You need to provide injection token because angular doesnt kbow what class it should provide for ServiceType. Do you have one or multiple services extending getAllProvider? If you have one, you can just provide it.

If you have many, then you cant do whatvyou want to do, at least not in this exact way. You will have to provide different in different components, depending what servoce does some component need

1
Saren Tasciyan On

I am not sure if I understand your problem but to me it looks like you don't need the second generic type at all:

@Component({...})
export class AutoCompleteInput<Type extends AutocompleteValue> implements ControlValueAccessor, MatFormFieldControl<Type | undefined> {
    ...
    constructor(protected service: APIService<Type>, ...) {}
    ...
}

That way Angular knows your type. I guess you were overcomplicating with the second type. I can't imagine at the moment, why you may want to make service type generic. It is not completely unimaginable though.

0
VPNer On

So far, the best workaround is a mixture of this article and @Saren Tasciyan's suggestion: https://www.damirscorner.com/blog/posts/20211210-GenericBaseComponentsInAngular.html

@Component({template: ''})
export abstract class AutoCompleteInput<Type extends AutocompleteValue> implements ControlValueAccessor, MatFormFieldControl<Type | undefined> {
    ...
    constructor(protected service: APIService<Type>, ...) {}
    ...
}

Then I will only have to duplicate the template for each derived class, which is not ideal but also not so bad. Note that I needed an abstract class to prevent this generic class from initiation.