Angular
元素就是打包成自定义元素的Angular 组件
。所谓自定义元素就是一套与具体框架无关的用于定义新HTML
元素的Web
标准。@angular/elements
包导出了一个createCustomElement()
API
,它在Angular 组件接口
与变更检测功能和内置DOM API
之间建立了一个桥梁。
Angular
组件:Component
是 Directive的子类:它是一个装饰器,用于把某个类标记为Angular 组件
,并为它配置一些元数据,以决定该组件在运行期间该如何处理、实例化和使用。
选项 |
---|
moduleId :包含该组件的那个模块的 ID 。该组件必须能解析模板和样式表中使用的相对 URL 。 SystemJS 在每个模块中都导出了 __moduleName 变量。在 CommonJS 中,它可以设置为 module.id 。 |
template :Angular 组件的内联模板 templateUrl :Angular 组件模板文件的 URL 。两者二选一即可。 |
styleUrls :一个或多个 URL ,指向包含本组件 CSS 样式表的文件。 |
styles :本组件用到的一个或多个内联 CSS 样式。 |
animations :一个或多个动画 trigger() 调用,包含一些 state() 和 transition() 定义。 参见动画和相关的 API 文档。 |
interpolation :改写默认的插值表达式起止分界符({{ 和 }} ) |
entryComponents :这个NgModule 中也有,在Component 中用的应该不多 |
preserveWhitespaces :为 true 则保留,为 false 则从编译后的模板中移除可能多余的空白字符。 空白字符就是指那些能在 JavaScript 正则表达式中匹配 \s 的字符。默认为 false
|
changeDetection
:用于当前组件的变更检测策略。该策略是下列值之一:
-
ChangeDetectionStrategy
.OnPush
把策略设置为CheckOnce
(按需)。 -
ChangeDetectionStrategy
.Default
把策略设置为CheckAlways
。
encapsulation
:供模板和 CSS
样式使用的样式封装策略。取值为:
-
ViewEncapsulation.Native
:使用Shadow DOM
。它只在原生支持Shadow DOM
的平台上才能工作。 -
ViewEncapsulation.Emulated
:使用垫片(shimmed
)CSS
来模拟原生行为。 -
ViewEncapsulation.None
:Use global CSS without any encapsulation
. -
ViewEncapsulation.None
:使用全局CSS
,不做任何封装。
如果没有提供,该值就会从CompilerOptions
中获取它。默认的编译器选项是ViewEncapsulation.Emulated
`
viewProviders
:定义一组可注入对象,它们在视图的各个子节点中可用。参见例子。重点看下这里:
template: `<needs-greeter></needs-greeter>`
class Greeter {
greet(name:string) {
return 'Hello ' + name + '!';
}
}
@Directive({
selector: 'needs-greeter'
})
class NeedsGreeter {
greeter:Greeter;
constructor(greeter:Greeter) {
this.greeter = greeter;
}
}
@Component({
selector: 'greet',
viewProviders: [
Greeter
],
template: `<needs-greeter></needs-greeter>`
})
class HelloWorld {
}
继承自 Directive 装饰器
选项 |
---|
selector :这个 CSS 选择器用于在模板中标记出该指令,并触发该指令的实例化。 |
inputs :列举某个指令的一组可供数据绑定的输入属性; outputs :列举一组可供事件绑定的输出属性。(这俩属性一般不用了,用对应的装饰器替代 ) |
providers :一组依赖注入令牌,它允许 DI 系统为这个指令或组件提供依赖。这个在NgModule 一节已有较详细介绍 |
exportAs :定义一个名字,用于在模板中把该指令赋值给一个变量。 |
queries :已有对应的属性装饰器,后面具体介绍
|
host :使用一组键-值对,把类的属性映射到宿主元素的绑定(Property 、Attribute 和事件 )。也已有对应的属性装饰器,后面具体介绍
|
jit :如果为 true ,则该指令/组件将会被 AOT 编译器忽略,始终使用 JIT 编译。 |
几种元数据--属性装饰器:组件之间的交互 详解
A. 组件的输入 inputs
& @Input
angular
允许使用两种形式来定义组件的输入,一种是在装饰器@Component
中使用inputs
来定义,另一种是使用@Input
来定义。
首先先介绍在装饰器中使用的输入。inputs
接收的是一个字符串数组,用来指定我们输入的键名。
@Component({
selector: 'my-component',
inputs: ['name']
})
class MyComponent {
name: string;
}
name
就会对应我们组件中的name
变量。
然后我们定义一个组件,当然不可避免有的时候会在其他的组件的模板中使用,所以就可以这样写。
上级组件:
export class AppComponent {
myName = 'zhangsan';
...
}
上级组件的模板:
<app-messages [name]="myName"></app-messages>
方括号
[]
:数据绑定,也叫输入绑定。将等号右边的变量绑定在左边[]
中的变量上。
我们的组件:
@Component({
selector: 'app-messages',
inputs: ['name'],
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
name: string;
}
这里我们就用name
接受了上级组件的myName
。
通过上面的图,就很容易看输入数据的对应关系。
然后我们打印一下看看变量是否成功输入了。
export class MessagesComponent implements OnInit {
name: string;
ngOnInit() {
console.log(this.name);
}
}
成功输入!
@Input
:上面我们实现了组件的数据输入,但是angular
并没有满足现状,还提供另外一种输入的方法,就是@Input
。
@Component({
selector: 'my-component'
})
class MyComponent {
@Input() name: string;
}
只要在我们的组件中定义变量的时候使用@Input
装饰器就行了。对比上面我们使用inputs
时,少了一个二次声明。这种方法感觉数据的传递少了一层关系,更加易于理解,而且代码也更加的工整。
通过 setter
截听输入属性值的变化
子组件NameChildComponent
的输入属性 name
上的这个 setter
,会 trim
掉名字里的空格,并把空值替换成默认字符串。
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-name-child',
template: '<h3>"{{name}}"</h3>'
})
export class NameChildComponent {
private _name = '';
@Input()
set name(name: string) {
this._name = (name && name.trim()) || '<no name set>';
}
get name(): string { return this._name; }
}
NameParentComponent
import { Component } from '@angular/core';
@Component({
selector: 'app-name-parent',
template: `
<h2>Master controls {{names.length}} names</h2>
<app-name-child *ngFor="let name of names" [name]="name">
</app-name-child>
`
})
export class NameParentComponent {
// Displays 'Dr IQ', '<no name set>', 'Bombasto'
names = ['Dr IQ', ' ', ' Bombasto '];
}
通过ngOnChanges()
来截听输入属性值的变化(类似vue
的watch
?)
当需要监视多个、交互式输入属性的时候,本方法比用属性的
setter
更合适,看下面案例:
VersionChildComponent
组件 会监测输入属性 [major]
和 [minor]
的变化,并把这些变化编写成日志以报告这些变化。
import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
@Component({
selector: 'app-version-child',
template: `
<h3>Version {{major}}.{{minor}}</h3>
<h4>Change log:</h4>
<ul>
<li *ngFor="let change of changeLog">{{change}}</li>
</ul>
`
})
export class VersionChildComponent implements OnChanges {
@Input() major: number;
@Input() minor: number;
changeLog: string[] = [];
ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
let log: string[] = [];
for (let propName in changes) {
let changedProp = changes[propName];
let to = JSON.stringify(changedProp.currentValue);
if (changedProp.isFirstChange()) {
log.push(`Initial value of ${propName} set to ${to}`);
} else {
let from = JSON.stringify(changedProp.previousValue);
log.push(`${propName} changed from ${from} to ${to}`);
}
}
this.changeLog.push(log.join(', '));
}
}
VersionParentComponent
提供 [minor]
和 [major]
值,把修改它们值的方法绑定到按钮上。
import { Component } from '@angular/core';
@Component({
selector: 'app-version-parent',
template: `
<h2>Source code version</h2>
<button (click)="newMinor()">New minor version</button>
<button (click)="newMajor()">New major version</button>
<app-version-child [major]="major" [minor]="minor"></app-version-child>
`
})
export class VersionParentComponent {
major = 1;
minor = 23;
newMinor() {
this.minor++;
}
newMajor() {
this.major++;
this.minor = 0;
}
}
B. 组件输出outputs
& @Output
说完了组件的输入,下面我们就该聊聊组件的输出了。要将数据从组件中传递出去,就要使用输出绑定
。
<button (click)="display()"></button>
圆括号()
: 事件绑定,又叫输出绑定。这里我们监听click
事件,然后触发display
方法。
除了click
,angular
还有很多内置的事件,当然,我们在编写自己的组件的时候,也可以自定义一个事件,来与外部通信。
自定义事件,需要做三件事情:
- 1.在
@Component
配置中,制定outputs
配置项 - 2.在配置的属性中,设置一个
EventEmitter
(事件触发器) - 3.在适当的时候,也就是要触发的方法中,通过
EventEmitter
触发事件
下面看一下示例:
@Component({
selector: 'my-component',
outputs: ['newEvent']
})
export class MyComponent {
newEvent: EventEmitter<string>;
constructor() {
this.newEvent = new EventEmitter();
}
display(): void {
this.newEvent.emit("test event");
}
}
然后我们就可以通过上面模板中的代码实现输出了。
如果想在一个父级
的组件中使用这个输出,就要使用我们自己的事件了。下面看一个示例:
父级组件:
export class AppComponent {
...
showEvent(message: string) {
console.log(hello: ${message});
}
}
父级模板:
<app-messages (newEvent)="showEvent($event)">
</app-messages>
我们的组件:
@Component({
selector: 'app-messages',
outputs: ['newEvent'],
templateUrl: './messages.component.html'
})
export class MessagesComponent {
newEvent: EventEmitter<string>;
constructor(private messageService: MessageService) {
this.newEvent = new EventEmitter();
}
display(): void {
this.newEvent.emit('test event');
}
}
我们的组件模板:
<button (click)="display()">触发</button>
然后点击触发,可以看到输出hello:test event
。数据输出成功!
好了我们再来梳理整个输出过程:
1.我们自定以一个组件,通过内置的click事件
触发display
方法,这时就会触发我们自定义的事件:newEvent
。
2.当事件触发的时候,他会执行上一级的方法:showEvent
。
3.我们的事件输出了一个字符串test event
,然后通过$event
获取这个输出结果,并当做参数传给上一级的方法showEvent
@Output
:同输入相同,angular
也为我们提供了输出的第二种方式:@Output
。用法与@input
类似
export class MessagesComponent {
@Output() newEvent: EventEmitter<string>;
}
C. host
@HostBinding()
和@HostListener()
在自定义指令时非常有用。@HostBinding()
可以为指令的宿主元素添加类、样式、属性等,而@HostListener()
可以监听宿主元素上的事件。
@Component({
selector: 'demo-component',
host: {
'(click)': 'onClick($event.target)', // 事件
'role': 'nav', // 属性
'[class.pressed]': 'isPressed', // 类
}
})
export class DemoComponent {
isPressed: boolean = true;
onClick(elem: HTMLElement) {
console.log(elem);
}
}
等价于@HostBinding
、@HostListener
:
@Component({
selector: 'demo-component'
})
export class DemoComponent {
@HostBinding('attr.role') role = 'nav';
@HostBinding('class.pressed') isPressed: boolean = true;
@HostListener('click', ['$event.target'])
onClick(elem: HTMLElement) {
console.log(elem);
}
}
举例说明:实现一个在输入时实时改变字体和边框颜色
import { Directive, HostBinding, HostListener } from '@angular/core';
@Directive({
selector:'[appRainbow]'
})
export class RainbowDirective{
possibleColors = [
'darksalmon', 'hotpink', 'lightskyblue', 'goldenrod',
'peachpuff', 'mediumspringgreen', 'cornflowerblue',
'blanchedalmond', 'lightslategrey'
];
@HostBinding('style.color') color: string;
@HostBinding('style.borderColor') borderColor: string;
@HostListener('keydown') onKeydown() {
const colorPick =
Math.floor(Math.random()*this.possibleColors.length);
this.color = this.borderColor
= this.possibleColors[colorPick];
}}
说一下上面代码的主要部分:
- ① 为我们的指令取名为
appRainbow
- ② 定义我们需要展示的所有可能的颜色
- ③ 定义并用
@HostBinding()
装饰color
和borderColor
,用于设置样式 - ④ 用
@HostListener()
监听宿主元素的keydown
事件,为color
和borderColor
随机分配颜色
在页面上使用这个指令:
<input appRainbow>
效果如下:
D. queries
:类似于Vue
中的slot
相关的知识点
ContentChild
等价于@ContentChild
:
@Directive({
selector: 'li'
})
export class ListItem{ }
@Component({
selector: 'my-list',
template: `
<ul>
<ng-content></ng-content>
</ul>
`
})
export class MyListComponent {
@ContentChild(ListItem) items: QueryList<ListItem>;
}
占位符ng-content
支持select
属性,即类似vue中的slot的name
属性,可以占多个位置。
ContentChildren
:通过 Content Projection
方式设置的视图中获取匹配的多个元素
,返回的结果是一个 QueryList
集合。
parent.component.ts
import {
Component, ContentChildren, QueryList, AfterContentInit
} from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'exe-parent',
template: `
<p>Parent Component</p>
<ng-content></ng-content>
`
})
export class ParentComponent implements AfterContentInit {
@ContentChildren(ChildComponent)
childCmps: QueryList<ChildComponent>;
ngAfterContentInit() {
console.dir(this.childCmps);
}
}
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h4>Welcome to Angular World</h4>
<exe-parent>
<exe-child></exe-child>
<exe-child></exe-child>
</exe-parent>
`,
})
export class AppComponent { }
ViewChild
和ContentChild
的区别是啥呢?ContentChild
是通过占位标签<ng-content>
来将子组件嵌入父组件;而ViewChild
是直接写在父组件的template
中(viewChildren
就不多加说明喽!)
动态组件:ViewContainerRef
&ng-template
下面的例子展示了如何构建动态广告条:要在广告条中显示一系列不同的广告,几个不同的小组可能会频繁加入新的广告组件,你需要一种新的组件加载方式,它不需要在广告条组件的模板中引用固定的组件。
AdDirective
:注入了 ViewContainerRef
来获取对容器视图的访问权,这个容器就是那些动态加入的组件的宿主。
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[ad-host]',
})
export class AdDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
<ng-template>
元素是动态加载组件的最佳选择,因为它不会渲染任何额外的输出
@Component({
template:`
<div class="ad-banner-example">
<h3>Advertisements</h3>
<ng-template ad-host></ng-template>
</div>`
})
export class AdBannerComponent implements OnInit, OnDestroy {
@Input() ads: AdItem[];
currentAdIndex = -1;
@ViewChild(AdDirective, {static: true}) adHost: AdDirective;
interval: any;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
ngOnInit() {
this.loadComponent();
this.getAds();
}
ngOnDestroy() {
clearInterval(this.interval);
}
loadComponent() {
... 具体方法实现及说明见官方文档...
}
getAds() {
this.interval = setInterval(() => {
this.loadComponent();
}, 3000);
}
}
生命周期钩子
点击上述链接,查看原文...
生命周期的顺序如下图:红色
部分钩子angular
只会触发一次
,而绿色
钩子会触发多次。(我不懂:一般情况下,如果要实现check
钩子,代码一定要非常简洁&轻量级,不然,分分钟内存泄露。)
import {
Component, OnInit, Input, DoCheck, AfterContentInit, OnChanges,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy
} from '@angular/core';
import { SimpleChanges } from '@angular/core/src/metadata/lifecycle_hooks';
let nextId: number = 1;
@Component({
selector: 'app-test-demo',
templateUrl: './test-demo.component.html',
styleUrls: ['./test-demo.component.css']
})
export class TestDemoComponent implements
OnChanges, OnInit, DoCheck,
AfterContentInit, AfterContentChecked, AfterViewInit,
AfterViewChecked, OnDestroy {
@Input()
public stock: string = "";
logIt(msg: string) {
console.log(`${nextId++} ${msg}`);
}
constructor() {
this.logIt('-- constructor方法' + this.stock);
}
/**当被绑定的输入属性的值发生变化时调用,
首次调用一定会发生在ngOnInit()之前。*/
ngOnChanges(changes: SimpleChanges) {
let currentVal = changes['stock'].currentValue;
this.logIt('-- ngOnChanges方法' + this.stock);
}
/**当Angular完成组件的创建和引入时,将调用此回调。
它也会在Angular显示数据绑定属性时初始化*/
ngOnInit() {
this.logIt('-- ngOnInit方法');
}
//需要检查组件或指令的输入属性时
ngDoCheck() {
this.logIt('-- ngDoCheck');
}
//当把内容投影进组件之后调用
ngAfterContentInit() {
this.logIt('-- ngAfterContentInit');
}
//每次完成被投影组件内容的变更检测之后调用
ngAfterContentChecked() {
this.logIt('-- ngAfterContentChecked');
}
//初始化完组件视图及其子视图之后调用
ngAfterViewInit() {
this.logIt('-- ngAfterViewInit');
}
//每次做完组件视图和子视图的变更检测之后调用
ngAfterViewChecked() {
this.logIt('-- ngAfterViewChecked');
}
/**当Angular每次销毁指令/组件之前调用并清扫。
一般切换路由的时候,就会调用该组件的ngOnDestroy接口*/
ngOnDestroy() {
this.logIt('-- ngOnDestroy');
}
}
//调用:
<app-test-demo [stock]="title" ></app-test-demo>
运行效果: