【问题标题】:Declare a component with generic type声明一个具有泛型类型的组件
【发布时间】:2018-04-04 06:36:02
【问题描述】:

是否可以在 Angular 4 中声明具有泛型类型的组件?

以下代码导致构建错误:

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

    //...
}

执行ng serve时的错误是:

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

示例:

以下示例尝试实现一个通用数据表,其中@Input() data 从一个“调用此组件”的组件更改为另一个。 问题是BehaviorSubject&lt;any[]&gt; 是否可以更改为BehaviorSubject&lt;T[]&gt;,其中T 将是传递给组件的泛型类型?

@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() {}
}

【问题讨论】:

  • 框架如何知道组件的通用参数应该是什么?
  • 你想用这个解决什么问题?
  • @jonrsharpe 我正在尝试实现一个列出数据的通用组件。数据模型的类型可能会根据使用此组件的页面而改变。我编辑了我的问题,以举例说明我在类中使用泛型类型的位置
  • @jonrsharpe 您的意思是使用通用组件是一个糟糕的概念?从技术上讲,我可以使用类型 any 而不是 generic 类型,但这是一个好习惯吗?
  • 目前还不清楚您要达到的目标,因此上面的评论。如果没有上下文,例如您想抽象出两个具体的 Ts 的示例,这可能是一个 XY 问题(参见例如 xyproblem.info)。

标签: angular typescript angular-components typescript-generics


【解决方案1】:

您也可以像这样通过 ViewChild 访问 Type 参数:

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);
  }
}

【讨论】:

  • 那么如何使用BarComponentT 类型参数呢?目前还不清楚当BarComponent 被实例化时,你期望收到什么作为参数?当您控制其实例化时使用通用具体类是有意义的(因此您可以控制要传递的参数)。但在组件的情况下——它是实例化它的角度框架本身——并且框架不知道你的参数并且将无法传递有意义的东西
  • 实际上 angular 足够聪明,可以从输入中找出T。假设您的通用组件中有Input() data: T。当您使用该组件并分配 [data]="someTypedDataVariable" 时,角度编译器将使用 someTypedDataVariable 的类型来实例化通用组件。这就是角度材料表(基于 cdk 表)的工作原理。这与 Angular 语言服务配对还将为您提供类型安全 + typeahead iniside mat-table 列定义。供参考:github.com/angular/components/blob/master/src/cdk/table/…
【解决方案2】:

你可以声明它,但不能直接使用它。你可以这样做:

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(); }
}

【讨论】:

  • 此方法有效,但我仍在寻找更通用的组件形式:我不使用...extends Form&lt;ModelOne&gt;,而是寻找export class ModelOneForm extends Form&lt;T&gt; 之类的东西,并且此代码会引发编译错误消息Cannot find name 'T'
  • @Componet? ?
  • @KarolDepka,应该是一个类型;我修好了它。 :)
【解决方案3】:

你可以这样考虑。为数据创建一个接口,如下所示:

interface ListItem {
  info: string;
  ...
}

将要列出的数据转换为符合接口,从而可以由 ListDataComponent 解释。然后你的 ListDataComponent 就可以根据界面中的属性列出数据了。

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() {
    }
}

【讨论】:

    【解决方案4】:

    我做了类似于 Jun711 的事情。我创建了一个接口,然后我的组件使用该接口。然后我只是为其他类扩展接口。我只是传入一个扩展接口类型的数组。

    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() {}
    }
    

    那么用法就很简单了:

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

    缺点是包含组件必须确定完整类型,但优点是随着我遵循 Liskov 和 ISP,我的组件变得更加可重用。

    【讨论】:

      【解决方案5】:

      我建议为每种显示的数据类型创建一个包含多个子组件的父列表组件,然后使用[ngSwitch]*ngSwitchCase 来确定要显示的内容。

      @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
      }
      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-04-02
        • 2018-12-14
        • 1970-01-01
        • 1970-01-01
        • 2015-12-14
        • 2019-04-02
        • 1970-01-01
        相关资源
        最近更新 更多