【问题标题】:Angular2 : render a component without its wrapping tagAngular2:渲染一个没有包装标签的组件
【发布时间】: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>

有没有什么方法可以渲染相同的东西,但没有包装 &lt;my-result&gt; 标签,同时仍然使用组件来单独负责渲染表格行?

我看过ng-contentDynamicComponentLoaderViewContainerRef,但据我所知,它们似乎没有提供解决方案。

【问题讨论】:

标签: javascript angular


【解决方案1】:

你可以使用属性选择器

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

然后像这样使用它

<td myTd></td>

【讨论】:

  • setAttribute 不是我的代码。但我已经想通了,我需要在我的模板中使用实际的顶级标签作为我的组件的标签,而不是ng-container,所以新的工作用法是&lt;ul smMenu class="nav navbar-nav" [submenus]="root?.Submenus" [title]="root?.Title"&gt;&lt;/ul&gt;
  • 您不能在 ng-container 上设置属性组件,因为它已从 DOM 中删除。这就是为什么您需要在实际的 HTML 标记上设置它。
  • @AviadP。非常感谢...我知道需要进行一些细微的更改,但我对此感到困惑。所以而不是这个:&lt;ul class="nav navbar-nav"&gt; &lt;li-navigation [navItems]="menuItems"&gt;&lt;/li-navigation&gt; &lt;/ul&gt;我使用了这个(在将 li-navigation 更改为属性选择器之后)&lt;ul class="nav navbar-nav" li-navigation [navItems]="menuItems"&gt;&lt;/ul&gt;
  • 如果您尝试增强材料组件,例如 会抱怨你只能有一个组件选择器而不是多个。我可以去 但随后将 mat-toolbar 放在 div 中
  • 此解决方案适用于简单情况(如 OP),但如果您需要创建一个 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 的兄弟元素.问题是一个额外的幽​​灵元素仍然破坏了网格 7 列“grid-template-columns:repeat(7, 1fr);”它正在渲染 8 个元素。我的结果和 7 个列标题的 1 个幽灵占位符。解决此问题的方法是通过将 简单地隐藏在您的标签中。在那之后,css 网格系统就像一个魅力。
  • 这个解决方案即使在更复杂的情况下也能工作,其中my-result 需要能够创建新的my-result 兄弟姐妹。因此,假设您有一个my-result 的层次结构,其中每一行都可以有“子”行。在这种情况下,使用属性选择器将不起作用,因为第一个选择器进入 tbody,但第二个选择器不能进入内部 tbodytr
  • 当我使用它时如何避免出现 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 在内部使用通常称为 ViewComponent 的数据结构来表示组件视图 和所有更改检测操作,包括 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);
      }
    }
    

    【讨论】:

    • 由于某种原因,这个解决方案对我不起作用
    猜你喜欢
    • 2019-02-09
    • 2021-08-17
    • 1970-01-01
    • 1970-01-01
    • 2015-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多