0%

EveryNote

记录每天学习中发现的知识点

总结TemplateRef与ViewContainerRef

TemplateRef: 用于表示内嵌的template模板元素,可以创建内嵌视图(createEmbeddedView),可以访问到封装后的nativeElement,模板经过渲染后会替换成comment元素

ViewContainerRef: 用于表示一个视图容器,可添加一或多个视图,通过ViewContainerRef实例,可以基于TemplateRef创建内嵌视图,并指定插入位置,主要创建管理内嵌视图

1
2
3
4
5
6
7
8
<template #tpl>
...
</template>
@ViewChild('tpl') tplRef: TemplateRef<any>;
@ViewChild('tpl', { read: ViewContainerRef }) tplVcRef: ViewContainerRef;
ngAfterViewInit(){
this.tplVcRef.createEmbeddedView(this.tplRef);
}

注意:@ViewChild属性装饰器若未设置read属性,默认返回ElementRef对象实例

指令的一般应用场景

分类:内置指令, 自定义指令

主要考虑自定义指令的属性指令与结构指令

自定义属性指令实现简写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Directive({
selector: '[directivename]' // 这里定义的selector为什么使用中括号,是为了结合input装饰器获取属性值
})
export class SomeDirective {
private _default = '[someValue]' // 旨在指令类内部定义默认值
@Input(directivename) directiveAnotherName; // 输入属性
constructor(private el: ElementRef, private renderer: Renderer){ // 引用类实例化,其中renderer对象提供许多api供渲染元素

}
@HostListener(eventName) eventName(){ // 监听宿主元素事件

}

<h1 [changeColor]="'red'"></h1>
}

自定义结构指令是实现简写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Directive({
selector: '[directivename]' // 这里定义的selector为什么使用中括号,是为了结合input装饰器获取属性值
})
export class SomeDirective {
@Input(directivename)
set condition(newCondition: boolean) { // 这里用了getter、setter存取器, 进行属性值的动态监听
if(newCondition){
this.tplVc.createEmbeddedView(this.tpl); // 创建内嵌视图,可以设置第二个参数{$implicit: somevalue}, 则angular提供了let模板语法,允许在生成的上下文是定义和传递
}else{
this.tplVc.clear(); // 清除内嵌视图
}
}
constructor(private tpf: TemplateRef, private tplVc: VireContainerRef){ // 引用类实例化,用于创建内嵌视图

}

<h1 *structureDirective = true></h1> // 这里用了angular结构性指令的语法糖,原理同*ngIf
}

总结以上属性指令与结构指令

1 ElementRef与Renderer等的作用:支持跨平台,从底层封住,统一了api接口; 2 TemplateRef与ViewContainerRef的作用: 前面有总结过他俩的作用;
*3 angular2中指令与组件的关系:组件继承与指令,并扩展了与视图的关系

自定义debounceClick指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Directive({
selector: '[directiveDebounceClick]'
})
export class SomeDirective {
@Input('debounceTime') debounceTime: string;
@Output('debounceClick') debounceClick = new EventEmitter();
private clicks = new Subject<any>(); // 定义subject处理点击事件
constructor(){ }
@HostListener('click',['$event']) // 监听宿主元素上的点击事件,第二个参数用于将事件传递给eventClick方法
eventClick(event){
event.preventDefault();
event.stopPropagation(); // 阻止事件默认行为与事件冒泡
this.clicks.next(event); // 发送新值
}

ngOnInit(){
this.clicks.debounceTime(this.debounceTime) // 去抖动,时间自定义
.subscribe(x => this.debounceClick.emit(x)); // 调用emit方法发出事件
}

<button directiveDebounceClick [debounceTime] ='300' (debounceClick)="log($event)"></button>
}
ng-content包装器

如果你尝试在 Angular 中编写可重复使用的组件,则可能会接触到内容投射的概念:

1
2
3
4
5
6
7
8
9
10
11
import { Component } from '@angular/core';

@Component({
selector: 'wrapper',
template: `
<div class="box">
<ng-content></ng-content>
</div>
`
})
class Wrapper {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component } from '@angular/core';

@Component({
selector: 'wrapper',
template: `
<div class="box red">
<ng-content></ng-content>
</div>
<div class="box blue">
<ng-content select="counter"></ng-content>
</div>
`,
styles: [`
.red {background: red;}
.blue {background: blue;}
`]
})
export class Wrapper { }

将包装器的不同子项投影到模板的不同部分。 支持一个 select 属性,可以让你在特定的地方投射具体的内容。该属性支持 CSS 选择器(my-element,.my-class,[my-attribute],…)来匹配你想要的内容。如果 ng-content 上没有设置 select 属性,它将接收全部内容,或接收不匹配任何其他 ng-content 元素的内容。

1
2
3
4
<wrapper>
<span>This is not a counter</span>
<counter></counter>
</wrapper>

counter组件被正确投影到第二个蓝色框中,而 span 元素最终会在全部红色框中。

ngProjectAs

内部组件会被隐藏在另一个更大的组件中。你只需要将其包装在额外的容器中即可应用 ngIf 或 ngSwitch。无论什么原因,通常情况下,你的内部组件不是包装器的直接子节点。所以需要使用ngProjectAs属性,将它用于指定的元素上。

表单自定义验证规则

举例:邮件匹配

定义用户类型接口

1
2
3
4
5
6
7
export interface User {
name: string;
account: {
email: string;
confirm: string;
}
}

邮件匹配规则函数

1
2
3
4
5
6
7
8
export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
const email = control.get('email');
const confirm = control.get('confirm');
if (!email || !confirm){
return;
}
return email.value === confirm.value ? null : { nomatch: true };
}

表单模版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Component, OnInit} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { emailMatcher } from './email-matcher';
@Component({
selector: 'signup=form',
template: `
<form novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
<label>
name...
</label>
<div *ngIf="user.get('name').touched && user.get('name').hasError('required')">
name is required
</div>
<div formGroupName = "account">
<label>
<span>Email address</span>
<input type="email" placeholder="Your email address" formControlName="email">
</label>
<label>
<span>Confirm address</span>
<input type="email" placeholder="Confirm your email address" formControlName="confirm">
</label>
<div *ngIf="user.get('account').touched && user.get('name').hasError('nomatch')">
name is required
</div>
</div>
<button type="submit" [disabled]="user.invalid">Sign up</button>
</form>
`
})
export class SignUpFormCOmponent implements OnInit {
user: FormGroup;
constructor(private fb: FormBuilder){}
ngOnInit() {
this.user = this.fb.group({
name: ['', Validators.required],
account: this.fb.group({
email: ['', Validators.required],
confirm: ['', Validators.rquired],
}, {validator: emailMatcher})
})
}
}

对比创建表单自定义验证指令

模版

1
2
<input name="password" formControlName="password" validateEqual="password">
<input name="confirmpassword" formControlName="confirmpassword" validateEqual="password">

validateEqual指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Directive, forwardRef, Attribute } from '@angular/core';
import { Validator, AbstractControl, NG_VALIDATORS } from '@angular/forms';
@DIrective({
selector: '[validateEqual][formControlName], [validateEqual][formControl],[validateEqual][ngModel]' ,
providers: [{
provide: NG_VALIDATORS,
useExisting: forward(() => EqualValidator),
multi: true
}]
扩展
})
export class EqualValidator implements Validator {
constructor(@Attribute('validateEqual') public validateEqual: string){}
validate(control: AbstractControl): { [key:string]: boolean } {
let v = control.value; 使用指令的控件自身值: confirmpassword
let e = control.root.get(this.validateEqual); 指令指定的控件: passwordControl;
if (e && v !== e.value) {
return { validateEqual: false}
}
return null;
}
}

验证器添加到指定控件,造成只有该控件数值变化才会进行验证,先输入confirmPassword造成验证器失效,需要给验证器添加reverse属性

Forward Reference

不论es6、es7、还是ts作为开发语言,现阶段最终都会编译为es5的代码。es5中只有function并没有class。造成js代码编译阶段只有变量声明和函数声明会自动提升,而函数表达式并不会自动提升。所以要解决此问题可以使用Angular2提供的forward reference特性进行解决(Buffer顺序在Socket之前声明也可以,但是angular项目中模块组件化开发确认注入依赖的顺序性会带来更大负担)。

es6+中的class不进行自动提升主要为了解决继承父类时,父类不可用的问题
1
2
3
4
5
6
7
8
import { forwardRef, Injectable } from '@angular2/core'
@Injectable()
class Socket {
constructor(@Inject(forwardRef(() => Buffer)) private buffer) {}
}
class Buffer {
constructor(@Inject(BUFFER_SIZE) private size: Number) {}
}

OpaqueToken

OpaqueToken允许创建基于字符串的Token类,在Provider中使用。只需导入Opaque类。

1
2
3
4
5
6
7
import { OpaqueToken } from '@angular/core';
const CONFIG_TOKEN = new OpaqueToken('config');
export const THIRDPARTYLIBPROVIDERS = [
{
provide: CONFIG_TOKEN, useClass: ThirdPartyConfig
}
]

OpaqueToken类的定义

1
2
3
4
5
6
export class OpaqueToken {
constructor(protected _desc: string) {}
toString() : string {
return `Token${this._desc}`
}
}

OpaqueToken类的使用

1
2
3
4
5
6
7
8
9
import { ReflectiveInjector, OpaquToken } from '@angular/core';
const t = new OpaquToken('value');
const injector = ReflectiveInjector.resolveAndCreate([
{
provide: t,
useValue: 'bindingValue'
}
]);
injector.get(t) // 'bindingValue'

InjectionToken (Angular4+)

使用ValueProvider

1
2
3
4
5
6
7
8
9
10
11
@NgModule({
...,
providers: [
{
provide: 'apiUrl',
userValue: 'http://localhost:4200/heroes'
}
],
bootstrap: [AppComponent]
})
export class AppModule {}

注入ProviderValue

1
2
3
4
5
6
7
8
9
@Injectable()
export class HeroService {
constructor(
private loggerService: LoggerService,
@Inject('apiUrl') private apiUrl: String
){
console.log(apiUrl) // http://localhost:4200/heroes
}
}

如果引入了第三方库且名称相同就产生了问题

1
2
3
4
5
6
export const THIRD_PARTY_PROVIDERS = [
{
provide: 'apiUrl', // 与localhost:4200相同
userValue: 'http://192.168.18.59:4200'
}
]

更新Provider配置信息则如下

1
2
3
4
5
6
7
8
9
10
11
12
13
import { THIRD_PARTY_PROVIDERS } from './third-party';
@NgModule({
...,
providers: [
{
provide: 'apiUrl',
useValue: 'http://localhost:4200/heros'
},
THIRD_PARTY_PROVIDERS
],
bootstrap: [AppComponent]
})
export class AppModule { }

localhost:4200被覆盖

使用字符串作为Token起冲突了。所以利用InjectionToken(Angular4+)统一管理Token信息。

1
2
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken<string>('apiUrl');

OpaqueToken 与 InjectionToken 异同点

相同点

  • 创建可在Provider中使用的Token

不同点

  • 前者ng2的类,后者ng4+引入的类,继承自OpaqueToken,且引入了泛型用于定义所关联的依赖对象的类型

Es6 Set Map

Es6提供了新的数据结构Set。类似于数组,但是成员都是唯一的,没有重复的值。接受一个数组或类数组对象作为参数

1
2
const set = new Set([1,2,3,4,2]);
[...set] // [1,2,3,4]
1
2
3
const set = new Set();
[1,2,2,2,3,4].map(x => set.add(x));
set // [1,2,3,4]

Set内部,两个对象不相等,两个NaN相等,用length检测,可以用for…of遍历

  • add(value)

  • delete(value)

  • has(value)

  • clear()

Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。如果你需要“键值对”的数据结构,Map比Object更合适。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键, 初始化Map需要一个二维数组,或者直接初始化一个空Map

1
2
var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95
1
2
3
4
5
6
7
var m = new Map();
var o = {p: "Hello World"};
m.set(o, "content")
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
只有对同一个对象的引用,Map结构才视为同一个键
1
2
3
4
var map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
上面代码的set和get方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined。

ModuleWithProviders

创建一个共享模块,包含部分功能性模块、管道、指令、和服务。对于服务,通常作为单例的服务可能被多次提供,可以通过在共享模块内部返回ModuleWithProviders对象的静态方法forRoot解决这类问题, (相对于将service注入在NgModule,通过forRoot方法返回具有NgModule属性的ModuleWithProviders对象,可以解决service多次提供的情况)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { NgModule, ModuleWithProviders } from '@angular/core';
import { someDirective, somePipe, someService } from './functions';
@NgModule({
declarations: [
somePipe,
someDirective
],
exports: [
somePipe,
someDirective
]
})
export class SharedModule {
static forRoot() : ModuleWithProviders {
return {
ngModule: SharedModule,
providers: [ someService ]
}
}
}

NgModule中并不提供服务,在模块类中定义forRoot静态方法,返回ModuleWithProviders接口对象,在应用模块中导入共享模块并调用静态方法forRoot来提供服务和其他指令管道等,这样根模块会把他得providers添加到根模块的服务提供商中,确切的说是angular会先累加所有的显式注入的提供商,然后进一步追加其他模块的提供商到@NgModule.providers中,可以确保显式添加的提供商优先级大于从其他模块导入的;

1
2
3
4
5
6
7
8
9
import { SharedModule } from './shared'
//:...
@NgModule({
imports: [
SharedModule.forRoot()
],
//:...
})
export class AppModule {}

不调用forRoot方法则会只访问共享的管道和指令,不在提供服务

1
2
3
4
5
6
7
8
9
import { SharedModule } from './shared'
//:...
@NgModule({
imports: [
SharedModule、
],
//:...
})
export class AppModule {}
Angular2中没有模块级别的service,所有在NgModule中声明的Provider都是注册在跟级别的DI中

shadow DOM 选择器

使用emulated进行样式隔离时,可以访问适用于shadow DOM的css选择器

宿主

1
2
3
4
5
6
:host {
color: red; // <song-track>
}
:host(.selected) {
color: red; // <song-track class="selected">
}

样式依赖于祖先元素

它会在组件的宿主元素的祖先元素中查找汽配的祖先元素直到文档的根

1
2
3
4
5
6
:host-content(.selected) {
color: red;
}
:host-content(#selected) {
color: red
}

宿主元素或后代元素(跨边界)

它会覆盖任何封装的宿主元素或者其子元素

1
2
3
4
5
6
:host /deep/ .selected{
color: red;
}
:host >>> .selected{
color: red;
}
angular-cli 启动的项目使用deep而不是>>>