77

Is it possible to declare a component with a generic type in Angular 4?

The following code causes build errors:

export class MyGenericComponent<T> implements OnInit {
    @Input()  data: BehaviorSubject<T[]>;

    //...
}

The error when executing ng serve is:

ERROR in C:/.../my-generic.module.ts (5,10): Module '"C:/.../my-generic.component"' has no exported member 'MyGenericComponent'.

Example:

The following example is an attempt to implement a generic data table where @Input() data changes from one component 'calling this component' to another. The question is could BehaviorSubject<any[]> be changed to BehaviorSubject<T[]> where T would be the generic type passed to the component?

@Component({
  selector: 'my-data-list',
  templateUrl: './data-list.component.html',
  styleUrls: ['./data-list.component.css']
})
export class DataListComponent implements OnInit {
  @Input()  data: BehaviorSubject<any[]>;
  @Output() onLoaded = new EventEmitter<boolean>();

  private tableDataBase : TableDataBase = new TableDataBase();
  private dataSource : TableDataSource | null;

  constructor() { }

  ngOnInit() {
    this.tableDataBase.dataChange = this.data;
    this.dataSource = new TableDataSource(this.tableDataBase);
    this.onLoaded.emit(true);
  }
}

class TableDataBase {
  dataChange: BehaviorSubject<any[]> = new BehaviorSubject<any[]>([]);

  get data(): any[] {
    return this.dataChange.value;
  }
}

class TableDataSource extends DataSource<any> {

  constructor(private tableDataBase: TableDataBase) {
    super();
  }

  connect(): Observable<any[]> {
    return Observable.of(this.tableDataBase.data);
  }

  disconnect() {}
}
Strider
  • 3,539
  • 5
  • 32
  • 60
  • 1
    How would the framework know what the generic argument should be for the component? – Igor Oct 23 '17 at 16:19
  • 2
    What's the problem you're trying to solve with this? – jonrsharpe Oct 23 '17 at 16:21
  • 4
    @jonrsharpe I am trying to implement a generic component that lists data. The type of the data model may change depending on the page using this component. I edited my question to give an example to where I am using the generic type in the class – Strider Oct 23 '17 at 17:54
  • I understand the idea of a generic *class*, but how can a *component* do this? Surely the bindings will be different for each T? Sure you can extract common methods to a superclass, but beyond that it's unclear what you expect. – jonrsharpe Oct 23 '17 at 18:11
  • 1
    @jonrsharpe You mean that it's a bad concept to work with generic components? Technically, I could work with the type _any_ instead of a _generic_ type, but would it be a good practice? – Strider Oct 24 '17 at 09:25
  • 1
    It's not really clear what you're trying to achieve, hence the comment above. Without the context, like an example of two concrete `T`s you'd like to abstract out, this is likely an XY problem (see e.g. http://xyproblem.info). – jonrsharpe Oct 24 '17 at 09:26
  • 1
    @jonrsharpe I've just added an example in my post, I hope it could help – Strider Oct 24 '17 at 10:09

5 Answers5

51

You can also access the Type parameter through the ViewChild like this:

export class Bazz {
  name: string;

  constructor(name: string) {
    this.name = name;   
  }
}

@Component({
  selector: 'app-foo',
  template: `<div>{{bazz?.name}}</div>`,
  exportAs: 'appFoo'
})
export class FooComponent<T> {
  constructor() {}
  private _bazz: T;

  set bazz(b: T) {
    this._bazz = b;
  }

  get bazz(): T {
   return this._bazz;
  }
}

@Component({
  selector: 'app-bar',
  template: `<app-foo #appFoo></app-foo>`,
  styleUrls: ['./foo.component.scss'],
})
export class BarComponent<T> implements OnInit {
  @ViewChild('appFoo') appFoo: FooComponent<Bazz>;

  constructor() {}

  ngOnInit() {
    this.appFoo.bazz = new Bazz('bizzle');
    console.log(this.appFoo.bazz);
  }
}
Matt Ringer
  • 1,358
  • 11
  • 17
  • 2
    So how to you use `T` type argument of the `BarComponent`? It's still unclear what do you expect to receive as an argument, when `BarComponent` is instantiated? It's make sense to use generic concrete class when you are controlling its instantiation (so you can control what argument to pass). But in case of component - it's the angular framework itself which instantiates it - and the framework is not aware of your parameter and will not be able to pass something making sense – Denis Itskovich Jun 29 '19 at 03:08
  • 22
    Actually angular is smart enough to figure out the `T` from inputs as well. Lets say you have `Input() data: T` in your generic component. When you use that component and assign `[data]="someTypedDataVariable"` the angular compiler will use the type of `someTypedDataVariable` to instanciate the generic component. This is how angular material table (based on cdk table) works. This paired with the angular language service will ALSO provide you with type safety + typeahead iniside mat-table column definitions. For reference: https://github.com/angular/components/blob/master/src/cdk/table/table.ts – ntziolis Feb 23 '20 at 10:58
10

You can declare it, but cannot use it directly. You can do something like this:

export abstract class Form<T> implements OnInit, OnChanges {
  someMethod() { throw 'Dont use directly' }
  otherMethod() { return 'Works!'; }
  // Note that below will cause compilation error
  //   TypeError: Object prototype may only be an Object or null: undefined
  // You cannot use protected in this usecase
  protected anotherMethod() { }
}

@Component({})
export class ModelOneForm extends Form<ModelOne> {
  someMethod() { return this.otherMethod(); }
}
biolauri
  • 535
  • 7
  • 18
Sasxa
  • 40,334
  • 16
  • 88
  • 102
  • This method works, but I am still looking for more generic form of a component: Instead of using `...extends Form`, I am looking for something like `export class ModelOneForm extends Form`, and this code throws a compilation error with the message `Cannot find name 'T'` – Strider Oct 23 '17 at 17:39
  • 1
    Hmm `@Componet?` ? – KarolDepka Dec 20 '17 at 21:49
  • @KarolDepka, should be a type; i fixed it. :) – biolauri Oct 12 '18 at 11:26
2

You can consider this way. Create an interface for the data such as following:

interface ListItem {
  info: string;
  ...
}

Transform the data that you want to list to comply with the interface and thus can be interpreted by the ListDataComponent. Your ListDataComponent can then list the data according to the properties in the interface.

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'data-list',
  templateUrl: './data-list.component.html',
  styleUrls: ['./data-list.component.scss']
})
export class DataListComponent implements OnInit {
    @Input() public items: ListItem[];

    constructor() {
    }

    ngOnInit() {
    }
}
Jun
  • 2,942
  • 5
  • 28
  • 50
0

I would recommend creating a parent list component with multiple child components for each type of data displayed, then use [ngSwitch] and *ngSwitchCase to determine what to display.

@Component({
  selector: 'app-list',
  template: `
    <ng-container *ngFor="let item in list$ | async" [ngSwitch]="item.type">
      <app-list-item-one [item]="item" *ngSwitchCase="listItemType.One"></app-list-item-one>
      <app-list-item-two [item]="item" *ngSwitchCase="listItemType.Two"></app-list-item-two>
    </ng-container>
  `
})
export class ListComponent extends OnInit {
  list$: Observable<ListItem[]>

  constructor(
    private listApi: ListApiService
  ) { }

  ngOnInit() {
    this.list$ = this.listApi.getList(...)
  }
}

@Component({
  selector: 'app-list-item-one',
  template: `
    {{ item.aProperty }}
  `
})
export class ListItemOneComponent {
  @Input() item: ListItemOne
}

@Component({
  selector: 'app-list-item-two',
  template: `
    {{ item.bProperty }}
  `
})
export class ListItemTwoComponent {
  @Input() item: ListItemTwo
}

export class ListItem {
  id: string
}

export class ListItemOne {
  aProperty: string
}

export class ListItemTwo {
  bProperty: string
}

Trevor
  • 13,085
  • 13
  • 76
  • 99
  • This looks like a cleaver approach to me, rather than forcing the consumer to pick the correct component for the job. I don't know why it was downvoted. – Ε Г И І И О Jul 18 '22 at 11:04
0

I did something similar to Jun711. I created an interface, and then my component uses that interface. Then I just extend the interface for the other classes. I just pass in an array of a type that extends the interface.

export interface INameable {
    name: string;
}

export interface IPerson extends INameable { title: string; }
export interface IManager extends INameable { Employees: IPerson[]; }

@Component({
    selector: 'nameable',
    templateUrl: './nameable.component.html',
    styleUrls: ['./nameable.component.scss'],
})
export class NameableComponent implements OnInit {

    @Input() names: INameable[] = [];
    @Output() selectedNameChanged = new EventEmitter<INameable>();

    constructor() {}
    ngOnInit() {}
}

then usage is pretty simple:

<nameable [names]="PersonList" (selectedNameChanged)="personChangedHandler($event)"></nameable>
<nameable [names]="ManagerList" (selectedNameChanged)="mangagerChangedHandler($event)"></nameable>

The downside is that the containing component has to determine the full type, but the upside is my components become more reusable as I follow Liskov & ISP.

Scott I
  • 71
  • 1
  • 2