首页 > 解决方案 > 如何在 Angular 6+ 中将子组件模板转入其父级?

问题描述

回到 AngularJS,我创建了一个简单的智能表格组件,我们的团队可以像这样使用它:

<smart-table items="vm.people">
    <column field="name">{{row.firstname}} {{row.lastname}}</column>
    <column field="age">{{vm.getAge(row.birthday)}}</column>
</smart-table>

这是一个人为的例子,但它是这样工作的。它生成了带有标题的表格,并将<column>标签的内部内容正确地用作每个单元格<td>元素)的模板。

现在,我正在尝试将其移植到 Angular (6+)。到目前为止,使用@ContentChildren我能够轻松提取列列表。

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

@Component({
    selector: 'app-root',
    template: `
<app-table [data]="data">
    <app-column title="name">{{name}}</app-column>
    <app-column title="age">{{birthday}}</app-column>
</app-table>
`,
})
export class AppComponent {
    data = [{
        name: 'Lorem Ipsum',
        birthday: new Date(1980, 1, 23),
    }, {
        name: 'John Smith',
        birthday: new Date(1990, 4, 5),
    }, {
        name: 'Jane Doe',
        birthday: new Date(2000, 6, 7),

    }];
}

@Component({
    selector: 'app-column',
    template: ``,
})
export class ColumnComponent implements OnInit {
    @Input() title: string;

    constructor() { }

    ngOnInit() {
    }

}

@Component({
    selector: 'app-table',
    template: `
<table>
    <thead>
    <th *ngFor="let column of columns">{{column.title}}</th>
    </thead>
    <tbody>
    <tr *ngFor="let row of data">
        <td *ngFor="let column of columns">
        <!-- <ng-container *ngTemplateOutlet="column.title;context:row" ></ng-container> -->
        </td>
    </tr>
    </tbody>
</table>
`,
})
export class TableComponent implements OnInit {
    @Input() data: any[];

    @ContentChildren(ColumnComponent) columns: QueryList<ColumnComponent>;

    constructor() { }

    ngOnInit() {
    }

}

这将呈现以下 HTML:

<table>
    <thead>
        <!--bindings={"ng-reflect-ng-for-of": "[object Object],[object Object"}-->
        <th>name</th>
        <th>age</th>
    </thead>
    <tbody>
        <!--bindings={"ng-reflect-ng-for-of": "[object Object],[object Object"}-->
        <tr>
            <!--bindings={"ng-reflect-ng-for-of": "[object Object],[object Object"}-->
            <td></td>
            <td></td>
        </tr>
        <tr>
            <!--bindings={"ng-reflect-ng-for-of": "[object Object],[object Object"}-->
            <td></td>
            <td></td>
        </tr>
        <tr>
            <!--bindings={"ng-reflect-ng-for-of": "[object Object],[object Object"}-->
            <td></td>
            <td></td>
        </tr>
    </tbody>
</table>

但现在我被困在试图将组件的内容插入模板中。我已经在这里阅读了几个答案(这个这个)。但是我遇到的问题是,要么您拥有一组静态模板,要么将它们插入静态位置。 就我而言,我需要在运行时使用该模板。<app-column><app-table>*ngFor

在 AngularJS 中,我使用了以下代码:

function compile(tElement) {
    const columnElements = tElement.find('column');
    const columns = columnElements.toArray()
        .map(cEl => ({
            field: cEl.attributes.field.value,
            header: cEl.attributes.header.value,
        }));

    const template = angular.element(require('./smart-table.directive.html'));

    tElement.append(template);

    // The core of the functionality here is that we generate <td> elements and set their
    // content to the exact content of the "smart column" element.
    // during the linking phase, we actually compile the whole template causing those <td> elements
    // to be compiled within the scope of the ng-repeat.
    const tr = template.find('tr[ng-repeat-start]');
    columnElements.toArray()
        .forEach(cEl => {
            const td = angular.element('<td/>')
                .html(cEl.innerHTML);
            tr.append(td);
        });
    const compile = $compile(template);

    // comment out originals
    columnElements.wrap(function () {
        return `<!-- ${this.outerHTML} -->`;
    });

    return function smartTableLink($scope) {
        $scope.vm.columns = columns;
        compile($scope);
    };
}

标签: angular

解决方案


多亏了 Meriton 的指点,这就是我最终得到的结果。

正如他所建议的,我将app-column组件更改为指令。该指令必须出现在ng-template元素上:

@Directive({
    selector: 'ng-template[app-column]',
})
export class ColumnDirective {
    @Input() title: string;
    @ContentChild(TemplateRef) template: TemplateRef<any>;
}

app-table组件变为:

@Component({
    selector: 'app-table',
    template: `
    <table>
        <thead>
        <th *ngFor="let column of columns">{{column.title}}</th>
        </thead>
        <tbody>
        <tr *ngFor="let row of data">
            <td *ngFor="let column of columns">
            <ng-container
                [ngTemplateOutlet]="column.template"
                [ngTemplateOutletContext]="{$implicit:row}">
            </ng-container>
            </td>
        </tr>
        </tbody>
    </table>
    `,
})
export class TableComponent {
    @Input() data: any[];
    @ContentChildren(ColumnDirective) columns: QueryList<ColumnDirective>;
}

虽然一开始我因为不得不与我的一些队友(还不是非常精通 Angular 的团队......)接触而有点ng-template反感,但出于几个原因,我最终喜欢上了这种方法。首先,代码仍然易于阅读:

<app-table [data]="data">
    <ng-template app-column title="name" let-person>{{person.name}}</ng-template>
    <ng-template app-column title="age" let-person>{{person.birthday}}</ng-template>
</app-table>

其次,因为我们必须在上面的代码中指定模板上下文的变量名let-person,它允许我们保留业务上下文。所以,如果我们现在谈论动物而不是人(在不同的页面上),我们可以很容易地写:

<app-table [data]="data">
    <ng-template app-column title="name" let-animal>{{animal.name}}</ng-template>
    <ng-template app-column title="age" let-animal>{{animal.birthday}}</ng-template>
</app-table>

最后,实现完全保留对父组件的访问。所以,假设我们添加一个year()从生日中提取年份的方法,在app-root组件上,我们可以像这样使用它:

<app-table [data]="data">
    <ng-template app-column title="name" let-person>{{person.name}}</ng-template>
    <ng-template app-column title="year" let-person>{{year(person.birthday)}}</ng-template>
</app-table>

推荐阅读