4

I have a recursive data structure (recursive via children property) as below:

export interface IExecutableLog {
    round: number;
    log: string;
}

export interface IExecutableResult {
    uid: string;
    name: string;
    desc: string;
    status: string;
    passedRounds: number;
    totalRound: number;
    children?: IExecutableResult[];
    statementType?: string;
    logs?: IExecutableLog[];
}

export interface ISummary {
    title: string;
    jobName: string;
    timestamp: Date;
    tester: string;
    result: string;
    executionId: string;
    testJobId: string;
    resultDetails: IExecutableResult;
}

And I want to display data of Isummary type from backend.

I tried define a component as below:

// pats-report-element.component.ts
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { IExecutableResult } from '../pats-report.interface';

@Component({
    selector: 'pats-report-element',
    templateUrl: 'app/report/basic/pats-report-element.html',
    encapsulation: ViewEncapsulation.None,
})
export class  PatsReportElementComponent {
    @Input()
    public data: IExecutableResult;
}
// app/report/basic/pats-report-element.html
<tr>
    <td>{{data?.name}}</td>
    <td>{{data?.status}}</td>
    <td>{{data?.rounds}}</td>
    <td>{{data?.passedRounds}}</td>
    <td>{{data?.rounds > 0 ? (data.passedRounds / data.rounds) * 100 + "%" : "NA"}}</td>
    <td>{{data?.timestamp | date:"y-MM-dd HH:mm:ss"}}</td>
</tr>

<template [ngIf]="data && data.children">
    <template ngFor let-item [ngForOf]="data.children" let-i="index">
        <pats-report-element [data]="item"></pats-report-element>
    </template>
</template>

But the rendered DOM will include the host element 'pats-report-element' which is not valid inside a <table></table>tag. The DOM is as below: enter image description here

I checked the angular2 doc, and seems attribute-directives is the right choice. But I cannot find a way to use template with attribute-directives just like what I do in PatsReportElementComponent.

So what's the correct way to achieve my goal?

[Update 1]

Tried @Günter Zöchbauer's suggestion, still not resolve my issue. The rendered DOM is still not as expected (the <tr></tr> still not being flattened).

enter image description here

ricky
  • 2,058
  • 4
  • 23
  • 49
  • Looks like http://stackoverflow.com/questions/37746516/use-component-in-itself/37747022#37747022 and http://stackoverflow.com/questions/38716105/angular2-render-a-component-without-its-wrapping-tag/38716164#38716164 – Günter Zöchbauer Aug 24 '16 at 09:28
  • I don't think there is a way to do that. I think you need to flatten the data and then do `*ngFor` over the flattened data. – Günter Zöchbauer Aug 25 '16 at 08:51
  • @GünterZöchbauer Yeah, seems like that's really not working ... not so nice. Flattening the data before providing it to the user can make the app quite slow, when large data is fetched. – PeMa Apr 26 '18 at 12:37

4 Answers4

1

You can use an attribute selector

@Component({
  selector: '[pats-report-element]',

then you can add your element like

<tr pats-report-element [data]="item"></tr>

There is no way to get an element rendered without it's selector element.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Thanks for your answer! Can we use attribute selector with @Component? I only saw using attribute selector with @Directive. Would you please point where is the document? I've updated my code according to your answer, but there is exception: ` Template parse errors: Can't bind to 'data' since it isn't a known property of 'pats-report-element'. 1. If 'pats-report-element' is an Angular component and it has 'data' input, then verify that it is part of this module. ...` – ricky Aug 24 '16 at 09:42
  • Works the same with `@Component()`. A component **is a** directive ;-) just with an additional view. – Günter Zöchbauer Aug 24 '16 at 09:44
  • Got exception: **zone.js:461 Unhandled Promise rejection: Template parse errors: Can't bind to 'data' since it isn't a known property of 'pats-report-element'. 1. If 'pats-report-element' is an Angular component and it has 'data' input, then verify that it is part of this module. 2. If 'pats-report-element' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schema' of this component to suppress this message.** Could you see anything wrong? – ricky Aug 24 '16 at 09:52
  • Fix the exception and tried your approach, but still not get what I want. Please see my update on the origin post. Thanks again! – ricky Aug 25 '16 at 08:51
1

After many efforts, I still failed to get it done with native angular2. And I ended up with a 3rd party library--ag-grid-ng2. It works well except it's overkill for my requirements.

ricky
  • 2,058
  • 4
  • 23
  • 49
0

I am not sure if it will solve your problem, in the component you are referring to the same component recursively in the template, you should self-reference it in your directive.

// pats-report-element.component.ts import { Component, Input, ViewEncapsulation } from '@angular/core'; import { IExecutableResult } from '../pats-report.interface';

@Component({
    selector: 'pats-report-element',
    directives: [forwardRef(() => PatsReportElementComponent )], // <<<<<< change
    templateUrl: 'app/report/basic/pats-report-element.html',
    encapsulation: ViewEncapsulation.None,
})
export class  PatsReportElementComponent {
    @Input()
    public data: IExecutableResult;
}
// app/report/basic/pats-report-element.html
<tr>
    <td>{{data?.name}}</td>
    <td>{{data?.status}}</td>
    <td>{{data?.rounds}}</td>
    <td>{{data?.passedRounds}}</td>
    <td>{{data?.rounds > 0 ? (data.passedRounds / data.rounds) * 100 + "%" : "NA"}}</td>
    <td>{{data?.timestamp | date:"y-MM-dd HH:mm:ss"}}</td>
</tr>

<template [ngIf]="data && data.children">
    <template ngFor let-item [ngForOf]="data.children" let-i="index">
        <pats-report-element [data]="item"></pats-report-element>
    </template>
</template>

See if this works for you.

Savaratkar
  • 1,974
  • 1
  • 24
  • 44
0

Recursive tables

You can put another <table> for level's childes inside <my-component-selector>. Just do it after current level's table tag closed:

<table>
    <tbody>
        <tr>
            <td>{{level.id}}</td>
            <td>{{level.title}}</td>
        </tr>
    </tbody>
</table>
<div *ngFor="let item of level.childLevels">
    <my-component-selector [level]="item">
         <!-- Here will be inserted another instance of this HTML file with itself table and itself recursion -->
    </my-component-selector>
</div>

Where you have component for each hierarhy level like this:

@Component({
    selector: 'my-component-selector',
    templateUrl: './my-recursive.component.html'
})
export class MyRecursiveComponent implements OnInit {

    @Input() level: LevelItem;

    onInit() {
        if (!level) {
             loadRootLevelByNetwork();
        }
    }

}

Here level variable are set from outer component by [level]="item" in HTML or loaded by network (for root component).

Evgeny Nozdrev
  • 1,530
  • 12
  • 15