一、起项目
根据官方教程,执行以下:
git clone https://github.com/angular/quickstart.git quickstart
cd quickstart
npm install
npm start
然后要把下面文件略微改一下(官方没有写),否则编译出的js和ts混在一起,很乱
//tsconfig.json
{
"compilerOptions": {
"target": "es5",
"outDir": "./built",//*加这一句,将编译的js文件归到这个目录
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [ "es2015", "dom" ],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true
}
}
这时直接运行会报错,还需要改一下这个文件:
//systemjs.config.js
......
map: {
// our app is within the app folder
app: 'built/app',
......
这时候npm start
就行了
不过还是推荐用angular-cli起项目,用了一下确实很方便,但是需要注意一点,国内的特殊网络环境。直接用ng new xxxxx
命令一般会在Installing packages for tooling via npm.
这个地方卡死。解决办法是用ng new xxxx --skip-npm
命令,然后进入文件夹npm i。还有一个坑就是在mac os上,这个命令创建的目录不在当前文件夹下,而是在/home/user下。别的系统我没试。
要点记录:
-
双向绑定必须要导入FormsModule
//app.module.ts
import {FormsModule} from '@angular/forms';
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
-
命名约定
文件名用烤串命名法
-
angular moduleId的作用(angular-cli工程不用)
- 在英雄教程里看到这个,当时不明白什么意思,google后找到答案: 这个是相对路径使用的。
- 在使用commonJS的前提下,不使用angular moduleId将从根路径开始寻找,使用后是从相对路径,webpack不受影响。
//不使用angular moduleId:
@Component({
selector: 'my-component',
templateUrl: 'app/components/my.component.html', <- Starts from base path
styleUrls: ['app/components/my.component.css'] <- Starts from base path
})
//使用angular moduleId
@Component({
moduleId: module.id,
selector: 'my-component',
templateUrl: 'my.component.html', <- relative to the components current path
styleUrls: ['my.component.css'] <- relative to the components current path
})
//tsconfig.json
{
"compilerOptions": {
"module": "commonjs", <- need to change this if you want to use module.id property
...
-
注意:使用官方的angular-cli产生的工程,这个angular moduleId要去掉,否则会报错,因为编译模式是es6
-
使用router导航的两种方式:
//在html中导航
<a class="col-1-4" *ngFor="let hero of heroes" [routerLink]="['/detail',hero.id]">
//--------------------------------------------------------------------------
//在组件js中导航
//从angular router库中导入Router组件
import { Router } from '@angular/router';
//...
export class HeroesComponent implements OnInit {
//依赖注入
constructor(private router:Router) { }
//...
gotoDetail():void {
//调用navigate()
this.router.navigate(['/detail',this.selectedHero.id]);
}
}
-
架构概览
Angular 模块(无论是根模块还是特性模块)都是一个带有@NgModule装饰器的类。
NgModule是一个装饰器函数,它接收一个用来描述模块属性的元数据对象。其中最重要的属性是:
<u>imports数组中应该只有NgModule类。不要放置其它类型的类。</u>
- providers: 服务的创建者,并加入到全局服务列表中,可用于应用任何部分。
- bootstrap: 指定应用的主视图(称为根组件),它是所有其它视图的宿主。只有根模块才能设置bootstrap
属性。
元数据
@Component({
moduleId: module.id,
selector: 'hero-list',
templateUrl: 'hero-list.component.html',
providers: [ HeroService ]
})
export class HeroListComponent implements OnInit {
解释一下:
@Component是装饰器,它把紧随其后的HeroListComponent类标记成了组件类
@Component装饰器中我们传入了一些数据,这些数据叫做元数据
其它装饰器包括@Injectable、@Input和@Output
等。所以装饰器后必须有(),即使你不打算向其中传入任何元数据
指令 (directive)
指令概览
在 Angular 中有三种类型的指令:
- Components—组件 — 拥有模板的指令
- Structural directives—结构型指令 — 通过添加和移除 DOM 元素改变 DOM 布局的指令。
- Attribute directives—属性型指令 — 改变元素显示和行为的指令。
组件是这三种指令中最常用的。 你在快速起步例子中第一次见到组件。
结构型指令修改视图的结构。例如,NgFor 和 NgIf。
属性型指令改变一个元素的外观或行为。例如,内置的 NgStyle 指令可以同时修改元素的多个样式。
Angular 模板是动态的。当 Angular 渲染它们时,它会根据指令提供的操作对 DOM 进行转换。严格来说组件就是一个指令,但是组件非常独特,并在 Angular 中位于中心地位,所以在架构概览中,我们把组件从指令中独立了出来。还有两种其它类型的指令:结构型指令和属性 (attribute) 型指令。
- 结构型指令通过在 DOM 中添加、移除和替换元素来修改布局。
<li *ngFor="let hero of heroes"></li>
<hero-detail *ngIf="selectedHero"></hero-detail>
- 属性型 指令修改一个现有元素的外观或行为。 在模板中,它们看起来就像是标准的 HTML 属性,故名。
//ngModel指令就是属性型指令的一个例子,它实现了双向数据绑定。 ngModel修改现有元素(一般是<input>)的行为:设置其显示属性值,并响应 change 事件。
<input [(ngModel)]="hero.name">
Angular 还有少量指令,它们或者修改结构布局(例如 ngSwitch), 或者修改 DOM 元素和组件的各个方面(例如 ngStyle和 ngClass)。
依赖注入
constructor(private service: HeroService) { }
简单的说,必须在要求注入HeroService之前,在注入器中注册HeroService的Provider。 Provider用于创建并返回一个服务,通常是服务类本身。
<u>我们可以在模块或组件中注册提供商。</u>
通常会把提供商添加到根模块上,以便在任何地方使用服务的同一个实例。
安全导航操作符 ( ?. ) 和空属性路径
The null hero's name is {{nullHero.firstName}}
当currentHero为空的时候,应用崩溃了。
解决方案有:
<div *ngIf="nullHero">The null hero's name is {{nullHero.firstName}}</div>
//或者
The null hero's name is {{nullHero && nullHero.firstName}}
这些方法都有价值,但是会显得笨重,特别是当这个属性路径非常长的时候。 Angular 安全导航操作符 (?.) 是在属性路径中保护空值的更加流畅、便利的方式。 表达式会在它遇到第一个空值的时候跳出。 显示是空的,但应用正常工作,而没有发生错误。
<!-- No hero, no problem! -->
The null hero's name is {{nullHero?.firstName}}
用户输入
- 模板引用变量
<p>
<input #name (keyup)="0">
</p>
{{name.value}}
angular可以通过$event获得DOM,并通过$event.target.value这样来获得input的值,但不建议这么做,因为这样相当于把html DOM元素暴露给了组件,提倡的做法是利用上面的模板引用变量
另一个例子:
@Component({
selector: 'key-up2',
template: `
<input #box (keyup)="onKey(box.value)">
<p>{{values}}</p>
`
})
export class KeyUpComponent_v2 {
values = '';
onKey(value: string) {
this.values += value + ' | ';
}
}
更为完整
@Component({
selector: 'key-up4',
template: `
<input #box
//keyup.enter)键盘事件过滤器
(keyup.enter)="update(box.value)"
//失去焦点事件
(blur)="update(box.value)">
<p>{{value}}</p>
`
})
export class KeyUpComponent_v4 {
value = '';
update(value: string) { this.value = value; }
}
小结
- 使用模板变量来引用元素
- 传递数值,而非元素
- 保持模板语句简单
表单
NgModel 指令不仅仅跟踪状态。它还使用特定的 Angular CSS 类来更新控件,以反映当前状态。 可以利用这些 CSS 类来修改控件的外观,显示或隐藏消息。
input type="text" class="form-control" id="name" required
[(ngModel)]="model.name" name="name" #name="ngModel">
<div class="alert alert-danger"
[hidden]="name.valid || name.pristine"> name 是必填字段</div>
- name="ngModel"相当于定义“模板引用变量”,并将其初始化为ngModel。这样在下面的警告框就能根据#name(当前ngModel)的状态,显示或者隐藏(当控件是有效的 (valid) 或全新的 (pristine) 时,隐藏消息。 “全新的”意味着从它被显示在表单中开始,用户还从未修改过它的值。)
表单的reset()
添加用户时,这个name项并不是“全新的”了,要通过调用表单的reset()方法重置控件的“全新”状态。
<button type="button" class="btn btn-default"
(click)="this.model = new Hero(2, '', ''); heroForm.reset();">
New Hero</button>
使用ngForm和ngSubmit校验并提交表单
<form (ngSubmit)="onSubmit()" #userForm="ngForm">
<button type="submit" class="btn btn-primary"
[disabled]="!userForm.form.valid">Submit</button>
- 什么是ngForm?
NgForm指令为form元素扩充了额外的特性。 它持有通过ngModel指令和name属性为各个元素创建的那些控件,并且监视它们的属性变化,包括有效性。 它还有自己的valid属性,只有当其中所有控件都有效时,它才有效。
通过userForm变量把按钮的disabled属性绑定到表单的整体有效性。
现在,删除姓名,们违反了“必填姓名”规则,它还是像以前那样显示出错误信息。同时,Submit 按钮也被禁用了。
模板语法
- attribute 是由 HTML 定义的。property 是由 DOM (Document Object Model) 定义的。
- attribute 初始化 DOM property,然后它们的任务就完成了。property 的值可以改变;attribute 的值不能改变。
- 在 Angular 的世界中,attribute 唯一的作用是用来初始化元素和指令的状态。 当进行数据绑定时,只是在与元素和指令的 property 和事件打交道,而 attribute 就完全靠边站了。
- 数据绑定的目标是 DOM 中的某些东西。 这个目标可能是(元素 | 组件 | 指令的)property、(元素 | 组件 | 指令的)事件,或(极少数情况下) attribute 名。
- 属性绑定还是插值表达式?
//我们通常得在插值表达式和属性绑定之间做出选择。 下列这几对绑定做的事情完全相同:
<p>![]({{heroImageUrl}}) is the <i>interpolated</i> image.</p>
<p><img [src]="heroImageUrl"> is the <i>property bound</i> image.</p>
<p><span>"{{title}}" is the <i>interpolated</i> title.</span></p>
<p>"<span [innerHTML]="title"></span>" is the <i>property bound</i> title.</p>
在多数情况下,插值表达式是更方便的备选项。
attribute 绑定
如前所述,html中有一些元素只有attribute没有property,如colspan,我们知道angular只能绑定到propertiy,如果非要绑定到attribute需要特殊的语法:
//attr前缀,一个点 (.) 和 attribute 的名字组成
<td [attr.colspan]="1 + 1">One-Two</td>
父子组件通讯
- 父 → 子(@Input())
//父组件
<app-todo-footer [itemCount]="todos?.length"></app-todo-footer>
//子组件
import { Component, OnInit,Input } from '@angular/core';
@Input()
itemCount:number;
- 子 → 父(@Output()、EventEmitter):
//子组件
template: `
<div>
<button (cilick)="sendEvent();">Event Emitter Test</button>
</div>`
// component
@Output()
onRemoveTodo = new EventEmitter<Todo>();
sendEvent(todo:Todo){
this.onRemoveTodo.emit(todo);
}
//父组件,收到eventEmitter消息后执行handleEvent方法,
//通过$event拿到传过来的Todo类型值
<child (eventEmitter)="handleEvent($event)" ></child>
如何将模块(Module)独立出来
绝大部分摘自@接灰的电子产品神级码农
模块>组件, Module is greater than Component
- 可以看到,模块包括三种视图类:component/directive/pipe,使用
declarations:[...]
导入 - 模块还可以包括其它模块,使用imports导入,如
imports: [
BrowserModule,
FormsModule,
HttpModule,
TodoModule,
InMemoryWebApiModule.forRoot(TodoDbService),
routing
],
随着应用逐渐变大,模块化势在必行,如,登录模块,注册模块。。。。如何把一个应用用模块组织起来?
- 建立模块文件,如todo.module.ts
import { CommonModule } from '@angular/common';
import{ NgModule } from '@angular/core';
import {HttpModule} from '@angular/http';
import {FormsModule} from '@angular/forms';
import {routing } from './todo.routes';
import { TodoComponent } from './todo.component';
import { TodoFooterComponent } from './todo-footer/todo-footer.component';
import { TodoHeaderComponent } from './todo-header/todo-header.component';
import { TodoService } from './todo.service';
@NgModule({
imports: [
CommonModule,
FormsModule,
HttpModule,
routing
],
declarations:[
TodoComponent,
TodoFooterComponent,
TodoHeaderComponent
],
providers : [
TodoService
]
})
export class TodoModule{}
可以看到其和app.module.ts非常类似,不同的地方有2处:
- 使用CommonModule而不是BrowserModule
导入 BrowserModule 会让该模块公开的所有组件、指令和管道在 AppModule 下的任何组件模板中直接可用,而不需要额外的繁琐步骤。CommonModule 提供了很多应用程序中常用的指令,包括 NgIf 和 NgFor 等。BrowserModule 导入了 CommonModule 并且 重新导出 了它。 最终的效果是:只要导入 BrowserModule 就自动获得了 CommonModule 中的指令。几乎所有要在浏览器中使用的应用的 根模块 ( AppModule )都应该从 @angular/platform-browser 中导入 BrowserModule 。在其它任何模块中都 不要导入 BrowserModule,应该改成导入 CommonModule 。 它们需要通用的指令。它们不需要重新初始化全应用级的提供商。
- routing的写法:
import { Routes, RouterModule } from '@angular/router';
import { TodoComponent } from './todo.component';
export const routes: Routes = [
{
path: 'todo',
component: TodoComponent
}
];
//注意是forChild
export const routing = RouterModule.forChild(routes);
forRoot只能用于根目录,所有非根模块的其他模块路由都只能用forChild
添加了自定义模块后根模块的变动:
- 引入TodoModule
import {TodoModule} from './todo/todo.module';
...
@NgModule({
declarations: [
AppComponent,
LoginComponent
],
imports: [
...
TodoModule,
routing,
...
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
2.routing的变动:
由
{
path: 'todo',
component: TodoComponent
},
变为
{
path: 'todo',
redirectTo:'todo'
},
去掉了TodoComponent的依赖,而且更改todo路径定义为redirecTo到todo路径,但没有给出组件,这叫做“无组件路由”,也就是说后面的事情是TodoModule负责的。
一般来说,如果要生成某个模块下的组件,输入ng g c 模块名称/组件名称。在命令行窗口键入ng g c todo/todo-item,angular-cli会十分聪明的帮你在todo目录下建好TodoItem组件,并且在TodoModule中声明。
通过路由传参
- router中定义动态路由:
{
path: 'todo/:filter',
component: TodoComponent
}
- Component.ts中:
//引入相关类
import { Router, ActivatedRoute, Params } from '@angular/router';
//依赖注入
constructor(
private route: ActivatedRoute,
private router: Router
) { }
//初始时定义路由解析
ngOnInit() {
this.getTodos();
this.filterTodos();
}
filterTodos(){
this.route.params.forEach((params:Params)=>{
let filter = params['filter'];
this.service.filterTodos(filter)
.then(todos=>this.todos=[...todos]);
})
}
再看看service类(用到的服务器是json-server):
//根据路由筛选
filterTodos(filter: string): Promise<Todo[]> {
switch(filter){
case 'ACTIVE': return this.http
.get(`${this.api_url}?completed=false`)
.toPromise()
.then(res => res.json() as Todo[])
.catch(this.handleError);
case 'COMPLETED': return this.http
.get(`${this.api_url}?completed=true`)
.toPromise()
.then(res => res.json() as Todo[])
.catch(this.handleError);
default:
return this.getTodos();
}
}
乱七八糟写了一堆,就当是笔记吧