使用Angular2构建一个简单的Todo应用

think2011 edited this page Feb 23, 2016 · 19 revisions

Angular2越来约火,以至于很多js开发者都想用它写一些例子。当然,你可能更希望有一些Angular2令人兴奋的新特性的文章出来。好,那这篇文章我就展示给大家如何使用Angular2来创建一个Todo应用。在这篇文章中,你将会学习如何使用组件、模板、数据绑定等一些重要的东东。让我们现在就开始吧。

Angular2当前还处在α版本,被不停的修改着。查看quickstart应用可以看到最新的修改记录。

这里有个例子,可以下载下来。如果你有啥问题可以提交到devmag.io去参与讨论。

获得种子应用

Angular团队已经把开发Angular2应用所需要的依赖都放到qiuckstart app中。你可以直接在github上下载到你的机器上。

现在你可以使用你最喜欢的文本编辑器打开这个app,开始编码了。我们注意到,当前的根目录是quickstart,我们把它重命名为todaoapp。

创建组件

Angular 1.x中有指令的概念,而在Angular2中我们可以直接创建组件。每个组件都有两部分:视图和控制器。视图是你的组件中得HTML模板,而控制器则提供js的行为。你可能听说Angular2不再有控制器了,但是我要告诉你,确切的说应该是控制器作为组件的一部分了。那一个组件到底长啥样呢,让我们创建一个todo应用来揭开它的面纱。

找到根目录创建TodoApp.es6文件。.es6后缀意味着我们将要遵循ES6的语法,当然你也可以使用普通的.js作为后缀。现在我们把如下内容贴到该文件中去。

    import {Component, Template, bootstrap,Foreach} from 'angular2/angular2';
    import {TodoStore} from 'services/TodoStore';
    @Component({
      selector: 'todo-app',
      componentServices: [
          TodoStore
      ]
    })
    @Template({
      url: 'templates/todo.html',
      directives: [Foreach]
    })
    class TodoApp {

      todoStore : TodoStore;

      constructor(todoStore: TodoStore) {
        this.todoStore = todoStore;
      }

      add($event,newtodo){
          if($event.which === 13){
            this.todoStore.add(newtodo.value);
            newtodo.value = '';
          }
      }

      toggleTodoState(todo){
        todo.done = !todo.done;
      }

    }

    bootstrap(TodoApp);

解释

前两行代码用于引入ES6模板。第一行代码是从angular2/angular2下引入组件、模板、bootstrap和forEach。第二行代码从模块services/TodoStore中引入了我们的TodoStore服务。不要为Angular能否找到这些模块而担心,后面我们会解释。

现在,我们需要创建我们组件的控制器:名称为TodoApp:

    class TodoApp{

    }

在上面的ES6类代码中,有几个注解引起我们的兴趣:

@Component: 这标识TodoApp是一个组件。而参数是一个对象,包含一个选择器属性,用来表示这个组件应该使用啥HTML选择器;同时这个对象还包含componentServices用来标示我们的组件依赖的服务。在我们的例子中,只依赖了TodoStore,后面我们会创建它的。

@Template: 这个注解标识它将是我们组件中使用的模板。在我们的例子中是templates/todo.html。因此我们的url属性指向了模板位置。类似的,指令属性表示我们模板中用到的指令。我们将会使用一个简单的指令ForEach到我们的模板中,通过制定指令属性就可以了,很简单。

现在我们将增加一个构造函数到我们的TodoApp类中:

    constructor(todoStore: TodoStore) {
       this.todoStore = todoStore;
    }

参数todoStore:TodoStore告诉Angular的DI系统要在初始化阶段注入TodoStore服务的实例到我们组件中。这就是IoC,我们不需要自己去获得依赖,而是让Angular自动的实例化依赖,然后注入到我们组件中。所以我们拿到了TodoStore的实例化,并存到变量totoStore中。

我们先把add和toggleTodoState方法放在一边,现在我们来看下我们的TodoStore服务。

创建服务

首先在services目录下创建一个名为TodoStore.js的文件,同时把下面的代码复制进去:

    export class TodoStore {
        constructor(){
            this.todoList = [];
        }
        add(item){
            this.todoList.unshift({text:item,done:false});
        }
    }

解释

TodoStore是一个简单的js类,它有一个构造函数和一个add方法。构造函数创建了一个简单的数组todoList用于存放我们的todo对象。我们在模板中使用这个数组去渲染todo数据。每当一个新的toto对象被创建时候都会调用add方法。add方法接收文本参数后包装成一个对象放入到todoList数组中。注意下,done属性用来表示todo对象是否被完成了。最后我们使用export关键字来导出这个class。

现在回到TodoApp.es6,来看下我们如何导入上面的类的。

创建模板

在templates目录下创建todo.html文件,内容如下:

    <style>
        @import url(http://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css);
        @import url(http://fonts.googleapis.com/css?family=Open+Sans);
    </style>
    <style>
        .container{
            font-family: 'Open Sans', sans-serif;
                margin-top: 200px;
        }
        .done{
            text-decoration: line-through;
            color : #999999;
        }
        .bottom-offset{
            margin-bottom: 20px;
        }
    </style>

    <div class="container">
        <div class="row">
            <div class="col-xs-4 col-xs-offset-4">
                <div class="bottom-offset">
                    <input type="text" class="form-control" placeholder="Write something here" autofocus #newtodo (keyup)="add($event,newtodo)"/>
                </div>
                <div *foreach="#todo in todoStore.todoList">
                    <input type="checkbox" [checked]="todo.done" (click)="toggleTodoState(todo)"/> <span [class.done]="todo.done">{{todo.text}}</span>
                </div>
            </div>
            <div class="col-xs-4"></div>
        </div>
    </div>

解释

每个模板都有自己的样式。你可以先创建一个样式表,然后使用@import来引入到模板来。我们使用@import引入了Bootstrap得样式和google的字体样式。另外还有一些定制的css样式,当然你也可以写到另一个css文件中然后引入进来。

模板中又如下两部分:

  • 供用户输入的输入框
  • 显示todo对象的区域

来看下如何创建text输入框:

    <input type="text" class="form-control" placeholder="Write something here" autofocus #newtodo (keyup)="add($event,newtodo)"/>

现在我们创建魔法般的双向数据绑定。不像Angular1.x,这里通过加入#modelname就可以实现数据绑定了。在这个例子中我们把模型newtodo绑定在了Input输入框中,输入框会和模型newtodo的数据自动同步的。同时我们想在按enter键的时候创建一个todo对象,因此我们在keyup事件上写了TodoApp#add()方法。

    (keyup)="add($event,newtodo)"

现在我们来创建一个展示todo列表的div,看下面的代码:

<div *foreach="#todo in todoStore.todoList">
   <input type="checkbox" [checked]="todo.done" (click)="toggleTodoState(todo)"/>
   <span [class.done]="todo.done">{{todo.text}}</span>
</div>

我们使用*foreach来循环todo列表,通过#todo对象来取出todoStore.todoList中每个对象。

针对每个对象,我们有一个checkbox和一个todo内容文本。内容文本通过{{todo.text}}来展示,如果该todo已经完成,则加上一个class,看下代码:[class.done]="todo.done"。当你看到[]包围着一个属性的时候,则说明这个属性值是数据绑定表达式。因此当todo.done变化的时候,done属性会被新增和移除。类似的,我们使用[]来包围着checkbox的checked属性。下一步我们要做的就是当checkbox被点击的时候要触发TodoApp#toggleTodoState()方法。

再来看TodoApp.es6文件

现在我们看下TodoApp中得两个方法:

* add()

    add($event,newtodo){
      if($event.which === 13){
        this.todoStore.add(newtodo.value);
        newtodo.value = '';
      }
    }

add方法有两个参数:$event和newtodo。该方法会每当keyup事件被触发的时候执行。在方法内部,我们做了一个是否是enter键的判断,如果是,则调用this.todoStore.add(newtodo.value)。注意,虽然我们在模板中引入的是模型newtodo,但是实际的值是存在newtodo.value中得。下一步是清除掉newtodo.value,这样输入框中得取值也会被清掉(因为双向数据绑定),我们就可以新增新的todo项了。

* toggleTodoState()

    toggleTodoState(todo){
        todo.done = !todo.done;
    }

这个方法会在checkbox被点击的时候触发,我们只是简单的toggle了todo.done属性的值。这样,因为有数据绑定,我们会看到已经完成的todo会有删除线效果(还记得.done的class样式么)。

创建index.html

最后一步是在跟目录下创建index.html来加载我们的主模块,内容如下:

    <html>
      <head>
        <title>Angular 2 Todo App</title>
        <script src="/dist/es6-shim.js"></script>
      </head>
      <body>
        <todo-app>Warming up...</todo-app>
        <script>
          System.paths = {
              'angular2/*':'/angular2/*.js',
              'rtts_assert/*': '/rtts_assert/*.js',
              'services/*' : '/services/*.js',
              'todoApp': 'TodoApp.es6'
          };
          System.import('todoApp');
        </script>
      </body>
    </html>

这里有一些注意点:

  • /dist/es6-shim.js应该包含在head标签里面,该文件包含了Traceur, ES6模块加载器, System, Zone以及Traceur的注解选项。
  • 我们的TodoApp组件的选择器指向的是 <todo-app>Warming up...</todo-app>Warming up...会在组件加载完成之前一直显示着。
  • System.paths告诉Angular到哪里去寻找模块。我们指定了属性'services/' : '/services/.js',这就是为什么Angular能够找到TodoApp.es6中TodoStore得原因。类似,我们还制定了todoApp的路径和一些其他必须得模块。
  • 最后,System.import('todoApp')加载了todoApp模块,这样就可以完成整个应用了。如果你仔细研究TodoApp.es6,你会发现最后一行bootstrap(TodoApp),它是用来帮助bootstrap来初始化模块的。

运行

到根目录下,输入http-server。你就可以通过http://localhost:8080来访问了。

注意:如果你没有http-server,你可以通过如下命令安装下:

sudo npm install -g http-server

我希望你已经很愉快的阅读了这篇文章。我对Angular2.0充满兴奋。如果你有啥想法可以直接留言或者参与devmag.io上得讨论。

参考文档:http://www.htmlxprs.com/post/54/creating-a-super-simple-todo-app-using-angular-2-tutorial