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 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
听起来很有趣
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 属性(例如 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>
你可以看看this stackblitz for a demo. A simpler case is shown in this other stackblitz,里面的组件是直接在外部组件模板中声明的
您不能在此处使用输出,因为您无法从嵌入的内容中获取输出,但有几种方法可以实现此目的...
这一切都依赖于TemplateRef
类型和ViewChild
或ContentChildren
,所有这些都可以从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。两者同样有效,但取决于您的用例。
有两种可能的方法可以实现所需的行为。
- 使用 angular CDK 的 PortalModule
- 通过创建使用 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 的更多信息。
我有一个外部组件(蓝绿色),带有一些 flexbox 工具栏和一堆我自己的 ui-button
按钮。还有一个内部组件,主要是在棕色区域做它的事情(如你所料)。
但是, 取决于内部组件(有几个组件来回切换),必须在其中插入更多上下文按钮 top/bottom酒吧.
(CSS 外部工具栏的绝对定位技巧不是一种选择,取决于大小和便利性,外部工具栏的位置、大小等可能会有很大差异。 .)
现在我的问题是:
我能否以某种方式传递对某些占位符 (黑色方括号)(就像常规 content projection/transclusion)的引用,并用来自子组件?
用 ngTemplate[Outlet] perhaps? And/or using @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
听起来很有趣
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 属性(例如 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>
你可以看看this stackblitz for a demo. A simpler case is shown in this other stackblitz,里面的组件是直接在外部组件模板中声明的
您不能在此处使用输出,因为您无法从嵌入的内容中获取输出,但有几种方法可以实现此目的...
这一切都依赖于TemplateRef
类型和ViewChild
或ContentChildren
,所有这些都可以从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。两者同样有效,但取决于您的用例。
有两种可能的方法可以实现所需的行为。
- 使用 angular CDK 的 PortalModule
- 通过创建使用 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 的更多信息。