前言

内容投影和ng-content是可以让我们最大程度构建可重用组件的Angular功能之一。我们来构造一个小组件,一个Font Awesomne输入框。我们设计这个组件的目标是为了构造一个带有图标的文本框。

最终的样子如图所示:

ng-content

不使用ng-content的话会遇到什么问题?先来尝试下不用内容投影的话,我们的组件会遇到什么问题。

首先看模板:

class="fa" [ngClass]="classes">    #input    (focus)="inputFocus = true"    (blur)="inputFocus = false"    (keyup)="value.emit(input.value)"/>

用classes对象来控制展示的图标,然后用inputFocus获得焦点进入input,通过组件的HostBinding来给组件应用外边框。

import {Component, EventEmitter, HostBinding, Input, OnInit, Output} from '@angular/core';

({    selector: 'app-fa-input',    templateUrl: './fa-input.component.html',    styleUrls: ['./fa-input.component.scss']})export class FaInputComponent implements OnInit {    () icon: string;    () value = new EventEmitter<string>();    inputFocus: boolean = false;    get classes() {        const cssClasses = {            fa: true,        }        cssClasses['fa' + this.icon] = true;        return cssClasses;    }    ('class.focus')    get focus() {        console.log(this.inputFocus);        return this.inputFocus;    }}

// css部分::host{    border: 1px solid grey;}input{    border: none;    outline: none;}:host(.focus) {    border: 1px solid blue;}

看样式文件可以知道,组件内部的input元素被移除了自带的样式。但我们给宿主元素加上了边框,让组件看起来像原生的html input元素。当input获取到焦点的时候,通过将.focus类添加到宿主元素来模拟输入框获得焦点。然后看看如何使用这个组件:

<div>    <h1>FA Inputh1>    <i class="fa fa-heart">i>    <app-fa-input icon="envelope" (value)="onNewValue($event)">app-fa-input>div>

使用的时候,我们只需要向组件传递一个图标的名称和接收input输入值的函数即可。让我们回顾下我们是如何设计这个组件的:

  • 作为组件公共api的一部分,我们有一个图标的属性,该属性定义了需要显示的图标。

  • 组件有个名为value的自定义输出事件,该事件在input元素输入值发生变化时发出新的值。

  • 为了实现焦点功能,我们在组件内部的input元素上绑定了blur和focus事件,通过@HostBinding在宿主元素上增加或删除focuscss 类。

  • 这个组件可以满足我们的需求。但是假设我们的需求发生了变更,我们马上会陷入到新的麻烦中。

问题1:如何支持所有的input属性?

我们的组件目前只是预定义了blur和focus属性,那我们需要增加其他属性,比如type,autocomplete、placeholder等,咋办?那我们只能被迫去修改组件,使其可以支持这样调用:

input icon="envelope" type="text" placeholder="email" autocomplete="off" (value)="onNewValue($event)">input>

在组件类中需要接收这些属性:

export class FaInputComponent implements OnInit {    // ...    () icon: string;    () placeholder: string;    () type: string;    () autocomplete: string;    () value = new EventEmitter<string>();    // ...}

在模板中应用:

class="fa" [ngClass]="classes">    #input    [placeholder]="placeholder"    [type]="type"    [autocomplete]="autocomplete"    (focus)="inputFocus = true"    (blur)="inputFocus = false"    (keyup)="value.emit(input.value)"/>

总而言之,我们要将需要处理的属性,从消费处一直传递到组件内部,然后在组件内部从组件类到组件的模板。虽然是很麻烦,但这样是可行的。但是,还有其他更棘手的问题。

问题2:如何和Angular Form 集成?

我们的组件是个带图标的输入框,那么它的作用不仅仅是展示,它的重点功能是表单的一个输入元素,那么我们很可能需要和Angular Form集成,那么我们咋办?还是如上面一样,我们需要将表单的所有属性,比如formControlName全部转发到组件内部。

问题3:检测普通浏览器事件

我们想在组件上检测到标准浏览器的dom事件怎么办?比如keydown事件?也还是和上面一样,我们需要通过组件内部检测然后在消费的地方去提供处理方法。也是可行的,但是好像我们的这个设计变得很不好,这样慢慢的会很臃肿。这种设计不是个很好的解决方法。

问题4:自定义属性

在构建表单时,第三方系统可能希望填写某些自定义的html数据属性,比如类似于:data-之类的属性用于其他作用。这会变得非常难办,因为我们无法预知这些属性的名字。那么,到目前为止,我们这种设计的关键问题是什么?关键问题是,我们将input元素隐藏到了组件模板中。在需要调用这个组件的地方和组件内部形成了一个屏障。我们可以用内容投影来重构组件,以解决上面的问题。

使用ng-content内容投影来重构组件

让我们重新设计组件Api,与其将输入元素隐藏在组件内部,不如将其提供为组件本身的内容元素(content element)。那么我们在调用的地方应该是这样的:

input icon="envelope">      <input type="text" placeholder="email"/>input>

需要注意的是,我们这里的input元素不是存在于组件内部,而是作为组件的html标签的一部分“内容”。实际上,这种api在html标准元素中非常常见,比如选择框:

<select>   <option value=1>oneoption>   <option value=2>twooption>select>

Angular Core 确实允许我们做同样的事情。我们可以使用@ContentChild和@ContentChildren装饰器来查询组件HTML内容的所标记的内容。并将其在内部模板用作配置API。如果有必要,我们还可以将区域中的内容直接用作组件的内容。我们需要改造fa-input组件:

<mat-icon>{{icon}}mat-icon><ng-content>ng-content>

为省事我这里使用了Angular Material的图标。

import {Component, Input, OnInit} from '@angular/core';({    selector: 'app-fa-input',    templateUrl: './fa-input.component.html',    styleUrls: ['./fa-input.component.scss']})export class FaInputComponent {    () icon: string;    constructor() {    }}

然后在其他组件中使用这个组件:

input icon="mail_outline">    <input type="text" name="email"/>input>

页面需要的元素都是OK的,我们这里的input也作为投影的内容显示在了组件的内部。但是好像css没有应用上啊,那投影的元素的样式如何处理?给投影的元素应用css样式;目前的样式是定义在组件的样式fa-input.component.scss之中:

input{    border: none;    outline: none;}

为啥不起作用?因为这些样式位于链接到组件的样式文件内,所以它们会被赋予一个运行时的属性,这个属性是该组件模板中所有html元素独有的属性。目前元素没应用上,我们可以给mat-icon写个样式来观察下:

.mat-icon{    color: red;}

// 然后查看运行后的页面,我们查看控制板板里面的css有:.mat-icon[_ngcontent-hhd-c122] {    color: red;}

对应的html有:

<app-fa-input _ngcontent-hhd-c144 icon="mail_outline" _nghost-hhd-c122 ng-reflect-icon="mail_outline">    <mat-icon _ngcontent-hhd-c122 role="img" class="mat-icon">mail_outlinemat-icon>    <input _ngcontent-hhd-c144 type="text" name="email">app-fa-input>

我们可以看到,组件内部的元素是拥有一个特定的属性_ngcontent-hhd-c122,组件内部链接的样式也是有一个属性_ngcontent-hhd-c122,这可以让组件内部的样式不去干涉外部的元素。这是非常有用的。而input元素是外部投影进来的,所以它的属性是_ngcontent-hhd-144,组件内部的样式是应用不上去的,这就是为啥我们样式不起作用的原因。我们需要加上::ng-deep来使样式穿透。

::ng-deep input{    border: none;    outline: none;}

这样看起来是好的,但是有个隐患,我们在外层使用组件的地方加上一个

input:

input icon="mail_outline">    <input type="text" name="email"/>input><input type="text" name="email"/>

好嘛,两个input都被应用上了样式。甚至于我们去别的组件,不是父子组件,只是在这个页面组件树的其他组件中加上input,发现都应用上了,看来这个样式使用::ng-deep之后就变成了全局的css了。这样会造成一些不可控的问题。如何解决呢?我们只需要在样式前面加上:host来限定下即可:

:host ::ng-deep input{    border: none;    outline: none;}

这样,发现只在投影到组件内部的元素才会应用这个样式。所以,我们的需求是样式既要应用在当前组件,也需要应用到投影进来的元素。我们使用:host ::ng-deep就可以完美解决。再在控制台下查看下样式:

[_nghost-unf-c122] input {    border: none;    outline: none;}

正如我们所看到的,这个样式的作用域依旧是当前的组件内容,但是他们也会穿透到投影到当前组件的元素。如何与投影内容交互?前面我们尝试了将组件内的样式应用到投影的元素中,现在我们尝试下和投影的内容进行交互。我们无法在ng-content标签上创建交互,也没法在其上绑定事件监听。相对的,与投影内容做交互做好的方法是以单独的指令去操作。这里为了示例,我就不创建新的指令了,而是使用Angular Material的matInput指令。首先在将matInput挂到input元素上:

input icon="mail_outline">    <input matInput type="text" name="email"/>input>

然后在指令中通过@ContentChild修饰符获取到投影进来的input元素:

export class FaInputComponent implements OnInit {    @Input() icon: string;    @ContentChild(MatInput)    input:MatInput;}

然后通过这个指令去模拟input获取到焦点的过程:

@HostBinding('class.focus')get focus() {    console.log('input', this.input.focused);    return this.input ? this.input.focused : false;}

// 相对应的css样式:.fa-input{    padding: 3px 8px;    display: flex;    justify-content: flex-start;    align-items: center;}:host ::ng-deep input{    border: none;    outline: none;}:host(.focus){    border: 1px solid blue;}

最后的效果:

多插槽(Multi-Slot)内容投影

到目前为止,我们基本是一个ng-content将内容投影进来,但是假如我们想投影一部分或者几个部分呢?前面是在fa-input组件内部定义了icon,然后将input从外部投影到了组件内部。那么我希望可以将两个都投影进来。fa-input组件只是提供一个空壳子。可以通过ng-content的select属性来获取到组件tag标记中的内容进行部分投影。我们可以修改fa-input组件的模板内容:

<div class="fa-input">    图标:    <ng-content select="mat-icon">ng-content>    输入框:    <ng-content select="input">ng-content>

    <ng-content>ng-content>div>

然后在使用的地方:

<app-fa-input icon="mail_outline">    <mat-icon>mail_outlinemat-icon>    <input matInput autocomplete="off" type="text" name="email"/>app-fa-input>

最后可以看到,我们包括在组件tag中的内容会被分配到我们希望他们出现的地方。来解读下上面的“插槽”。上面两个ng-content的select属性查找组件tag标记中的内容的特定元素,匹配后就投影进来,不带select的ng-content会将没有匹配的内容投影到组件中去。我们也可以查找具有特定类的元素,以结合多个选择器。例如,根据类选择某个input:

<div class="fa-input">    图标:    <ng-content select="mat-icon">ng-content>    输入框:    <ng-content select="input.text">ng-content>    <ng-content>ng-content>div>

对应的使用的地方:

<app-fa-input>    <mat-icon>mail_outlinemat-icon>    <input matInput class="text" autocomplete="off" type="text" name="email"/>    <input autocomplete="off" type="file" name="email"/>    <p>其他的一些投影p>app-fa-input>

可以看到投影到了具体的input。

最后

当然,这篇文章是我根据angular blog angular-ng-content这篇文章的翻译和解读,觉得啰嗦或者说不清楚的话,可以去看原文。

参考链接

https://blog.angular-university.io/angular-ng-content

投影元素直接隔离_Angular ngcontent 内容投影相关推荐

  1. 投影元素直接隔离_摸着夜色上露台开投影,是巴塞罗那设计师的浪漫

    总有人说,世界为你关上一扇门,定会为你留有一扇窗. 在家闷上个把月,窗户直接担起了连接人们与外界的通道.既然观众出不了门,那不如让加油打气的海报们,自己爬上墙好了--人们打开窗子就能撞上. 平面设计师 ...

  2. 通过单步调试的方式学习 Angular 中带有选择器的内容投影使用方式

    问题描述 我创建了一个 selector 为 app-content-section 的 Component,用于容纳内容投影(content projection): 这个 Component 的模 ...

  3. Angular 内容投影 content projection 关于选择器问题的单步调试

    问题描述 我定义了一个能够允许消费者 Component 将其自定义内容通过 content projection 投射进来的 Component: import { Component } from ...

  4. Angular 内容投影 content projection 的一个问题的单步调试

    问题描述 我使用如下代码测试一个最简单的 Angular 内容投影场景: import { Component } from '@angular/core';@Component({selector: ...

  5. Angular 内容投影 II

    内容投影是一种模式,你可以在其中插入或投影要在另一个组件中使用的内容. 简单来说,angular的内容投影就相当于vue的内容插槽slot.所有一下就能懂了. 1.单插槽内容投影 单插槽内容投影是指创 ...

  6. Angular 基于自定义指令的内容投影 content projection 问题的单步调试

    问题描述 本文涉及到的代码位置:https://github.com/wangzixi-diablo/ngDynamic 我有一个能接受内容投影的 Angular Component: 具体投影内容, ...

  7. angular 内容投影

    app HTML <div class="wrapper"><h2>我是父组件</h2><div>这个div定义在父组件中</ ...

  8. 理解ArcIMS投影元素

    ArcIMS 中的坐标系统由 ArcIMS 空间服务器( Spatial Server )通过三个 ArcXML 投影( projection )元素来管理:?  COORDSYS -输入的数据图层的 ...

  9. Angular 内容投影出现 No provider for TemplateRef found 错误的单步调试

    问题描述 本文涉及到的代码位置:https://github.com/wangzixi-diablo/ngDynamic 我有一个能接受内容投影的 Angular Component: 使用如下代码消 ...

最新文章

  1. 全国ps职称计算机试题及答案,最新职称计算机考试photoshop练习题
  2. iOS 即时聊天键盘处理
  3. 手动创建1个基于xml配置的springmvc 项目(without Maven)
  4. Android检查是否自启动,android – 如何检查我的应用程序是否是默认启动器
  5. python 学习源
  6. 怎样打开mysql进程数_mysql查看最大打开进程数
  7. 对一个“老”架构的重新思考
  8. USB转TTL|mcuisp使用
  9. python信息安全工具之端口扫描器
  10. iOS开发——性能优化的25个建议和技巧
  11. java进阶(1)之Euraka和Feign的结合使用
  12. 北京邮电大学计算机网络教材,北京邮电大学《计算机网络》4.pdf
  13. 电路原理 的 一些基础知识
  14. 【锂电池】关于4.2V锂电池充电IC的一些记录
  15. 再论由内而外造就自己
  16. .glusterfs_如何在Ubuntu 20.04上使用GlusterFS创建冗余存储池
  17. 痞子衡嵌入式:ARM Cortex-M文件那些事(5)- 映射文件(.map)
  18. 基于流量分析的安全检测解决方案
  19. memory_max_target/memory_target设置过大报ORA-00845错误
  20. DIY TCP/IP IP模块和ICMP模块的实现1

热门文章

  1. 从vivo 大规模特征存储实践中学点经验
  2. VMware Workstation虚拟机“”繁忙——解决方案
  3. javaSE各阶段练习题--面向对象-static-String-StringBuilder
  4. leetcode 423. Reconstruct Original Digits from English | 423. 从英文中重建数字(Java)
  5. leetcode 377. Combination Sum IV | 377. 组合总和 Ⅳ(动态规划)
  6. java text 格式化_java.text.DecimalFormat类十进制格式化
  7. 【二分查找万能模板,告别死循环、告别越界】Leecode 34. 在排序数组中查找元素的第一个和最后一个位置
  8. 【解题报告】Leecode 500. 键盘行——Leecode每日一题系列
  9. 7-4 求链式线性表的倒数第K项(最佳解法)(List容器)
  10. ab压力测试(了解ab工具,实验对网页进行测试)