Angular Components 系列笔记 - Part 1

820 阅读7分钟

ng-content

<ng-content>是angular提供一组标签,简单的说,通过该标签,可以从父组件向子组件的模板中注入内容;也可以将ng-content看作一种特殊的占位符,使用该占位符,可以动态的将任何元素插入到自定义的组件中。类似于在angular模板中通过{{variable}}插入变量。

下面分别使用{{}}和ng-content实现一个button组件,button的label信息从父组件传入:

  • 使用{{label}} + @Input,显示从父组件传到子组件的值
// abb-button-a.component.ts
import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'add-button-a',
  template: `
    <button>{{label}}</button>
  `
})

export class AddButtonAComponent implements OnInit {
  @Input() label: string;
  constructor() { }
  ngOnInit() {
  }
}

上述代码中创建了一个简单的button组件,通过@Input获取父组件传递的label,并通过{{}}表达式将其显示在button上。在app.component.html中加载该组件:

// app.component.html
<add-button-a label='add-button-a'></add-button-a>
  • 使用ng-content构建相同的button组件
// add-button-b.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'add-button-b',
  template: 
    `<button>
      <ng-content></ng-content>
    </button>`,
})
export class AddButtonBComponent implements OnInit {
  constructor() { }

  ngOnInit() {
  }
}

在上述add-button-b组件中,不再使用Input获取父组件的值,使用ng-content作为按钮文字的占位符;当需要使用该组件时,可直接在父组件中插入对应的值:

// app.component.html
<add-button-b>
  <span>add-button-b</span>
</add-button-b>

对比以上两种方式,通过ng-content定义通用组件时,不要再额外定义变量接受父组件传递的值,仅定义通用的模板,其内部具体的内容在真正使用该组件时再关注。

  • multiple ng-content & Selector 在定义通用组件时,如果同时使用多个ng-content占位符,如何确保每个占位符插入准确的内容呢?ng-content支持使用选择器,既可以使用CSS选择器,用户也可自定义选择器。
  • CSS 选择器 - 标签选择器和class选择器
    改造上述的add-button-b组件, 使其包含两个,分别将标签(span)和class(.text)作为选择器:
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'add-button-b',
  template: 
    `<button>
    <!-- add the select attribute to ng-content -->
      <ng-content select='span'></ng-content>
    </button>
      <ng-content select='.text'></ng-content>
    `,
})
export class AddButtonBComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }
}

在父组件,按照选择器的规则进行插值,span标签及内容将映射到button上,而<div class='text'>Hello World</div>将显示在button下方:

<add-button-b>
  <span>add-button-b</span>
  <div class='text'>Hello World</div>
</add-button-b>
  • 自定义选择器
    除了上述CSS选择器外,可以使用方括号[ ]自定义选择器,如下再模板中定义select="[button-label]"select='[hello-text]':
@Component({
  selector: 'add-button-b',
  template: 
    `<button (click)='onClick()'>
      <ng-content select="[button-label]"></ng-content>
    </button>
      <ng-content select='[hello-text]'></ng-content>
    `,
})

在父组件中应用该组件时,添加相应的属性button-labelhello-text,即可将相应的内容插入到对应的为止:

<add-button-b>
  <span button-label>add-button-b</span>
  <div hello-text>Hello World</div>
</add-button-b>

@ContentChild & @ContentChildren

前面已经了解了ng-content的用法,那么如何获取通过ng-content插入到组件中的内容呢?Angular提供了ContentChild和ContentChildren两种装饰器,通过这两种装饰器,可以在类中可以获取通过ng-content插入到组件模板中的任意元素。
以ContentChild为例,它可以接受一系列的参数,各个参数的含义此处不介绍。

下面举个简单的例子,分别创建MessageContainerComponent和MessageComponent,并通过ng-content将MessageComponent插入到MessageContainerComponent的模板中:

// message-container.component.ts
import { Component, OnInit, ContentChild, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-message-container',
  template: `
    <div>
      <span>Message Container</span>
      <ng-content></ng-content>
    </div>
  `,
  styleUrls: ['./message-container.component.css']
})
export class MessageContainerComponent implements OnInit {
  constructor() { }
  ngOnInit() { }
}
// message.component.ts
import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-message',
  template: `
    <h2>{{message}}</h2>
  `,
  styleUrls: ['./message.component.css']
})
export class MessageComponent implements OnInit {
  @Input() message;
  constructor() { }

  ngOnInit() { }
}

在父组件中加载MessageContainerComponent:

<app-message-container>
  <app-message message='Hello World!!'></app-message>
</app-message-container>

页面可正常渲染,并将messageComponent加载到messageContainerComponent中。

此时,在MessageContainerComponent类中引入ContentChild,并通过@ContentChild获取MessageComponent对象:

import { Component, OnInit, ContentChild, AfterContentInit } from '@angular/core';
import { MessageComponent } from '../message/message.component';

@Component({
  selector: 'app-message-container',
  template: `
    <div>
      <span>Message Container</span>
      <ng-content select='app-message'></ng-content>
    </div>
  `,
  styleUrls: ['./message-container.component.css']
})
export class MessageContainerComponent implements OnInit, AfterContentInit {
  // 通过@ContentChild创建变量
  @ContentChild(MessageComponent, {static: true}) MessageComponent: MessageComponent;
  constructor() { }
  ngOnInit() { }
  //在ngAfterContentInit生命周期函数中获取messageContainerComponentChild对象
  ngAfterContentInit() {
    console.log(this.messageContainerComponentChild);
  }
}

如果多个元素通过ng-content插入到组件中,可以使用@ContentChildren获取:

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-message-container>
    // 生成多个app-message
      <app-message *ngFor='let m of messageList' [message]='m'></app-message>
    </app-message-container>`,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  messageList = ['Hello World', 'Hello Angular'];
}

在MessageContainerComponent通过ContentChildren获取元素集合:

// message-container.component.ts
import { Component, OnInit, ContentChild, AfterContentInit, ContentChildren, QueryList } from '@angular/core';
import { MessageComponent } from '../message/message.component';

@Component({
  selector: 'app-message-container',
  template: `
    <div>
      <span>Message Container</span>
      <ng-content select='app-message'></ng-content>
    </div>
  `,
  styleUrls: ['./message-container.component.css']
})

export class MessageContainerComponent implements OnInit, AfterContentInit {
  // 通过ContentChildren创建包含MessageComponent的元素集合messageContainerComponentChildren
  @ContentChildren(MessageComponent) messageContainerComponentChildren: QueryList<MessageComponent>;

  constructor() { }

  ngOnInit() { }

  ngAfterContentInit() {
    console.log(this.messageContainerComponentChildren);
  }
}

控制台输出元素列表如下:

当使用<ng-content>插入到组件中的不是组件,而是HTML元素时,通过contentChild获取到的自然也不再是实例对象,而是原生DOM元素封装后的引用:

// app.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  template: `
    <app-message-container>
      <span #message>Message Info</span>
    </app-message-container>`,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
}

// message=container.component.ts
import { Component, OnInit, AfterContentInit, ContentChild, ElementRef } from '@angular/core';
import { MessageComponent } from '../message/message.component';

@Component({
  selector: 'app-message-container',
  template: `
    <div>
      <span>Message Container</span>
      <ng-content></ng-content>
    </div>
  `,
  styleUrls: ['./message-container.component.css']
})
export class MessageContainerComponent implements OnInit, AfterContentInit {
  @ContentChild('message', {static: false}) message;

  constructor() { }

  ngOnInit() { }

  ngAfterContentInit() {
    console.log(this.message);// ElementRef
    console.log(this.message.nativeElement);
  }
}

上述代码中,往组件中插入了<span #message>Message Info</span>元素,并以message作为选择器,在MessageContainerComponent中获取该元素,得到的是ElementRef,是在原生DOM元素上的一层封装,可以通过ElementRef.nativeElement获取原生的DOM元素。

综上,通过<ng-content>插入到组件中的子组件或HTML元素,都可通过ContentChild或ContentChildren获取,插入的子组件渲染完成后,会触发ngAfterContentInit周期函数,因此通常在该生命是周期函数中获取相应组件的实例。

@ViewChild and @ViewChildren

@ViewChild和@ViewChildren也是angular提供的非常实用的装饰器,利用这些装饰器,可以获取到任意模板元素的引用。

  • ViewChild 重构前一个例子中的MessageContainerComponent,在其模板直接引入Message组件,并使用ViewChild获取模板中对应元素的引用:
import { Component, OnInit, AfterViewInit, ViewChild } from '@angular/core';
import { MessageComponent } from '../message/message.component';

@Component({
  selector: 'app-message-container',
  template: `
    <div>
      <span>Message Container</span>
      <h2 #header>Hello World</h2>
      <app-message #messageComponent message='Hello Shanghai'></app-message>
    </div>
  `,
  styleUrls: ['./message-container.component.css']
})
export class MessageContainerComponent implements OnInit, AfterViewInit {
  // Selector: 'messageComponent'
  @ViewChild('messageComponent', {static: false}) messageComponent: MessageComponent;
  // Selector: Header
  @ViewChild('header', {static: false}) header;

  constructor() { }

  ngOnInit() { }

  ngAfterViewInit() {
    console.log(this.messageComponent);
    console.log(this.header);
  }
}

上述代码中,分别通过ViewChild获取了组件(MessageComponent)和DOM元素(h2)的引用。不同的是,对于组件的引用,获取到的是组件的实例,而对于DOM元素的引用,获取到的是ElementRef,是在原生DOM基础上进行封装的对象,可以通过ElementRef.nativeElement进一步获取原生的DOM对象。

页面渲染完成后,控制台输出如下:

需要注意的是,页面初始化完成后,会调用ngAfterViewInit方法;因此,通常在ngAfterViewInit生命周期函数中才能获取到元素的引用。

  • ViewChildren 从字面上,很容易理解ViewChildren和ViewChild的不同,viewChild获取的是单个元素的引用,而ViewChildren获取的是多个元素引用的集合(QueryList)。
import { Component, OnInit, AfterViewInit, ViewChildren, QueryList } from '@angular/core';
import { MessageComponent } from '../message/message.component';

@Component({
  selector: 'app-message-container',
  template: `
    <div>
      <span>Message Container</span>
      <app-message #messageComponents *ngFor='let m of messageList' [message]='m'></app-message>
    </div>
  `,
  styleUrls: ['./message-container.component.css']
})

export class MessageContainerComponent implements OnInit, AfterViewInit {
  messageList: string[] = ['Hello World', 'Hello Shanghai'];
  @ViewChildren('messageComponents') messageComponents: QueryList<MessageComponent>;
  constructor() { }

  ngOnInit() { }

  ngAfterViewInit() {
    console.log(this.messageComponents);
  }
}

控制台输出如下:

@ContentChild(@ContentChildren) VS @ViewChild(@ViewChildren)

@ContentChild(@ContentChildren)与@ViewChild(@ViewChildren)的使用场景不同:

  • @ContentChild(@ContentChildren): 获取组件中直接插入的任何指令、组件或元素,在ngAfterContentInit生命周期函数中获取;
  • @ContentChild(@ContentChildren):获取通过<ng-content>插入到组件中的元素,在ngAfterViewInit生命周期函数中获取。