Angular2 :渲染一个没有包装标签的组件

Angular2 : render a component without its wrapping tag

本文关键字:包装 标签 组件 一个 Angular2      更新时间:2023-09-26

我正在努力寻找一种方法来做到这一点。在父组件中,模板描述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 组件实际上比此处显示的要复杂得多,因此我想将其行为放在单独的组件和文件中。

生成的 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,但据我所知,它们似乎没有提供解决方案。

您可以使用属性选择器

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

然后像这样使用它

<td myTd></td>

你需要"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);
}

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

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

所以在你的情况下:

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

my-result html

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

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

在元素上使用此指令

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

现在的另一个选择是@angular-contrib/common包中提供的ContribNgHostModule

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

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

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