Angular 2 包含:我可以将插槽内容向上传递给父组件吗

Angular 2 transclusion: Can I pass slot content upward to a parent Component

我有一个外部组件(蓝绿色),带有一些 flexbox 工具栏和一堆我自己的 ui-button 按钮。还有一个内部组件,主要是在棕色区域做它的事情(如你所料)。

但是, 取决于内部组件(有几个组件来回切换),必须在其中插入更多上下文按钮 top/bottom酒吧.

(CSS 外部工具栏的绝对定位技巧不是一种选择,取决于大小和便利性,外部工具栏的位置、大小等可能会有很大差异。 .)


现在我的问题是:

我能否以某种方式传递对某些占位符 (黑色方括号)(就像常规 content projection/transclusion)的引用,并用来自子组件?

ngTemplate[Outlet] perhaps? And/or using @Output 之类的东西?

我想“向上传递”的次数超过 plain textsimple <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听起来很有趣

We can also take the template itself and instantiate it anywhere on the page, using the ngTemplateOutlet directive:

正在检查,仍然不确定如何...

我看到了几种方法,它们都取决于应用程序的复杂性。最简单的就是有一个包含蓝绿色和棕色组件的宿主组件。

所以结构代码如下:

  • host.component
    • 蓝色-green.component
    • brown.component

蓝绿色和棕色组件应该是无状态的,只有 @Input() 告诉显示什么的参数和 @Output() 事件(可能带有负载)发出组件已单击的事实。

宿主组件将是无状态的,并且会订阅这些 @Output() 并控制蓝绿色和棕色组件的呈现方式。

我在一些非常小的功能项目中做到了这一点 - 它完美无缺。如果您需要一个示例来展示这一点 - 让我知道。

其他选择是创建一个包含状态的单例服务,所有组件都会订阅该状态,如果事情更复杂,您可以采用状态管理库。

如果每个内部组件为页眉内容定义一个模板,为页脚内容定义另一个模板,并将这些模板关联到具有特定名称的 public 属性(例如 headerfooter), 外部组件可以访问这些属性并将每个模板的内容嵌入到 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>

并将它们关联到属性 headerfooter:

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

假设内部组件插入到父组件中带有路由器出口,将模板引用变量#inner关联到出口:

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

并使用该变量访问内部组件的 headerfooter 属性,并将它们的内容插入外部组件:

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

你可以看看this stackblitz for a demo. A simpler case is shown in this other stackblitz,里面的组件是直接在外部组件模板中声明的

您不能在此处使用输出,因为您无法从嵌入的内容中获取输出,但有几种方法可以实现此目的...

这一切都依赖于TemplateRef类型和ViewChildContentChildren,所有这些都可以从angular核心导入:

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

使用共享服务

您实际上可以使用服务来非常有效地传递模板。

考虑一个通过 Subject 传递模板的简单服务,如下所示:

@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 来做到这一点,但这种方法同样适用于路由器插座。

然后是一个child通过服务发送模板的组件:

@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')
  }
}

with template 定义要传递的模板 ng-template:

<h2>Child 1 Content</h2>

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

像这样使用它:

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

这将有效地将 child 模板从 child 传递给主机,并且该模板中定义的函数仍然会 运行 在 child 中。这里唯一真正的陷阱是你需要确保在 onDestroy 钩子中清理自己。如果给定的 child 没有特殊模板,那也没关系。模板出口中什么也没有。如果 child 组件需要在其他上下文中可用,只需将 templateService 标记为可选,并仅在提供时设置模板。

blitz(用页眉/页脚扩展):https://stackblitz.com/edit/angular-7-master-v81afu

使用内容Children

另一种方法是使用 ContentChildren,如果您这样定义主机(与以前相同的模板和 children,但没有服务内容):

@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
    })
  }
}

那么你只需要在使用主机时标记可能的children:

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

这里的问题是您需要确保一次最多有一个标记的内容 child 可用。否则它只会采用第一个(或者您可以定义您想要找到所需模板的任何逻辑)。空检查使没有模板的 child 完全可以接受。

blitz(用页眉/页脚扩展):https://stackblitz.com/edit/angular-7-master-8gc4yb

重要提示: 无论哪种情况,您都必须清理。在 child 被销毁后在主机中保留对 child 模板的引用会导致内存泄漏或错误行为的可能性,因为 children 将无法正确销毁或垃圾​​收集。

服务方法更通用/灵活一些,例如您可以轻松地从children传递模板到children,或者您可以从child切换模板] 很容易,或者您可以使用路由器插座而不是嵌入来完成。但是 ContentChildren 更直接一些,尽管它只适用于 ng-content。两者同样有效,但取决于您的用例。

有两种可能的方法可以实现所需的行为。

  1. 使用 angular CDK 的 PortalModule
  2. 通过创建使用 javascript dom api 将元素从子组件移动到父组件的自定义指令。

下面是对这两种解决方案的快速解释。

1.使用 PortalModule Demo Link

您可以在子组件的模板文件中定义模板。

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

然后使用 @viewChid 装饰器在子组件文件中获取该模板的引用。

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

然后您可以使用该模板引用创建一个 TemplatePortal,并在需要时将该门户传递给父级。

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 or 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 个节点,因此与这些元素绑定的所有事件仍然有效。

我们可以修改指令以接受 class 名称或 ID,并仅移动那些具有 class 名称或 ID 的元素。

希望对您有所帮助。我建议阅读文档以获取有关 PortalModule 的更多信息。