Создание динамического повторителя с включением ng-контента

Я пытаюсь создать универсальный компонент, привязанный к массиву произвольных объектов, который позволяет динамически добавлять и удалять строки, когда представление каждой строки также произвольно определяется компонентами главным компонентом, который его использует.

Обратите внимание, что MasterComponent — это произвольный компонент, который будет реализован для разных страниц и предназначен для самостоятельной работы и не определяется какими-либо метаданными или внешним источником.

На данный момент у меня есть следующие компоненты:

Шаблон RepeaterComponent:

<input type="button" value="Add" (click)="addRow()">
<div class="repeater" *ngFor="let row of repeaterArray">
    <div class="repeaterRow">
        <input type="button" value="Remove" (click)="removeRow(row.rowId)">
        <ng-content select="row"></ng-content>
     </div>
</div>

Шаблон MasterComponent:

<repeater [repeaterArray]="repeaterObj">
    <row>
        <field-textbox [data]="row.name" [label]="'Name'"></field-textbox>
        <field-textbox [data]="row.description" [label]="'Description'"></field-textbox>
    </row>
</repeater>

Компонент <field-textbox> — это пользовательский компонент, который я использую для инкапсуляции простых входных данных, содержащих некоторые дополнительные данные, которые мне нужно использовать.

MasterComponent содержит объект, который для этого экземпляра выглядит следующим образом:

repeaterObj = [
{
    "rowId": 1,
    "name": "First brand",
    "description": "First description"
},
{
    "rowId": 2,
    "name": "Second brand",
    "description": "Second description"
},
{
    "rowId": 3,
    "name": "Third brand",
    "description": "Third description"
}
];

У этого подхода есть две проблемы, для которых я не могу найти решения.

  1. Селектор ng-content идентичен для всех строк, когда ngFor дублирует шаблон, что оставляет мне только одну точку включения ng-content после рендеринга.
  2. Нет ссылки на переменную row из объявления ngFor во включенных директивах <field-textbox>, поэтому я не могу правильно связать данные.

Есть ли лучший подход к реализации RepeaterComponent, который дал бы мне наименьшее количество усилий для создания новых MasterComponents различных произвольных структур и разных шаблонов?


person VRuter    schedule 27.07.2016    source источник


Ответы (1)


Для этого вы можете использовать ngTemplateOutlet.

Ниже приведены шаги по реализации динамического повторителя:

Первый шаг — предоставить TemplateRef в качестве дочернего элемента RepeaterComponent:

<repeater [repeaterArray]="repeaterObj">
  <ng-template>
    ...
  </ng-template>
</repeater>

Второй шаг — запросить этот шаблон в RepeaterComponent через @ContentChild:

export class RepeaterComponent { 
  @ContentChild(TemplateRef) itemTemplate: TemplateRef<any>;
  ...

Третий шаг — использование ngTemplateOutlet для отображения шаблона:

@Component({
  selector: 'repeater',
  template: `
    <input type="button" value="Add" (click)="addRow()">
    <div class="repeater" *ngFor="let row of repeaterArray">
        <div class="repeaterRow">
            <input type="button" value="Remove" (click)="removeRow(row.rowId)">
            <ng-template <== this line
                    [ngTemplateOutlet]="itemTemplate"
                    [ngTemplateOutletContext]="{ $implicit: row }">
                </ng-template>
        </div>
    </div>`
})
export class RepeaterComponent { 
  @Input() repeaterArray: Array<any>;
  @ContentChild(TemplateRef) itemTemplate: TemplateRef<any>;
  ...
}

Четвертый шаг — использовать ссылку на row внутри TemplateRef внутри MasterComponent (вернемся к нашему первому шагу):

<repeater [repeaterArray]="repeaterObj">
  <template let-row>
    <field-textbox [data]="row.name" [label]="'Name'"></field-textbox>
    <field-textbox [data]="row.description" [label]="'Description'"></field-textbox>
  </template>
</repeater>

Обратите внимание: мы передаем ngOutletContext подобный объект со свойством $implicit.

использование ключа $implicit в объекте контекста установит его значение по умолчанию.

Это работает следующим образом:

[ngTemplateOutletContext]="{ $implicit: row }"  ==> <template let-row>

[ngTemplateOutletContext]="{ item: row }"       ==> <template let-row="item">

ngOutletContext доступен только с Angular 2 версии 2.0.0-rc.2

Вы можете попробовать соответствующий plunkr (обновлено до 5.0.0)

person yurzui    schedule 27.07.2016
comment
Похоже, это именно то, что я искал, спасибо! Единственное, что, похоже, работает не так, как ожидалось, — это двусторонняя привязка к компоненту field-textbox с помощью [(ngModel)]. Следующий Plunker демонстрирует проблему при попытке изменить два разных поля ввода с помощью такая же привязка. В шаблоне компонента приложения вы можете видеть, что обычная привязка <input> работает по назначению, а также обновляет привязку объекта, но изменение текстового поля ‹field-textbox› не применяет привязку обратно к объекту. - person VRuter; 27.07.2016
comment
Попробуйте использовать EventEmitter для двусторонней привязки, например plnkr.co/edit/XpEDWD75bfnEtibKDKW7?p=preview - person yurzui; 27.07.2016
comment
Да, это работает. Я ожидаю, что привязка будет правильно распространяться без необходимости реализации двусторонней привязки для компонента ‹field-textbox›, хотя привязка работает при использовании вне шаблона с [ngTemplateOutlet]. - person VRuter; 27.07.2016
comment
Это то же самое поведение, что и без ngTemplateOutlet plnkr.co/edit/ZP88WQsexq3UQpLTNriA?p=preview. . ngTemplateOutlet не творит чудес. Это просто передача данных внутри TemplateRef - person yurzui; 27.07.2016
comment
Ах, ты прав. Я думал о привязке к определенному экземпляру класса, а не к свойству JSON. Спасибо еще раз! - person VRuter; 27.07.2016