ng new demo(项目名称)
cd demo
ng serve --open
定义 AppModule,这个根模块会告诉 Angular 如何组装该应用。 目前,它只声明了 AppComponent。 稍后它还会声明更多组件。
参考链接
ng generate component components/news
ng g c components/header # 缩写形式
一般组件创建再comments目录下,所以创建自定义组件时,指定目录components
组件定义:
官方文档
*ngIf 根据表达式是否成立,决定是否展示DOM标签。
*ngIf else指令
结构性指令都依赖于ng-template,*ngIf实际上就是ng-template指令的[ngIf]属性。
官方文档
模板变量可以帮助你在模板的另一部分使用这个部分的数据。
使用模板变量,你可以执行某些任务,比如响应用户输入或微调应用的表单。
类似于AngularJS中的过滤器。
官方文档
AsyncPipe – 自动订阅模板中的Observable或Promise
DecimalPipe – 数字转字符串,并可以指定格式
JsonPipe – 将值转成json
TitleCasePipe – 把首字母大写,其它小写
SlicePipe – 截取Array或String
PercentPipe – 数字转百分比
LowerCasePipe和UpperCasePipe – 转化小写或大写
DatePipe – 格式化日期
KeyValuePipe – 使ngFor可以循环Object或Map对象
其他管道: http://bbs.itying.com/topic/5bf519657e9f5911d41f2a34
创建管道: ng g p pipes/exponential-strength
通过默认情况下,管道会定义成纯的(pure),这样 Angular 只有在检测到输入值发生了纯变更时才会执行该管道。
纯变更是指原始输入值(比如 String、Number、Boolean 或 Symbol )的变更,或是引用对象的变更(比如 Date、Array、Function、Object)。
使用纯管道,Angular 会忽略复合对象中的变化,例如往现有数组中新增的元素,因为检查原始值或对象引用比对对象中的差异进行深度检查要快得多。Angular 可以快速判断是否可以跳过执行该管道并更新视图。
如下示例:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'flyingHeroes'
})
export class FlyingHeroesPipe implements PipeTransform {
transform(value) {
return value.filter(item => item.canFly);
}
}
调用管道过滤会飞的英雄:
<input type="text" #box (keyup.enter)="addHero(box.value)" placeholder="hero name" />
<div *ngFor="let hero of (heroes | flyingHeroes)">
{{hero.name}}
div>
这时再出发添加英雄,由于heroes是列表,添加元素并不会引起引用对象的改变,因此管道检测不到变更,页面不会显示新增的会飞英雄:
组件初始加载时hero为undefined,如果在html中获取hero.name会报错。 这里使用hero?.name可以保证页面正常渲染而不报错,数据加载后会显示出正确内容。
官方文档
内容投影是一种模式,你可以在其中插入或投影要在另一个组件中使用的内容。例如,你可能有一个 Card 组件,它可以接受另一个组件提供的内容。
在组件模板中通过 ng-content 设置插槽,ng-content 元素只是一个占位符,不会创建真正的 DOM 元素。
一个组件可以具有多个插槽。每个插槽可以指定一个选择器,该选择器会决定将哪些内容放入该插槽。
使用此模式,必须指定希望投影内容出现在的位置。可以通过使用 ng-content 的 select 属性来完成此任务。
将 select 属性添加到 ng-content 元素。 Angular 使用的选择器支持标签名、属性、CSS 类和 :not 伪类的任意组合。
官方文档
功能类似@ViewChild,用于获取外部投影到组件中的标签内容;
// shadow.component.ts
import { AfterViewInit, Component, ContentChild, ElementRef, OnInit, ViewChild } from '@angular/core';
@Component({
selector: 'app-shadow',
templateUrl: './shadow.component.html',
styleUrls: ['./shadow.component.css']
})
export class ShadowComponent implements OnInit, AfterViewInit {
// 通过 ContentChild 获取外部投影到组件中的内容
@ContentChild('h3') h3El: ElementRef;
constructor() { }
ngOnInit(): void {
}
ngAfterViewInit(): void {
console.log('h3El', this.h3El);
}
}
<div>
<h3>这是shadow组件自己的内容h3>
<ng-content>ng-content>
div>
<app-shadow>
<h3 #h3>这是投影到ng-content插槽中的内容h3>
app-shadow>
官方文档
功能类似@ViewChildren,用于获取外部投影到组件中的获取元素或指令的 QueryList;
每当添加、删除或移动子元素时,此查询列表都会更新,并且其可观察对象 changes 都会发出新值。
如果组件需要有条件地渲染内容、多次渲染内容、默认内容,则应该使用 ngTemplateOutlet 指令。
在这种情况下,不建议使用 ng-content 元素,因为只要组件的使用者提供了内容,即使该组件从未定义 ng-content 元素或该 ng-content 元素位于 ngIf 语句的内部,该内容也总会被初始化。
官方文档
结构型指令,根据一个提前备好的 TemplateRef 插入一个内嵌视图。
用途:自定义组件,由外部传入一段模板用于显示。
// tpl-outlet.component.ts
import { Component, Input, OnInit, TemplateRef } from '@angular/core';
@Component({
selector: 'app-tpl-outlet',
templateUrl: './tpl-outlet.component.html',
styleUrls: ['./tpl-outlet.component.css']
})
export class TplOutletComponent implements OnInit {
// 定义render参数用于接收外部传入的templateRef
@Input() render: TemplateRef<any>;
constructor() { }
ngOnInit(): void {
}
}
<div>
<h3>tpl-outlet works!h3>
<div *ngIf="true">
<ng-container
[ngTemplateOutlet]="render || defaultTpl">
ng-container>
div>
div>
<ng-template #defaultTpl>
这是组件默认内容
ng-template>
<app-tpl-outlet [render]="parentTpl">app-tpl-outlet>
<ng-template #parentTpl>
这是外部传入的内容
ng-template>
因为ngTemplateOutlet是结构型指令,所以也可以使用 * 语法简写:
ngTemplateOutlet 指令也可以用在ng-template上,效果和ng-container一样,两者都是起到占位符承载 ngTemplateOutlet 指令的作用。
官方文档
当 Angular 实例化组件类并渲染组件视图及其子视图时,组件实例的生命周期就开始了。生命周期一直伴随着变更检测,Angular 会检查数据绑定属性何时发生变化,并按需更新视图和组件实例。当 Angular 销毁组件实例并从 DOM 中移除它渲染的模板时,生命周期就结束了。当 Angular 在执行过程中创建、更新和销毁实例时,指令就有了类似的生命周期。
输入属性发生变化是触发,但组件内部改变输入属性是不会触发的。注意:如果组件没有输入,或者使用它时没有提供任何输入,那么框架就不会调用 ngOnChanges()。
只在组件/指令初始化调用一次,在它之前调用的顺序是 constructor -> ngOnChanges -> ngOnInit 官方建议在这个钩子中获取组件初始化的数据,而构造函数中只适合写简单的逻辑,比如初始化局部变量。
每当 Angular 每次销毁指令/组件之前调用并清扫。 在这儿反订阅可观察对象和分离事件处理器,以防内存泄漏。
<button class="btn btn-primary" (click)="show = !show">togglebutton>
<app-life-cycle title="aaa" *ngIf="show">app-life-cycle>
import { Component, Input, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-life-cycle',
templateUrl: './life-cycle.component.html',
styleUrls: ['./life-cycle.component.css']
})
export class LifeCycleComponent implements OnInit {
constructor() {
console.log('constructor');
}
ngOnChanges(changes: SimpleChanges): void {
console.log('ngOnChanges');
}
ngOnInit(): void {
console.log('ngOnInit');
}
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');
}
}
注意:
Angular 框架会通过此机制将应用程序 UI 的状态与数据的状态同步。变更检测器在运行时会检查数据模型的当前状态,并在下一轮迭代时将其和先前保存的状态进行比较。
当应用逻辑更改组件数据时,绑定到视图中 DOM 属性上的值也要随之更改。变更检测器负责更新视图以反映当前的数据模型。类似地,用户也可以与 UI 进行交互,从而引发要更改数据模型状态的事件。这些事件可以触发变更检测。
使用默认的(“CheckAlways”)变更检测策略,变更检测器将遍历每个视图模型上的视图层次结构,以检查模板中的每个数据绑定属性。在第一阶段,它将所依赖的数据的当前状态与先前状态进行比较,并收集更改。在第二阶段,它将更新页面上的 DOM 以反映出所有新的数据值。
如果设置了 OnPush(“CheckOnce”)变更检测策略,则变更检测器仅在显式调用它或由 @Input 引用的变化或触发事件处理程序时运行。这通常可以提高性能。
事件:页面 click、submit、mouse down……
XHR:从后端服务器拿到数据
Timers:setTimeout()、setInterval()
一种数据流模型,它总是在一个方向(从父到子)上检查组件树是否有变化,以防止在变更检测中出现循环。
在实践中,这意味着 Angular 中的数据会在变更检测过程中向下流动。父组件可以很容易地改变子组件中的值,因为父组件是先检查的。但是,如果子组件在更改检测期间(反转预期的数据流)尝试更改其父组件中的值,则可能会导致错误,因为父组件已经渲染过了。在开发模式下,如果你的应用尝试这样做,Angular 会抛出 ExpressionChangedAfterItHasBeenCheckedError 错误,而不是沉默地渲染新值。
为了避免这个错误,进行此类更改的生命周期钩子方法中就要触发一次新的变更检测。这次新的变更检测与之前那次的方向一样,但可以成功获得新值。
如下:
从上面的图中可以看出,变更检测树中包含了所有的组件,因为某个组件的数据变化了,Angular并不清楚会不会影响到其他组件,因此就将所有的组件检查一遍,这样保证了所有的组件都能正常的渲染。但是这样带来的问题就是在大型的项目中,去检测所有组件性能上会有影响。
解决办法:设置 OnPush(“CheckOnce”)变更检测策略,这样变更检测只会在当前组件及其子组件中生效,但其他相邻兄弟组件不会被监测,变更检测器默认会跳过当前组件,仅在显式调用它 或 由 @Input 引用的变化 或 触发事件处理程序时运行,这通常可以提高性能。
设置OnPush策略后只有以下4种情况会触发变更检测(定时器已无法触发变更检测了)。
配置OnPush策略后,定时器已经不能触发变更检测,click事件、父组件中修改子组件的输入变量也可以触发:
设置了OnPush策略,又必须在定时器中去触发变更检测刷新页面,那么可以通过手动触发的方式:
官方文档
:host 选择是是把宿主元素作为目标的唯一方式。
它选中的是组件模板标签,比如,相当于在父组件的style中选择 app-user {}
当宿主标签上有 active class时生效
:host(.active) {
border-width: 3px;
}
当某个祖先元素有 CSS 类 theme-light 时,才会把 background-color 样式应用到组件内部的所有 .title 元素中
找到根元素(html标签)为止
:host-context(.theme-light) .title {
background-color: #95f04c;
}
官方文档
目前调用组件的方式都是在模板中通过< app-XXX> app-XXX>调用的,但是组件的模板不会永远是固定的,应用可能会需要在运行期间加载一些新的组件。
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output } from '@angular/core';
@Component({
selector: 'app-alerts',
templateUrl: './alerts.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 AlertsComponent implements OnInit {
options = {
content: "",
theme: "primary"
}
// 子组件中点击关闭弹框时通知父组件销毁子组件
@Output() closed = new EventEmitter<void>();
// 动态设置弹框主题颜色
get wrapCls(): string {
return 'alert alert-' + this.options.theme;
}
// 动态设置弹框内容
setOptions(options) {
this.options = { ...this.options, ...options };
}
constructor() { }
ngOnInit(): void {
}
}
<div style="margin-top: 100px;">
<span [class]="wrapCls" role="alert">{{ options.content }}span>
<button class="btn btn-warning" (click)="closed.emit()">关闭提示button>
div>
import { ApplicationRef, Component, ComponentFactoryResolver, ComponentRef, EmbeddedViewRef, Injector } from '@angular/core';
import { AlertsComponent } from './components/alerts/alerts.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
// 引入alerts组件类
private container: AlertsComponent;
// 提供对组件实例和相关对象的访问,并提供销毁实例的方法。
private componentRef: ComponentRef<AlertsComponent>;
constructor(
// 一个简单的注册表,它将 Components 映射到生成的 ComponentFactory 类,该类可用于创建组件的实例。
// 用于获取给定组件类型的工厂,然后使用工厂的 create() 方法创建该类型的组件。
private cfr: ComponentFactoryResolver,
// 依赖注入,子组件可能有使用依赖注入,创建子组件时需要传入依赖注入。
private inject: Injector,
// 对页面上运行的 Angular 应用程序的引用。
private appRef: ApplicationRef
) {}
showAlert() {
// 如果没有创建子组件,则实例化子组件
if (!this.container) {
this.container = this.getContainer();
}
// 调用组件的某个方法执行逻辑,比如下面这个传参
this.container.setOptions({ content: '一段提示', theme: 'warning' });
}
private getContainer(): AlertsComponent {
// 创建指定类型的组件工厂(生产指定类型的组件)
const factory = this.cfr.resolveComponentFactory<AlertsComponent>(AlertsComponent);
// 根据指定的类型,创建组件的示例
this.componentRef = factory.create(this.inject);
// 将组件试图添加到试图树中,以激活变更检测
this.appRef.attachView(this.componentRef.hostView);
// 将组件到模版(包括app-alert标签),添加到body最后;也可以通过ng-template确定插入位置
document.body.appendChild((this.componentRef.hostView as EmbeddedViewRef<{}>).rootNodes[0] as HTMLElement);
// 监听组件销毁事件,在组件调用了destroy()后会调用onDestroy(),可以在这里做一些收尾工作
this.componentRef.onDestroy(() => {
console.log('componentRef destory');
});
// 获取子组件实例,相当于用@ViewChild获取子组件一样
const { instance } = this.componentRef;
// 监听子组件到output事件,子组件触发关闭时会发送信号,在父组件中调用destory()销毁
instance.closed.subscribe(() => {
this.componentRef.destroy();
this.container = null;
});
// 返回组件实例
return instance;
}
}
<button class="btn btn-primary" (click)="showAlert()">创建子组件button>
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-forms',
templateUrl: './forms.component.html',
styleUrls: ['./forms.component.css']
})
export class FormsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
public cityList: string[] = ["北京", "上海", "深圳", "广州"];
public username: string = "";
public peopleInfo: any = {
username: "",
sex: "1",
city: "上海",
hobby: [
{title: "吃饭", checked: false},
{title: "睡觉", checked: false},
{title: "敲代码", checked: true}
],
remarks: ""
}
public getData() {
console.log(this.peopleInfo);
}
}
<h2>人员登记系统h2>
<div class="people_list">
<ul>
<li>姓名:<input type="text" [(ngModel)]="peopleInfo.username">li>
<li>性别:
<input type="radio" value="1" name="sex" id="sex1" [(ngModel)]="peopleInfo.sex"><label for="sex1">男label>
<input type="radio" value="2" name="sex" id="sex2" [(ngModel)]="peopleInfo.sex"><label for="sex2">女label>
li>
<li>城市:
<select name="city" id="city" [(ngModel)]="peopleInfo.city">
<option [value]="item" *ngFor="let item of cityList">{{ item }}option>
select>
li>
<li>爱好:
<span *ngFor="let i=index; let item of peopleInfo.hobby">
<input type="checkbox" [id]="'checked'+i" [(ngModel)]="item.checked"> <label [for]="'checked'+i">{{ item.title }}label>
span>
li>
<li>备注:
<textarea name="remarks" id="remarks" cols="30" rows="10" [(ngModel)]="peopleInfo.remarks">textarea>
li>
ul>
<br>
<br>
<br>
<br>
<br>
<br>
<br>
<button (click)="getData()">获取数据button>
<br>
<br>
<div>{{ peopleInfo | json }}div>
div>
h2 {
text-align: center;
}
ul,ol {
list-style-type: none;
}
.people_list {
width: 400px;
margin: 20px auto;
/* padding: 0px; */
border: 1px solid #eee;
}
.people_list li {
height: 20px;
margin: 10px auto;
}
button {
float: right;
}
指令是为 Angular 应用程序中的元素添加额外行为的类。
官方文档
官方文档
使用属性型指令,可以更改 DOM 元素和 Angular 组件的外观或行为。
创建指令:ng generate directive directives/highlight
@Directive() 装饰器的配置属性会指定指令的 CSS 属性选择器 [appHighlight]。
检测用户何时将鼠标移入或移出元素以及如何通过设置或清除突出显示颜色来进行响应。
指令中有些数据可能需要用户指定,比如设置的背景颜色。
官方文档
ng-template 和 ng-container 都用于包裹元素,默认都不会渲染出来。
在TS中< ng-template>对应的是 templateRef 类型。
在TS中< ng-container >对应的是 viewContainerRef 类型。
Angular 的 ng-template是视图元素, 它定义了一个默认情况下不渲染任何内容的模板。使用 < ng-template> 可以手动渲染内容,以完全控制内容的显示方式。
ng-template本身不会渲染在视图中,在视图渲染之前Angular 会将ng-template替换为注释< !–container–> 。其中包裹的元素默认不会渲染出来,必须搭配结构型指令,ng-template中包裹的元素才能渲染出来。
在下面的示例中,Angular 不会渲染中间的 “Mid!”,因为它被 < ng-template> 包裹着。
<p>Hip!p>
<ng-template>
<p>Mid!p>
ng-template>
<p>Hooray!p>
下面的例子中,在ng-template 上添加结构型指令ngIf后可以显示出来:
<p>Hip!p>
<ng-template [ngIf]="true">
<p>Mid!p>
ng-template>
<p>Hooray!p>
ng-container 元素是一个逻辑结构,可用于对其他 DOM 元素进行分组,ng-container 本身不会在 DOM 树中渲染。
使用结构型指令时,通常都需要一个根元素作为结构型指令的宿主。例如:< li>元素作为ngFor指令的宿主。
<ul>
<li *ngFor="let item of list">li>
ul>
当原始的html没有一个宿主元素时,可以引入一个div或span标签包裹原始html,使其成为宿主元素,并将结构型附加到这个div上。但是这种方式引入了一个容器元素div可能会存在问题。
问题1:比如css中设置了div标签中的字体都是red,那么引入div作为宿主元素就会导致div所包裹的元素中字体变为red。
问题2:select标签默认下级标签时option,那么就不能把option标签再包裹进div或span中。
解决办法:ng-container是视图容器,和ng-template一样Angular不会渲染它,它不需要搭配结构型指令也可以显示出包裹的元素。
ng-container是由Angular解释器负责识别处理的语法元素,类似于js中的条件语法 if 块中的花括号,将一组元素包裹起来;
<ng-container >
<p>bbbp>
ng-container>
<p>
aaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbb
cccccccccccccccccc
dddddddddddddddddd
p>
<p>
aaaaaaaaaaaaaaaaaa
<span *ngIf="false">bbbbbbbbbbbbbbbbbbspan>
cccccccccccccccccc
dddddddddddddddddd
p>
<p>
aaaaaaaaaaaaaaaaaa
<ng-container *ngIf="false">bbbbbbbbbbbbbbbbbbng-container>
cccccccccccccccccc
dddddddddddddddddd
p>
<p *ngIf="true">aaap>
<ng-container>
<ng-template [ngIf]="true">
<p>bbbp>
ng-template>
ng-container>
在指令类中要获取ng-template需要使用TemplateRef类;要获取视图容器需要使用ViewContainerRef类,创建视图时需要调用ViewContainerRef的createEmbeddedView()方法。
TemplateRef类文档
ViewContainerRef类文档
创建指令:ng generate directive directives/unless
下面创建一个和ngIf功能相反的指令,ture时隐藏,false时显示:
import { Directive, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appUnless]'
})
// 1. UnlessDirective指令会通过 Angular 生成 创建一个嵌入的视图,
// 然后将该视图插入到该指令的宿主元素 紧后面的视图容器中(不是父子元素,是兄弟元素);
export class UnlessDirective implements OnChanges{
private hasView = false;
@Input() appUnless: boolean = false;
constructor(
// 2. TemplateRef 可以获取视图 的内容;
private templateRef: TemplateRef<any>,
// 3. ViewContainerRef 可以获取视图容器。
private viewContainer: ViewContainerRef
) { }
// 4. 监听输入属性变更
// 每当condition的值更改时,Angular 都会设置 appUnless 属性。
// 如果condition是false,并且 Angular 以前尚未创建视图,则此 setter 会导致视图容器从模板创建出嵌入式视图。
// 如果condition为true,并且当前正显示着视图,则此 setter 会清除容器,这会导致销毁该视图。
// 监听输入属性变更有两种方式:
// (1) 生命周期函数:ngOnChanges
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
if (!changes.appUnless.currentValue && !this.hasView) {
// 实例化一个内嵌视图,并把它插入到该容器中。
this.viewContainer.createEmbeddedView(this.templateRef);
this.hasView = true;
} else if (changes.appUnless.currentValue && this.hasView) {
// 销毁本容器中的所有视图。
this.viewContainer.clear();
this.hasView = false;
}
}
// (2)添加一个带 setter 的 @Input() 属性 appUnless
// @Input() set appUnless(condition: boolean) {
// if (!condition && !this.hasView) {
// this.viewContainer.createEmbeddedView(this.templateRef);
// this.hasView = true;
// } else if (condition && this.hasView) {
// this.viewContainer.clear();
// this.hasView = false;
// }
// }
}
结构型指令(例如 *ngIf)上的星号 * 语法是简写形式。 Angular 将结构型指令前面的星号转换为围绕宿主元素及其后代的 < ng-template >标签。
<div *ngIf="hero" class="name">{{hero.name}}div>
完整的写法是:*ngIf 指令移到了 < ng-template> 上,它成为绑定在方括号 [ngIf] 中的属性。 < div> 的其余部分(包括其 class 属性)移到了 < ng-template> 内部。
<ng-template [ngIf]="hero">
<div class="name">{{hero.name}}div>
ng-template>
Angular 不会创建真正的 < ng-template> 元素,只会将 < div> 和注释节点占位符渲染到 DOM 中。
简写形式:
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
({{i}}) {{hero.name}}
div>
<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
<div [class.odd]="odd">({{i}}) {{hero.name}}div>
ng-template>
这里,ngFor 结构型指令相关的所有内容都应用到了 < ng-template> 中。而元素上的所有其他绑定和属性应用到了宿主元素 < div> 上。在这个例子中,[class.odd]=“odd” 就留在了 < div> 中。
let 关键字会声明一个模板输入变量,可以在模板中引用该变量。在这个例子中,解析器将 let hero、let i 和 let odd 转换为名为 let-hero、let-i 和 let-odd 的变量。 let-i 和 let-odd 变量变为 let i=index 和 let odd=odd 。 Angular 会将 i 和 odd 设置为上下文中 index 和 odd 属性值。
变更检测器会在视图的 DOM 中查找能匹配上该选择器的第一个元素或指令。 如果视图的 DOM 发生了变化,出现了匹配该选择器的新的子节点,该属性就会被更新。
用于获取模板中的指令、子组件、普通的HTML DOM元素、templateRef。
官方文档
官方文档
QueryList API文档
ViewChildren获取模板元素的方法和ViewChild基本一致,只是返回结果为QueryList类型,将匹配到的多个元素放在了QueryList中。
下面两种获取pannel子组件的方式结果不一样,通过 #pannel只能获取到2个Pannel组件实例l;通过PannelComponent可以获取到3个实例;
前面已经学习了通过ng-template和ng-container在html包裹其他DOM元素,手动控制视图渲染。
那么如何在ts中控制视图渲染呢?
ng-template 对应到ts类中就是 templateRef类型。
ng-container 对应到ts类中就是 viewContainerRef类型。viewContainerRef官方文档
官方文档
表示一个内嵌模板,它可用于实例化内嵌的视图。 要想根据模板实例化内嵌的视图,请使用 ViewContainerRef 的 createEmbeddedView() 方法。
如下,通过ViewChild获取ng-template元素,其类型为TemplateRef。
在生命周期函数中打印元素是注释类型,即html中渲染成注释 < !-- container -->。
官方文档
可以将一个或多个视图附着到组件中的容器。
import { AfterViewInit, Component, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-tpl-container',
templateUrl: './tpl-container.component.html',
styleUrls: ['./tpl-container.component.css']
})
export class TplContainerComponent implements OnInit, AfterViewInit {
// 通过ViewChild获取html中的ng-template元素,类型为TemplateRef
@ViewChild('firstTemplate', {read: TemplateRef}) firstTemplate: TemplateRef<any>;
// 通过ViewChild获取html中的ng-container元素,类型为ViewContainerRef
@ViewChild('firstContain', {read: ViewContainerRef}) firstContain: ViewContainerRef;
constructor() { }
ngOnInit(): void {
}
ngAfterViewInit(): void {
// 将TemplateRef插入视图容器ViewContainer有两种办法:
// 方法一:
// 1. 通过TemplateRef生成 ViewRef视图
// const viewRef = this.firstTemplate.createEmbeddedView(null);
// 2. 调用ViewContainer的insert方法,将ViewRef 视图插入容器中
// this.firstContain.insert(viewRef);
// 方法二:
// 直接调用ViewContainer的createEmbeddedView方法传入TemplateRef
this.firstContain.createEmbeddedView(this.firstTemplate, {index: 0});
}
}
<ng-template #firstTemplate>
<p>aaaap>
ng-template>
<ng-container #firstContain>
<p>bbbp>
ng-container>
在ngFor等指令中可以使用 let创建局部变量,实际上就是使用了视图的上下文中的参数:
import { AfterViewInit, Component, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-tpl-container',
templateUrl: './tpl-container.component.html',
styleUrls: ['./tpl-container.component.css']
})
export class TplContainerComponent implements OnInit {
@ViewChild('firstTemplate', {read: TemplateRef}) firstTemplate: TemplateRef<any>;
@ViewChild('firstContain', {read: ViewContainerRef}) firstContain: ViewContainerRef;
constructor() { }
ngOnInit(): void {
}
insert() {
// $implicit: "defaultVal" ,test: "test111" 设置上下文参数
this.firstContain.createEmbeddedView(this.firstTemplate, {$implicit: "defaultVal" ,test: "test111", index: 0});
}
}
<ng-template #firstTemplate let-te="test" let-def>
<p>aaaap>
<p>te--- {{ te }}p>
<p>def--- {{ def }}p>
ng-template>
<button class="btn btn-primary" (click)="insert()">插入视图button>
<ng-container #firstContain>
<p>bbbp>
ng-container>
官方文档
组件不应该直接获取或保存数据,它们应该聚焦于展示数据,而把数据访问的职责委托给某个服务。
ng g service my-new-service
# 创建到指定目录下面
ng g service services/storage
官方文档
依赖注入(DI)是一种设计模式, 也有相应的框架,比如InversifyJS。
Angular 有自己的 DI 框架, DI 框架会在实例化该类时向其提供这个类所声明的依赖项。
import { Injectable } from '@angular/core';
// 引入默认英雄数据
import Heros from 'src/app/configs/heros';
// 在Angular中,使用@Injectable 修饰的类,可以被依赖注入
@Injectable({
// 在根模块中提供服务
providedIn: 'root'
})
export class HeroService {
constructor() { }
// 服务提供获取英雄数据的方法
getHero() {
return Heros
}
}
import { Component, OnInit } from '@angular/core';
import { Hero, HeroArg } from 'src/app/configs/types';
import { HeroService } from 'src/app/services/hero.service';
@Component({
selector: 'app-heros',
templateUrl: './heros.component.html',
styleUrls: ['./heros.component.css']
})
export class HerosComponent implements OnInit {
searchParams: HeroArg = {
name: '',
job: '',
sort: 'desc'
};
heros: Hero[];
// 1. 不使用依赖注入时,要使用服务就得先声明服务 并在构造函数中 new HeroService();
// heroService: HeroService;
// constructor() {
// this.heroService = new HeroService();
// }
// 2. 在构造函数中依赖注入服务,框架会实例化服务并绑定在属性上
constructor(private heroService: HeroService) { }
ngOnInit(): void {
this.heros = this.heroService.getHero();
}
search() {
console.log(this.searchParams);
}
}
import { Injectable } from '@angular/core';
@Injectable()
export class LoggerService {
logs: string[] = [];
constructor() { }
log(message: string) {
this.logs.push(message);
console.log(message);
}
}
app.moudle.ts
类提供者,使用useClass在NgModule中提供该服务
providers: [
LoggerService,
// { provide: LoggerService, useClass: LoggerService } 与上面写法相同
]
providers: [
// provide 参数是令牌, userClass可以指定会实例化的类,如果两者相同则可以简写为 LoggerService
{ provide: LoggerService, useClass: BetterLogger }
]
useExisting指向的服务一定是已经注册过的,这是和useClass的区别之一
providers: [
UserLoggerService,
// 如果用useClass, 则会得到两份UserLoggerService实例,使用useExisting则可以复用上面已经实例化的UserLoggerService
{ provide: LoggerService, useExisting: UserLoggerService }
]
对于很简单的值,没必要把它做成一个类,可用useValue提供简单的值
providers: [{ provide: Logger, useValue: 'simpleValue' }]
上面每个provide都是一个类,那么也可以用其它数据类型作为令牌
providers: [{ provide: 'httpApi', useValue: '123.com' }]
注入方式
class AppComponent {
constructor(@Inject('httpApi') readonly api){}
}
官方文档
一般情况下无法使用ts接口作为令牌
interface AppConfig {
apiEndpoint: string;
title: string;
}
无法用AppConfig作为令牌:
[
{
provide: AppConfig,
useValue: {
apiEndpoint: 'api.heroes.com',
title: 'Dependency Injection'
}
}]
但又想要限制值的类型,可以借助InjectionToken
import { InjectionToken } from '@angular/core';
// 参数是该令牌的一个描述,可选
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
[
{
provide: APP_CONFIG,
useValue: {
apiEndpoint: 'api.heroes.com',
title: 'Dependency Injection'
}
}]
注入方式
class AppComponent {
constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}
}
直接注册的服务没办法在实例化时传入参数,可以通过useFactory() 自定义实例化服务 user-logger.service
import { Injectable } from '@angular/core';
import {UserService} from './user.service';
import {LoggerService} from './logger.service';
@Injectable()
export class UserLoggerService extends LoggerService {
constructor(private userService: UserService, extra: string) {
super();
console.log('UserLoggerService', extra);
}
log(message: string) {
const name = this.userService.user.name;
super.log(`Message to ${name}: ${message}`);
}
}
{
provide: UserLoggerService,
useFactory(userServe: UserService) {
return new UserLoggerService(userServe, 'factory msg');
},
deps: [UserService] // 依赖其它服务的话,要列在这里
}
官方文档
在@NgModule() 或 @Injectable() 提供的服务称为 ModuleInjector。
在 @Directive() 或 @Component()里提供的服务称为 ElementInjector。
默认情况下,Angular 始终从当前的 Injector 开始,并一直向上搜索。修饰符使你可以更改开始(默认是自己)或结束位置。
之前providedIn: ‘root’ 指定从根模块中提供该服务 也可以像下面这样指定其它模块提供该服务
@Injectable({
providedIn: ComponentsModule
})
如果这么做,意味着这个服务无法该模块自身内部使用,会报一个循环依赖的错误只能在其它引入了ComponentsModule的NgModule中,才能使用该服务。
上面说了,组件会从自身开始寻找服务,然后遍历父组件的ElementInjector,直到找到为止。
viewProviders也有这个特性,区别是,对投影内容不可见
<app-mobile-list>
<app-mobile-content>app-mobile-content>
app-mobile-list>
MobileContent无法找到MobileList里用viewProviders提供的服务。
官方文档
组件 A 是 组件B 父组件,通过Angular的API Query、QueryList、ViewChildren 和 ContentChildren等,可以在父组件中拿到子组件的实例。但是不存在用于获取父组件实例的公共 API。
不过,由于每个组件的实例都会添加到注入器的容器中,因此可以通过 Angular 的依赖注入来访问父组件。
父组件
@Component({
selector: 'alex',
template: `
{{name}}
`,
})
export class AlexComponent extends Base
{
name = 'Alex';
}
子组件
@Component({
selector: 'cathy',
template: `
Cathy
{{alex ? 'Found' : 'Did not find'}} Alex via the component class.
`
})
export class CathyComponent {
// 通过将父组件注入到构造函数中,来获取父组件的属性
constructor( @Optional() public alex?: AlexComponent ) { }
}
还有其他方式见官方文档。
官方文档
父组件不仅可以给子组件传递简单的数据,还可把自己的方法以及整个父组件传给子组件。
参考 Angular 中操作DOM、父组件调用子组件方法。
官方文档
当 Angular 使用构造函数新建一个组件或指令后,就会按顺序在特定时刻调用生命周期钩子方法。
参考手册
中文手册
RxJS 是 ReactiveX 编程理念的 JavaScript 版本。ReactiveX 来自微软,它是一种针对异步数据 流的编程。简单来说,它将一切数据,包括 HTTP 请求,DOM 事件或者普通数据等包装成流 的形式,然后用强大丰富的操作符对流进行处理,使你能以同步编程的方式处理异步数据, 并组合不同的操作符来轻松优雅的实现你所需要的功能。
RxJS 是一种针对异步数据流编程工具,或者叫响应式扩展编程;可不管如何解释 RxJS 其目 标就是异步编程,Angular 引入 RxJS 为了就是让异步可控、更简单。
RxJS 里面提供了很多模块。这里我们主要给大家讲 RxJS 里面最常用的 Observable 和 fromEvent。
常见的异步编程的几种方法:
从下面例子可以看到 RxJS 和 Promise 的基本用法非常类似,除了一些关键词不同。Promise 里面用的是 then() 和 resolve(),而 RxJS 里面用的是 next() 和 subscribe()。
Rxjs 可以通过 unsubscribe() 可以撤回 subscribe 的动作,异步数据返回后,不作处理:
如果想让异步里面的方法多次执行,比如下面代码。 这一点 Promise 是做不到的,对于 Promise 来说,最终结果要么 resole(兑现)、要么 reject (拒绝),而且都只能触发一次。如果在同一个 Promise 对象上多次调用 resolve 方法, 则会抛异常。而 Observable 不一样,它可以不断地触发下一个值,就像 next() 这个方法的名字所暗示的那样。
Angular5.x 以后 get、post 和和服务器交互使用的是 HttpClientModule 模块。
异常处理:
命令创建项目 ng new angualrdemo08 时选择带路由:
会自动生成如下配置:
<header class="header">
<a routerLink="/home">首页a>
<a [routerLink]="[ '/news' ]">新闻a>
<a routerLink="/product">商品a>
header>
<router-outlet>router-outlet>
/* app.component.css:根组件样式文件 */
.header {
height: 44px;
line-height: 44px;
background: #000;
color: #fff;
}
.header a {
color: #fff;
padding: 10px;
cursor: pointer;
/* 取消a标签下划线 */
text-decoration: none;
}
// app-routing.module.ts :配置路由
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './components/home/home.component';
import { NewsComponent } from './components/news/news.component';
import { ProductComponent } from './components/product/product.component';
// 配置路由
const routes: Routes = [
{path: "home", component: HomeComponent},
{path: "news", component: NewsComponent},
{path: "product", component: ProductComponent},
// 默认路由两种方式
// 匹配不到路由的时候加载默认的组件 或者 路由跳转
{ path: '**', /* 匹配任意的路由 */
// component:HomeComponent, 可以指定组件,也可以重定向到其他路由,一般使用重定向
redirectTo:'home' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }