Angular 2 transclusion: Могу ли я передать содержимое слота вверх родительскому компоненту - PullRequest
5 голосов
/ 26 февраля 2020

У меня есть внешний компонент (сине-зеленый) с несколькими панелями инструментов flexbox с кучей моих ui-button кнопок. И внутренний компонент, в основном выполняющий свои функции в коричневой области (как и следовало ожидать).

Однако зависит на внутреннем компоненте (есть несколько переключаемых вперед и назад), в эту верхнюю / нижнюю панель необходимо вставить еще несколько контекстных кнопок.

(CSS трюки с абсолютным позиционированием) снаружи это не вариант, в зависимости от размера и удобства внешние панели инструментов могут сильно различаться по положению, размеру и т. д.)


Теперь мой вопрос :

Могу ли я как-нибудь передать ссылку на какой-нибудь заполнитель (черные квадратные скобки) (точно так же, как обычные проекция / включение контента ) и получить их заполнены содержимым, исходящим от дочернего компонента?

С чем-то вроде ngTemplate [Outlet] возможно? И / или , используя @ Output ?

Я хочу «передать вверх» больше, чем plain text или simple <b>rich</b> <i>html</i>, но в идеале верный angular код шаблона, включая пользовательские компоненты, такие как

    <ui-button icon="up.svg" (click)='...'></ui-button>
    <ui-button icon="down.svg" (click)='...'></ui-button>

..., ведущие в верхняя панель внешнего компонента:

<section class='some-flexbox'>
    <ui-button icon="home.svg" (click)='...'></ui-button>

    <ui-button icon="up.svg" (click)='...'></ui-button>
    <ui-button icon="down.svg" (click)='...'></ui-button>

    <ui-button icon="help.svg" (click)='...'></ui-button>
    <ui-button icon="account.svg" (click)='...'></ui-button>
</section>

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

обновление

ngTemplateOutlet звучит довольно интересно

Мы также можем взять сам шаблон и создать его экземпляр в любом месте на странице , используя директиву ngTemplateOutlet:

Изучая, все еще не зная, как ...

Ответы [ 4 ]

3 голосов
/ 21 марта 2020

Существует два возможных способа достижения желаемого поведения.

  1. Использование angular CDK PortalModule
  2. Путем создания пользовательская директива, которая использует javascript dom apis для перемещения элемента из дочернего компонента в родительский компонент.

Вот краткое объяснение обоих решений.

1. Использование PortalModule Демо-ссылка

Вы можете определить шаблон внутри файла шаблона дочернего компонента.

<ng-template #templateForParent>
    <button (click)="onTemplateBtnClicked('btn from child')">
    Button from Child
  </button>
</ng-template>

затем захватите ссылку на этот шаблон внутри вашего дочернего файла компонента, используя @viewChid decorator.

@ViewChild("templateForParent", { static: true }) templateForParent: TemplateRef<any>;

, затем вы можете создать TemplatePortal, используя этот шаблон ref и pass этот портал к родителю, когда вы хотите.

ngAfterViewInit(): void {
    const templatePortal = new TemplatePortal(this.templateForParent, this.viewContainerRef);
    setTimeout(() => {
      this.renderToParent.emit(templatePortal);
    });
  }

И файл шаблона родительского компонента может выглядеть следующим образом.

<div fxLayout="row" fxLayoutAlign="space-between center">
  <button (click)="onBtnClick('one')">one</button>
  <button (click)="onBtnClick('two')">two</button>
  <ng-container [cdkPortalOutlet]="selectedPortal"></ng-container> <!-- pass the portal to the portalOutlet -->
  <app-button-group (renderToParent)="selectedPortal = $event"></app-button-group>
  <button (click)="onBtnClick('five')">five</button>
</div>

и все. Теперь вы представили шаблон родительскому компоненту, и у компонента все еще есть привязки для события click. И обработчик события щелчка определяется внутри дочернего компонента.

Таким же образом вы можете использовать ComponentPortal или DomPortal

2. Используя пользовательскую директиву Demo Link

Вы можете создать одну директиву angular следующим образом, которая будет перемещать элементы из его хост-компонента в родительский компонент хост-компонента.

import {Directive,TemplateRef,ElementRef,OnChanges,SimpleChanges,OnInit,Renderer2,DoCheck} from "@angular/core";

@Directive({
  selector: "[appRenderToParent]"
})
export class RenderToParentDirective implements OnInit {
  constructor(private elementRef: ElementRef<HTMLElement>) {}

  ngOnInit(): void {
    const childNodes = [];
    this.elementRef.nativeElement.childNodes.forEach(node =>
      childNodes.push(node)
    );
    childNodes.forEach(node => {
      this.elementRef.nativeElement.parentElement.insertBefore(
        node,
        this.elementRef.nativeElement
      );
    });

    this.elementRef.nativeElement.style.display = "none";
  }
}

И затем вы можете использовать эту директиву для любого компонента.

<div fxLayout="row" fxLayoutAlign="space-between center">
  <button (click)="onBtnClick('one')">one</button>
  <button (click)="onBtnClick('two')">two</button>
  <app-button-group appRenderToParent></app-button-group>
  <button (click)="onBtnClick('five')">five</button>
</div>

здесь <app-button-group> имеет следующий файл шаблона.

<button (click)="onBtnClick('three')">three</button>
<button (click)="onBtnClick('four')">four</button>

Таким образом, наша директива переместит оба элемент кнопки к родительскому компоненту своего хоста. Мы просто перемещаем узлы DOM в дереве DOM, поэтому все события, связанные с этими элементами, будут работать.

Мы можем изменить директиву так, чтобы она принимала имя класса или идентификатор и перемещала только те элементы, которые с имя класса или идентификатор.

Надеюсь, это поможет. Я предлагаю прочитать документы для получения дополнительной информации о PortalModule.

0 голосов
/ 18 марта 2020

Если каждый внутренний компонент определяет шаблон для содержимого заголовка и другой шаблон для содержимого нижнего колонтитула и связывает эти шаблоны с публикуемыми c свойствами, имеющими определенное имя c (например, header и footer), внешний компонент может получить доступ к этим свойствам и встроить содержимое каждого шаблона в ngTemplateOutlet.

Определить шаблоны во внутреннем компоненте:

<ng-template #header1>
  <button (click)="onChild1HeaderClick('A')">Command A</button>
  <button (click)="onChild1HeaderClick('B')">Command B</button>
</ng-template>
<ng-template #footer1>
  <button (click)="onChild1FooterClick('C')">Command C</button>
</ng-template>

и связать их со свойствами header и footer:

@ViewChild("header1", { static: true }) header: TemplateRef<any>;
@ViewChild("footer1", { static: true }) footer: TemplateRef<any>;

Предполагая, что внутренний компонент вставлен в родительский компонент с выходом маршрутизатора, свяжите ссылочную переменную шаблона #inner с выходом:

<router-outlet #inner="outlet"></router-outlet>

и используйте эту переменную для доступа к свойствам header и footer внутреннего компонента и для вставки их содержимого во внешний компонент:

<section>
  ...
  <ng-container *ngTemplateOutlet="inner.isActivated ? inner.component.header : null">
  </ng-container>
</section>
...
<section>
  ...
  <ng-container *ngTemplateOutlet="inner.isActivated ? inner.component.footer : null">
  </ng-container>
</section>

Можно посмотреть этот стекблиц для демонстрации. Более простой случай показан в этом другом стеке , где внутренний компонент объявлен непосредственно в шаблоне внешнего компонента.

0 голосов
/ 18 марта 2020

Вы не можете использовать вывод здесь, так как вы не можете получить вывод из включенного контента, но есть несколько способов сделать sh это ...

все это зависит от TemplateRef введите ViewChild или ContentChildren, все из которых можно импортировать из angular core:

import { TemplateRef, ViewChild, ContentChildren } from '@angular/core'

Использование общего сервиса

Вы можете использовать сервис для передачи шаблонов довольно эффективно.

рассмотрим простой сервис для передачи шаблонов через субъект, например:

@Injectable()
export class TemplateService {
  // allow undefined for resetting
  private templateSource = new Subject<TemplateRef<any> | undefined>()
  template$ = this.templateSource.asObservable()
  setTemplate(template?: TemplateRef<any>) {
    this.templateSource.next(template)
  }
}

и хост-компонент, который предоставляет и подписывается на него:

@Component({
  selector: 'host',
  templateUrl: './host.component.html',
  // providing the service here makes sure the children of the component (and ONLY this children of this component) will have access to the same service instance
  providers: [TemplateService]
})
export class HostComponent  {
  childTemplate?: TemplateRef<any>

  constructor(private templateService: TemplateService) {
    // receive templates from the service and assign
    this.templateService.template$.subscribe(t => this.childTemplate = t)
  }
}

с шаблоном, который определяет ngTemplateOutlet:

<div>
  <h1>Host Content</h1>
  <ng-container *ngTemplateOutlet="childTemplate"></ng-container>
</div>

<ng-content></ng-content>

* ПРИМЕЧАНИЕ: вы сказали, что переключаете содержимое внутри своего компонента довольно часто, я предполагаю, что вы используете ng-content, чтобы сделать это, но этот метод будет одинаково хорошо работает с выходом маршрутизатора.

затем дочерний компонент, который отправляет шаблон через службу:

@Component({
  selector: 'child1',
  templateUrl: './child1.component.html',
})
export class Child1Component  {
  // access template with ViewChild
  @ViewChild('childTemplate')
  childTemplate?: TemplateRef<any>

  constructor(private templateService: TemplateService) {
  }

  ngAfterViewInit() {
    // set here in afterViewInit hook when ViewChild is available
    this.templateService.setTemplate(this.childTemplate)
  }

  ngOnDestroy() {
    // need to clean up
    this.templateService.setTemplate()
  }

  childFunc() {
    console.log('child func called')
  }
}

с шаблоном, который определяет шаблон для передачи с помощью ng-template:

<h2>Child 1 Content</h2>

<ng-template #childTemplate><button (click)="childFunc()">Run Child Func</button></ng-template>

используйте это так: * 103 1 *

<host>
  <child1></child1>
</host>

это эффективно передаст дочерний шаблон от дочернего к хосту, а функции, определенные в этом шаблоне, все еще будут выполняться в дочернем. Единственный настоящий недостаток в том, что вам нужно убедиться, что вы убираете себя в хуке onDestroy. Если у данного ребенка нет специального шаблона, это тоже хорошо. Просто ничего не будет в шаблоне розетки. Если дочерние компоненты должны быть доступны в других контекстах, просто пометьте templateService как необязательный и установите шаблон только в том случае, если он предоставляется.

blitz (расширен заголовком / нижним колонтитулом): https://stackblitz.com/edit/angular-7-master-v81afu

Использование дочерних элементов контента

Альтернативой является использование ContentChildren, если вы определили свой хост таким же образом (тот же шаблон и дочерние элементы, что и раньше, но без вспомогательного содержимого):

@Component({
  selector: 'host',
  templateUrl: './host.component.html',
})
export class HostComponent  {
  childTemplate?: TemplateRef<any>

  // use content children to access projected content
  @ContentChildren('child')
  children: QueryList<any>  

  ngAfterContentInit() {
    // set from initial child
    this.childTemplate = (this.children.first || {}).childTemplate
    // listen for changes and reset
    this.children.changes.subscribe(child => {
      this.childTemplate = (child.first || {}).childTemplate
    })
  }
}

тогда вам просто нужно пометить возможных дочерних элементов при использовании вашего хоста:

<host>
  <child1 #child></child1>
</host>

Здесь вы должны убедиться, что у вас есть только один отмеченный дочерний контент, доступный одновременно. , В противном случае он просто возьмет первый (или вы можете определить любой лог c, который вы хотите найти шаблон, который вы хотите). Нулевые проверки делают ребенка без шаблона совершенно приемлемым.

блиц (расширенный с верхним / нижним колонтитулом): https://stackblitz.com/edit/angular-7-master-8gc4yb

ВАЖНО: В любом случае вы ДОЛЖНЫ убирать. удержание ссылки на шаблон дочернего элемента в хосте после уничтожения дочернего элемента создает потенциальную возможность утечки памяти или некорректного поведения, поскольку дочерние элементы не смогут быть уничтожены или правильно собран мусор.

Метод обслуживания является более общим / гибким, например, вы можете легко передать шаблон от дочерних элементов, или вы можете легко переключить шаблон из дочернего элемента, или вы можете сделать это с выходом маршрутизатора вместо включения. Но ContentChildren немного проще, хотя он работает только с ng-контентом. Оба одинаково действительны, но зависят от вашего варианта использования.

0 голосов
/ 16 марта 2020

Я вижу несколько способов сделать это, все они зависят от сложности приложения. Самый простой - иметь хост-компонент, содержащий сине-зеленые и коричневые компоненты.

Итак, структурный код выглядит следующим образом:

  • host.component
    • blue- green.component
    • brown.component

Сине-зеленые и коричневые компоненты должны иметь состояние без состояния, только @Input() аргументы, указывающие, что отображать, и @Output() событий (возможно, с полезной нагрузкой), отражающих тот факт, что компонент щелкнул.

Хост-компонент не будет иметь состояния и будет подписываться на эти @Output() и контролировать способ визуализации сине-зеленых и коричневых компонентов.

Я делал это в некоторых проектах с очень маленькими функциями - это работало безупречно. Если вам нужен пример, демонстрирующий это - дайте мне знать.

Другими вариантами будет создание одноэлементного сервиса, который будет содержать состояние, и все компоненты будут подписываться на это состояние, если все еще сложнее, вы можете принять состояние библиотеки управления.

...