Angular2: 呈现一个没有包装标记的组件

我正在努力寻找一种方法来做到这一点。在父组件中,模板描述 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元素 (见 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-contentDynamicComponentLoaderViewContainerRef,但他们似乎没有提供一个解决方案,这是我所能看到的。

74115 次浏览

您可以使用属性选择器

@Component({
selector: '[myTd]'
...
})

然后把它当成

<td myTd></td>

对元素使用此指令

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

您需要“ ViewContainerRef”并在 my-result 组件中执行以下操作:

返回文章页面

<ng-template #template>
<tr>
<td>Lisa</td>
<td>333</td>
</tr>
</ng-template>

返回文章页面

@ViewChild('template', { static: true }) template;




constructor(
private viewContainerRef: ViewContainerRef
) { }


ngOnInit() {
this.viewContainerRef.createEmbeddedView(this.template);
}

属性选择器是解决这个问题的最佳方法。

所以对你来说:

<table>
<thead>
<tr>
<th>Name</th>
<th>Time</th>
</tr>
</thead>
<tbody my-results>
</tbody>
</table>

结果出来了

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() {
}


}

My-result html

  <tr my-result [entry]="entry" *ngFor="let entry of entries"><tr>

我的结果是

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() {
}


}

My-result html

  <td>\{\{ entry.name }}</td>
<td>\{\{ entry.time }}</td>

见工作堆栈闪电: https://stackblitz.com/edit/angular-xbbegx

现在的另一种选择是从 @angular-contrib/common包裹提供的 ContribNgHostModule

在导入模块之后,您可以将 host: { ngNoHost: '' }添加到 @Component装饰器中,并且不会呈现包装元素。

你可以尝试使用新的 cssdisplay: 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>

@ Shlomi Aharoni答案的改进。通常使用 渲染器2来操作 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);
}
}

这对我很有用,它可以避免 ExpressionChangedAfterItHasBeenCheckedError 错误。

子组件 :

@Component({
selector: 'child-component'
templateUrl: './child.template.html'
})


export class ChildComponent implements OnInit {
@ViewChild('childTemplate', {static: true}) childTemplate: TemplateRef<any>;


constructor(
private view: ViewContainerRef
) {}


ngOnInit(): void {
this.view.createEmbeddedView(this.currentUserTemplate);
}


}

父组成部分 :

<child-component></child-component>