Angular2: 渲染组件时去除包裹标签

194
我正在努力寻找一种方法来做这件事。在父组件中,模板描述了一个 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>
  <td>{{ entry.name }}</td>
  <td>{{ entry.time }}</td>
</tr>

我没有直接将这段代码放在父组件中(避免使用myResult组件)的原因是,myResult组件实际上比这里展示的更复杂,所以我想将其行为放在一个单独的组件和文件中。
生成的DOM结构看起来很糟糕。我认为这是因为它是无效的,因为标签只能包含元素(参见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>

有没有办法使用子组件来渲染表格行,而不需要包裹在标签中?
我已经查看了ng-content、DynamicComponentLoader和ViewContainerRef,但据我所见,它们似乎没有提供解决方案。

请问您能否展示一个可工作的例子? - zakaria amine
4
正确的答案在那里,有一个 Plunker 示例 https://dev59.com/0FYN5IYBdhLWcg3w7MC5 - sancelot
3
所有提出的答案都无法正常工作或者不完整。 正确的答案在这里被描述,并附有一个plunker示例 https://dev59.com/0FYN5IYBdhLWcg3w7MC5 - sancelot
8个回答

164
你可以使用属性选择器。
@Component({
  selector: '[myTd]'
  ...
})

然后像这样使用它

<td myTd></td>

2
不,setAttribute 不是我的代码。但我已经弄清楚了,我需要在模板中使用实际的顶级标签作为组件的标签,而不是 ng-container,所以新的工作用法是 <ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"></ul> - Aviad P.
2
你不能在 ng-container 上设置属性组件,因为它已从 DOM 中移除。这就是为什么你需要将其设置在实际的 HTML 标签上。 - Robouste
2
@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> - Mahesh
6
如果你想增强材料组件(如<mat-toolbar my-toolbar-rows>),那么这个答案就不适用了。因为它会抱怨你只能有一个组件选择器而不是多个。你可以使用<my-toolbar-component>,但这样会将mat-toolbar放在<div>中。 - Rusty Rob
2
这个解决方案适用于简单的情况(如OP的情况),但如果您需要创建一个内部可以生成更多“my-results”的“my-results”,那么您需要ViewContainerRef(请查看@Slim's answer below)。这个解决方案在这种情况下不起作用的原因是第一个属性选择器进入了一个tbody,但内部选择器应该在哪里呢?它不能在一个tr中,也不能将一个tbody放在另一个tbody中。 - Daniel
显示剩余16条评论

76
你需要使用 "ViewContainerRef",并在 "my-result" 组件内执行以下操作:

.html

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

.ts

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


constructor(
  private viewContainerRef: ViewContainerRef
) { }

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

10
运作得很好!谢谢。我在 Angular 8 中使用了以下代码代替:@ViewChild('template', {static: true}) template; - AlainD.
5
这个方法行得通。我遇到了这样的情况:使用CSS的display: grid属性时,将<my-result>标签作为父级元素的第一个元素,且所有子元素都为my-result 的同级元素。问题在于多出来一个幽灵元素,破坏了由"grid-template-columns:repeat(7, 1fr);"定义的网格的七列,并使其渲染了八个元素。其中一个幽灵占位符是为my-result保留的,另外七个则是列标题。解决这个问题的方法很简单,只需在标签中添加 <my-result style="display:none"> 隐藏它即可。之后,CSS网格系统就可以完美运作了。 - RandallTo
1
当我使用这个功能时,如何避免出现ExpressionChangedAfterItHasBeenCheckedError错误? - gyozo kudor
3
如果有人在使用this.viewContainerRef时出现错误,将代码从ngOnInit移动到ngAfterViewInit中。 - Cafn
4
谢谢!这是唯一对我有效的方法。除了你的代码之外,我还在最后执行了 this.viewContainerRef.element.nativeElement.remove() ,以移除原始(空)组件。 - ZolaKt
显示剩余8条评论

50

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

2
"display: contents" 就是我需要的。它允许子组件继承样式,而不会被额外的子组件名称标签(选择器)干扰。 - M_Idrees
不回答原问题:“渲染一个没有包装标签的组件”。 - alex351
1
我认为值得注意的是,使用display: contents可能会导致一些无障碍性问题,因此应该谨慎使用(至少在所有主要浏览器中完全安全之前):https://caniuse.com/css-display-contents - ScriptyChris

24

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

那么在你的情况下:

<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


选择器:'my-results,[my-results]',然后我可以在HTML标签中将my-results用作属性。这是正确的答案。它适用于Angular 8.2。 - Don Dilanga

14

在您的元素上使用此指令

@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 中,您可以像往常一样调用卡片元素,例如:

<div class="cards-container">
   <card></card>
</div>

输出结果将是:

<div class="cards-container">
   <div class="card" remove-wrapper>
      This is my card component
   </div>
</div>

8
出现错误是因为 'parentElement.parentNode' 为空。 - emirhosseini
3
这不会影响Angular的变更检测和虚拟DOM吗? - Ben Winding
@BenWinding 不会,因为Angular在内部使用一个常称为“视图”或“组件视图”的数据结构来表示组件,并且包括ViewChildren在内的所有变更检测操作都在视图上运行,而非DOM。我已经测试了此代码,可以确认所有DOM监听器和Angular绑定都能正常工作。 - Ali Almutawakel
这个解决方案不起作用,抛出 null 并破坏了 ng 结构。 - Shaybc
这将完全删除该组件。 - Bopsi

8
现在的另一个选择是来自@angular-contrib/common提供的ContribNgHostModule。导入该模块后,您可以将host: { ngNoHost: '' } 添加到您的@Component装饰器中,这样就不会呈现任何包装元素。

8
仅仅是评论一下,因为看起来这个软件包似乎不再维护了,并且在使用 Angular 9+ 版本时出现了错误。 - Greg Van Gorp

3

改进了@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);
  }
}

由于某些原因,这个解决方案对我没有起作用。 - Shaybc

2

这对我很有用,可以避免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>

你能回答我的问题吗?https://stackoverflow.com/q/76146903/14344959 - Harsh Patel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接