欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

Angular开发实践(八): 使用ng-content进行组件内容投射 有大用

在Angular中,组件属于特殊的指令,它的特殊之处在于它有自己的模板(html)和样式(css)。因此使用组件可以使我们的代码具有强解耦、可复用、易扩展等特性。通常的组件定义如下:

demo.component.ts:

import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'demo-component',
    templateUrl: './demo.component.html',
    styleUrls: ['./demo.component.scss']
})
export class DemoComponent implements OnInit {

    constructor() {
    }

    ngOnInit() {
    }
}

demo.component.html:

<div class="demo">
    <h2>
        demo-component - 我是一个简单的组件
    </h2>
</div>

demo.component.scss:

.demo {
    padding: 10px;
    border: 2px solid red;

    h2 {
        margin: 0;
        color: #262626;
    }
}

此时我们引用该组件,就会呈现该组件解析之后的内容:

<demo-component></demo-component>

假设现在有这样的需求,这个组件能够接受外部投射进来的内容,也就是说组件最终呈现的内容不仅仅是本身定义的那些,那该怎么做呢?这时就要请出本文的主角 ng-content

简单投射

我们先从最简单开始,在 demo.component.html 中添加 ,修改后的 demo.component.html 和 demo.component.scss 如下:

demo.component.html:

<div class="demo">
    <h2>
        demo-component - 可嵌入外部内容的组件
    </h2>
    <div class="content">
        <ng-content></ng-content>
    </div>
</div>

demo.component.scss:

.demo {
    padding: 10px;
    border: 2px solid red;

    h2 {
        margin: 0;
        color: #262626;
    }

    .content {
        padding: 10px;
        margin-top: 10px;
        line-height: 20px;
        color: #FFFFFF;
        background-color: #de7d28;
    }
}

为了效果展示特意将 所在的容器背景色定义为橙色。

这时我们在引用该组件时可以从外部投射内容,外部内容将在橙色区域显示:

<demo-component>
    我是外部嵌入的内容
</demo-component>

针对性投射

如果同时存在几个 ,那外部内容将如何进行投射呢?

我们先看个示例,为了区别,我再新增一个蓝色区域的 ,修改后的 demo.component.html 和 demo.component.scss 如下:

demo.component.html:

<div class="demo">
    <h2>
        demo-component - 可嵌入外部内容的组件
    </h2>
    <div class="content">
        <ng-content></ng-content>
    </div>
    <div class="content blue">
        <ng-content></ng-content>
    </div>
</div>

demo.component.scss:

.demo {
    padding: 10px;
    border: 2px solid red;

    h2 {
        margin: 0;
        color: #262626;
    }

    .content {
        padding: 10px;
        margin-top: 10px;
        line-height: 20px;
        color: #FFFFFF;
        background-color: #de7d28;
        
        &.blue {
            background-color: blue;
        }
    }
}

引用该组件:

<demo-component>
    我是外部嵌入的内容
</demo-component>

此时,我们将看到外部内容投射到了蓝色区域:

当然,如果你将橙色区域代码放在蓝色区域代码的后面,那么外部内容就会投射到橙色区域:

所以从上面的示例我们可以看出,如果同时存在简单的 ,那么外部内容将投射在组件模板最后的那个 中。

那么知道这个问题,我们可能会想,能不能将外部内容有针对性的投射相应的 中呢?答案显然是可以的。

为了处理这个问题, 支持一个 select 属性,可以让你在特定的地方投射具体的内容。该属性支持 CSS 选择器(标签选择器、类选择器、属性选择器、...)来匹配你想要的内容。如果 ng-content 上没有设置 select 属性,它将接收全部内容,或接收不匹配任何其他 ng-content 元素的内容。

直接看例子,修改后的 demo.component.html 和 demo.component.scss 如下:

demo.component.html:

<div class="demo">
    <h2>
        demo-component - 可嵌入外部内容的组件
    </h2>
    <div class="content">
        <ng-content></ng-content>
    </div>
    <div class="content blue">
        <ng-content select="header"></ng-content>
    </div>
    <div class="content red">
        <ng-content select=".demo2"></ng-content>
    </div>
    <div class="content green">
        <ng-content select="[name=demo3]"></ng-content>
    </div>
</div>

demo.component.scss:

.demo {
    padding: 10px;
    border: 2px solid red;

    h2 {
        margin: 0;
        color: #262626;
    }

    .content {
        padding: 10px;
        margin-top: 10px;
        line-height: 20px;
        color: #FFFFFF;
        background-color: #de7d28;

        &.blue {
            background-color: blue;
        }

        &.red {
            background-color: red;
        }

        &.green {
            background-color: green;
        }
    }
}

从上面代码可以看到,蓝色区域将接收 标签 header 那部分内容,红色区域将接收 class为"demo2"的div 的那部分内容,绿色区域将接收 属性name为"demo3"的div 的那部分内容,橙色区域将接收其余的外部内容(开始,我是外部嵌入的内容,结束)。

引用该组件:

<demo-component>
    开始,我是外部嵌入的内容,
    <header>
        我是外部嵌入的内容,我在header中
    </header>
    <div class="demo2">
        我是外部嵌入的内容,我所在div的class为"demo2"
    </div>
    <div name="demo3">
        我是外部嵌入的内容demo,我所在div的属性name为"demo3"
    </div>
    结束
</demo-component>

此时,我们将看到外部内容投射到了指定的 中。

扩展知识

ngProjectAs

现在我们知道通过 ng-content 的 select 属性可以指定外部内容投射到指定的 中。

而要能正确的根据 select 属性投射内容,有个限制就是 - 不管是 标签 headerclass为"demo2"的div还是 属性name为"demo3"的div,这几个标签都是作为 组件标签 直接子节点

那如果不是作为直接子节点,会是什么情况呢?我们简单修改下引用 demo-component 组件的代码,将 标签header 放在一个div中,修改如下:

<demo-component>
    开始,我是外部嵌入的内容,
    <div>
        <header>
            我是外部嵌入的内容,我在header中
        </header>
    </div>
    <div class="demo2">
        我是外部嵌入的内容,我所在div的class为"demo2"
    </div>
    <div name="demo3">
        我是外部嵌入的内容demo,我所在div的属性name为"demo3"
    </div>
    结束
</demo-component>

此时,我们看到 标签 header 那部分内容不再投射到蓝色区域中了,而是投射到橙色区域中了。原因就是 <ng-content select="header"></ng-content> 无法匹配到之前的 标签 header,故而将这部分内容投射到了橙色区域的 <ng-content></ng-content> 中了。

为了解决这个问题,我们必须使用 ngProjectAs 属性,它可以应用于任何元素上。具体如下:

<demo-component>
    开始,我是外部嵌入的内容,
    <div ngProjectAs="header">
        <header>
            我是外部嵌入的内容,我在header中
        </header>
    </div>
    <div class="demo2">
        我是外部嵌入的内容,我所在div的class为"demo2"
    </div>
    <div name="demo3">
        我是外部嵌入的内容demo,我所在div的属性name为"demo3"
    </div>
    结束
</demo-component>

通过设置 ngProjectAs 属性,让 标签header 所在的 div 指向了 select="header",此时 标签 header 那部分内容又投射到蓝色区域了:

<ng-content> 不“产生”内容

做个试验

做个试验,先定义一个 demo-child-component 组件:

import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'demo-child-component',
    template: '<h3>我是demo-child-component组件</h3>'
})
export class DemoChildComponent implements OnInit {

    constructor() {
    }

    ngOnInit() {
        console.log('demo-child-component初始化完成!');
    }
}

demo-component 组件修改为:

import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'demo-component',
    template: `
        <button (click)="show = !show">
            {{ show ? 'Hide' : 'Show' }}
        </button>
        <div class="content" *ngIf="show">
            <ng-content></ng-content>
        </div>
    `
})
export class DemoComponent implements OnInit {
    show = true;

    constructor() {
    }

    ngOnInit() {
    }
}

然后在 demo-component 中 投射 demo-child-component:

<demo-component>
    <demo-child-component></demo-child-component>
</demo-component>

此时,在控制台我们看到打印出 demo-child-component初始化完成! 这些文字。但是当我们点击按钮进行切换操作时,demo-child-component初始化完成! 就不再打印了,这意味着我们的 demo-child-component 组件只被实例化了一次 - 从未被销毁和重新创建。

为什么会出现这样的情况呢?

出现原因

<ng-content> 不会 "产生" 内容,它只是投影现有的内容。你可以认为它等价于 node.appendChild(el) 或 jQuery 中的 $(node).append(el) 方法:使用这些方法,节点不被克隆,它被简单地移动到它的新位置。因此,投影内容的生命周期将被绑定到它被声明的地方,而不是显示在地方。

这也从原理解释了前面那个问题:如果同时存在几个 ,那外部内容将如何进行投射呢?

这种行为有两个原因:期望一致性和性能。什么 "期望的一致性" 意味着作为开发人员,可以基于应用程序的代码,猜测其行为。假设我写了以下代码:

<demo-component>
    <demo-child-component></demo-child-component>
</demo-component>

很显然 demo-child-component 组件将被实例化一次,但现在假如我们使用第三方库的组件:

<third-party-wrapper>
    <demo-child-component></demo-child-component>
</third-party-wrapper>

如果第三方库能够控制 demo-child-component 组件的生命周期,我将无法知道它被实例化了多少次。其中唯一方法就是查看第三方库的代码,了解它们的内部处理逻辑。将组件的生命周期被绑定到我们的应用程序组件而不是包装器的意义是,开发者可以掌控计数器只被实例化一次,而不用了解第三方库的内部代码。

性能的原因 更为重要。因为 ng-content 只是移动元素,所以可以在编译时完成,而不是在运行时,这大大减少了实际应用程序的工作量。

解决方法

为了让组件能够控制投射进来的子组件的实例化,我们可以通过两种方式完成:在我们的内容周围使用 <ng-template> 元素及 ngTemplateOutlet,或者使用带有 "*" 语法的结构指令。为简单起见,我们将在示例中使用 <ng-template> 语法。

demo-component 组件修改为:

import { Component, OnInit } from '@angular/core';

@Component({
    selector: 'demo-component',
    template: `
        <button (click)="show = !show">
            {{ show ? 'Hide' : 'Show' }}
        </button>
        <div class="content" *ngIf="show">
            <ng-container [ngTemplateOutlet]="template"></ng-container>
        </div>
    `
})
export class DemoComponent implements OnInit {
    @ContentChild(TemplateRef) template: TemplateRef;
    show = true;

    constructor() {
    }

    ngOnInit() {
    }
}

然后我们将 demo-child-component 包含在 ng-template 中:

<demo-component>
    <ng-template>
        <demo-child-component></demo-child-component>
    </ng-template>
</demo-component>

此时,我们在点击按钮进行切换操作时,控制台都会打印出 demo-child-component初始化完成! 这些文字。

参考资源

ng-content: The hidden docs


来自   https://www.cnblogs.com/laixiangran/p/8723166.html




angular中的ng-content

       
0.312                字数 739阅读 2,380            


在开发一个前端程序时,肯定会有很多相似的逻辑或布局,这时就需要提取公共组件或样式来尽可的复用代码。在angular中,提供了ng-content来更方便的编写组件和布局。

ng-content

  • 在组件中嵌入内容

  • 在组件中嵌入模板代码

  • select属性支持css选择器(”#id”,”.class”等等)

使用
下面以表单中常见的布局为例:


   
           
<form-item>
    <form-item-label>
        手机号码
    </form-item-label>
    <form-item-wrap>
        <input  name="phone" [(ngModel)]="phone"  placeholder="请输入手机号"/>
    </form-item-wrap>
</form-item>
   

我们封装了三个组件:form-item form-item-label form-item-wrap
我们期望form-item-label中的内容展示页面文字说明部分,form-item-wrap展示页面用户输入部分。

<form-item>
    <form-item-label>
    <!-- your html code -->
    </form-item-label>
    <form-item-wrap>
    <!-- your html code -->
    </form-item-wrap>
</form-item>
   

form-item组件代码:

import { Component, ContentChild } from '@angular/core';
import { FormItemLabelComponent } from './form-item-lable.component';
import { FormItemWrapComponent } from './form-item-wrap.component';
@Component({
  selector: 'form-item',
  templateUrl: './form-item.component.html'
})
export class FormItemComponent {
  @ContentChild(FormItemLabelComponent) itemLabel;
  @ContentChild(FormItemWrapComponent) itemWrap;
}
html:
<div>
    <div class="css实现">
        <ng-content select="form-item-label" *ngIf="itemLabel"></ng-content>
    </div>
    <div class="css实现">
      <ng-content select="form-item-wrap" *ngIf="itemWrap"></ng-content>
    </div>
</div>
   

form-item-label组件代码:

import { Component } from '@angular/core';
({
  selector: 'form-item-label',
  template: '<ng-content></ng-content>'
})
export class FormItemLabelComponent {
}
   

form-item-wrap组件代码:

import { Component } from '@angular/core';
({
  selector: 'form-item-wrap',
  template: '<ng-content></ng-content>'
})
export class FormItemWrapComponent {
}
   

在组件form-item中通过ng-content的select属性(使用的是css的element选择器)投影form-item-label和form-item-wrap组件内容。

在form-item-label和form-item-wrap中则使用ng-content来嵌入模板代码,如果没有ng-content则组件中的模板代码无法展示。(组件内css样式不再展示)

ContentChild

  • ContentChild 用来从通过 Content Projection 方式 (ng-content) 设置的视图中获取匹配的元素

  • 在父组件的 ngAfterContentInit 生命周期钩子中才能成功获取通过 ContentChild 查询的元素

它和viewChild是不同的

  • ViewChild 用来从模板视图中获取匹配的元素

  • 在父组件的 ngAfterViewInit 生命周期钩子中才能成功获取通过 ViewChild 查询的元素

与css直接布局对比优势

本文中注册页面视效是左右布局,有些ux设计可能是上下布局,可能是点击操作弹出文字说明布局等等,这些布局抽象成建模分为两部分:文字说明部分和用户操作部分。使用ng-content编写组件,实际上相当于页面领域的建模,form-item-label是文字描述部分,form-item-wrap是用户操作部分,不论ux ui的设计如何,但是建模是一定的。

组件化的优势

  1. 牺牲我一个,幸福千万家

  2. 以表单举例:设计师要求左边部分带必选符号”*“或者”:”,如果是各个业务的开发童鞋来写,后期设计一旦变更,就会存在更换不及时,整个产品用户体验不一致的情况,而如果通过form-item组件内部搞定这件事情,只需要开发这个组件的同事更新组件即可,保证了整体的一致性

  3. 项目团队有一个css高手保证组件样式即可,极大的提升开发效率

总结

ng-content是组件化的神器,用好ng-content对一个项目有极大的便利。


来自  https://www.jianshu.com/p/e2c37c24e871

普通分类: