Gary McGill
Gary McGill

Reputation: 27516

Can I have a component that doesn't render a pseudo-element?

I'm trying to create a set of components that will allow me to render a table, where the "user" only needs to provide:

So, ideally, you might use these components to render a 3-column table like so:

<app-table [data]='sampleData'>
  <td>{{cell.toString().toUpperCase()}}</td>
  <td class="myClass">{{cell}}</td>
  <app-cell [cell]="cell"></app-cell>
</app-table>

...the idea being that the app-table component would render the three app-column components for every row, and magically provide the cell value to each column as cell. The user of the component then has full control over how each column is rendered - including what classes are applied to the td tag, etc.

I've not had much luck trying to achieve this. The best I've come up with so far can be seen in this StackBlitz, where I have an app-table component that renders the tr, and which expects its content to include one ng-template for each column. The markup is not nearly as clean as the pseudo-markup above:

<app-table [data]='sampleData'>
  <ng-template let-cell="cell"><td>{{cell.toString().toUpperCase()}}</td></ng-template>
  <ng-template let-cell="cell"><td class="myClass">{{cell}}</td></ng-template>
  <ng-template let-cell="cell"><td><app-cell [data]="cell"></app-cell></td></ng-template>
</app-table>

It's a bit ugly because of all the extra markup required to pass the data down into the template, but that's not the biggest problem.

You'll notice that the last "column" uses an app-cell component, but that component is still wrapped inside a td tag:

<ng-template let-cell="cell">
  <td>
    <app-cell [data]="cell"></app-cell>
  </td>
</ng-template>

That's not really what I wanted: I'd like the app-cell component to provide the td tag itself, since otherwise it can't (for example) set classes on that element.

Currently, the app-cell component has a HostBinding that adds a class ("foo"), but that gets added to the app-cell element itself, not to the td element where it's needed:

<td>
  <app-cell class="foo">
    <div>...</div>
  </app-cell>
</td>

The obvious thing to do is to move the td tag into the app-cell component, but if I do that, then the rendered HTML looks like this:

<app-cell class="foo">
  <td>
    <div>...</div>
  </td>
</app-cell>

[OK, so the class is still applied in the wrong place, but it's easy to see how to move it down to the td tag.]

However, that extra pseudo-element sitting between the tr and td elements worries me - the browser I'm using doesn't seem to mind, but I suspect that much of my CSS is going to be thrown by it, because there are selectors that expect the td to be a direct child of the tr.

Is there a way to get rid of that extra pseudo-element? Ordinarily I might look at using a directive, but I don't see how I can do that here, because the app-cell component needs a template, and directives don't have those...

Upvotes: 3

Views: 498

Answers (2)

Bunyamin Coskuner
Bunyamin Coskuner

Reputation: 8859

These types of components are my favorite. At the end of the day, you want something like following assuming you have a data like this

carData = [{
   model: 'Mercedes',
   year: 2015,
   km: 10000
}, {
   model: 'Audi',
   year: 2016,
   km: 5000
}];

And you want to display first two columns as is and for the last one you want to display some custom template.

<app-table [data]="carData">
   <app-cell header="Model" field="model"></app-cell>
   <app-cell header="Year" field="year"></app-cell>
   <app-cell header="Km" field="km" styleClass="km-cell">
      <ng-template custom-cell-body let-car>
          {{car.km | number}} km
      </ng-template>
   </app-cell>
</app-table>

Here is what you can do, (I got this trick out of Angular Material and Primeng which works just fine)

You can find a working example here

First define a directive for custom body templates for cells. (You can do the same thing for headers as well)

@Directive({selector: '[custom-cell-body]'})
export class AppCellBodyTemplate {
  constructor(public template: TemplateRef<any>) {}
}

And let's define our AppCellComponent

app-cell.component.ts

@Component({
  selector: 'app-cell',
  template: `<ng-content></ng-content>`
})
export class AppCellComponent {
  @Input() header;
  @Input() field;
  @Input() styleClass;

  @ContentChild(AppCellBodyTemplate) customBody: AppCellBodyTemplate;
}

Let's glue them all together at AppTableComponent

app-table.component.ts

@Component({
  selector: 'app-table',
  templateUrl: './app-table.component.html' 
})
export class AppTableComponent {
  @Input() data: any[];

  @ContentChildren(AppCellComponent) cells: QueryList<AppCellComponent>;
}

app-table.component.html

  <table>
    <thead>
      <tr>
        <!-- loop through cells and create header part of the table -->
        <ng-container *ngFor="let cell of cells">
          <th>{{cell.header}}</th>
        </ng-container>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let row of data">
        <!-- loop through cells -->
        <ng-container *ngFor="let cell of cells">
          <td [ngClass]="cell.styleClass">
            <!-- if cell has a custom body, render that -->
            <ng-container *ngIf="cell.customBody; else defaultBody">
              <ng-container *ngTemplateOutlet="cell.customBody.template;
                context: {$implicit: row}">
              </ng-container>
            </ng-container>
            <!-- else render default cell body -->
            <ng-template #defaultBody>{{row[cell.field]}}</ng-template>
          </td>
        </ng-container>
      </tr>
    </tbody>
  </table>

Here are some points needed to be explained.

  • template: TemplateRef<any> within AppCellBodyTemplate Since, we are going to use custom-body-cell with ng-templates all the time, we can get a reference of the template defined within. This template will be used within *ngTemplateOutlet
  • @ContentChild(AppCellBodyTemplate) customBody within AppCellComponent is to detect whether there is a custom-body-cell defined as ContentChild
  • @ContentChildren(AppCellComponent) cells: QueryList<AppCellComponent> is to get instance of cells (you did the similar thing).
  • context: {$implicit: row} is to expose some data to the consumers of this component. $implicit makes you enable to retrieve this context with any template variable you want. Since, I used $implicit here, I was able to do this <ng-template custom-cell-body let-car> Otherwise, I would have to define my car variable something like this let-car="rowData"

Upvotes: 3

Gary McGill
Gary McGill

Reputation: 27516

Answering my own question - because I found an answer in the "related" sidebar: I can use an attribute selector rather than an element selector.

This StackBlitz shows the results.

Upvotes: 0

Related Questions