Как я могу включить шаблон дочернего компонента в его родительский в 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);
    };
}
2
0
1 082
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

В Angular проекция контента и создание шаблона - это разные вещи. То есть проекция контента проектирует узлы существующий, и вам понадобится структурная директива для создания узлов по запросу.

То есть column должен стать структурной директивой, которая передает свой шаблон родительскому smart-table, который затем может добавлять его в свой ViewContainer так часто, как это необходимо.

Спасибо. Именно такое руководство я искал! Мы исследуем этот маршрут и продолжим.

Eric Liprandi 13.09.2018 23:04
Ответ принят как подходящий

Вот что у меня получилось благодаря указателям Меритон.

Как он предложил, вместо этого я заменил свой компонент 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>;
}

Хотя сначала меня немного смутило то, что мне пришлось показывать ng-template некоторым из моих товарищей по команде (пока не очень разбирающейся в Angular команде ...), в итоге мне понравился этот подход по нескольким причинам. Во-первых, код по-прежнему легко читается:

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

Другие вопросы по теме