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

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

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

第二節:用 Form 表單做一個登入控制元件

對於 login 元件的小改造

hello-angular\src\app\login\login.component.ts 中更改其模板為下面的樣子

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input type="text">
      <button>Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}複製程式碼

我們增加了一個文字輸入框和一個按鈕,儲存後返回瀏覽器可以看到結果

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s1_input_button_added.png-109.6kB

接下來我們嘗試給Login按鈕新增一個處理方法 <button (click)="onClick()">Login</button>(click)表示我們要處理這個button的click事件,圓括號是說發生此事件時,呼叫等號後面的表示式或函式。等號後面的onClick()是我們自己定義在LoginComponent中的函式,這個名稱你可以隨便定成什麼,不一定叫onClick()。下面我們就來定義這個函式,在LoginComponent中寫一個叫onClick()的方法,內容很簡單就是把“button was clicked”輸出到Console。

  onClick() {
    console.log('button was clicked');
  }複製程式碼

返回瀏覽器,並按F12調出開發者工具。當你點選Login時,會發現Console視窗輸出了我們期待的文字。

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s1_handle_click_method.png-141kB

那麼如果要在onClick中傳遞一個引數,比如是上面的文字輸入框輸入的值怎麼處理呢?我們可以在文字輸入框標籤內加一個#usernameRef,這個叫引用(reference)。注意這個引用是的input物件,我們如果想傳遞input的值,可以用usernameRef.value,然後就可以把onClick()方法改成onClick(usernameRef.value)

<div>
  <input #usernameRef type="text">
  <button (click)="onClick(usernameRef.value)">Login</button>
</div>複製程式碼

在Component內部的onClick方法也要隨之改寫成一個接受username的方法

  onClick(username) {
    console.log(username);
  }複製程式碼

現在我們再看看結果是什麼樣子,在文字輸入框中鍵入“hello”,點選Login按鈕,觀察Console視窗:hello被輸出了。

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s1_input_button_ref.png-141.1kB

好了,現在我們再加一個密碼輸入框,然後改寫onClick方法可以同時接收2個引數:使用者名稱和密碼。程式碼如下:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input #usernameRef type="text">
      <input #passwordRef type="password">
      <button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

  onClick(username, password) {
    console.log('username:' + username + "\n\r" + "password:" + password);
  }

}複製程式碼

看看結果吧,在瀏覽器中第一個輸入框輸入“wang”,第二個輸入框輸入“1234567”,觀察Console視窗,Bingo!

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s1_username_password_ref.png-141.8kB

建立一個服務去完成業務邏輯

如果我們把登入的業務邏輯在onClick方法中完成,當然也可以,但是這樣做的耦合性太強了。設想一下,如果我們增加了微信登入、微博登入等,業務邏輯會越來越複雜,顯然我們需要把這個業務邏輯分離出去。那麼我們接下來建立一個AuthService吧, 首先我們在src\app下建立一個core的子資料夾(src\app\core),然後命令列中輸入 ng g s core\auth (s這裡是service的縮寫,core\auth是說在core的目錄下建立auth服務相關檔案)。auth.service.tsauth.service.spec.ts這個兩個檔案應該已經出現在你的目錄裡了。

下面我們為這個service新增一個方法,你可能注意到這裡我們為這個方法指定了返回型別和引數型別。這就是TypeScript帶來的好處,有了型別約束,你在別處呼叫這個方法時,如果給出的引數型別或返回型別不正確,IDE就可以直接告訴你錯了。

import { Injectable } from '@angular/core';

@Injectable()
export class AuthService {

  constructor() { }

  loginWithCredentials(username: string, password: string): boolean {
    if(username === 'wangpeng')
      return true;
    return false;
  }

}複製程式碼

等一下,這個service雖然被建立了,但仍然無法在Component中使用。當然你可以在Component中import這個服務,然後例項化後使用,但是這樣做並不好,仍然時一個緊耦合的模式,Angular2提供了一種依賴性注入(Dependency Injection)的方法。

什麼是依賴性注入?

如果不使用DI(依賴性注入)的時候,我們自然的想法是這樣的,在login.component.ts中import引入AuthService,在構造中初始化service,在onClick中呼叫service。

import { Component, OnInit } from '@angular/core';
//引入AuthService
import { AuthService } from '../core/auth.service';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input #usernameRef type="text">
      <input #passwordRef type="password">
      <button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  //宣告成員變數,其型別為AuthService
  service: AuthService;

  constructor() {
    this.service = new AuthService();
  }

  ngOnInit() {
  }

  onClick(username, password) {
    //呼叫service的方法
    console.log('auth result is: ' + this.service.loginWithCredentials(username, password));
  }

}複製程式碼

這麼做呢也可以跑起來,但存在幾個問題:

  • 由於例項化是在元件中進行的,意味著我們如果更改service的建構函式的話,元件也需要更改。
  • 如果我們以後需要開發、測試和生產環境配置不同的AuthService,以這種方式實現會非常不方便。

下面我們看看如果使用DI是什麼樣子的,首先我們需要在元件的修飾器中配置AuthService,然後在元件的建構函式中使用引數進行依賴注入。

import { Component, OnInit } from '@angular/core';
import { AuthService } from '../core/auth.service';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input #usernameRef type="text">
      <input #passwordRef type="password">
      <button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
    </div>
  `,
  styles: [],
  //在providers中配置AuthService
  providers:[AuthService]
})
export class LoginComponent implements OnInit {
  //在建構函式中將AuthService示例注入到成員變數service中
  //而且我們不需要顯式宣告成員變數service了
  constructor(private service: AuthService) {
  }

  ngOnInit() {
  }

  onClick(username, password) {
    console.log('auth result is: ' + this.service.loginWithCredentials(username, password));
  }

}複製程式碼

看到這裡你會發現我們仍然需要import相關的服務,這是import是要將型別引入進來,而provider裡面會配置這個型別的例項。當然即使這樣還是不太爽,可不可以不引入AuthService呢?答案是可以。

我們看一下app.module.ts,這個根模組檔案中我們發現也有個providers,根模組中的這個providers是配置在模組中全域性可用的service或引數的。

providers: [
    {provide: 'auth',  useClass: AuthService}
    ]複製程式碼

providers是一個陣列,這個陣列呢其實是把你想要注入到其他元件中的服務配置在這裡。大家注意到我們這裡的寫法和上面優點區別,沒有直接寫成

providers:[AuthService]複製程式碼

而是給出了一個物件,裡面有兩個屬性,provide和useClass,provide定義了這個服務的名稱,有需要注入這個服務的就引用這個名稱就好。useClass指明這個名稱對應的服務是一個類,本例中就是AuthService了。這樣定義好之後,我們就可以在任意元件中注入這個依賴了。下面我們改動一下login.component.ts,去掉頭部的import { AuthService } from '../core/auth.service';和元件修飾器中的providers,更改其建構函式為

onstructor(@Inject('auth') private service) {
  }複製程式碼

我們去掉了service的型別宣告,但加了一個修飾符@Inject('auth'),這個修飾符的意思是請到系統配置中找到名稱為auth的那個依賴注入到我修飾的變數中。當然這樣改完後你會發現Inject這個修飾符系統不識別,我們需要在@angular/core中引用這個修飾符,現在login.component.ts看起來應該是下面這個樣子

import { Component, OnInit, Inject } from '@angular/core';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input #usernameRef type="text">
      <input #passwordRef type="password">
      <button (click)="onClick(usernameRef.value, passwordRef.value)">Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  constructor(@Inject('auth') private service) {
  }

  ngOnInit() {
  }

  onClick(username, password) {
    console.log('auth result is: ' + this.service.loginWithCredentials(username, password));
  }

}複製程式碼

雙向資料繫結

接下來的問題是我們是否只能通過這種方式進行表現層和邏輯之間的資料交換呢?如果我們希望在元件內對資料進行操作後再反饋到介面怎麼處理呢?Angular2提供了一個雙向資料繫結的機制。這個機制是這樣的,在元件中提供成員資料變數,然後在模板中引用這個資料變數。我們來改造一下login.component.ts,首先在class中宣告2個資料變數username和password。

  username = "";
  password = "";複製程式碼

然後去掉onClick方法的引數,並將內部的語句改造成如下樣子:

console.log('auth result is: '
      + this.service.loginWithCredentials(this.username, this.password));複製程式碼

去掉引數的原因是雙向繫結後,我們通過資料成員變數就可以知道使用者名稱和密碼了,不需要在傳遞引數了。而成員變數的引用方式是this.成員變數
然後我們來改造模板:

    <div>
      <input type="text"
        [(ngModel)]="username"
        />
      <input type="password"
        [(ngModel)]="password"
        />
      <button (click)="onClick()">Login</button>
    </div>複製程式碼

[(ngModel)]="username"這個看起來很彆扭,稍微解釋一下,方括號[]的作用是說把等號後面當成表示式來解析而不是當成字串,如果我們去掉方括號那就等於說是直接給這個ngModel賦值成“username”這個字串了。方括號的含義是單向繫結,就是說我們在元件中給model賦的值會設定到HTML的input控制元件中。[()]是雙向繫結的意思,就是說HTML對應控制元件的狀態的改變會反射設定到元件的model中。ngModel是FormModule中提供的指令,它負責從Domain Model(這裡就是username或password,以後我們可用繫結更復雜的物件)中建立一個FormControl的例項,並將這個例項和表單的控制元件繫結起來。同樣的對於click事件的處理,我們不需要傳入引數了,因為其呼叫的是剛剛我們改造的元件中的onClick方法。現在我們儲存檔案後開啟瀏覽器看一下,效果和上一節的應該一樣的。本節的完整程式碼如下:

//login.component.ts
import { Component, OnInit, Inject } from '@angular/core';

@Component({
  selector: 'app-login',
  template: `
    <div>
      <input type="text"
        [(ngModel)]="username"
        />
      <input type="password"
        [(ngModel)]="password"
        />
      <button (click)="onClick()">Login</button>
    </div>
  `,
  styles: []
})
export class LoginComponent implements OnInit {

  username = '';
  password = '';

  constructor(@Inject('auth') private service) {
  }

  ngOnInit() {
  }

  onClick() {
    console.log('auth result is: '
      + this.service.loginWithCredentials(this.username, this.password));
  }

}複製程式碼

表單資料的驗證

通常情況下,表單的資料是有一定的規則的,我們需要依照其規則對輸入的資料做驗證以及反饋驗證結果。Angular2中對錶單驗證有非常完善的支援,我們繼續上面的例子,在login元件中,我們定義了一個使用者名稱和密碼的輸入框,現在我們來為它們加上規則。首先我們定義一下規則,使用者名稱和密碼都是必須輸入的,也就是不能為空。更改login.component.ts中的模板為下面的樣子

    <div>
      <input required type="text"
        [(ngModel)]="username"
        #usernameRef="ngModel"
        />
        {{usernameRef.valid}}
      <input required type="password"
        [(ngModel)]="password"
        #passwordRef="ngModel"
        />
        {{passwordRef.valid}}
      <button (click)="onClick()">Login</button>
    </div>複製程式碼

注意到我們只是為username和password兩個控制元件加上了required這個屬性,表明這兩個控制元件為必填項。通過#usernameRef="ngModel"我們重新又加入了引用,這次的引用指向了ngModel,這個引用是要在模板中使用的,所以才加入這個引用如果不需要在模板中使用,可以不要這句。{{表示式}}雙花括號表示解析括號中的表示式,並把這個值輸出到模板中。這裡我們為了可以顯性的看到控制元件的驗證狀態,直接在對應控制元件後輸出了驗證的狀態。初始狀態可以看到2個控制元件的驗證狀態都是false,試著填寫一些字元在兩個輸入框中,看看狀態變化吧。

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s2_form_validation.png-8.5kB

我們是知道了驗證的狀態是什麼,但是如果我們想知道驗證失敗的原因怎麼辦呢?我們只需要將{{usernameRef.valid}}替換成{{usernameRef.errors | json}}|是管道操作符,用於將前面的結果通過管道輸出成另一種格式,這裡就是把errors物件輸出成json格式的意思。看一下結果吧,返回的結果如下

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s2_form_validation_errors.png-11kB

如果除了不能為空,我們為username再新增一個規則試試看呢,比如字元數不能少於3。

      <input type="text"
        [(ngModel)]="username"
        #usernameRef="ngModel"
        required 
        minlength="3"
        />複製程式碼

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s2_form_validation_errors_multiple.png-14.4kB

現在我們試著把{{表示式}}替換成友好的錯誤提示,我們想在有錯誤發生時顯示錯誤的提示資訊。那麼我們來改造一下template。

    <div>
      <input type="text"
        [(ngModel)]="username"
        #usernameRef="ngModel"
        required
        minlength="3"
        />
        {{ usernameRef.errors | json }}
        <div *ngIf="usernameRef.errors?.required">this is required</div>
        <div *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</div>
      <input required type="password"
        [(ngModel)]="password"
        #passwordRef="ngModel"
        />
        <div *ngIf="passwordRef.errors?.required">this is required</div>
      <button (click)="onClick()">Login</button>
    </div>複製程式碼

ngIf也是一個Angular2的指令,顧名思義,是用於做條件判斷的。*ngIf="usernameRef.errors?.required"的意思是當usernameRef.errors.requiredtrue時顯示div標籤。那麼那個?是幹嘛的呢?因為errors可能是個null,如果這個時候呼叫errorsrequired屬性肯定會引發異常,那麼?就是標明errors可能為空,在其為空時就不用呼叫後面的屬性了。

如果我們把使用者名稱和密碼整個看成一個表單的話,我們應該把它們放在一對<form></form>標籤中,類似的加入一個表單的引用formRef

    <div>
      <form #formRef="ngForm">
        <input type="text"
          [(ngModel)]="username"
          #usernameRef="ngModel"
          required
          minlength="3"
          />
          <div *ngIf="usernameRef.errors?.required">this is required</div>
          <div *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</div>
        <input type="password"
          [(ngModel)]="password"
          #passwordRef="ngModel"
          required
          />
          <div *ngIf="passwordRef.errors?.required">this is required</div>
        <button (click)="onClick()">Login</button>
      </form>
    </div>複製程式碼

這時執行後會發現原本好用的程式碼出錯了,這是由於如果在一個大的表單中,ngModel會註冊成Form的一個子控制元件,註冊子控制元件需要一個name,這要求我們顯式的指定對應控制元件的name,因此我們需要為input增加name屬性

    <div>
      <form #formRef="ngForm">
        <input type="text"
          name="username"
          [(ngModel)]="username"
          #usernameRef="ngModel"
          required
          minlength="3"
          />
          <div *ngIf="usernameRef.errors?.required">this is required</div>
          <div *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</div>
        <input type="password"
          name="password"
          [(ngModel)]="password"
          #passwordRef="ngModel"
          required
          />
          <div *ngIf="passwordRef.errors?.required">this is required</div>
        <button (click)="onClick()">Login</button>
        <button type="submit">Submit</button>
      </form>
    </div>複製程式碼

既然我們增加了一個formRef,我們就看看formRef.value有什麼吧。
首先為form增加一個表單提交事件的處理
<form #formRef="ngForm" (ngSubmit)="onSubmit(formRef.value)">
然後在元件中增加一個onSubmit方法

  onSubmit(formValue) {
    console.log(formValue);
  }複製程式碼

你會發現formRef.value中包括了表單所有填寫項的值。

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s2_form_validation_form_submit.png-27.7kB

有時候在表單項過多時我們需要對錶單項進行分組,HTML中提供了fieldset標籤用來處理。那麼我們看看怎麼和Angular2結合吧:

    <div>
      <form #formRef="ngForm" (ngSubmit)="onSubmit(formRef.value)">
        <fieldset ngModelGroup="login">
          <input type="text"
            name="username"
            [(ngModel)]="username"
            #usernameRef="ngModel"
            required
            minlength="3"
            />
            <div *ngIf="usernameRef.errors?.required">this is required</div>
            <div *ngIf="usernameRef.errors?.minlength">should be at least 3 charactors</div>
          <input type="password"
            name="password"
            [(ngModel)]="password"
            #passwordRef="ngModel"
            required
            />
            <div *ngIf="passwordRef.errors?.required">this is required</div>
          <button (click)="onClick()">Login</button>
          <button type="submit">Submit</button>
        </fieldset>
      </form>
    </div>複製程式碼

<fieldset ngModelGroup="login">意味著我們對於fieldset之內的資料都分組到了login物件中。

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s2_form_validation_fieldset.png-43.5kB

接下來我們改寫onSubmit方法用來替代onClick,因為看起來這兩個按鈕重複了,我們需要去掉onClick。首先去掉template中的<button (click)="onClick()">Login</button>,然後把<button type="submit">標籤後的Submit文字替換成Login,最後改寫onSubmit方法。

  onSubmit(formValue) {
    console.log('auth result is: '
      + this.service.loginWithCredentials(formValue.login.username, formValue.login.password));
  }複製程式碼

在瀏覽器中試驗一下吧,所有功能正常工作。

驗證結果的樣式自定義

如果我們在開發工具中檢視網頁原始碼,可以看到

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s2_form_validation_form_styling.png-92.5kB

使用者名稱控制元件的HTML程式碼是下面的樣子:在驗證結果為false時input的樣式是ng-invalid

<input 
    name="username" 
    class="ng-pristine ng-invalid ng-touched" 
    required="" 
    type="text" 
    minlength="3" 
    ng-reflect-minlength="3" 
    ng-reflect-name="username">複製程式碼

類似的可以實驗一下,填入一些字元滿足驗證要求之後,看input的HTML是下面的樣子:在驗證結果為true時input的樣式是ng-valid

<input 
    name="username" 
    class="ng-touched ng-dirty ng-valid" 
    required="" 
    type="text" 
    ng-reflect-model="ssdsds" 
    minlength="3" 
    ng-reflect-minlength="3" 
    ng-reflect-name="username">複製程式碼

知道這個後,我們可以自定義不同驗證狀態下的控制元件樣式。在元件的修飾符中把styles陣列改寫一下:

  styles: [`
    .ng-invalid{
      border: 3px solid red;
    }
    .ng-valid{
      border: 3px solid green;
    }
  `]複製程式碼

儲存一下,返回瀏覽器可以看到,驗證不通過時

Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s2_form_validation_style_fail.png-8.9kB

驗證通過時是這樣的:
Angular 從 0 到 1 (二)史上最簡單的 Angular 教程
c2_s2_form_validation_style_pass.png-4.6kB

最後說一下,我們看到這樣設定完樣式後連form和fieldset都一起設定了,這是由於form和fieldset也在樣式中應用了.ng-valid.ng-valid,那怎麼解決呢?只需要在.ng-valid加上input即可,它表明的是應用於input型別控制元件並且class引用了ng-invalid的元素。

  styles: [`
    input.ng-invalid{
      border: 3px solid red;
    }
    input.ng-valid{
      border: 3px solid green;
    }
  `]複製程式碼

很多開發人員不太瞭解CSS,其實CSS還是比較簡單的,我建議先從Selector開始看,Selector的概念弄懂後Angular2的開發CSS就會順暢很多。具體可見W3School中對於CSS Selctor的參考css-tricks.com/multiple-cl…

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

進一步的練習

  • 練習1:如果我們想給username和password輸入框設定預設值。比如“請輸入使用者名稱”和“請輸入密碼”,自己動手試一下吧。
  • 練習2:如果我們想在輸入框聚焦時把預設文字清除掉,該怎麼做?
  • 練習3:如果我們想把預設文字顏色設定成淺灰色該怎麼做?

紙書出版了,比網上內容豐富充實了,歡迎大家訂購!
京東連結: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 (八)

相關文章