zoukankan      html  css  js  c++  java
  • Angular2入门教程-2 实现TodoList App


    http://codin.im/2016/09/15/angular2-tutorial-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的几个基本特性,弄明白这些以后,基本上就可以开始开发一些简单的应用了。


  • 相关阅读:
    expandablelistview学习在listView里面嵌套GridView
    App数据格式之解析Json
    不应和应该在SD卡应用应用
    9 个用来加速 HTML5 应用的方法
    Android设计模式系列-索引
    ObjectiveC语法快速参考
    App列表显示分组ListView
    进程、线程和协程的图解
    Python多进程原理与实现
    Python多线程的原理与实现
  • 原文地址:https://www.cnblogs.com/ztguang/p/12645296.html
Copyright © 2011-2022 走看看