使用Ionic2开发Todo应用

 

本文使用Ionic2从头建立一个简单的Todo应用,让用户可以做以下事情:

  • 查看todo列表
  • 添加新的todo项
  • 查看todo详情
  • 保存 todo到持久化存储

0 开始之前

本教程需要你了解基本的Ionic 2概念。已经在电脑上安装了Ionic 2。如果没有,先去安装和学习吧。

1 创建新的Ionic 2工程

我们将通过生成一个基于“空白”模板的新项目开始。这是一个空的项目框架,但有一些示例代码供我们使用。

运行以下命令创建新项目

ionic start ionic-todo blank --v2

一旦代码生成,在文本编辑器打开项目。可以看到Ionic 2项目的基本结构, 这些是由Ionic CLI生成的代码。

基本上,我们的应用程序中的所有组件(我们的应用程序将由不同的组件组成)将在 src 文件夹中(包括app文件夹中的根组件和在pages文件夹中我们所有的页面组件)。一个组件将包括一个模板(.html文件),类定义(.ts文件),或者一些样式(.scss文件)。同组件类似,您还可能创建诸如服务services(如稍后我们将创建的数据服务),但没有模板和样式,但在结构上类似一个正常的组件。这些服务也被称作“providers”将被放置在一个providers文件夹。

现在,只有一个HomePage组件,设置一个虚拟视图。在我们的应用程序中我们要修改这个来显示的所有待办事项列表。

先从自动生成的**src/app/app.component.ts文件开始来看一下:

import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar } from 'ionic-native';

import { HomePage } from '../pages/home/home';

@Component({
  template: `<ion-nav [root]="rootPage"></ion-nav>`
})
export class MyApp {
  rootPage = HomePage;

  constructor(platform: Platform) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      StatusBar.styleDefault();
    });
  }
}

app.component.ts文件中定义了根组件(root component)。相比其他组件该组件是特殊的,因为它是第一个组件被加载到应用程序,从那里我们可以显示更多的组件,可以添加更多的组件等等。基本上,我们的应用程序结构就像一棵树,根组件就是树的根。

因此,重要的是我们的根组件(root component)知道在哪里可以找到我们的HomePage主页,因为需要将它设置为root page根页面。注意,我们导入(importing)HomePage 在这个文件主页的顶部,然后在下面的代码中设置它作为根页面( root page**):

rootPage: any = HomePage;

我们可以在构造函数上面声明变量,像上面这样的使其成员变量 member variables,这意味着他们可以通过引用this.myVal在整个类中被被访问,同时,它也将在您的模板中可用。 : any 只是一个TypeScript语言的内容,意味着rootPage可以是任何(any)类型。如果你不适应 TypeScript,并感到困惑,那也不用担心——你可以把类型抛开,您的应用程序仍然会工作的很好。我不会在本教程中使用类型,除了依赖注入是不可替代的地方(我们将稍后介入)。如果你想知道更多关于在Ionic 2中使用类型,应该学习TypeScript或ECMAScript 6相关知识。
root page 根页面是您应用程序显示的第一个页面,然后你可以从这里导航到其他页面。改变Ionic 2应用程序中的视图可以通过改变这一根页面,或 push 推或 pop弹出视图。推一个视图将会改变展现,弹出它将删除当前视图并回到前面的视图。关于导航的更详细的解释,我推荐看看一个相关的Ionic 2导航指南。

2. 设置主页(Home page)

现在我们已经建立了基本的应用程序,让故事开始吧。首先,让我们建立todo列表模板。

2.1 创建模板

按照下面的内容修改 src/pages/home/home.html :

<ion-header>
  <ion-navbar color="secondary">
    <ion-title>
        Todos!
    </ion-title>
    <ion-buttons end>
        <button ion-button icon-only (click)="addItem()"><ion-icon name="add-circle"></ion-icon></button>
    </ion-buttons>
  </ion-navbar>
</ion-header>
 <ion-content>
  <ion-list>
    <ion-item *ngFor="let item of items" (click)="viewItem(item)">{{item.title}}</ion-item>
  </ion-list>
</ion-content>

注意这里使用的语法在列表中使用*ngFor*,创建了一个速记到嵌入的模板中。这样就不用迭代输出了:

<ion-item *ngFor="let item of items" (click)="viewItem(item)">{{item.title}}</ion-item>

根据DOM(文档对象模型),嵌入式模板将会为每个项(items)创建特定的数据。所以,如果我们的items数组(稍后将定义在类定义)有4项,那么< ion-item >将渲染四次。还要注意,我们使用的 let item ,循环分配一个items数组项给item。这允许我们引用其属性,并传递到viewItem函数。

我们将标题设置为Todos(待办事项)!我们设计一个按钮使用< ion-buttons >。因为这里有个end属性,按钮将被放置在end的位置。不同属性的行为可能会有所不同,取决于在什么平台上运行,以iOS为例,将end会将按钮放到导航栏的右边。还要注意,按钮本身我们给它一个属性的ion-button将会使用Ionic 2 的按钮样式,而icon-only样式将会让按钮只包含一个图标没有文本。

我们使用 (click) 来附加一个点击监听器到这个元素,这里将在在home.ts中调用addItem()函数。

2.2 创建类

按如下修改src/pages/home/home.ts

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
 @Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
   public items;
   constructor(public navCtrl: NavController) {
   }
   ionViewDidLoad(){
     this.items = [
      {title: 'hi1', description: 'test1'},
      {title: 'hi2', description: 'test2'},
      {title: 'hi3', description: 'test3'}
    ];
   }
   addItem(){
   }
   viewItem(){
   }
 }

还记得大明湖畔的夏雨荷吗?哦不是,还记得之前我们如何给homePage分配一个any类型变量吗?现在我们在构造函数中分配一个NavController类型给navCtrl参数。这就是Ionic 2 的依赖注入工作模式,基本上是一种方式告诉应用程序“我们希望通过navCtrl引用到NavController”。通过添加公共关键字在它面前,它会自动创建一个成员变量。这意味着我们现在可以引用NavController通过在类里任意使用this.navCtrl。

现在我们已经建立了一些假的数据(我们使用ionViewDidLoad生命周期钩子,这将在页面加载时被触发),您应该能够看到它已经在列表中渲染了:


使用Ionic2开发Todo应用
 
Todos 列表页面


在运行 ionic serve 时,因为既然我们导入了NavController服务,我们就可以在这个组件pushpop视图,如下所示:

this.navCtrl.push(SOME_PAGE);

或者

ionic g page AddItemPage

我们已经创建了添加和查看项目的方法,在更进一步之前我们不得不先创建 AddItemPage andItemDetailPage 组件。

2.3 添加项目

我们将要创建一个新组件让我们添加新的todo项。当然,这只是一个简单的表单提供了标题描述来创建todo。

运行如下命令来生成一个add-item页面

ionic g page AddItemPage

任何时候当我们创建一个新页面,我们需要确保该页面被导入(imported)到我们的 app.module.ts,然后在entryComponents和declarations数组中被声明。

按如下修改 src/app/app.module.ts :

import { NgModule } from '@angular/core';
import { IonicApp, IonicModule } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { AddItemPage } from '../pages/add-item-page/add-item-page';
 @NgModule({
  declarations: [
    MyApp,
    HomePage,
    AddItemPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    AddItemPage
  ],
  providers: []
})
export class AppModule {}

就像上次,我们先创建组件的模版。

2.4 建立新增项目模版

按如下内容修改 src/pages/add-item-page/add-item-page.html :

<ion-header>
  <ion-toolbar color="secondary">
    <ion-title>
        Add Item
    </ion-title>
        <ion-buttons end>
            <button ion-button icon-only (click)="close()"><ion-icon name="close"></ion-icon></button>
        </ion-buttons>
    </ion-toolbar>
</ion-header>
 <ion-content>
    <ion-list>
       <ion-item>
        <ion-label floating>Title</ion-label>
        <ion-input type="text" [(ngModel)]="title"></ion-input>
      </ion-item>
       <ion-item>
        <ion-label floating>Description</ion-label>
        <ion-input type="text" [(ngModel)]="description"></ion-input>
      </ion-item>
     </ion-list>
     <button full ion-button color="secondary" (click)="saveItem()">Save</button> 
 </ion-content>

这里没有什么太疯狂的开始。这次我们定义了另一个按钮,简单地调用了定义在add-item-page.ts中的saveItem函数。我们还有另一个按钮指向一个close方法——因为这个页面作为一个Mode模式的页面,我们希望能把页面关闭,所以我们也会在add-item-page.ts定义这个方法。
现在我们有一些输入框了,它们又有[(ngModel)]属性,这个就是双向绑定。任何作用到title字段的改变都将立即影响到add-tiem-page.ts(我们马上要讲到)里面的this.title成员变量。反之亦然,任何this.title上的改变都将立即影响到模版。
同样注意到我们的保存按钮上使用了full属性,这个方便的小属性帮助我们设置按钮宽度为full。

2.5 建立添加项的类

现在我们将要建立一个类给我们的添加项组件。

按如下内容修改 add-item-page.ts

import { Component } from '@angular/core';
import { NavController, ViewController } from 'ionic-angular';
 @Component({
  selector: 'page-add-item-page',
  templateUrl: 'add-item-page.html'
})
export class AddItemPage {
   title;
  description;
   constructor(public navCtrl: NavController, public view: ViewController) {
   }
   saveItem(){
     let newItem = {
      title: this.title,
      description: this.description
    };
     this.view.dismiss(newItem);
   }
   close(){
    this.view.dismiss();
  }
 }

这里我们导入了一个怪异的服务:ViewController,可以用于模态(Modals)页面的关闭(dismiss)。

除此之外,我们创建了saveItem函数来创建newItem对象,它使用当前的标题描述值(即我们建立双向数据绑定,无论用户输入什么),然后我们关闭视图,同时我们也传入了newItem在dismiss方法中。这将允许我们建立一个侦听器,当回到主页(就是那个启动这个页面的另外一个页面)时获取数据。通过这种方式,我们可以从一个页面传递数据到另一个页面(然而,记住,模态不需要在页面之间传递数据)。

2.6 在主页保存新增项

就像我提到的,我们把要保存的数据返回发送给HomePage。我们现在导入import我们新增的AddItemPage组件到HomePage,当用户点击新增时我们就创建出该视图。

按如下内容修改 src/pages/home/home.ts :

import { Component } from '@angular/core';
import { ModalController, NavController } from 'ionic-angular';
import { AddItemPage } from '../add-item-page/add-item-page'
 @Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {
   public items = [];
   constructor(public navCtrl: NavController, public modalCtrl: ModalController) {
   }
   ionViewDidLoad(){
   }
   addItem(){
     let addModal = this.modalCtrl.create(AddItemPage);
     addModal.onDidDismiss((item) => {
           if(item){
            this.saveItem(item);
          }
     });
     addModal.present();
   }
   saveItem(item){
    this.items.push(item);
  }
   viewItem(item){
   }
 }

你看上面这个文件的顶部,可以发现我们现在导入importAddItemPage组件。这时我们就可以用这个页面创建模态页面了,具体看addItem方法。注意我们这里建立了一个onDidDismiss监听器,这样就可以获取模态关闭时回传的数据,并通过saveItem方法保存。现在,我们仅通过将数据push到items数组,最终,我们将保存到数据库。
我们已经移除了假数据,因为现在用户输入通过saveItem方法被添加到了this.items。我们将items初始为空。

2.7 查看项目

现在,我们想要一个功能,就是用户点击todo列表里面的某一项,然后可以看到该项的细节信息(例如:这里只有描述可以看了,实际可以根据需要扩展,呵呵)。要做这个我们应该知道这是又要创建一个新组件了啊。

还记得如何创建页面吗,运行下面的代码创建一个 item-detail 页面:

ionic g page ItemDetailPage

time and time again,我们需要在 app.module.ts 文件中设置一下,三件事:import,declarations, entryComponents:

import { NgModule } from '@angular/core';
import { IonicApp, IonicModule } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { AddItemPage } from '../pages/add-item-page/add-item-page';
import { ItemDetailPage } from '../pages/item-detail-page/item-detail-page';
 @NgModule({
  declarations: [
    MyApp,
    HomePage,
    AddItemPage,
    ItemDetailPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    AddItemPage,
    ItemDetailPage
  ],
  providers: []
})
export class AppModule {}

按照顺序是该写模版了,开始:

千篇一律,按照下面的内容自行修改 src/pages/item-detail-page/item-detail-page.html :

<ion-header>
  <ion-navbar color="secondary">
    <ion-title>
      {{title}}
    </ion-title>
  </ion-navbar>
</ion-header>
<ion-content>
  <ion-card>
    <ion-card-content>
      {{description}}
    </ion-card-content>
  </ion-card>
</ion-content>

相比其他模版,这里相当的直白。我们只是使用< ion-card >指令简单装饰下,并输出标题和描述,值将在item-detail-page.ts中定义。

好,继续按照下面的内容自行修改 src/pages/item-detail-page/item-detail-page.ts :

import { Component } from '@angular/core';
import { NavParams } from 'ionic-angular';
@Component({
  selector: 'page-item-detail-page',
  templateUrl: 'item-detail-page.html'
})
export class ItemDetailPage { 
  title;
  description; 
  constructor(public navParams: NavParams){ 
  } 
  ionViewDidLoad() {
    this.title = this.navParams.get('item').title;
    this.description = this.navParams.get('item').description;
  } 
}

当我们把这个页面将传入的数据项,点击,然后我们把物品的标题和描述,使用NavParams。

现在我们要做的是在home.ts 内设置 viewItem 函数和导入新的细节页面。

src/pages/home/home.ts 修改如下:

viewItem(item){
  this.navCtrl.push(ItemDetailPage, {
    item: item
  });
}

添加的导入代码放在 src/pages/home/home.ts 的顶部:

import { ItemDetailPage } from '../item-detail-page/item-detail-page';

这时就可以push出项目的细节页面,然后传入被点击的项目。如果你现在点击存在于列表中的项目,你可能看到如下界面:


使用Ionic2开发Todo应用
 
项目细节页面

3 持久化数据保存

Todo应用程序现在将基本工作,但数据没有被存储在任何地方只要你刷新应用程序你将失去你所有的数据(不理想)。

现在我们要做的是创建一个服务被称为Data用来处理存储和检索数据。我们将使用Ionic 2提供的Stroage服务来帮助我们做到这一点。Stroage服务是Ionic 2的通用存储服务,它负责存储数据的最佳方式,同时提供了一致的API供我们使用。

这意味着,如果您正在设备上运行,安装了SQLite插件,那么它将使用一个本地SQLite数据库进行存储,否则它将退回到使用基于浏览器的存储(可能被操作系统擦除)。

运行下面代码创建服务

ionic g provider Data

data.ts 代码修改如下:

import { Storage } from '@ionic/storage';
import {Injectable} from '@angular/core'; 
@Injectable()
export class Data { 
  constructor(public storage: Storage){ 
  } 
  getData() {
    return this.storage.get('todos');  
  } 
  save(data){
    let newData = JSON.stringify(data);
    this.storage.set('todos', newData);
  } 
}

这个是有点不同于我们已经创建的组件(它可能更合适认为是service)。我们不使用@component装饰,而使用@Injectable声明这个类。

在构造函数中,我们建立一个 Storage 服务的引用。

数组中save函数简单地将所有的项放入数组并保存到存储,每当项目变化我们将调用这个函数。

我们还将需要设置的Storage服务,以及 Data provider,在我们 app.module.ts 文件。

src/app/app.module.ts 修改如下:

import { NgModule } from '@angular/core';
import { IonicApp, IonicModule } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { AddItemPage } from '../pages/add-item-page/add-item-page';
import { ItemDetailPage } from '../pages/item-detail-page/item-detail-page';
import { Storage } from '@ionic/storage';
import { Data } from '../providers/data'; 
@NgModule({
  declarations: [
    MyApp,
    HomePage,
    AddItemPage,
    ItemDetailPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    AddItemPage,
    ItemDetailPage
  ],
  providers: [Storage, Data]
})
export class AppModule {}

请注意,我们已经声明这些在providers的数组,而不是declarationsentryComponents数组。

现在我们需要更新。ts使用这项新服务。

src/pages/home/home.ts 文件修改如下:

import { Component } from '@angular/core';
import { ModalController, NavController } from 'ionic-angular';
import { AddItemPage } from '../add-item-page/add-item-page'
import { ItemDetailPage } from '../item-detail-page/item-detail-page';
import { Data } from '../../providers/data'; 
@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage { 
  public items = []; 
  constructor(public navCtrl: NavController, public modalCtrl: ModalController, public dataService: Data) { 
    this.dataService.getData().then((todos) => { 
      if(todos){
        this.items = JSON.parse(todos); 
      } 
    }); 
  } 
  ionViewDidLoad(){ 
  } 
  addItem(){ 
    let addModal = this.modalCtrl.create(AddItemPage); 
    addModal.onDidDismiss((item) => { 
          if(item){
            this.saveItem(item);
          } 
    }); 
    addModal.present(); 
  } 
  saveItem(item){
    this.items.push(item);
    this.dataService.save(this.items);
  } 
  viewItem(item){
    this.navCtrl.push(ItemDetailPage, {
      item: item
    });
  } 
}

这是我们最后的一些代码。再次,我们importing数据服务,通过传递给构造函数。我们依然设置 items 开始是空的,使用数据服务获取数据。
重要的是要注意getData 返回promise而不是数据本身。抓取的数据存储是异步的,这意味着我们的应用程序将继续运行当数据加载时。promise让我们数据完成加载时执行一些操作,而不需要暂停整个应用程序。

最后,我们还添加一个调用save 函数保存在数据服务当一个新的条目被添加。现在该函数将马上更新我们的新数据条目数组,但items也将被复制保存到数据服务,以便下次我们回到应用程序是可用。

4 总结

在本教程中我们已经介绍了如何实现很多Ionic 2应用的常用功能:

  • 创建视图
  • 监听和处理事件
  • 视图之间的导航
  • 在视图之间传递数据
  • 建立双向数据绑定
  • 保存数据

显然还有很多我们可以做,使这个应用程序更漂亮,添加删除和编辑笔记的能力等等。

相关推荐