Angular(2)
1. TemplateRef
就是 Template 的类型
https://segmentfault.com/a/1190000008672478
<h1>hello world</h1>
<div class="box" #box>box</div>
<ng-template #tpl>
<span>hello</span>
</ng-template>
import { AfterViewInit, Component, ElementRef, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit,AfterViewInit{
@ViewChild("tpl",{read:TemplateRef}) readonly tpl:TemplateRef<any>
@ViewChild("box") readonly box:ElementRef
constructor(
) {
//console.log("constructor",this.tpl)
}
ngOnInit(): void {
//console.log("ngOnInit",this.tpl)
}
ngAfterViewInit(): void {
const viewRef = this.tpl.createEmbeddedView(null)
console.log("tpl",this.tpl)
console.log("box",this.box)
console.log("ngAfterViewInit",this.tpl.createEmbeddedView(null))
this.box.nativeElement.appendChild(viewRef.rootNodes[0])
}
}
我们发现 @Component template 中定义的 <template> 模板元素,渲染后被替换成 comment 元素(comment是注释节点),其内容为 "template bindings={}" 。此外我们通过 @ViewChild 获取的模板元素,是 TemplateRef_ 类的实例,接下来我们来研究一下 TemplateRef 类:
2. EmbeddedViewRef
EmbeddedView 的类型
import { AfterViewInit, Component, ElementRef, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit,AfterViewInit{
@ViewChild("tpl",{read:TemplateRef}) readonly tpl:TemplateRef<any>
@ViewChild("box") readonly box:ElementRef
@ViewChild("first",{read:ViewContainerRef}) readonly first:ViewContainerRef
constructor(
) {
//console.log("constructor",this.tpl)
}
ngOnInit(): void {
//console.log("ngOnInit",this.tpl)
}
ngAfterViewInit(): void {
const viewRef = this.tpl.createEmbeddedView(null)
console.log("tpl",this.tpl)
console.log("box",this.box)
console.log("first",this.first)
//console.log("ngAfterViewInit",this.tpl.createEmbeddedView(null))
// this.box.nativeElement.appendChild(viewRef.rootNodes[0])
this.first.createEmbeddedView(this.tpl)
}
}
3. ViewContainerRef
就是Container (视图容器) 的类型
import {AfterViewInit, Component, EmbeddedViewRef, OnInit, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core';
@Component({
selector: 'app-tpl-container',
templateUrl: './tpl-container.component.html'
})
export class TplContainerComponent implements OnInit, AfterViewInit {
// 获取模板中的元素(组件、ng-template、dom)
@ViewChild('box') readonly boxEl: ElementRef;
@ViewChild('firstTpl', { read: TemplateRef }) readonly firstTpl: TemplateRef<any>;
@ViewChild('secondTpl', { read: TemplateRef }) readonly secondTpl: TemplateRef<any>;
@ViewChild('thirdTpl', { read: TemplateRef }) readonly thirdTpl: TemplateRef<any>;
@ViewChild('fourthTpl', { read: TemplateRef }) readonly fourTpl: TemplateRef<any>;
@ViewChild('freeTpl', { read: TemplateRef }) readonly freeTpl: TemplateRef<any>;
@ViewChild('firstContainer', { read: ViewContainerRef, static: true }) readonly firstContain: ViewContainerRef;
@ViewChild('secondContainer', { read: ViewContainerRef, static: true }) readonly secondContain: ViewContainerRef;
private freeViewRef: EmbeddedViewRef<any>;
constructor() {
// console.log('constructor');
}
insert(tpl: TemplateRef<any>) {
this.firstContain.insert(tpl.createEmbeddedView(null));
}
insertAll() {
[this.secondTpl, this.thirdTpl, this.fourTpl].forEach(tpl => {
this.firstContain.insert(tpl.createEmbeddedView(null));
});
}
getOne() {
console.log(this.firstContain.get(2));
console.log(this.firstContain.indexOf(this.freeViewRef));
}
insertFree() {
this.firstContain.insert(this.freeViewRef, 1);
}
move() {
// 不需要事先插入也可以移动(定好位置再插入)
this.firstContain.move(this.freeViewRef, 2);
}
move2To4() {
const view = this.firstContain.detach(1);
this.firstContain.insert(view, 3);
}
move2ToOther() {
const view = this.firstContain.detach(1);
this.secondContain.insert(view);
}
ngOnInit(): void {
// console.log('onInit');
}
ngAfterViewInit(): void {
console.log('ngAfterViewInit');
this.freeViewRef = this.freeTpl.createEmbeddedView({ $implicit: 'defaultValue', free: 'aa' });
// console.log(this.firstTpl);
// const viewRef = this.firstTpl.createEmbeddedView(null);
// console.log('viewRef', viewRef);
/*this.boxEl.nativeElement.appendChild(viewRef.rootNodes[0]);*/
setTimeout(() => {
this.firstContain.createEmbeddedView(this.firstTpl);
}, 0);
}
}
<div class="box" #box>
<button class="btn btn-primary mr-1" (click)="insert(secondTpl)">insert second</button>
<button class="btn btn-primary mr-1" (click)="insert(thirdTpl)">insert third</button>
<button class="btn btn-primary mr-1" (click)="insert(fourthTpl)">insert fourth</button>
<button class="btn btn-primary mr-1" (click)="insertAll()">insert all</button>
<button class="btn btn-secondary mr-1" (click)="insertFree()">insert free</button>
<button class="btn btn-info mr-1" (click)="getOne()">get one</button>
<button class="btn btn-success mr-1" (click)="move()">move free</button>
<button class="btn btn-success mr-1" (click)="move2To4()">把第二个移动到第四个位置上</button>
<button class="btn btn-success" (click)="move2ToOther()">把第二个移动到其他容器中</button>
<p>长度:{{ firstContain?.length }}</p>
</div>
<ng-template #firstTpl>
<p>first tpl content</p>
</ng-template>
<ng-template #secondTpl>
<p>第二段template</p>
</ng-template>
<ng-template #thirdTpl>
<p>第三段template</p>
</ng-template>
<ng-template #fourthTpl>
<p>第四段template</p>
</ng-template>
<ng-template #freeTpl>
<p>自由的template</p>
</ng-template>
<p>first container:
<ng-container #firstContainer></ng-container>
</p>
<hr>
<p> second container:<ng-container #secondContainer></ng-container>
</p>
4. NgTemplateOutlet指令
父组件调用TplOutletComponent传入自定义的dom
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-tpl-outlet [render]="customTpl"></app-tpl-outlet>
<ng-template #customTpl let-def let-val="value">
<b> {{def}} ----- {{val}}</b>
</ng-template>
`
})
export class ItemDetailComponent { }
import { Component, Input, TemplateRef } from '@angular/core';
@Component({
selector: 'app-tpl-outlet',
template: `
<div>
<ng-container [ngTemplateOutlet]="render || defaultTpl" [ngTemplateOutletContext]="myContext"></ng-container>
<ng-container *ngTemplateOutlet="render || defaultTpl; context: myContext"></ng-container>
<!-- 用在ng-template上也可以 -->
<ng-template [ngTemplateOutlet]="render || defaultTpl" [ngTemplateOutletContext]="myContext"></ng-template>
<ng-template *ngTemplateOutlet="render || defaultTpl; context: myContext"></ng-template>
</div>
<ng-template #defaultTpl>
<b>这是默认的内容</b>
</ng-template>
`
})
export class TplOutletComponent {
@Input () render: TemplateRef<any>;
myContext = {$implicit: 'World', value: 'Svet'};
}
import { Component, Input, TemplateRef } from '@angular/core';
@Component({
selector: 'app-tpl-outlet',
template: `
<div>
<ng-container [ngTemplateOutlet]="render || defaultTpl"></ng-container>
<ng-template #defaultTpl>
<b>这是默认的内容</b>
</ng-template>
</div>
`
})
export class TplOutletComponent {
@Input () render: TemplateRef<any>;
}
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-tpl-outlet [render]="customTpl"></app-tpl-outlet>
<ng-template #customTpl>
<p>这是自定义的内容</p>
</ng-template>
`
})
export class ItemDetailComponent { }
5. 组件投影(插槽)
具名插槽
子
<div class="shadow">
<h3>hello shadow</h3>
<div class="head bg-primary">
我是头
<ng-content select=".head"></ng-content>
</div>
<div class="body">
我是身体
<ng-content select=".body"></ng-content>
</div>
<div class="foot bg-danger">
我是尾巴
<ng-content select=".foot"></ng-content>
</div>
</div>
父
<app-structural>
<div class="head">
头部content
</div>
<div class="body">
默认主体内容content
</div>
<div class="foot">
尾部content
</div>
</app-structural>
子
import { Component, Input, TemplateRef } from '@angular/core';
@Component({
selector: 'app-shadow',
template: `
<div class="shadow">
<div class="head">
<ng-content select=".head"></ng-content> //具名插槽
</div>
<div class="body">
<ng-content select="[attr]"></ng-content>
<ng-content select="article"></ng-content> //
<ng-content></ng-content> //普通插槽
</div>
<div class="foot">
<ng-content select=".foot"></ng-content> //具名插槽
</div>
</div>`
})ts
export class ShadowComponent {}
父
调用ShadowComponent:
- select=".head"
- select="article" 元素选择器
- select="[attr]"
- 直接
<ng-content></ng-content>
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-shadow [visible]="true">
<div class="head">这是head的投影</div>
<div attr>这是attr的投影内容</div>
<article>这是article的投影内容</article>
<b style="color: #007bff">这是默认的投影内容</b>
<div class="foot">这是foot的投影</div>
</app-shadow>
`
})
export class AppComponent {}
6. ViewChild
最好在ngAfterViewInit之后,获取模版上的内容
元数据属性:
- selector - 用于查询的指令类型或名字。
- read - 从查询到的元素中读取另一个令牌。
- static - 在变更检测之前解析查询结果为true,在变更检测之后解析为false。默认为false。
获取普通元素dom
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
@Component({
selector: 'app-view-child',
template: `
<section>
<h3>获取dom</h3>
<div class="box" #box>
<p>box</p>
</div>
</section>
`,
styles: []
})
export class ViewChildComponent implements OnInit, AfterViewInit {
@ViewChild('box') private boxEl: ElementRef;
constructor() {
// TypeError: Cannot read property 'nativeElement' of undefined
console.log('0', this.boxEl.nativeElement);
}
ngOnInit(): void {
// TypeError: Cannot read property 'nativeElement' of undefined
console.log('1', this.boxEl.nativeElement);
}
ngAfterViewInit(): void {
console.log('2', this.boxEl.nativeElement); // 正确
}
}
上面例子中的boxEl,默认在变更检测之后才会获取到元素,而ngAfterViewInit就是在变更检测之后才会调研
static属性
默认在变更检测之后才会获取到目标元素,可开启static,这样组件初始化的时候,变更检测前就能获取到目标
export class ViewChildComponent implements OnInit, AfterViewInit {
@ViewChild('box', { static: true }) private boxEl: ElementRef;
constructor() {
// TypeError: Cannot read property 'nativeElement' of undefined
console.log('0', this.boxEl.nativeElement);
}
ngOnInit(): void {
console.log('1', this.boxEl.nativeElement); // 正确
}
ngAfterViewInit(): void {
console.log('2', this.boxEl.nativeElement); // 正确
}
}
可以看到在constructor里是拿不到模板元素的,建议如果目标从一开始就显示在模版上
即没有被ngIf等指令操控,就开启static
获取子组件(指令)
以组件为例,获取到组件实例后可以访问子组件到属性和方法,指令用法和组件一摸一样
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
@Component({
selector: 'app-view-child-panel',
templateUrl: './view-child-panel.component.html'
})
export class ViewChildPanelComponent implements OnInit {
readonly name = 'panel';
constructor() { }
ngOnInit(): void {}
}
//======================================================================//
@Component({
selector: 'app-view-child',
template: `
<section>
<h3>获取子组件</h3>
<app-view-child-panel></app-view-child-panel>
</section>
`,
styles: []
})
export class ViewChildComponent implements OnInit, AfterViewInit {
@ViewChild(ViewChildPanelComponent, { static: true }) private panel: ViewChildPanelComponent;
constructor() {}
ngOnInit(): void {}
ngAfterViewInit(): void {
// console.log(2, this.boxEl.nativeElement);
console.log(this.panel.name);
}
}
获取子组件(指令) 写法2
也可以通过模版引用变量获取子组件
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
@Component({
selector: 'app-view-child',
template: `
<section>
<h3>获取子组件</h3>
<app-view-child-panel #myPanel></app-view-child-panel>
</section>
`,
styles: []
})
export class ViewChildComponent implements OnInit, AfterViewInit {
@ViewChild('myPanel', { read: ViewChildPanelComponent, static: true }) private panel: ViewChildPanelComponent;
constructor() {}
ngOnInit(): void {}
ngAfterViewInit(): void {
// console.log(2, this.boxEl.nativeElement);
console.log(this.panel.name);
}
}
获取宿主<app-panel>
import { Component, ElementRef, OnInit } from '@angular/core';
@Component({
selector: 'app-panel',
templateUrl: './panel.component.html',
styleUrls: ['./panel.component.scss']
})
export class PanelComponent implements OnInit {
readonly name = "张三"
constructor(readonly el: ElementRef) { }
ngOnInit(): void {
}
}
@Component({
selector: 'app-view-child',
templateUrl: './view-child.component.html',
styles: [
]
})
export class ViewChildComponent implements OnInit,AfterViewInit {
@ViewChild('panel',{static:true}) private panelInstance:PanelComponent;
constructor() { }
ngOnInit(): void {}
ngAfterViewInit(): void {
// console.log(this.panelInstance.name)
// console.log(this.panelInstance)
console.log(this.panelInstance.el.nativeElement)
}
}
获取template
上节(TemplateRef和ViewContainerRef)已经演示过了
7. ViewChildren
与ViewChild类似,它可以批量获取模板上相同选择器的元素并存放到QueryList类中
ViewChildren没有static属性
批量获取子组件和dom元素
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
@Component({
selector: 'app-view-child',
template: `
<section>
<h3>获取dom</h3>
<div class="box" #box>
<p>box</p>
</div>
<div class="box" #box>
<p>box</p>
</div>
</section>
<section #box>
<h3>获取子组件</h3>
<app-view-child-panel #myPanel></app-view-child-panel>
<app-view-child-panel #myPanel></app-view-child-panel>
<app-view-child-panel #myPanel></app-view-child-panel>
</section>
`,
styles: []
})
export class ViewChildComponent implements OnInit, AfterViewInit {
@ViewChild('box', { static: true }) private boxEl: ElementRef;
@ViewChildren('box') private boxEls: QueryList<ElementRef>;
@ViewChild(ViewChildPanelComponent, { static: true }) private panel: ViewChildPanelComponent;
@ViewChildren(ViewChildPanelComponent) private panels: QueryList<ViewChildPanelComponent>;
constructor() {}
ngOnInit(): void {}
ngAfterViewInit(): void {
console.log(this.panels);
console.log(this.boxEls);
}
}
QueryList
模板元素集合,详细用法参考文档
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
@Component({
selector: 'app-view-child',
template: `
<section>
<h3>获取子组件</h3>
<button class="btn btn-primary" (click)="showMidPanel = !showMidPanel">toggle mid</button>
<app-view-child-panel #myPanel></app-view-child-panel>
<app-view-child-panel #myPanel *ngIf="showMidPanel"></app-view-child-panel>
<app-view-child-panel #myPanel></app-view-child-panel>
</section>
`,
styles: [ ]
})
export class ViewChildComponent implements OnInit, AfterViewInit {
@ViewChildren(ViewChildPanelComponent) private panels: QueryList<ViewChildPanelComponent>;
constructor() {}
ngOnInit(): void {}
ngAfterViewInit(): void {
this.panels.changes.subscribe(changes => {
console.log('changes', changes);
});
}
}
8. ContentChild
用法类似ViewChild, 获取投影中的组件或指令还有元素dom等
获取投影中的组件
父
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
@Component({
selector: 'app-content-child-panel',
template: `
<app-content-child>
<div class="head">
这是头部
</div>
<app-content-child-panel></app-content-child-panel>
<ul #list>
<li>aaa</li>
<li>bbb</li>
</ul>
</app-content-child>
`,
styles: []
})
export class ContentChildPanelComponent implements OnInit {
constructor() { }
ngOnInit(): void {}
alert() {
alert('aa');
}
}
子
@Component({
selector: 'app-content-child',
template: `
<div class="content-child-box">
<h2>这是content child组件</h2>
<div class="head" style="border: 1px solid; margin: 10px 0;">
<ng-content select=".head"></ng-content>
</div>
<ng-content></ng-content>
</div>
`,
styles: []
})
export class ContentChildComponent implements AfterViewInit {
// 无法获取dom元素
// @ContentChild('.head', { static: true }) private headEl: ElementRef;
// @ContentChild('list', { static: true }) private listEl: ElementRef;
@ContentChild(ContentChildPanelComponent, { static: true }) private panel: ContentChildPanelComponent;
constructor() { }
ngAfterViewInit(): void {
this.panel.alert();
}
}
9. ContentChildren
用法类似ViewChildren, 批量获取投影中到组件或指令
<app-content-child>
<div class="head">
这是头部
<app-content-child-panel></app-content-child-panel>
</div>
<app-content-child-panel></app-content-child-panel>
<app-content-child-panel></app-content-child-panel>
<ul #list>
<li>aaa</li>
<li>bbb</li>
</ul>
</app-content-child>
export class ContentChildComponent implements AfterViewInit {
@ContentChildren(ContentChildPanelComponent) private panels: QueryList<ContentChildPanelComponent>;
constructor() { }
ngAfterViewInit(): void {
console.log(this.panels); // 只有两个结果
}
}
descendants属性
这是ContentChildren特有的属性,上个例子少拿类一个panel组件,原因是默认只寻找直属子panel 而.head里但panel组件,并非直属,所以拿不到,想要寻找到所有层级的panel组件,就开启descendants
@ContentChildren(ContentChildPanelComponent, { descendants: true }) private panels: QueryList;
10. 管道
内置管道
之前讲模板表达式时,已经用过管道了几个内置管道,下面是常用的内置管道:
- AsyncPipe -- 自动订阅模板中的Observable或Promise
- DatePipe -- 格式化日期
- DecimalPipe -- 数字转字符串,并可以指定格式
- KeyValuePipe -- 使ngFor可以循环Object或Map对象
- JsonPipe -- 将值转成json
- TitleCasePipe -- 把首字母大写,其它小写
- SlicePipe -- 截取Array或String
- PercentPipe -- 数字转百分比
- LowerCasePipe和UpperCasePipe -- 转化小写或大写
自定义管道
在Angular中可以自定义带参数的管道;只要遵循下面三条规则:
-
利用@pipe装饰器声明管道的名字
-
实现PipeTransform接口
-
如果是全局使用,则 include your pipe in the
declarations
array of the AppModule;如果想要局部使用, 则 provide it in the providers array of your NgModule.
ng g pipe xxx
下面定义一个将数字指数化的管道
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'exponentialStrength'
})
export class ExponentialStrengthPipe implements PipeTransform {
transform(value: number, exponent?: number): number {
return Math.pow(value, isNaN(exponent) ? 1 : exponent);
}
}
调用:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h2>Power Boost Calculator</h2>
<div>Normal power: <input [(ngModel)]="power"></div>
<div>Boost factor: <input [(ngModel)]="factor"></div>
<!--<p>Super power boost: {{ 2 | exponentialStrength }}</p>-->
<p>Super power boost: {{ power | exponentialStrength: factor }}</p>
`,
})
export class AppComponent {
power = 5;
factor = 1;
}
非纯管道
https://www.cnblogs.com/juliazhang/p/11228988.html
默认的管道都是纯的,Angular 会忽略复合对象中的变化,即管道只会检查原始值或对象引用
可如果数组中的元素变化,增删改,由于引用没有变化,所以不会执行管道的逻辑
@Pipe({
name: 'flyingHeroesImpure',
pure: false
})
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import {ExponentialStrengthPipe} from './exponential-strength.pipe';
import {FlyingHeroesImpurePipe} from './flying-heroes-impure.pipe';
@NgModule({
declarations: [
ExponentialStrengthPipe,
FlyingHeroesImpurePipe
],
imports: [
CommonModule
],
exports: [
ExponentialStrengthPipe,
FlyingHeroesImpurePipe
]
})
export class PipeModule { }
import { Pipe, PipeTransform } from '@angular/core';
import {Hero} from '../types';
@Pipe({
name: 'flyingHeroesImpure',
pure: false
})
export class FlyingHeroesImpurePipe implements PipeTransform {
transform(allHeroes: Hero[]) {
console.log('transform');
return allHeroes.filter(hero => hero.canFly);
}
}
code
import { Component } from '@angular/core';
interface Hero {
id: string;
name: string;
canFly?: boolean;
}
const HEROES = [
{
id: 'hero_0',
name: '盖伦',
canFly: false
},
{
id: 'hero_1',
name: '赵信',
canFly: false
},
{
id: 'hero_2',
name: '嘉文',
canFly: false
},
{
id: 'hero_3',
name: '易大师',
canFly: false
},
{
id: 'hero_3',
name: '泰达米尔',
canFly: true
}
];
@Component({
selector: 'app-root',
template: `
<input type="text" #box (keyup.enter)="addHero(box.value)" placeholder="hero name" />
<button (click)="reset()">Reset</button>
<div *ngFor="let hero of (heroes | flyingHeroes)">
{{hero.name}}
</div>
`,
})
export class AppComponent {
heroes: Hero[] = [];
canFly = true;
constructor() { this.reset(); }
ngOnInit(): void { }
addHero(name: string) {
name = name.trim();
if (name) {
// 不改变引用没有用
this.heroes.push({ id: 'flier_' + Date.now(), name, canFly: this.canFly });
}
}
reset() { this.heroes = HEROES.slice(); }
}
上面往数组里push值,由于数组引用不变,管道逻辑不会再次执行,两个方法:
- 改变引用
this.heroes = [
...this.heroes,
{ id: 'flier_' + Date.now(), name, canFly: this.canFly }
];
- 将管道标记为非纯的
@Pipe({
name: 'flyingHeroes',
pure: false
})
11. 生命周期
从实例化组件,渲染组件模板时,各声明周期就已开始
ngOnChanges
输入属性发生变化是触发,但组件内部改变输入属性是不会触发的
import { Component, Input, OnInit, OnChanges } from '@angular/core';
@Component({
selector: 'app-life-cycle',
templateUrl: `
点击按钮不会触发ngOnChanges
<button (click)="title = 'self title'">set title self</button>
<p>title: {{ title }}</p>
`
})
export class LifeCycleComponent implements OnInit, OnChanges {
@Input() title: string;
constructor() {
console.log('constructor', this.title); // undefined
console.log('constructor', this.content); // 一段content
}
ngOnChanges(changes: SimpleChanges): void {
console.log('changes', changes);
}
ngOnInit(): void {
console.log('ngOnInit', this.title);
}
}
ngOnInit
只在组件/指令初始化调用一次,在它之前调用的顺序是 constructor -> ngOnChanges -> ngOnInit 官方建议在这个钩子中获取组件初始化的数据,而构造函数中只适合写简单的逻辑,比如初始化局部变量
constructor在设置输入属性前调用,也就是说,在constructor里拿不到未初始化的输入属性
import { Component, Input, OnInit } from '@angular/core';
export class LifeCycleComponent implements OnInit {
@Input() title: string;
@Input() content = '一段content';
constructor() {
console.log('constructor', this.title); // undefined
console.log('constructor', this.content); // 一段content
}
ngOnInit(): void {
console.log('ngOnInit', this.title);
}
}
ngOnDestroy
组件销毁时触发一次,在这里应该清理一些残存的状态(事件、定时器等)
<button class="btn btn-primary" (click)="show = !show">toggle</button>
<app-life-cycle title="aaa" *ngIf="show"></app-life-cycle>
所有钩子的触发顺序
export class LifeCycleComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
@Input() title: string;
constructor() {
console.log('constructor', this.title);
}
ngOnChanges(changes: SimpleChanges): void {
console.log('ngOnChanges');
}
ngOnInit(): void {
console.log('ngOnInit', this.title);
}
ngDoCheck(): void {
console.log('ngDoCheck');
}
ngAfterContentInit(): void {
console.log('ngAfterContentInit');
}
ngAfterContentChecked(): void {
console.log('ngAfterContentChecked');
}
ngAfterViewInit(): void {
console.log('ngAfterViewInit');
}
ngAfterViewChecked(): void {
console.log('ngAfterViewChecked');
}
ngOnDestroy(): void {
console.log('ngOnDestroy');
}
}
AfterContent
AfterContentInit
组件投影初始化后调用一次
AfterContentInit
AfterContentInit后以及每次投影内容改变后调用
当组件或父组件发生变更检测后都会调用下面三个钩子 ngDoCheck ngAfterContentChecked ngAfterViewChecked
12. 变更检测
触发变更检测的时机
- 事件:页面 click、submit、mouse down……
- XHR:从后端服务器拿到数据
- Timers:setTimeout()、setInterval()
单向数据流
假设其中某个组件中触发的变更检测,就会从根组件开始,从上至下,挨个检测一遍,直到最后一级组件全部检测完毕
已经检测完的组件,不允许在被子组件修改,这就是单向数据流
onPush策略下触发变更检测的时机
定时器已无法触发变更检测了
- 组件的@Input引用发生变化。
- 组件的 DOM 事件,包括它子组件的 DOM 事件,比如 click、submit、mouse down。
- Observable 订阅事件,同时设置 Async pipe。
- 手动使用ChangeDetectorRef.detectChanges()、ChangeDetectorRef.markForCheck()、ApplicationRef.tick()方法
阻断了一个组件的变更检测后,他和他的子组件都不会检测了
api:ChangeDetectorRef
change.component
<div class="change">
<h2>这是change组件</h2>
<p> heroName: {{ heroName }} </p>
<div class="sec">
<button class="btn btn-primary" (click)="arms ='长剑'">改变武器</button>
<app-change-child [arms]="arms" (childInit)="heroName = '龙归'"></app-change-child>
</div>
</div>
//========================================================
//child组件改变change组件 heroName值,触发变更检测报错
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-change',
templateUrl: './change.component.html',
styles: [`
.change { width: 800px;height: 400px;background-color: #6f42c1;}
.change h2 {color: #fff;}
.change p {color: #fff; border: 1px solid #17a2b8;}`
]
})
export class ChangeComponent implements OnInit {
heroName = '卡特';
arms = '多兰剑';
constructor() { }
ngOnInit(): void {}
}
change-child.component
<div class="change-child">
<h3>这是child组件</h3>
<p>childName: {{ childName }}</p>
//从父组件按钮点击改变武器arms,默认多兰剑,点击大剑,剥离组件后组件退出检测群,不能改变且不显示武器
//但OnChanges钩子依然可用,在里面重新附着child组件到组间树
<p>武器: {{ arms }}</p>
<button class="btn btn-danger" (click)="position = '上'">改变位置</button>
<app-change-grandson [position]="position"></app-change-grandson>
</div>
//===================================================
//childName = 'VN',3秒后改为EZ
//注入ChangeDetectorRef,从检测树剥离child组件
import {ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core';
@Component({
selector: 'app-change-child',
templateUrl: './change-child.component.html',
styles: [`
.change-child {width: 600px;height: 350px;background-color: #c16a56;}
.change-child h2 {color: #9cff61;}`
]
})
export class ChangeChildComponent implements OnInit, OnChanges {
childName = 'VN';
position = '下';
@Input() arms = '多兰剑';
@Output() childInit = new EventEmitter<void>();
constructor(private cdr: ChangeDetectorRef) {
this.cdr.detach(); //从检测树剥离child组件
}
ngOnChanges(changes: SimpleChanges): void {
console.log('changes', changes);
this.cdr.reattach(); //重新附着child组件到组间树
setTimeout(() => {
// this.cdr.detach();
}, 0);
}
ngOnInit(): void {
// this.childInit.emit();
setTimeout(() => {
this.childName = 'EZ';
}, 3000);
}
}
change-grandson.component
<div class="change-grand-son">
<h4>这是孙子组件</h4>
<button class="btn btn-success" (click)="grandSonName = '蛤蟆'">change grandson name</button>
<p>孙子名称: {{ grandSonName }}</p>
<p>位置: {{ position }}</p>
</div>
//=============================================================
//默认grandSonName = '河蟹',position: '上' | '下',父组件点击改变位置
//3秒后改grandSonName = 'f6',使用onpush策略后无法改变,markForCheck()手动变更检测,或者强制变更检测detectChanges()
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';
@Component({
selector: 'app-change-grandson',
templateUrl: './change-grandson.component.html',
styles: [`
.change-grand-son {width: 600px;height: 350px;background-color: #3a35c1;}
.change-grand-son h2 {color: #e44b51;}
.change-grand-son p { color: #fa6993;}`
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChangeGrandsonComponent implements OnInit, OnChanges {
@Input() position: '上' | '下';
grandSonName = '河蟹';
constructor(private cdr: ChangeDetectorRef) { }
ngOnChanges(changes: SimpleChanges): void {
console.log('changes position', changes);
}
ngOnInit(): void {
setTimeout(() => {
this.grandSonName = 'f6';
this.cdr.markForCheck();
// this.cdr.detectChanges();
}, 3000);
}
}
13. 组件样式
宿主选择器
在
@Component
的元数据中指定的样式只会对该组件的模板生效。
宿主选择器
:host 选择是是把宿主元素作为目标的唯一方式
它选中的是组件模板标签,比如,相当于在父组件的style中选择 app-user {}
当宿主标签上有 active class时生效
//子组件可通过:host选择宿主
<app-style></app-style>
:host {
display: block;
border: 1px solid black;
}
//选择有active的宿主
<app-style class="active"></app-style>
:host(.active) {
border-width: 3px;
}
祖先选择器
当某个祖先元素有 CSS 类 theme-light 时,才会把 background-color 样式应用到组件内部的所有 .title 元素中
找到根元素(html标签)为止
//选择宿主元素有.theme-light类的祖先
:host-context(.theme-light) .title {
background-color: #95f04c;
}
样式模块化
- 在 @Component 的元数据中指定的样式只会对该组件的模板生效
- 组件的样式不会影响到子组件中的模板
- 组件的样式不会影响到投影内容
视图封装模式
- ShadowDom -- 不进不出,没有样式能进来,组件样式出不去, 就自己玩
- Emulated -- 默认选项,只进不出,全局样式能进来,组件样式出不去
- None -- 能进能出,此时组件的样式是全局生效的,注意与其他组件发生样式冲突
@component({
selector:'app-example'
...
...
//encapsulation:ViewEncapsulation.ShadowDom
encapsulation:ViewEncapsulation.None
})
14. 动态组件
概念
如果说,之前在模版中调用的组件为静态组件(比如:app-xxx)
那么不用在模版里声明,而是通过ts动态插入到dom中到组件,可以视为动态组件
example
下面是一个弹层组件: alert.component.ts:
<div [class]="wrapCls" role="alert">
<span class="content">{{ options.content }}</span>
<i class="close" (click)="closed.emit()">×</i>
</div>
c
import {Component, OnInit, ChangeDetectionStrategy, Output, EventEmitter} from '@angular/core';
type AlertTheme = 'primary' | 'warning' | 'danger';
export interface AlertOption {
content: string;
theme?: AlertTheme;
}
@Component({
selector: 'app-alert',
templateUrl: './alert.component.html',
styles: [`
.close {
display: block;
width: 20px;
height: 20px;
position: absolute;
right: 10px;
top: 50%;
margin-top: -10px;
cursor: pointer;
}
`],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AlertComponent implements OnInit {
options: Required<AlertOption> = {
content: '',
theme: 'primary'
}
@Output() readonly closed = new EventEmitter<void>();
constructor() { }
get wrapCls(): string {
return 'alert alert-' + this.options.theme + ' fixed-top';
}
ngOnInit(): void {}
setOptions(options: AlertOption) {
// console.log('options', options);
this.options = { ...this.options, ...options };
}
}
c
<div class="demo">
<section class="m-auto">
<button class="btn btn-primary" (click)="showAlart()">show Alert</button>
</section>
</div>
<!--<app-alert></app-alert>-->
c
import {
ApplicationRef,
Component,
ComponentFactoryResolver,
ComponentRef,
Injector,
OnInit,
ViewEncapsulation
} from '@angular/core';
import {AlertComponent} from '../components/alert/alert.component';
@Component({
selector: 'app-example',
templateUrl: './example.component.html',
styleUrls: ['./example.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ExampleComponent implements OnInit {
private container: AlertComponent;
private componentRef: ComponentRef<AlertComponent>;
constructor(
private cfr: ComponentFactoryResolver,
private inject: Injector,
private appRef: ApplicationRef
) {}
ngOnInit(): void {}
showAlart() {
if (!this.container) {
this.container = this.getContainer();
}
this.container.setOptions({ content: '一段提示', theme: 'danger' });
}
private getContainer(): AlertComponent {
// 创建指定类型的组件工厂(生产指定类型的组件)
const factory = this.cfr.resolveComponentFactory<AlertComponent>(AlertComponent);
// 根据指定的类型,创建组件的实例
this.componentRef = factory.create(this.inject);
// console.log('componentRef location', this.componentRef.location);
// console.log('componentRef hostview', this.componentRef.hostView);
// 将组件视图添加到视图树中,以激活变更检测
this.appRef.attachView(this.componentRef.hostView);
// 将组件到模版(包括app-alert标签),添加到body最后
document.body.appendChild(this.componentRef.location.nativeElement);
// document.body.appendChild((this.componentRef.hostView as EmbeddedViewRef<{}>).rootNodes[0] as HTMLElement);
this.componentRef.onDestroy(() => {
console.log('已经销毁');
});
const { instance } = this.componentRef;
instance.closed.subscribe(() => {
this.componentRef.destroy();
this.container = null;
});
return instance;
}
}
entryComponents ?
v9和v10,动态组件都不需要entryComponents了,当然写了也没有问题 从v11开始,entryComponents可能被删除 v8及以前,动态组件一定要声明entryComponents