Angular 從 0 到 1 (三)史上最簡單的 Angular 教程

接灰的電子產品發表於2016-12-26

第一節:初識Angular-CLI
第二節:登入元件的構建
第三節:建立一個待辦事項應用
第四節:進化!模組化你的應用
第五節:多使用者版本的待辦事項應用
第六節:使用第三方樣式庫及模組優化用
第七節:給元件帶來活力
Rx--隱藏在Angular 2.x中利劍
Redux你的Angular 2應用
第八節:查缺補漏大合集(上)
第九節:查缺補漏大合集(下)

第三節:建立一個待辦事項應用

這一章我們會建立一個更復雜的待辦事項應用,當然我們的登入功能也還保留,這樣的話我們的應用就有了多個相對獨立的功能模組。以往的web應用根據不同的功能跳轉到不同的功能頁面。但目前前端的趨勢是開發一個SPA(Single Page Application 單頁應用),所以其實我們應該把這種跳轉叫檢視切換:根據不同的路徑顯示不同的元件。那我們怎麼處理這種檢視切換呢?幸運的是,我們無需尋找第三方元件,Angular官方內建了自己的路由模組。

建立routing的步驟

由於我們要以路由形式顯示元件,建立路由前,讓我們先把src\app\app.component.html中的<app-login></app-login>刪掉。

  • 第一步:在src/index.html中指定基準路徑,即在<header>中加入<base href="/">,這個是指向你的index.html所在的路徑,瀏覽器也會根據這個路徑下載css,影象和js檔案,所以請將這個語句放在header的最頂端。
  • 第二步:在src/app/app.module.ts中引入RouterModule:import { RouterModule } from '@angular/router';
  • 第三步:定義和配置路由陣列,我們暫時只為login來定義路由,仍然在src/app/app.module.ts中的imports中

    imports: [
      BrowserModule,
      FormsModule,
      HttpModule,
      RouterModule.forRoot([
        {
          path: 'login',
          component: LoginComponent
        }
      ])
    ],複製程式碼

    注意到這個形式和其他的比如BrowserModule、FormModule和HTTPModule表現形式好像不太一樣,這裡解釋一下,forRoot其實是一個靜態的工廠方法,它返回的仍然是Module,下面的是Angular API文件給出的RouterModule.forRoot的定義。

    forRoot(routes: Routes, config?: ExtraOptions) : ModuleWithProviders複製程式碼

    為什麼叫forRoot呢?因為這個路由定義是應用在應用根部的,你可能猜到了還有一個工廠方法叫forChild,後面我們會詳細講。接下來我們看一下forRoot接收的引數,引數看起來是一個陣列,每個陣列元素是一個{path: 'xxx', component: XXXComponent}這個樣子的物件。這個陣列就叫做路由定義(RouteConfig)陣列,每個陣列元素就叫路由定義,目前我們只有一個路由定義。路由定義這個物件包括若干屬性:

    • path:路由器會用它來匹配路由中指定的路徑和瀏覽器位址列中的當前路徑,如 /login 。
    • component:導航到此路由時,路由器需要建立的元件,如 LoginComponent
    • redirectTo:重定向到某個path,使用場景的話,比如在使用者輸入不存在的路徑時重定向到首頁。
    • pathMatch:路徑的字元匹配策略
    • children:子路由陣列
      執行一下,我們會發現出錯了
      Angular 從 0 到 1 (三)史上最簡單的 Angular 教程
      image_1b0hgdsiu87n1lha1kcahl51ckb9.png-233.2kB

      這個錯誤看上去應該是對於''沒有找到匹配的route,這是由於我們只定義了一個'login',我們再試試在瀏覽器位址列輸入:http://localhost:4200/login。這次仍然出錯,但錯誤資訊變成了下面的樣子,意思是我們沒有找到一個outlet去載入LoginComponent。對的,這就引出了router outlet的概念,如果要顯示對應路由的元件,我們需要一個插頭(outlet)來裝載元件。
      error_handler.js:48EXCEPTION: Uncaught (in promise): Error: Cannot find primary outlet to load 'LoginComponent'
      Error: Cannot find primary outlet to load 'LoginComponent'
      at getOutlet (http://localhost:4200/main.bundle.js:66161:19)
      at ActivateRoutes.activateRoutes (http://localhost:4200/main.bundle.js:66088:30)
      at http://localhost:4200/main.bundle.js:66052:19
      at Array.forEach (native)
      at ActivateRoutes.activateChildRoutes (http://localhost:4200/main.bundle.js:66051:29)
      at ActivateRoutes.activate (http://localhost:4200/main.bundle.js:66046:14)
      at http://localhost:4200/main.bundle.js:65787:56
      at SafeSubscriber._next (http://localhost:4200/main.bundle.js:9000:21)
      at SafeSubscriber.__tryOrSetError (http://localhost:4200/main.bundle.js:42013:16)
      at SafeSubscriber.next (http://localhost:4200/main.bundle.js:41955:27)複製程式碼
      下面我們把<router-outlet></router-outlet>寫在src\app\app.component.html的末尾,位址列輸入http://localhost:4200/login重新看看瀏覽器中的效果吧,我們的應用應該正常顯示了。但如果輸入http://localhost:4200時仍然是有異常出現的,我們需要新增一個路由定義來處理。輸入http://localhost:4200時相對於根路徑的path應該是空,即''。而我們這時希望將使用者仍然引導到登入頁面,這就是redirectTo: 'login'的作用。pathMatch: 'full'的意思是必須完全符合路徑的要求,也就是說http://localhost:4200/1是不會匹配到這個規則的,必須嚴格是http://localhost:4200
      RouterModule.forRoot([
        {
          path: '',
          redirectTo: 'login',
          pathMatch: 'full'
        },
        {
          path: 'login',
          component: LoginComponent
        }
      ])複製程式碼
      注意路徑配置的順序是非常重要的,Angular2使用“先匹配優先”的原則,也就是說如果一個路徑可以同時匹配幾個路徑配置的規則的話,以第一個匹配的規則為準。

但是現在還有一點小不爽,就是直接在app.modules.ts中定義路徑並不是很好的方式,因為隨著路徑定義的複雜,這部分最好還是用單獨的檔案來定義。現在我們新建一個檔案src\app\app.routes.ts,將上面在app.modules.ts中定義的路徑刪除並在app.routes.ts中重新定義。

import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';

export const routes: Routes = [
  {
    path: '',
    redirectTo: 'login',
    pathMatch: 'full'
  },
  {
    path: 'login',
    component: LoginComponent
  }
];

export const routing = RouterModule.forRoot(routes);複製程式碼

接下來我們在app.modules.ts中引入routing,import { routing } from './app.routes';,然後在imports陣列裡新增routing,現在我們的app.modules.ts看起來是下面這個樣子。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { AuthService } from './core/auth.service';
import { routing } from './app.routes';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    routing
  ],
  providers: [
    {provide: 'auth',  useClass: AuthService}
    ],
  bootstrap: [AppComponent]
})
export class AppModule { }複製程式碼

讓待辦事項變得有意義

現在我們來規劃一下根路徑'',對應根路徑我們想建立一個todo元件,那麼我們使用ng g c todo來生成元件,然後在app.routes.ts中加入路由定義,對於根路徑我們不再需要重定向到登入了,我們把它改寫成重定向到todo。

export const routes: Routes = [
  {
    path: '',
    redirectTo: 'todo',
    pathMatch: 'full'
  },
  {
    path: 'todo',
    component: TodoComponent
  },
  {
    path: 'login',
    component: LoginComponent
  }
];複製程式碼

在瀏覽器中鍵入http://localhost:4200可以看到自動跳轉到了todo路徑,並且我們的todo元件也顯示出來了。

Angular 從 0 到 1 (三)史上最簡單的 Angular 教程
image_1b0k2ba0d1qqraa51mj51hpdpeo9.png-81kB

我們希望的Todo頁面應該有一個輸入待辦事項的輸入框和一個顯示待辦事項狀態的列表。那麼我們先來定義一下todo的結構,todo應該有一個id用來唯一標識,還應該有一個desc用來描述這個todo是幹什麼的,再有一個completed用來標識是否已經完成。好了,我們來建立這個todo模型吧,在todo資料夾下新建一個檔案todo.model.ts

export class Todo {
  id: number;
  desc: string;
  completed: boolean;
}複製程式碼

然後我們應該改造一下todo元件了,引入剛剛建立好的todo物件,並且建立一個todos陣列作為所有todo的集合,一個desc是當前新增的新的todo的內容。當然我們還需要一個addTodo方法把新的todo加到todos陣列中。這裡我們暫且寫一個漏洞百出的版本。

import { Component, OnInit } from '@angular/core';
import { Todo } from './todo.model';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css']
})
export class TodoComponent implements OnInit {
  todos: Todo[] = [];
  desc = '';
  constructor() { }

  ngOnInit() {
  }

  addTodo(){
    this.todos.push({id: 1, desc: this.desc, completed: false});
    this.desc = '';
  }
}複製程式碼

然後我們改造一下src\app\todo\todo.component.html

<div>
  <input type="text" [(ngModel)]="desc" (keyup.enter)="addTodo()">
  <ul>
    <li *ngFor="let todo of todos">{{ todo.desc }}</li>
  </ul>
</div>複製程式碼

如上面程式碼所示,我們建立了一個文字輸入框,這個輸入框的值應該是新todo的描述(desc),我們想在使用者按了Enter鍵後進行新增操作((keyup.enter)="addTodo())。由於todos是個陣列,所以我們利用一個迴圈將陣列內容顯示出來(<li *ngFor="let todo of todos">{{ todo.desc }}</li>)。好了讓我們欣賞一下成果吧

Angular 從 0 到 1 (三)史上最簡單的 Angular 教程
image_1b0kgg9mnppf16pkip81b2hhbrm.png-90.1kB

如果我們還記得之前提到的業務邏輯應該放在單獨的service中,我們還可以做的更好一些。在todo資料夾內建立TodoService:ng g s todo\todo。上面的例子中所有建立的todo都是id為1的,這顯然是一個大bug,我們看一下怎麼處理。常見的不重複id建立方式有兩種,一個是搞一個自增長數列,另一個是採用隨機生成一組不可能重複的字元序列,常見的就是UUID了。我們來引入一個uuid的包:npm i --save angular2-uuid,由於這個包中已經含有了用於typescript的定義檔案,這裡就執行這一個命令就足夠了。由於此時Todo物件的id已經是字元型了,請更改其宣告為id: string;
然後修改service成下面的樣子:

import { Injectable } from '@angular/core';
import {Todo} from './todo.model';
import { UUID } from 'angular2-uuid';

@Injectable()
export class TodoService {

  todos: Todo[] = [];

  constructor() { }

  addTodo(todoItem:string): Todo[] {
    let todo = {
      id: UUID.UUID(),
      desc: todoItem,
      completed: false
    };
    this.todos.push(todo);
    return this.todos;
  }
}複製程式碼

當然我們還要把元件中的程式碼改成使用service的

import { Component, OnInit } from '@angular/core';
import { Todo } from './todo.model';
import { TodoService } from './todo.service';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css'],
  providers:[TodoService]
})
export class TodoComponent implements OnInit {
  todos: Todo[] = [];
  desc = '';
  constructor(private service:TodoService) { }

  ngOnInit() {
  }

  addTodo(){
    this.todos = this.service.addTodo(this.desc);
    this.desc = '';
  }
}複製程式碼

為了可以清晰的看到我們的成果,我們為chrome瀏覽器裝一個外掛,在chrome的位址列中輸入chrome://extensions,拉到最底部會看到一個“獲取更多擴充套件程式”的連結,點選這個連結然後搜尋“Angury”,安裝即可。安裝好後,按F12調出開發者工具,裡面出現一個叫“Angury”的tab。

Angular 從 0 到 1 (三)史上最簡單的 Angular 教程
image_1b0kr7gpn17td7v1p4s1qucuu313.png-273.8kB

我們可以看到id這時候被設定成了一串字元,這個就是UUID了。

建立模擬web服務和非同步操作

實際的開發中我們的service是要和伺服器api進行互動的,而不是現在這樣簡單的運算元組。但問題來了,現在沒有web服務啊,難道真要自己開發一個嗎?答案是可以做個假的,假作真時真亦假。我們在開發過程中經常會遇到這類問題,等待後端同學的進度是很痛苦的。所以Angular內建提供了一個可以快速建立測試用web服務的方法:記憶體 (in-memory) 伺服器。

一般來說,你需要知道自己對伺服器的期望是什麼,期待它返回什麼樣的資料,有了這個資料呢,我們就可以自己快速的建立一個記憶體伺服器了。拿這個例子來看,我們可能需要一個這樣的物件

class Todo {
  id: string;
  desc: string;
  completed: boolean;
}複製程式碼

對應的JSON應該是這樣的

{
  "data": [
    {
      "id": "f823b191-7799-438d-8d78-fcb1e468fc78",
      "desc": "blablabla",
      "completed": false
    },
    {
      "id": "c316a3bf-b053-71f9-18a3-0073c7ee3b76",
      "desc": "tetssts",
      "completed": false
    },
    {
      "id": "dd65a7c0-e24f-6c66-862e-0999ea504ca0",
      "desc": "getting up",
      "completed": false
    }
  ]
}複製程式碼

首先我們需要安裝angular-in-memory-web-api,輸入npm install --save angular-in-memory-web-api
然後在Todo資料夾下建立一個檔案src\app\todo\todo-data.ts

import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Todo } from './todo.model';

export class InMemoryTodoDbService implements InMemoryDbService {
  createDb() {
    let todos: Todo[] = [
      {id: "f823b191-7799-438d-8d78-fcb1e468fc78", desc: 'Getting up', completed: true},
      {id: "c316a3bf-b053-71f9-18a3-0073c7ee3b76", desc: 'Go to school', completed: false}
    ];
    return {todos};
  }
}複製程式碼

可以看到,我們建立了一個實現InMemoryDbService的記憶體資料庫,這個資料庫其實也就是把陣列傳入進去。接下來我們要更改src\app\app.module.ts,加入類引用和對應的模組宣告:

import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryTodoDbService } from './todo/todo-data';複製程式碼

然後在imports陣列中緊挨著HttpModule加上InMemoryWebApiModule.forRoot(InMemoryTodoDbService),

現在我們在service中試著呼叫我們的“假web服務”吧

import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { UUID } from 'angular2-uuid';

import 'rxjs/add/operator/toPromise';

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

@Injectable()
export class TodoService {

  //定義你的假WebAPI地址,這個定義成什麼都無所謂
  //只要確保是無法訪問的地址就好
  private api_url = 'api/todos';
  private headers = new Headers({'Content-Type': 'application/json'});

  constructor(private http: Http) { }

  // POST /todos
  addTodo(desc:string): Promise<Todo> {
    let todo = {
      id: UUID.UUID(),
      desc: desc,
      completed: false
    };
    return this.http
            .post(this.api_url, JSON.stringify(todo), {headers: this.headers})
            .toPromise()
            .then(res => res.json().data as Todo)
            .catch(this.handleError);
  }

  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error); 
    return Promise.reject(error.message || error);
  }
}複製程式碼

上面的程式碼我們看到定義了一個 api_url = 'api/todos',你可能會問這個是怎麼來的?分兩部分看,api/todos 中前面的api定義成什麼都可以,但後面這個todos是有講究的,我們回去看一下 src\app\todo\todo-data.ts 返回的 return {todos} ,這個其實是 return {todos: todos} 的省略表示形式,如果我們不想讓這個後半部分是todos,我們可以寫成{nahnahnah: todos}。這樣的話我們改寫成 api_url = 'blablabla/nahnahnah' 也無所謂,因為這個記憶體Web服務的機理是攔截Web訪問,也就是說隨便什麼地址都可以,記憶體Web服務會攔截這個地址並解析你的請求是否滿足RESTful API的要求

簡單來說RESTful API中GET請求用於查詢,PUT用於更新,DELETE用於刪除,POST用於新增。比如如果url是api/todos,那麼

  • 查詢所有待辦事項:以GET方法訪問api/todos
  • 查詢單個待辦事項:以GET方法訪問api/todos/id,比如id是1,那麼訪問api/todos/1
  • 更新某個待辦事項:以PUT方法訪問api/todos/id
  • 刪除某個待辦事項:以DELETE方法訪問api/todos/id
  • 增加一個待辦事項:以POST方法訪問api/todos

在service的建構函式中我們注入了Http,而angular的Http封裝了大部分我們需要的方法,比如例子中的增加一個todo,我們就呼叫this.http.post(url, body, options),上面程式碼中的.post(this.api_url, JSON.stringify(todo), {headers: this.headers})含義是:構造一個POST型別的HTTP請求,其訪問的url是this.api_url,request的body是一個JSON(把todo物件轉換成JSON),在引數配置中我們配置了request的header。

這個請求發出後返回的是一個Observable(可觀察物件),我們把它轉換成Promise然後處理res(Http Response)。Promise提供非同步的處理,注意到then中的寫法,這個和我們傳統程式設計寫法不大一樣,叫做lamda表示式,相當於是一個匿名函式,(input parameters) => expression=>前面的是函式的引數,後面的是函式體。

還要一點需要強調的是:在用記憶體Web服務時,一定要注意res.json().data中的data屬性必須要有,因為記憶體web服務坑爹的在返回的json中加了data物件,你真正要得到的json是在這個data裡面。

下一步我們來更改Todo元件的addTodo方法以便可以使用我們新的非同步http方法

  addTodo(){
    this.service
      .addTodo(this.desc)
      .then(todo => {
        this.todos = [...this.todos, todo];
        this.desc = '';
      });
  }複製程式碼

這裡面的前半部分應該還是好理解的:this.service.addTodo(this.desc)是呼叫service的對應方法而已,但後半部分是什麼鬼?...這個貌似省略號的東東是ES7中計劃提供的Object Spread操作符,它的功能是將物件或陣列“打散,拍平”。這麼說可能還是不懂,舉例子吧:

let arr = [1,2,3];
let arr2 = [...arr]; 
arr2.push(4); 

// arr2 變成了 [1,2,3,4]
// arr 儲存原來的樣子

let arr3 = [0, 1, 2];
let arr4 = [3, 4, 5];
arr3.push(...arr4);
// arr3變成了[0, 1, 2, 3, 4, 5]

let arr5 = [0, 1, 2];
let arr6 = [-1, ...arr5, 3];
// arr6 變成了[-1, 0, 1, 2, 3]複製程式碼

所以呢我們上面的this.todos = [...this.todos, todo];相當於為todos增加一個新元素,和push很像,那為什麼不用push呢?因為這樣構造出來的物件是全新的,而不是引用的,在現代程式設計中一個明顯的趨勢是不要在過程中改變輸入的引數。第二個原因是這樣做會帶給我們極大的便利性和程式設計的一致性。下面通過給我們的例子新增幾個功能,我們來一起體會一下。
首先更改src\app\todo\todo.service.ts

//src\app\todo\todo.service.ts
import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { UUID } from 'angular2-uuid';

import 'rxjs/add/operator/toPromise';

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

@Injectable()
export class TodoService {

  private api_url = 'api/todos';
  private headers = new Headers({'Content-Type': 'application/json'});
  constructor(private http: Http) { }
  // POST /todos
  addTodo(desc:string): Promise<Todo> {
    let todo = {
      id: UUID.UUID(),
      desc: desc,
      completed: false
    };
    return this.http
            .post(this.api_url, JSON.stringify(todo), {headers: this.headers})
            .toPromise()
            .then(res => res.json().data as Todo)
            .catch(this.handleError);
  }
  // PUT /todos/:id
  toggleTodo(todo: Todo): Promise<Todo> {
    const url = `${this.api_url}/${todo.id}`;
    console.log(url);
    let updatedTodo = Object.assign({}, todo, {completed: !todo.completed});
    return this.http
            .put(url, JSON.stringify(updatedTodo), {headers: this.headers})
            .toPromise()
            .then(() => updatedTodo)
            .catch(this.handleError);
  }
  // DELETE /todos/:id
  deleteTodoById(id: string): Promise<void> {
    const url = `${this.api_url}/${id}`;
    return this.http
            .delete(url, {headers: this.headers})
            .toPromise()
            .then(() => null)
            .catch(this.handleError);
  }
  // GET /todos
  getTodos(): Promise<Todo[]>{
    return this.http.get(this.api_url)
              .toPromise()
              .then(res => res.json().data as Todo[])
              .catch(this.handleError);
  }
  private handleError(error: any): Promise<any> {
    console.error('An error occurred', error); 
    return Promise.reject(error.message || error);
  }
}複製程式碼

然後更新src\app\todo\todo.component.ts

import { Component, OnInit } from '@angular/core';
import { TodoService } from './todo.service';
import { Todo } from './todo.model';

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css'],
  providers: [TodoService]
})
export class TodoComponent implements OnInit {
  todos : Todo[] = [];
  desc: string = '';

  constructor(private service: TodoService) {}
  ngOnInit() {
    this.getTodos();
  }
  addTodo(){
    this.service
      .addTodo(this.desc)
      .then(todo => {
        this.todos = [...this.todos, todo];
        this.desc = '';
      });
  }
  toggleTodo(todo: Todo) {
    const i = this.todos.indexOf(todo);
    this.service
      .toggleTodo(todo)
      .then(t => {
        this.todos = [
          ...this.todos.slice(0,i),
          t,
          ...this.todos.slice(i+1)
          ];
      });
  }
  removeTodo(todo: Todo) {
    const i = this.todos.indexOf(todo);
    this.service
      .deleteTodoById(todo.id)
      .then(()=> {
        this.todos = [
          ...this.todos.slice(0,i),
          ...this.todos.slice(i+1)
        ];
      });
  }
  getTodos(): void {
    this.service
      .getTodos()
      .then(todos => this.todos = [...todos]);
  }
}複製程式碼

更新模板檔案src\app\todo\todo.component.html

<section class="todoapp">
  <header class="header">
    <h1>Todos</h1>
    <input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="desc" (keyup.enter)="addTodo()">
  </header>
  <section class="main" *ngIf="todos?.length > 0">
    <input class="toggle-all" type="checkbox">
    <ul class="todo-list">
      <li *ngFor="let todo of todos" [class.completed]="todo.completed">
        <div class="view">
          <input class="toggle" type="checkbox" (click)="toggleTodo(todo)" [checked]="todo.completed">
          <label (click)="toggleTodo(todo)">{{todo.desc}}</label>
          <button class="destroy" (click)="removeTodo(todo); $event.stopPropagation()"></button>
        </div>
      </li>
    </ul>
  </section>
  <footer class="footer" *ngIf="todos?.length > 0">
    <span class="todo-count">
      <strong>{{todos?.length}}</strong> {{todos?.length == 1 ? 'item' : 'items'}} left
    </span>
    <ul class="filters">
      <li><a href="">All</a></li>
      <li><a href="">Active</a></li>
      <li><a href="">Completed</a></li>
    </ul>
    <button class="clear-completed">Clear completed</button>
  </footer>
</section複製程式碼

更新元件的css樣式:src\app\todo\todo.component.css

.todoapp {
    background: #fff;
    margin: 130px 0 40px 0;
    position: relative;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
                0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}
.todoapp input::input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
}
.todoapp h1 {
    position: absolute;
    top: -155px;
    width: 100%;
    font-size: 100px;
    font-weight: 100;
    text-align: center;
    color: rgba(175, 47, 47, 0.15);
    -webkit-text-rendering: optimizeLegibility;
    -moz-text-rendering: optimizeLegibility;
    text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
    position: relative;
    margin: 0;
    width: 100%;
    font-size: 24px;
    font-family: inherit;
    font-weight: inherit;
    line-height: 1.4em;
    border: 0;
    color: inherit;
    padding: 6px;
    border: 1px solid #999;
    box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}
.new-todo {
    padding: 16px 16px 16px 60px;
    border: none;
    background: rgba(0, 0, 0, 0.003);
    box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
    position: relative;
    z-index: 2;
    border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
    display: none;
}
.toggle-all {
    position: absolute;
    top: -55px;
    left: -12px;
    width: 60px;
    height: 34px;
    text-align: center;
    border: none; /* Mobile Safari */
}
.toggle-all:before {
    content: '❯';
    font-size: 22px;
    color: #e6e6e6;
    padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
    color: #737373;
}
.todo-list {
    margin: 0;
    padding: 0;
    list-style: none;
}
.todo-list li {
    position: relative;
    font-size: 24px;
    border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
    border-bottom: none;
}
.todo-list li.editing {
    border-bottom: none;
    padding: 0;
}
.todo-list li.editing .edit {
    display: block;
    width: 506px;
    padding: 12px 16px;
    margin: 0 0 0 43px;
}
.todo-list li.editing .view {
    display: none;
}
.todo-list li .toggle {
    text-align: center;
    width: 40px;
    /* auto, since non-WebKit browsers doesn't support input styling */
    height: auto;
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    border: none; /* Mobile Safari */
    -webkit-appearance: none;
    appearance: none;
}
.todo-list li .toggle:after {
    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
    content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
    word-break: break-all;
    padding: 15px 60px 15px 15px;
    margin-left: 45px;
    display: block;
    line-height: 1.2;
    transition: color 0.4s;
}
.todo-list li.completed label {
    color: #d9d9d9;
    text-decoration: line-through;
}
.todo-list li .destroy {
    display: none;
    position: absolute;
    top: 0;
    right: 10px;
    bottom: 0;
    width: 40px;
    height: 40px;
    margin: auto 0;
    font-size: 30px;
    color: #cc9a9a;
    margin-bottom: 11px;
    transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
    color: #af5b5e;
}
.todo-list li .destroy:after {
    content: '×';
}
.todo-list li:hover .destroy {
    display: block;
}
.todo-list li .edit {
    display: none;
}
.todo-list li.editing:last-child {
    margin-bottom: -1px;
}
.footer {
    color: #777;
    padding: 10px 15px;
    height: 20px;
    text-align: center;
    border-top: 1px solid #e6e6e6;
}
.footer:before {
    content: '';
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 50px;
    overflow: hidden;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
                0 8px 0 -3px #f6f6f6,
                0 9px 1px -3px rgba(0, 0, 0, 0.2),
                0 16px 0 -6px #f6f6f6,
                0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
    float: left;
    text-align: left;
}
.todo-count strong {
    font-weight: 300;
}
.filters {
    margin: 0;
    padding: 0;
    list-style: none;
    position: absolute;
    right: 0;
    left: 0;
}
.filters li {
    display: inline;
}
.filters li a {
    color: inherit;
    margin: 3px;
    padding: 3px 7px;
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: 3px;
}
.filters li a:hover {
    border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
    border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
    float: right;
    position: relative;
    line-height: 20px;
    text-decoration: none;
    cursor: pointer;
}
.clear-completed:hover {
    text-decoration: underline;
}
/*
    Hack to remove background from Mobile Safari.
    Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
    .toggle-all,
    .todo-list li .toggle {
        background: none;
    }
    .todo-list li .toggle {
        height: 40px;
    }
    .toggle-all {
        -webkit-transform: rotate(90deg);
        transform: rotate(90deg);
        -webkit-appearance: none;
        appearance: none;
    }
}
@media (max-width: 430px) {
    .footer {
        height: 50px;
    }
    .filters {
        bottom: 10px;
    }
}複製程式碼

更新src\styles.css為如下

/* You can add global styles to this file, and also import other style files */
html, body {
    margin: 0;
    padding: 0;
}
button {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font-size: 100%;
    vertical-align: baseline;
    font-family: inherit;
    font-weight: inherit;
    color: inherit;
    -webkit-appearance: none;
    appearance: none;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}
body {
    font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    line-height: 1.4em;
    background: #f5f5f5;
    color: #4d4d4d;
    min-width: 230px;
    max-width: 550px;
    margin: 0 auto;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-weight: 300;
}
:focus {
    outline: 0;
}
.hidden {
    display: none;
}
.info {
    margin: 65px auto 0;
    color: #bfbfbf;
    font-size: 10px;
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
    text-align: center;
}
.info p {
    line-height: 1;
}
.info a {
    color: inherit;
    text-decoration: none;
    font-weight: 400;
}
.info a:hover {
    text-decoration: underline;
}複製程式碼

現在我們看看成果吧,現在好看多了

Angular 從 0 到 1 (三)史上最簡單的 Angular 教程
image_1b11jlmes1nithths9q1n8ijqg9.png-78.9kB

本節程式碼:github.com/wpcfan/awes…

紙書出版了,比網上內容豐富充實了,歡迎大家訂購!
京東連結:item.m.jd.com/product/120…

Angular 從 0 到 1 (三)史上最簡單的 Angular 教程
Angular從零到一

第一節:Angular 2.0 從0到1 (一)
第二節:Angular 2.0 從0到1 (二)
第三節:Angular 2.0 從0到1 (三)
第四節:Angular 2.0 從0到1 (四)
第五節:Angular 2.0 從0到1 (五)
第六節:Angular 2.0 從0到1 (六)
第七節:Angular 2.0 從0到1 (七)
第八節:Angular 2.0 從0到1 (八)

相關文章