Angular2:渲染一个没有包装标签的组件
Posted
技术标签:
【中文标题】Angular2:渲染一个没有包装标签的组件【英文标题】:Angular2 : render a component without its wrapping tag 【发布时间】:2016-12-07 13:15:43 【问题描述】:我正在努力寻找一种方法来做到这一点。在父组件中,模板描述了table
及其thead
元素,但将tbody
的渲染委托给另一个组件,如下所示:
<table>
<thead>
<tr>
<th>Name</th>
<th>Time</th>
</tr>
</thead>
<tbody *ngFor="let entry of getEntries()">
<my-result [entry]="entry"></my-result>
</tbody>
</table>
每个 myResult 组件都呈现自己的tr
标签,基本上是这样的:
<tr>
<td> entry.name </td>
<td> entry.time </td>
</tr>
我没有将它直接放在父组件中(避免需要 myResult 组件)的原因是 myResult 组件实际上比这里显示的要复杂,所以我想把它的行为放在一个单独的组件中并且文件。
生成的 DOM 看起来很糟糕。我相信这是因为它是无效的,因为tbody
只能包含tr
元素(see MDN),但我生成的(简化的)DOM 是:
<table>
<thead>
<tr>
<th>Name</th>
<th>Time</th>
</tr>
</thead>
<tbody>
<my-result>
<tr>
<td>Bob</td>
<td>128</td>
</tr>
</my-result>
</tbody>
<tbody>
<my-result>
<tr>
<td>Lisa</td>
<td>333</td>
</tr>
</my-result>
</tbody>
</table>
有没有什么方法可以渲染相同的东西,但没有包装 <my-result>
标签,同时仍然使用组件来单独负责渲染表格行?
我看过ng-content
、DynamicComponentLoader
、ViewContainerRef
,但据我所知,它们似乎没有提供解决方案。
【问题讨论】:
你能举个例子吗? 正确的答案就在那里,有一个 plunker 样本***.com/questions/46671235/… 建议的答案都不起作用,或者不完整。此处使用 plunker 示例描述了正确答案***.com/questions/46671235/… 【参考方案1】:你可以使用属性选择器
@Component(
selector: '[myTd]'
...
)
然后像这样使用它
<td myTd></td>
【讨论】:
否setAttribute
不是我的代码。但我已经想通了,我需要在我的模板中使用实际的***标签作为我的组件的标签,而不是ng-container
,所以新的工作用法是<ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"></ul>
您不能在 ng-container 上设置属性组件,因为它已从 DOM 中删除。这就是为什么您需要在实际的 html 标记上设置它。
@AviadP。非常感谢...我知道需要进行一些细微的更改,但我对此感到困惑。所以而不是这个:<ul class="nav navbar-nav"> <li-navigation [navItems]="menuItems"></li-navigation> </ul>
我使用了这个(在将 li-navigation 更改为属性选择器之后)<ul class="nav navbar-nav" li-navigation [navItems]="menuItems"></ul>
如果您尝试增强材料组件,例如my-results
内部可以产生更多 my-results
,那么您需要 ViewContainerRef
(检查 @Slim's answer below) .这个解决方案在这种情况下不起作用的原因是第一个属性选择器进入了tbody
,但是内部选择器在哪里呢?它不能在 tr
中,也不能将 tbody
放在另一个 tbody
中。【参考方案2】:
您需要“ViewContainerRef”并在 my-result 组件中执行以下操作:
html:
<ng-template #template>
<tr>
<td>Lisa</td>
<td>333</td>
</tr>
</ng-template>
ts:
@ViewChild('template') template;
constructor(
private viewContainerRef: ViewContainerRef
)
ngOnInit()
this.viewContainerRef.createEmbeddedView(this.template);
【讨论】:
像魅力一样工作!谢谢你。我在 Angular 8 中使用了以下内容:@ViewChild('template', static: true) template;
这行得通。我遇到了一种情况,我正在使用 css display:grid 并且你不能让它仍然将 my-result
需要能够创建新的my-result
兄弟姐妹。因此,假设您有一个my-result
的层次结构,其中每一行都可以有“子”行。在这种情况下,使用属性选择器将不起作用,因为第一个选择器进入 tbody
,但第二个选择器不能进入内部 tbody
或 tr
。
当我使用它时如何避免出现 ExpressionChangedAfterItHasBeenCheckedError?
如果有人对 this.viewContainerRef 有错误,请将代码从 ngOnInit 移动到 ngAfterViewInit。【参考方案3】:
你可以尝试使用新的 css display: contents
这是我的工具栏 scss:
:host
display: contents;
:host-context(.is-mobile) .toolbar
position: fixed;
/* Make sure the toolbar will stay on top of the content as it scrolls past. */
z-index: 2;
h1.app-name
margin-left: 8px;
和html:
<mat-toolbar color="primary" class="toolbar">
<button mat-icon-button (click)="toggle.emit()">
<mat-icon>menu</mat-icon>
</button>
<img src="/assets/icons/favicon.png">
<h1 class="app-name">@robertking Dashboard</h1>
</mat-toolbar>
并在使用中:
<navigation-toolbar (toggle)="snav.toggle()"></navigation-toolbar>
【讨论】:
【参考方案4】:属性选择器是解决这个问题的最好方法。
所以在你的情况下:
<table>
<thead>
<tr>
<th>Name</th>
<th>Time</th>
</tr>
</thead>
<tbody my-results>
</tbody>
</table>
我的结果 ts
import Component, OnInit from '@angular/core';
@Component(
selector: 'my-results, [my-results]',
templateUrl: './my-results.component.html',
styleUrls: ['./my-results.component.css']
)
export class MyResultsComponent implements OnInit
entries: Array<any> = [
name: 'Entry One', time: '10:00',
name: 'Entry Two', time: '10:05 ',
name: 'Entry Three', time: '10:10',
];
constructor()
ngOnInit()
我的结果 html
<tr my-result [entry]="entry" *ngFor="let entry of entries"><tr>
我的结果 ts
import Component, OnInit, Input from '@angular/core';
@Component(
selector: '[my-result]',
templateUrl: './my-result.component.html',
styleUrls: ['./my-result.component.css']
)
export class MyResultComponent implements OnInit
@Input() entry: any;
constructor()
ngOnInit()
我的结果 html
<td> entry.name </td>
<td> entry.time </td>
查看工作中的 stackblitz:https://stackblitz.com/edit/angular-xbbegx
【讨论】:
selector: 'my-results, [my-results]',然后我可以使用 my-results 作为 HTML 中标签的属性。这是正确的答案。它适用于 Angular 8.2。【参考方案5】:在你的元素上使用这个指令
@Directive(
selector: '[remove-wrapper]'
)
export class RemoveWrapperDirective
constructor(private el: ElementRef)
const parentElement = el.nativeElement.parentElement;
const element = el.nativeElement;
parentElement.removeChild(element);
parentElement.parentNode.insertBefore(element, parentElement.nextSibling);
parentElement.parentNode.removeChild(parentElement);
示例用法:
<div class="card" remove-wrapper>
This is my card component
</div>
在父 html 中你像往常一样调用 card 元素,例如:
<div class="cards-container">
<card></card>
</div>
输出将是:
<div class="cards-container">
<div class="card" remove-wrapper>
This is my card component
</div>
</div>
【讨论】:
这是抛出一个错误,因为 'parentElement.parentNode' 为空 这不会与 Angular 的变更检测和虚拟 DOM 混为一谈吗? @BenWinding 不会 因为 Angular 在内部使用通常称为 View 或 Component 的数据结构来表示组件视图 和所有更改检测操作,包括 ViewChildren 在视图上运行,而不是在 DOM 上运行。我已经测试了这段代码,我可以确认所有 DOM 侦听器和角度绑定都在工作。 这个解决方案不起作用,抛出null并破坏ng结构 这会完全移除组件【参考方案6】:现在的另一种选择是ContribNgHostModule
可从@angular-contrib/common
package 获得。
导入模块后,您可以将host: ngNoHost: ''
添加到您的@Component
装饰器中,并且不会呈现包装元素。
【讨论】:
只是评论,因为它看起来不再维护这个包,并且 Angular 9+ 存在错误【参考方案7】:改进@Shlomi Aharoni 答案。使用Renderer2 操作 DOM 以使 Angular 保持在循环中通常是一种很好的做法,因为其他原因包括安全性(例如 XSS 攻击)和服务器端渲染。
指令示例
import AfterViewInit, Directive, ElementRef, Renderer2 from '@angular/core';
@Directive(
selector: '[remove-wrapper]'
)
export class RemoveWrapperDirective implements AfterViewInit
constructor(private elRef: ElementRef, private renderer: Renderer2)
ngAfterViewInit(): void
// access the DOM. get the element to unwrap
const el = this.elRef.nativeElement;
const parent = this.renderer.parentNode(this.elRef.nativeElement);
// move all children out of the element
while (el.firstChild) // this line doesn't work with server-rendering
this.renderer.appendChild(parent, el.firstChild);
// remove the empty element from parent
this.renderer.removeChild(parent, el);
组件示例
@Component(
selector: 'app-page',
templateUrl: './page.component.html',
styleUrls: ['./page.component.scss'],
)
export class PageComponent implements AfterViewInit
constructor(
private renderer: Renderer2,
private elRef: ElementRef)
ngAfterViewInit(): void
// access the DOM. get the element to unwrap
const el = this.elRef.nativeElement; // app-page
const parent = this.renderer.parentNode(this.elRef.nativeElement); // parent
// move children to parent (everything is moved including comments which angular depends on)
while (el.firstChild) // this line doesn't work with server-rendering
this.renderer.appendChild(parent, el.firstChild);
// remove empty element from parent - true to signal that this removed element is a host element
this.renderer.removeChild(parent, el, true);
【讨论】:
由于某种原因,这个解决方案对我不起作用以上是关于Angular2:渲染一个没有包装标签的组件的主要内容,如果未能解决你的问题,请参考以下文章