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. 管道

内置管道

之前讲模板表达式时,已经用过管道了几个内置管道,下面是常用的内置管道:

自定义管道

在Angular中可以自定义带参数的管道;只要遵循下面三条规则:

  1. 利用@pipe装饰器声明管道的名字

  2. 实现PipeTransform接口

  3. 如果是全局使用,则 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值,由于数组引用不变,管道逻辑不会再次执行,两个方法:

  1. 改变引用
this.heroes = [
   ...this.heroes,
   { id: 'flier_' + Date.now(), name, canFly: this.canFly }
];
  1. 将管道标记为非纯的
@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()">&times;</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

posted @ 2020-11-25 17:37  Daeeman  阅读(192)  评论(0编辑  收藏  举报