How to use ContentChild with ng-content without specifying the exact class of the content projected component?

457 Views Asked by At

I'm trying to create a general purpose container component for all tables so they all have same headers (possible buttons, filter-chips, search-field). I'm planning to use content projection with ng-content and passing data to the table using ContentChild. But this scenario works ONLY if the ContentChild is given the exact class of the component and not the base class. So I can't make it a general purpose container component that can be given any table component that extends the base class.

So I have

component-one.html

<app-table-container [data]="tableData">
  <app-some-table></app-some-table>
</app-table-container>

table-container.component.html

<!-- here above the content projected table would be common
 stuff for all tables -->
<ng-content select="[tableComponent]"></ng-content>

table-container.component.ts

@Component({
  selector: 'app-table-container',
  templateUrl: './table-container.component.html',
  styleUrls: ['./table-container.component.scss']
})
export class TableContainerComponent implements AfterContentInit {
  @Input() data: ITableStuff;
  @ContentChild(TableBaseComponent) tableComponent: TableBaseComponent;
  
  ngAfterContentInit(): void {
    // below fails because this.tableComponent is undefined
    this.tableComponent.data = this.data;
  }
}

table-base.component.ts

@Component({
  template: ''
})
export class TableBaseComponent implements AfterViewInit {
  @Input() data: ITableStuff;

  ngAfterViewInit(): void {
    console.log(this.data.name);
  }
}

some-table.ts

@Component({
  selector: 'app-some-table',
  template: '<p>some-table works!</p>'
})
export class SomeTableComponent extends TableBaseComponent implements {
  
  constructor() {
    super();
  }
}

However in TableContainerComponent.ngAfterContentInit the this.tableComponent is undefined. If I have:

@ContentChild(SomeTableComponent) tableComponent: SomeTableComponent;

it works. But then there's nothing general purpose in my container component.

1

There are 1 best solutions below

2
kemsky On

You have to create interface ISomeInterface and InjectionToken<ISomeInterface>:

export interface ISomeInterface { data: string[] }

export const mytoken = new InjectionToken<ISomeInterface>('ISomeInterface');

Make each table-like component implement interface and provide itself as InjectionToken<ISomeInterface>:

@Component({
    providers: [{provide: mytoken, useExisting: forwardRef(() => TableComponent)}]
})
export class TableComponent implements ISomeInterface {
    @Input()
    data: string[];
}

and then use it in ContentChild(mytoken):

@ContentChild(mytoken) tableComponent: ISomeInterface;

Instead of token you can use custom base class.