Angular2入门教程-2

Angular2入门教程-2 实现TodoList App

这是Angular2入门教程的第二部分,第一部分介绍了Angular2的特性和概念,以及一个Angular2项目的结构的代码。这一部分,我们就基于上一部分的介绍,来开始开发我们的App。

我们要实现的,是一个TodoList(待办事宜)的APP。下面就是这个app最终的效果
angular2-todo-app.jpg

 

如果你们想查看这个教程最终的源文件,可以直接查看项目地址。在下面的讲解中,对很多css样式的定义,就没有列出来说明。如果你们要按照这个教程完成这个应用,需要自己从这里查看相应的样式文件,当然也可以根据喜好去定义样式。

系统设计

即使一个简单的实例,我们也要从Angular2的编程思想出发,对系统进行总体的设计。

组件设计

首先,我们多次提到,Angular2是组件化、模块化的,我们开发一个Angular2的应用,也应该将系统设计成一个个组件,而且一个组件有可能包含多个子组件。就好比html是一个树形结构的DOM,一个Angular2应用也应该是一个树形结构的组件树。

对于这个TodoList的应用,也就是一个appModule,它包含2个组件,about和todolist。其中todolist又包含2个组件,一个待办事宜的列表组件,和一个待办事宜的详情组件。列表组件里,我们又把每一个任务显示封装成一个组件。组件树就是下面这样:
components.jpg

下面是把list组件和它的子组件item的图:

路由设计

接下来就考虑这个应用的页面跳转的逻辑,也就是路由设计。
这个应用的路由很简单,打开的时候,默认打开任务列表,点击一个任务时跳转到详情,点击详情里面的返回按钮,又回到列表。还有一个链接可以打开about页面。
routers.jpg

编码

上一部分我们讲到提供的项目模板提供了2个实例组件,这两个组件分别在2个文件夹里,我们保留about,把example文件夹删除,新建一个文件夹,叫todo,也就是说Todo模块会放在这个目录里面。上面设计的todo相关的组件有3个,list、item和detail。我们在todo文件夹里创建这3个目录,每个目录里面再创建相应的 .component.css、.component.html 和 .component.ts文件。

对于组件化的开发,我们可以采用自顶向下的开发流程,先开发根模块,再开发子模块;也可以自底向上的开发。对这个应用,我们采用混合的方式,先定义app模块和组件,然后定义好todo模块的定义,再开发todo模块的每个组件;最后我们再完善todo模块和路由,完成整个app的开发。对于业务代码的开发,大致流程是这样(由于一个Angular2应用的index文件和main.ts文件一般不需要修改,也跟具体业务开发没有多少关系,这里先不考虑):

  1. 先定义整个app的模块,AppModule。这个我们在上一部分的教程里面已经说明,我们先不用修改,当我们完成其他组件的开发以后,再需要完善这里面的内容。
  2. 定义app的路由。一般,我们都是在各个业务模块的定义里面,添加路由定义,然后在app路由里面引入各个模块的路由。而app模块的路由,在项目模板里面已经提供,开始无需修改,等开发完业务模块以后再修改即可。
  3. 再定义这个应用的根组件,AppComponent。这个我们在上一部分的教程里面也已经说明。我们只需要根据我们的设计修改app.component.html的内容。
  4. 定义Todo模块。这个阶段就需要开发todo模块需要的业务模型,包括model, service,还有就是TodoModule。我们先定义好模块的框架,等开发完成子组件以后,再修改TodoModule里面的内容。
  5. 开发todo模块的各个子组件,list, item和detail。
  6. 完善todo模块,定义todo模块的路由等。

在开始之前,如果还没有启动测试服务器,先启动:

 

1

 

npm start

 

这就会编译TypeScript文件,启动测试服务器,并监听文件修改,如果文件有修改,就会自动重新编译,然后刷新页面。打开浏览器,输入url: ‘http://localhost:3000‘ ,打开应用,就可以开始开发了。在开发过程中,不需要重新启动服务器,不需要刷新页面。

AppModule

模板中的app.module.ts文件先不修改,我们需要在开发完todo模块以后,在这里引入新的模块。

App Route

app的路由,我们直接使用项目模板提供的,暂时不需要修改。至于里面的定义及其语法描述,在上一部分介绍项目模板的时候已经说明。

AppComponent

AppComponent是app的入口,每个Angular2的应用都是先加载这个组件,一般这个组件只是包含应用的页面框架和样式。根据我们的页面设计,我们需要修改app.component.html。

 

1

2

3

4

5

 

<h1>Todos</h1>

<router-outlet></router-outlet>

<footer>

<a class="about" routerLink='/about'>About</a>

</footer>

 

在这个页面框架中,h1的标题的部分,下面的<router-outlet></router-outlet>就是根据路由定义加载相应的页面。最下面,有一个footer,里面有一个跳转到about页面的按钮。
AppComponent使用的样式就不多说了,你们可以直接查看实例的项目文件。在下面的说明中,就不会把样式也贴出来说明,读者可以自行查看实力项目的源文件。

Todo模块 - Todo Model

先在todo目录里面建一个文件todo.ts,这是我们的待办事宜的任务的定义:

 

1

2

3

4

5

6

7

8

 

export class Todo {

id: number;

title: string = '';

createdDate: Date = new Date();

complete: boolean = false;

constructor() { }

}

 

这个代码很直观,就是定义了几个属性,其中创建时间的初始值就是当前时间,是否完成的初始值是false.

Todo模块 - Todo Service

接下来,我们就写service的代码,我们创建一个todo.service.ts文件在todo目录里。他负责对任务的增删改查的处理。具体内容如下:

 

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

44

45

46

47

48

49

50

51

52

53

54

55

56

 

import {Injectable} from '@angular/core';

import {Todo} from './todo';

@Injectable()

export class TodoService {

// 为了生成一个自增的id,保存最后一个生成的id

lastId: number = 0;

todos: Todo[] = []; // 保存任务列表

constructor() {}

// 添加一个任务

addTodo(todo: Todo): TodoService {

if (!todo.id) {

todo.id = ++this.lastId;

}

this.todos.push(todo);

// 方法定义中指定返回类型是TodoService,所以这里返回this,也就是当前service对象。

return this;

}

// 从任务列表里删除一个任务

deleteTodoById(id: number): TodoService {

this.todos = this.todos.filter(todo => todo.id !== id);

return this;

}

// 更新一个任务

updateTodoById(id: number, values: Object = {}): Todo {

let todo = this.getTodoById(id);

if (!todo) {

return null;

}

Object.assign(todo, values); // 将更新的values对象的属性值赋给todo对象

return todo;

}

// 获取所有任务列表

getAllTodos(): Todo[] {

return this.todos;

}

// 根据Id获取任务

getTodoById(id: number): Todo {

return this.todos.filter(todo => todo.id === id).pop();

}

// 标记一个任务为完成/未完成

toggleTodoComplete(todo: Todo){

let updatedTodo = this.updateTodoById(todo.id, {

complete: !todo.complete

});

return updatedTodo;

}

}

 

在这个定义中,我们用@Injectable()标签来定义Service,这样,我们在应用的其他地方,就可以通过Angular2的依赖注入的特性,来自动获取该service对象的实例。

@Injectable()在Angular2中,叫Decorator,也就是装饰器,用来给下面的类TodoService添加额外的属性或方法。在Angular2中,大量使用这种装饰器来定义组件、模块、服务等。

Angular会维护一个service组件的容器,在应用中的某个地方需要用到这个TodoService的时候,我们不用自己创建这个对象的实例,而是通过Angular的Injector自动获取,这就是依赖注入。Angular的Injector会判断这个service的实例在容器中是否存在,如果不存在就创建一个放到容器里并返回,如果已存在,就返回这个实例。所以,在Angular的应用中,我们用service对象,除了实现业务逻辑,还可以用它来保存数据,或者在组件之剑传递参数。

需要注意的一点是,Angular2是组件化、模块化的,那么我们应该在哪一个组件范围内或者模块范围内来实现这个service的自动注入?还是说,在全局的应用系统范围内自动注入?所以,我们需要在一个组件或者模块的定义里面通过providers定义:

 

1

2

3

 

providers: [

TodoService, SomeOtherService

],

 

接下来我们在定义todo模块的时候,就需要用这种方式来定义TodoService,这样这个TodoService的实例在todo模块范围内就能够实现自动注入,并共用一个实例。

Todo模块

现在就可以开始写这个todo模块的定义:

 

1

2

3

4

5

6

7

8

9

10

11

12

 

import { NgModule } from '@angular/core';

import { CommonModule } from '@angular/common';

import { FormsModule } from '@angular/forms';

import { TodoService } from './todo.service';

@NgModule({

imports: [CommonModule, FormsModule ],

declarations: [],

providers: [TodoService]

})

export class TodoModule {}

 

在这里,我们定义了TodoModule,由于这个模块的子组件还没有开发,所以,declarations里面都是空的。我们上面说到,TodoService需要在整个todo模块范围内使用,所以我们在这个里面添加了providers: providers: [TodoService]

Todo组件 - item

在开发todo组件的时候,我们用自底向上的方式开发,先写item组件。这个组件是用于在list组件中显示每一个任务。对于每一个任务,我们可以标记这个任务已经完成,也可以彻底删除这个任务。然后,当点击一个任务的标题的时候,就会跳转到这个任务的详情页。下面就是根据这个需求编写的TodoItemComponent(item.componennt.ts文件):

 

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

 

import { Component, Input } from '@angular/core';

import { Router } from '@angular/router';

import { Todo } from '../todo';

import { TodoService } from '../todo.service';

@Component({

selector: 'todo-item',

templateUrl: 'app/todo/item/item.component.html',

styleUrls: ['app/todo/item/item.component.css']

})

export class TodoItemComponent {

@Input() todo: Todo;

constructor(private todoService: TodoService, private router: Router) { }

// 跳转到任务详情页

gotoDetail(todo) {

this.router.navigate(['/todo/detail', todo.id]);

}

// 标记一个任务完成/未完成

toggleTodoComplete(todo) {

this.todoService.toggleTodoComplete(todo);

}

// 删除一个任务

removeTodo(todo) {

this.todoService.deleteTodoById(todo.id);

}

}

在这个定义中,我们用@Component定义了一个组件。里面的selector: 'todo-item'表示在它的父组件(列表)的页面中,item组件的页面会显示到<todo-item>标签里面。
@Input() todo: Todo;这个表示在这个组件中有一个变量todo,它的值是从父组件获得的。
接下来就是他的构造函数:

 

1

 

constructor(private todoService: TodoService, private router: Router) { }

 

private todoService: TodoService这代表Angular会通过依赖注入的方式,将todoService作为一个内部属性。还有router也是通过注入的方式,将一个Router类型的对象作为属性。然后我们就可以在这个组件的其他方法里面使用这两个值。
下面的就是几个页面交互的方法,其他的都不用说吗,就看一下gotoDetail()方法,它使用Angular2的Router组建跳转到任务详情页面。

下面再看看item.componennt.html:

 

1

2

3

4

5

 

<div class="todo-item" [class.completed]="todo.complete">

<input class="toggle" type="checkbox" (click)="toggleTodoComplete(todo)" [checked]="todo.complete">

<label (click)="gotoDetail(todo)">{{todo.title}}</label>

<button class="destroy" (click)="removeTodo(todo)"></button>

</div>

 

这个模板里面的一些语法,可以参考官方的文档,这里只是简单说明一下。

这个[class.completed]="todo.complete"是根据todo变量的complete值,决定在当前这个div标签上是否要添加一个completed的class。

下面就是一个checkbox类型的input,(click)="toggleTodoComplete(todo)"这是给这个checkbox添加了一个点击事件,用户点击的时候调用toggleTodoComplete(todo),也就是上面TodoItemComponent里面的方法,如果这个任务未完成,它就更新他的状态为已经完成;如果已经完成的,就把状态更新为未完成。
后面的[checked]="todo.complete"表示根据这个任务的是否完成的状态todo.complete来设置这个checkbox是否是选中的状态。

再下面就是一个lebel表现,来显示这个任务的标题。这里用这种方式将组建里面的变量显示到页面上。它还添加了一个点击事件(click)="gotoDetail(todo)",用于在用户点击的时候跳转到详情页。

最后就是一个按钮,绑定了一个点击事件(click)="removeTodo(todo)",用来删除一个任务。

这个组件里面还有一个样式的定义文件item.component.css,这里就不多说了,你们可以直接查看实例的项目文件

Todo组件 - list

在list组件中,我们以列表的形式显示任务,在最上面还有一个新建任务的输入框。list.component.ts的内容如下:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

 

import { Component } from '@angular/core';

import { Todo } from '../todo';

import { TodoService } from '../todo.service';

@Component({

selector: 'todo-list',

templateUrl: 'app/todo/list/list.component.html',

styleUrls: ['app/todo//list/list.component.css']

})

export class TodoListComponent {

newTodo: Todo = new Todo();

constructor(private todoService: TodoService) { }

addTodo() {

this.todoService.addTodo(this.newTodo);

this.newTodo = new Todo();

}

get todos() {

return this.todoService.getAllTodos();

}

}

 

这个就很简单,定义了模板和样式文件,在构造函数中注入了TodoServiceaddTodo方法在用户新建任务的时候调用。
下面是定义了一个属性todos,但是定义的方式比较特别:

 

1

2

3

 

get todos() {

return this.todoService.getAllTodos();

}

 

它的意思是说定义一个属性todos,同时定义了它的get方法。

下面是list.component.html的内容:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

 

<section class="todoapp">

<header class="header">

<input class="new-todo" placeholder="Get things done!" autofocus="" [(ngModel)]="newTodo.title" (keyup.enter)="addTodo()">

</header>

<section class="main" *ngIf="todos.length > 0">

<ul class="todo-list">

<todo-item *ngFor="let todo of todos" [todo]="todo">

</todo-item>

</ul>

</section>

<footer class="footer" *ngIf="todos.length > 0">

<span class="todo-count"><strong>{{todos.length}}</strong> {{todos.length == 1 ? 'item' : 'items'}} left</span>

</footer>

</section>

 

其中,[(ngModel)]="newTodo.title"是绑定了一个component的变量newTodo.title到这个输入框,这样你输入的内容会赋值到变量newTodo.title上,如果在TodoListComponent里修改了这个变量的值,它也会更新显示到页面上。
这个输入框还有一个事件绑定:(keyup.enter)="addTodo()",表示当用户敲’输入键’(就是enter键)抬起的时候,就会触发addTodo()方法。
下面的<section>部分是用列表显示任务,它用*ngIf="todos.length > 0"来判断,如果任务列表长度大于1,就显示这个列表,否则就不显示。
下面就是用列表显示所有的任务:

 

1

2

 

<todo-item *ngFor="let todo of todos" [todo]="todo">

</todo-item>

 

这里用了一个*ngFor的语法,代表循环遍历todos,然后用<todo-item>显示任务项。这个标签<todo-item>对应的我们定义的TodoItemComponent 里面的selector,所以TodoItemComponent组件的内容会显示到这个html标签里面。[todo]="todo"表示从list组件里面绑定当前的todo实例变量到item组件里面的todo变量上。这样绑定以后,我们在item的组件和页面里面就可以使用这个变量进行显示和操作。

Todo组件 - detail

在item组件里面,我们有一个点击事件是跳转到任务详情,下面就看看这个详情组件TodoDetailComponent:

 

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

 

import { Component, OnInit } from '@angular/core';

import { ActivatedRoute, Router } from '@angular/router';

import { Todo } from '../todo';

import { TodoService } from '../todo.service';

@Component({

selector: 'todo-detail',

templateUrl: 'app/todo/detail/detail.component.html',

styleUrls: ['app/todo/detail/detail.component.css']

})

export class TodoDetailComponent implements OnInit {

selectedTodo: Todo;

constructor(private route: ActivatedRoute,

private router: Router,

private todoService: TodoService) {}

ngOnInit() {

let todoId = +this.route.snapshot.params['id'];

this.selectedTodo = this.todoService.getTodoById(todoId);

if (!this.selectedTodo) {

this.router.navigate(['/todo/list']);

}

}

goBack() {

window.history.back();

}

}

 

这个TodoDetailComponent有一个implements OnInit。这就是TypeScript的特性,意思就是这个组件实现了OnInit的接口,它有一个必须实现的方法ngOnInit()。当这个组件被创建的时候,这个ngOnInit()方法就会被调用,相当于一个初始化方法。
在这个初始化方法里面,我们从路由的参数里面获取了参数:

 

1

 

+this.route.snapshot.params['id'];

 

获取参数的方法有几种,这里用的snapshot,从它的字面意思也可以理解,它是用于这个页面是一次性的,每次跳转到这个页面后,会再跳转到其他页面,再次进来的时候会再重新初始化这个页面。而不是在当前页面,通过路由的变化而更新里面的内容。

然后,假如直接在地址栏输入一个url,像’/todo/detail/15’,如果这个id的任务不存在,就应该跳转到列表页:this.router.navigate(['/todo/list']); 。

todo 路由

我们完成了3个组建以后,就可以开始定义路由了。我们把todo模块需要的路由单独定义在一个文件’todo.routes.ts’里:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

 

import { Route } from '@angular/router';

import { TodoListComponent } from './list/list.component';

import { TodoDetailComponent } from './detail/detail.component';

export const TodoRoutes: Route[] = [

{

path: 'todo/list',

component: TodoListComponent

},

{

path: 'todo/detail/:id',

component: TodoDetailComponent

}

];

 

这就是定义了2个路由,分别是列表页和详情页,其中详情页路由有一个参数id,在url里面。在上面的detail组件里面,我们从参数里面获取了这个参数,用来获取任务信息。

完善todo模块

上面我们已经定义了todo模块,也就是TodoModule,但是当时我们还没有几个子组件,现在这些组件已经完成,我们就需要完善TodoModule,把这些组件都引入进来,下面就是这个模块的全部内容:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

 

import { NgModule } from '@angular/core';

import { CommonModule } from '@angular/common';

import { FormsModule } from '@angular/forms';

import { TodoListComponent } from './list/list.component';

import { TodoDetailComponent } from './detail/detail.component';

import { TodoItemComponent } from './item/item.component';

import { TodoService } from './todo.service';

@NgModule({

imports: [CommonModule, FormsModule ],

declarations: [TodoListComponent, TodoDetailComponent, TodoItemComponent],

providers: [TodoService]

})

export class TodoModule {}

 

在这个模块里面的declarations设置里面,我们把几个组件都加在这个里面,这就好像把几个组件一起打包到一个模块里。这样,我们在整个app的模块定义里面引入这个todo模块的时候,我们只需要引入这个TodoModule就可以,而不需要把这个模块里面的所有组件都一个个的引入。

将todo路由加到app路由里

上面我们定义好了todo模块的路由,我们还需要把这个路由加到整个app的路由定义里,不然是无法识别这些路由的。所以我们需要在app.routes.ts里面引入todo.routes。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

 

import { Routes } from '@angular/router';

import { AboutRoutes } from './about/about.routes';

import { TodoRoutes } from './todo/todo.routes';

export const routes: Routes = [

{

path: '',

redirectTo: '/todo/list',

pathMatch: 'full'

},

...AboutRoutes,

...TodoRoutes

];

 

在导出的路由里,我们设置默认路径是’/todo/list’,然后把TodoRoutes加入到路由里。

将todo模块加到app模块里

最后,我们还需要在我们的app模块里面把todo模块引入进来,最终的app模块的内容就是这样:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

 

import { NgModule } from '@angular/core';

import { BrowserModule } from '@angular/platform-browser';

import { RouterModule } from '@angular/router';

import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

import { AboutComponent } from './about/about.component';

import { TodoModule } from './todo/todo.module';

import { routes } from './app.routes';

@NgModule({

imports: [BrowserModule, FormsModule, RouterModule.forRoot(routes), TodoModule],

declarations: [AppComponent, AboutComponent],

bootstrap: [AppComponent]

})

export class AppModule {}

 

到这里,整个应用应该就开发完成了。在这个实例中,我们了解了Angular2的组件、模块,还有一些简单的模板,也介绍了Angular2的依赖注入的特性和service,还有路由。对于Angular的双向绑定,我们虽然没有单独说明,但是在讲解模板和组件定义的时候也提到一些。上面这些,其实就是Angular2的几个基本特性,弄明白这些以后,基本上就可以开始开发一些简单的应用了。

转载于:https://my.oschina.net/martin123/blog/776681

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值