Skip to content

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

jiyanliang edited this page Apr 7, 2015 · 19 revisions

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

Angular 2 is already popular and so many JavaScript developers are eagerly waiting to try it out. You may have the idea that Angular 2 is going to introduce a lot of new & exciting features and remove several old concepts. So, in this tutorial I will show you how to create a super simple Todo app using Angular 2.0. In the process you will also learn how to use Components, Templates, Data Binding and a few other important stuff. So, let's start!

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

Angular 2 is currently in Alpha preview and changing rapidly. Keep an eye on quickstart app for latest changes.

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

Here is a demo for you to check out. The code is also available on GitHub. If you have any questions please comment or visit devmag.io to participate in the discussion.

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

Getting the Seed App

获得种子应用

Angular team has put together a simple quick start app that provides you with all the dependencies required to write Angular 2 apps. So, head over to GitHub and download the repo to your machine.

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

Now you can open the app in your favourite text editor and start modifying it. The name of the root directory is quickstart. So, let's rename it to todoApp.

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

Creating Components

创建组件

In Angular 1.x we have directives. In Angular 2 we will create Components. Every component has two parts : View and Controller. View is the HTML template of your component while Controller provides JavaScript behaviour. You might have heard that Angular 2 doesn't have controllers anymore. Well, this is not exactly true as controllers are now a part of components. So, how does a component look like? Well, let's find it out by creating a component for our todo app.

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

Go the root directory and create a file called TodoApp.es6. The extension .es6 means that we are going to follow ES6 syntax here. You can also use the plain old extension .js. Now paste the following content into the file.

找到根目录创建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);

Explanation

解释

The first two statements are used to import ES6 modules. The very first line imports Component, Template, bootstrap and Foreach from the modules angular2/angular2. The second statement imports a custom service TodoStore from the module services/TodoStore. Don't worry about how Angular finds these modules for the time being.

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

Now, we need to create the controller for our component. Let's name it TodoApp and create the class as following :

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

class TodoApp{

}

It's a usual ES6 class with several annotations such as :

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

@Component : This signifies that TodoApp is a component. It accepts an object. The selector property indicates what should be the HTML selector for this component. componentServices indicates the services our component depends on. In our case there is only one dependency TodoStore, which is going to be created shortly.

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

@Template : This annotation specifies the template that should be used by our component. In our case it's templates/todo.html. So, we make the url property point to the template location. Similarly, directives property indicates the directives required by our template. We are going to need a single directive ForEach for our template. We will just specify this in directives property.

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

Now we will add a constructor to our TodoApp class as following :

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

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

The parameter todoStore: TodoStore tells Angular's DI system to inject an instance of the service TodoStore into our component during initialization. This is called IoC (Inversion of Control). Instead of obtaining our dependencies ourselves, we ask Angular to automatically instantiate the dependencies and inject them into our component. So, we take the instance of TodoStore and store it in an instance variable todoStore.

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

We will discuss about the methods add() and toggleTodoState() subsequently. Now let's take a look at our service TodoStore.

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

Creating the Service

创建服务

Let's create a file called TodoStore.js inside the directory services. Now paste the following content into the file :

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

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

Explanation

解释

TodoStore is a simple JavaScript class. It has a constructor and a single method add(). The constructor creates a simple array todoList which holds our todo items. We use this list in our template to render the todo items. The add() method is called everytime a new todo item is about to be created. It takes the todo text, creates an object and adds it to the todoList array. Note that done property indicates if the todo item is marked as done. Finally, we export the class with the help of export keyword.

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

Now go back to TodoApp.es6 and see how we are importing the above class.

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

Creating the template Let's create a file todo.html inside the directory templates. Here is the content of the file :

创建模板

在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>

Explanation

解释

Every template has its own style. You can basically create stylesheets and refer to them by using @import statements inside the templates. We are importing Bootstrap's CSS and Google Fonts via @import. We also have some custom CSS rules which are written inside <style> element. You can also have a different CSS file for this and import it here.

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

There are two parts in our template which are :

模板中又如下两部分:

Text Field which takes input from users. An area which shows a list of todo items.

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

We create the text field as following :

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

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

Now let's create our favourite magical two way data binding. Unlike Angular 1.x, you create data binding by writing #modelname. In this case we would like to bind the input field with a model newtodo. So, we add the attribute #newtodo to the input element. As a result whenever you write something into the field, it's automatically synced with the newtodo model. Also we would like to create a todo item when enter key is pressed. So, we will write the following to run a function TodoApp#add() on keyup event :

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

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

Now we need to create a div for showing a list of todo items. This is done as following :

现在我们来创建一个展示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>

We use *foreach to loop through the set of todo items. We select each item by writing #todo in todoStore.todoList.

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

For each item, we have a checkbox (to mark the item as done) and a todo text. The text is displayed by the expression {{todo.text}}. Also we apply a class called done when the done property is true. This strikes out the items that are completed. We do it by writing [class.done]="todo.done". When you see an attribute surrounded with [], it means the attribute value is a data binding expression. So, whenever todo.done changes, the class done is added/removed. Similarly, we also bind the checked property of the checkbox with todo.done by surrounding it with []. Next, we want to invoke the function TodoApp#toggleTodoState() whenever the checkbox is clicked.

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

Revisiting TodoApp.es6 Now, let's discuss about the two functions in TodoApp that we left intentionally earlier.

再来看TodoApp.es6文件

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

  • add()

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

The add function has two parameters : $event and newtodo. This is called every time keyup event is fired by the input field in our template. Inside the function we check whether enter key is pressed. If yes then we call this.todoStore.add() and pass newtodo.value as an argument. Note that although we refer to the model newtodo in our template with #newtodo, the actual value is stored in newtodo.value. The next step is clearing newtodo.value so that the input field is also cleared (due to two way data binding) and we can start adding new items.

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

  • toggleTodoState()

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

This is called whenever click event is fired by a checkbox. We simply toggle the value of todo.done property. As a result, data binding kicks in and we see a strikethrough effect in the corresponding todo item (Remember the .done class in template?).

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

Creating index.html The finally step is creating index.html in the root and loading our main module. Here is the content :

创建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>

You should note a few points here :

这里有一些注意点:

/dist/es6-shim.js should be loaded into the

. It contains Traceur, ES6 Module Loader, System, Zone, and Traceur options for meta-data annotations. We refer to our main component TodoApp with the selector Warming up.... The text Warming up is displayed while the module is loading. System.paths tells Angular where to look for modules. We have specified a property 'services/' : '/services/.js'. That's why Angular is able to find the module TodoStore when we say import {TodoStore} from 'services/TodoStore' inside TodoApp.es6. Similarly, we have specified the path for todoApp and other required modules. Finally, the statement System.import('todoApp') loads the module todoApp and kicks off the application. If you look into the file TodoApp.es6, you will see a line bootstrap(TodoApp) in the end. It helps bootstrap and initialize the module. Running the app Go to the root directory and type http-server. Now you can access the app at http://localhost:8080.

/dist/es6-shim.js应该包含在head标签里面,该文件包含了Traceur, ES6模块加载器, System, Zone以及Traceur的注解选项。 我们的TodoApp组件的选择器指向的是 Warming up...,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来访问了。

Note : If you don't have http-server, you can install it with the following command :

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

sudo npm install -g http-server

I hope you enjoyed reading this article. I am as excited as you about Angular 2.0. Let us know what you think in comments or discuss about it on devmag.io.

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

Clone this wiki locally